docs: initial import design sistema documentale + docugen-mcp
- Docs/sistema-documentale/: master architettura multi-wiki - Docs/docugen-mcp/: design + implementation plan MCP server Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,600 @@
|
|||||||
|
# DocuGen MCP — Design Document
|
||||||
|
|
||||||
|
> MCP server standalone per generazione di documenti Markdown
|
||||||
|
> a partire da un template (Markdown + frontmatter + prompt),
|
||||||
|
> contenuto grezzo e variabili. LLM via OpenRouter.
|
||||||
|
> Deploy Docker su VPS esterna, accessibile da qualsiasi host MCP
|
||||||
|
> (Claude Code, Claude Desktop, altri client conformi).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Indice
|
||||||
|
|
||||||
|
1. [Panoramica](#1-panoramica)
|
||||||
|
2. [Architettura generale](#2-architettura-generale)
|
||||||
|
3. [Componenti interni](#3-componenti-interni)
|
||||||
|
4. [Modello dati](#4-modello-dati)
|
||||||
|
5. [API MCP — tool esposti](#5-api-mcp--tool-esposti)
|
||||||
|
6. [Dataflow `document_generate`](#6-dataflow-document_generate)
|
||||||
|
7. [Gestione errori](#7-gestione-errori)
|
||||||
|
8. [Strategia di test](#8-strategia-di-test)
|
||||||
|
9. [Deploy e operatività](#9-deploy-e-operatività)
|
||||||
|
10. [Stack tecnologico](#10-stack-tecnologico)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Panoramica
|
||||||
|
|
||||||
|
### Scopo
|
||||||
|
|
||||||
|
`docugen-mcp` è un server MCP che espone strumenti per creare documenti Markdown professionali (fatture, lettere, preventivi, report) a partire da:
|
||||||
|
|
||||||
|
- un **template** persistito sul server (nome + frontmatter YAML + body Markdown con prompt e placeholder)
|
||||||
|
- un **contenuto grezzo** Markdown fornito dal client (es. "# Ordine ACME\n- 10 unità X...")
|
||||||
|
- un **dict di variabili** strutturate (es. `{cliente: "ACME", data: "2026-04-21", foto_prodotto: {kind: image, data_b64, mime}}`)
|
||||||
|
- **istruzioni libere** opzionali per tweak stilistici ("tono formale", "in inglese")
|
||||||
|
|
||||||
|
Il server assembla un prompt, chiama un modello LLM via OpenRouter e restituisce il Markdown finale al client.
|
||||||
|
|
||||||
|
### Principi
|
||||||
|
|
||||||
|
- **MCP protocol-first** — il server è un MCP server, non una API REST con wrapper MCP. FastMCP è il framework principale.
|
||||||
|
- **Standalone** — repo dedicato, nessuna dipendenza da DocuGen. DocuGen e altri sistemi lo usano come client.
|
||||||
|
- **Stateless sui documenti generati** — il server non tiene archivio dei MD prodotti (solo log/cost tracking in SQLite). Il client conserva l'output.
|
||||||
|
- **Persistenza solo dove serve** — template sul filesystem (ispezionabili a mano), asset efimeri con TTL, metrica generazioni in SQLite.
|
||||||
|
- **Validazione strict** — se il template dichiara `required_variables` e mancano, errore duro, niente chiamata LLM sprecata.
|
||||||
|
- **Errori espliciti** — codici stringa ben definiti, retry solo su errori transitori upstream.
|
||||||
|
|
||||||
|
### Cosa NON fa
|
||||||
|
|
||||||
|
- Non genera PDF né HTML (solo Markdown). Conversione fuori scope.
|
||||||
|
- Non tiene un archivio ricercabile dei documenti prodotti.
|
||||||
|
- Non fa OCR, estrazione metadati da documenti, ricerca semantica (ruolo di DocuGen).
|
||||||
|
- Non espone gestione utenti: una sola API key condivisa.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Architettura generale
|
||||||
|
|
||||||
|
### Approccio: FastMCP principale + FastAPI sub-app
|
||||||
|
|
||||||
|
Un singolo processo ASGI, un container Docker, un volume disco.
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────┐
|
||||||
|
│ Container docugen-mcp (VPS esterna) │
|
||||||
|
│ │
|
||||||
|
│ ┌────────────────────────────────────────┐ │
|
||||||
|
│ │ FastMCP (ASGI root) │ │
|
||||||
|
│ │ Streamable HTTP su /mcp │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ Tool esposti: │ │
|
||||||
|
│ │ • template_create │ │
|
||||||
|
│ │ • template_update │ │
|
||||||
|
│ │ • template_delete │ │
|
||||||
|
│ │ • template_list │ │
|
||||||
|
│ │ • template_get │ │
|
||||||
|
│ │ • document_generate │ │
|
||||||
|
│ └────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ mount FastAPI sub-app │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌────────────────────────────────────────┐ │
|
||||||
|
│ │ FastAPI sub-app │ │
|
||||||
|
│ │ GET /assets/{template}/{file} │ │
|
||||||
|
│ │ GET /generated/{gen_id}/{file} │ │
|
||||||
|
│ │ GET /health │ │
|
||||||
|
│ └────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ Moduli (librerie Python): │
|
||||||
|
│ • auth — middleware API key │
|
||||||
|
│ • template_store — filesystem │
|
||||||
|
│ • llm_client — OpenRouter (httpx) │
|
||||||
|
│ • renderer — prompt assembly │
|
||||||
|
│ • generation_store — SQLite + fs │
|
||||||
|
│ │
|
||||||
|
│ Volume Docker montato su /data: │
|
||||||
|
│ ├── templates/<nome>/ │
|
||||||
|
│ │ ├── template.md │
|
||||||
|
│ │ └── assets/* │
|
||||||
|
│ ├── generated/<gen_id>/ │
|
||||||
|
│ │ └── <var_name>.<ext> (TTL 30gg) │
|
||||||
|
│ └── docugen_mcp.db │
|
||||||
|
└──────────────────────────────────────────────┘
|
||||||
|
▲ │
|
||||||
|
│ HTTP(S) + API key │ HTTPS
|
||||||
|
│ ▼
|
||||||
|
┌─────┴──────────┐ ┌──────────┐
|
||||||
|
│ Host MCP │ │OpenRouter│
|
||||||
|
│ (Claude Code, │ │ │
|
||||||
|
│ Claude Desktop│ └──────────┘
|
||||||
|
│ altri) │
|
||||||
|
└────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Motivazioni architetturali
|
||||||
|
|
||||||
|
- **MCP è il prodotto.** FastMCP guida la struttura. FastAPI compare solo per ciò che l'MCP protocol non copre: file statici per asset immagini e health check.
|
||||||
|
- **Un processo unico** mantiene deploy banale: un container, un comando, un volume.
|
||||||
|
- **TLS fuori container.** Reverse proxy esterno (Nginx/Traefik/Caddy) gestisce certificati e termina HTTPS. Il container espone HTTP su porta interna.
|
||||||
|
- **Storage semplice.** Template sul filesystem (leggibili a mano, backup = copia cartella, git-friendly se l'operatore vuole). Metriche e indice TTL in SQLite per query strutturate. Nessun DB esterno.
|
||||||
|
|
||||||
|
### Scalabilità
|
||||||
|
|
||||||
|
Processo singolo basta per l'uso previsto (pochi client, decine di generazioni/minuto al massimo). Per picchi futuri: replicare il container dietro load balancer, spostare il volume su storage condiviso (NFS) o estrarre SQLite in Postgres. Scelte rimandate a quando il bisogno sarà concreto.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Componenti interni
|
||||||
|
|
||||||
|
### `auth` — middleware API Key
|
||||||
|
|
||||||
|
- Legge header `Authorization: Bearer <key>` da ogni richiesta (MCP e FastAPI).
|
||||||
|
- Confronta con env var `API_KEY` (singola chiave condivisa).
|
||||||
|
- Respinge 401 se assente/errata.
|
||||||
|
- Esenzione: `/health`.
|
||||||
|
|
||||||
|
### `template_store` — persistenza filesystem
|
||||||
|
|
||||||
|
- Path base: `{DATA_DIR}/templates/<nome>/`.
|
||||||
|
- File `template.md` = frontmatter YAML + body Markdown.
|
||||||
|
- Directory `assets/` contiene risorse statiche referenziate dal template.
|
||||||
|
- Frontmatter standard:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
name: fattura-commerciale
|
||||||
|
description: Fattura commerciale in italiano
|
||||||
|
model: anthropic/claude-sonnet-4 # opzionale, override default
|
||||||
|
required_variables:
|
||||||
|
- name: cliente
|
||||||
|
type: string
|
||||||
|
- name: data
|
||||||
|
type: string
|
||||||
|
- name: foto_prodotto
|
||||||
|
type: image # trigger per shape {kind, data_b64, mime}
|
||||||
|
instructions_hint: "tono formale, struttura blocchi"
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
- Body: Markdown con prompt di sistema e placeholder `{{cliente}}`, `{{foto_prodotto}}`, `./assets/logo.png`.
|
||||||
|
- API Python (async): `create(name, frontmatter, body, assets=None)`, `get(name)`, `update(name, ...)`, `delete(name)`, `list()`.
|
||||||
|
- Nome template: slug `^[a-z0-9][a-z0-9-]*$`. Rifiuta path traversal, chars speciali.
|
||||||
|
- Validazione frontmatter via Pydantic `TemplateFrontmatter`.
|
||||||
|
|
||||||
|
### `llm_client` — wrapper OpenRouter
|
||||||
|
|
||||||
|
- Unica responsabilità: mandare una richiesta OpenRouter e ritornare risposta + metrica utilizzo.
|
||||||
|
- httpx async. Retry con backoff esponenziale (max 3 tentativi) su timeout/5xx/429.
|
||||||
|
- Modello scelto da `frontmatter.model or env.LLM_MODEL_DEFAULT`.
|
||||||
|
- Ritorna `LLMResponse(text: str, model: str, tokens_in: int, tokens_out: int, cost_usd: float, latency_ms: int)`.
|
||||||
|
|
||||||
|
### `renderer` — orchestratore della generazione
|
||||||
|
|
||||||
|
Riceve `(template_name, content_md, variables, instructions)` e ritorna il Markdown finale.
|
||||||
|
|
||||||
|
Passi:
|
||||||
|
1. `template_store.get(template_name)` → `(frontmatter, body, assets_dir)`.
|
||||||
|
2. Validazione variabili (strict): ogni `required_variables` dev'essere presente con il tipo dichiarato. Fallimento → eccezione tipizzata senza chiamare LLM.
|
||||||
|
3. Generazione `gen_id = uuid4()`.
|
||||||
|
4. Per ogni variabile di tipo `image`: decodifica base64, valida mime, salva in `{DATA_DIR}/generated/{gen_id}/{var_name}.{ext}`, registra in `ephemeral_assets` con `expires_at = now + ASSET_TTL_DAYS`, sostituisce la variabile con URL assoluto `{PUBLIC_BASE_URL}/generated/{gen_id}/{var_name}.{ext}`.
|
||||||
|
5. Rewrite asset paths del template: `./assets/<file>` → `{PUBLIC_BASE_URL}/assets/{template_name}/{file}`.
|
||||||
|
6. Assembla prompt finale:
|
||||||
|
- **system**: body del template con placeholder sostituiti.
|
||||||
|
- **user**: contenuto grezzo + variabili formattate + `instructions` opzionali.
|
||||||
|
7. Chiama `llm_client.chat(...)` con il modello selezionato.
|
||||||
|
8. Registra la generazione in `generation_store` (success o error).
|
||||||
|
9. Ritorna `GenerationResult(generation_id, markdown, model, tokens, cost_usd, ephemeral_assets_urls, ephemeral_expires_at)`.
|
||||||
|
|
||||||
|
### `generation_store` — persistenza generazioni
|
||||||
|
|
||||||
|
- SQLite via aiosqlite. Due tabelle:
|
||||||
|
- `generations(id TEXT PK, timestamp INTEGER, template_name TEXT, model TEXT, tokens_in INTEGER, tokens_out INTEGER, cost_usd REAL, success INTEGER, error_msg TEXT)`
|
||||||
|
- `ephemeral_assets(generation_id TEXT, var_name TEXT, file_path TEXT, mime TEXT, created_at INTEGER, expires_at INTEGER, PRIMARY KEY (generation_id, var_name))`
|
||||||
|
- Cleanup job: task asyncio lanciato al bootstrap che ogni 24h cancella file e record con `expires_at < now()`.
|
||||||
|
|
||||||
|
### `mcp_server` — entry point FastMCP
|
||||||
|
|
||||||
|
- Definisce i 6 tool MCP con decoratori `@mcp.tool()`.
|
||||||
|
- Ogni tool fa validazione input (Pydantic), invoca il modulo responsabile, serializza la risposta.
|
||||||
|
- Monta la sub-app FastAPI sul path root.
|
||||||
|
|
||||||
|
### `config` — Pydantic Settings
|
||||||
|
|
||||||
|
Variabili d'ambiente (tutte con default dove sensato):
|
||||||
|
|
||||||
|
| Variabile | Default | Descrizione |
|
||||||
|
|-----------|---------|-------------|
|
||||||
|
| `API_KEY` | *(obbligatoria)* | Chiave di autenticazione client MCP |
|
||||||
|
| `OPENROUTER_API_KEY` | *(obbligatoria)* | Chiave OpenRouter |
|
||||||
|
| `OPENROUTER_BASE_URL` | `https://openrouter.ai/api/v1` | Endpoint OpenRouter |
|
||||||
|
| `LLM_MODEL_DEFAULT` | `anthropic/claude-sonnet-4` | Modello usato se il template non specifica override |
|
||||||
|
| `PUBLIC_BASE_URL` | *(obbligatoria)* | URL pubblico del server, usato per costruire link asset |
|
||||||
|
| `DATA_DIR` | `/data` | Path volume persistenza |
|
||||||
|
| `ASSET_TTL_DAYS` | `30` | TTL asset efimeri generati |
|
||||||
|
| `MAX_IMAGE_SIZE_MB` | `10` | Limite dimensione immagine base64 in `variables` |
|
||||||
|
| `LLM_TIMEOUT_SECONDS` | `60` | Timeout singola richiesta OpenRouter |
|
||||||
|
|
||||||
|
### Struttura sorgente
|
||||||
|
|
||||||
|
```
|
||||||
|
src/docugen_mcp/
|
||||||
|
├── __init__.py
|
||||||
|
├── main.py # bootstrap FastMCP + FastAPI mount
|
||||||
|
├── config.py
|
||||||
|
├── auth.py
|
||||||
|
├── mcp_tools.py # definizione tool MCP
|
||||||
|
├── http_routes.py # static asset + health (FastAPI)
|
||||||
|
├── template_store.py
|
||||||
|
├── llm_client.py
|
||||||
|
├── renderer.py
|
||||||
|
├── generation_store.py
|
||||||
|
└── models.py # Pydantic models condivisi
|
||||||
|
tests/
|
||||||
|
├── unit/
|
||||||
|
├── integration/
|
||||||
|
└── e2e/
|
||||||
|
pyproject.toml
|
||||||
|
Dockerfile
|
||||||
|
docker-compose.yml
|
||||||
|
.env.example
|
||||||
|
README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Modello dati
|
||||||
|
|
||||||
|
### Filesystem
|
||||||
|
|
||||||
|
```
|
||||||
|
{DATA_DIR}/
|
||||||
|
├── templates/
|
||||||
|
│ └── <nome-template>/
|
||||||
|
│ ├── template.md # frontmatter + body
|
||||||
|
│ └── assets/
|
||||||
|
│ └── logo.png # risorse statiche del template
|
||||||
|
├── generated/
|
||||||
|
│ └── <gen_id>/ # uuid4, uno per generazione
|
||||||
|
│ └── <var_name>.<ext> # immagini inline dal client (TTL)
|
||||||
|
└── docugen_mcp.db # SQLite
|
||||||
|
```
|
||||||
|
|
||||||
|
### SQLite
|
||||||
|
|
||||||
|
**`generations`**
|
||||||
|
|
||||||
|
| Campo | Tipo | Note |
|
||||||
|
|-------|------|------|
|
||||||
|
| `id` | TEXT PK | uuid4 della generazione |
|
||||||
|
| `timestamp` | INTEGER | unix epoch ms |
|
||||||
|
| `template_name` | TEXT | |
|
||||||
|
| `model` | TEXT | modello effettivamente usato |
|
||||||
|
| `tokens_in` | INTEGER | da OpenRouter usage |
|
||||||
|
| `tokens_out` | INTEGER | da OpenRouter usage |
|
||||||
|
| `cost_usd` | REAL | calcolato da OpenRouter |
|
||||||
|
| `success` | INTEGER | 0/1 |
|
||||||
|
| `error_msg` | TEXT NULL | codice errore se success=0 |
|
||||||
|
|
||||||
|
Indice su `timestamp` per query temporali sui costi.
|
||||||
|
|
||||||
|
**`ephemeral_assets`**
|
||||||
|
|
||||||
|
| Campo | Tipo | Note |
|
||||||
|
|-------|------|------|
|
||||||
|
| `generation_id` | TEXT | FK logica a `generations.id` |
|
||||||
|
| `var_name` | TEXT | nome variabile (es. `foto_prodotto`) |
|
||||||
|
| `file_path` | TEXT | path assoluto sul filesystem |
|
||||||
|
| `mime` | TEXT | `image/png`, `image/jpeg`, `image/webp` |
|
||||||
|
| `created_at` | INTEGER | unix epoch ms |
|
||||||
|
| `expires_at` | INTEGER | unix epoch ms |
|
||||||
|
|
||||||
|
PRIMARY KEY `(generation_id, var_name)`. Indice su `expires_at` per cleanup rapido.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. API MCP — tool esposti
|
||||||
|
|
||||||
|
### `template_create`
|
||||||
|
|
||||||
|
**Input:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "fattura-commerciale",
|
||||||
|
"frontmatter": {
|
||||||
|
"name": "fattura-commerciale",
|
||||||
|
"description": "...",
|
||||||
|
"model": "anthropic/claude-sonnet-4",
|
||||||
|
"required_variables": [
|
||||||
|
{"name": "cliente", "type": "string"},
|
||||||
|
{"name": "data", "type": "string"}
|
||||||
|
],
|
||||||
|
"instructions_hint": "..."
|
||||||
|
},
|
||||||
|
"body": "# Template Markdown con {{cliente}} placeholder...",
|
||||||
|
"assets": [
|
||||||
|
{"filename": "logo.png", "data_b64": "...", "mime": "image/png"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output:** `{"name": "fattura-commerciale", "created_at": "..."}` o errore.
|
||||||
|
|
||||||
|
### `template_update`
|
||||||
|
|
||||||
|
Stesso input di `template_create`, richiede nome esistente. Sovrascrive `template.md`; `assets` (se forniti) sostituiscono interamente la directory (nessun merge).
|
||||||
|
|
||||||
|
### `template_delete`
|
||||||
|
|
||||||
|
**Input:** `{"name": "fattura-commerciale"}`. Rimuove directory template.
|
||||||
|
|
||||||
|
### `template_list`
|
||||||
|
|
||||||
|
**Input:** vuoto. **Output:** lista di `{name, description, updated_at}`.
|
||||||
|
|
||||||
|
### `template_get`
|
||||||
|
|
||||||
|
**Input:** `{"name": "fattura-commerciale"}`. **Output:** frontmatter + body + elenco asset (nomi, mime, size).
|
||||||
|
|
||||||
|
### `document_generate`
|
||||||
|
|
||||||
|
**Input:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"template_name": "fattura-commerciale",
|
||||||
|
"content_md": "# Ordine ACME\n- 10 unità X",
|
||||||
|
"variables": {
|
||||||
|
"cliente": "ACME S.r.l.",
|
||||||
|
"data": "2026-04-21",
|
||||||
|
"foto_prodotto": {
|
||||||
|
"kind": "image",
|
||||||
|
"data_b64": "iVBOR...",
|
||||||
|
"mime": "image/png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"instructions": "tono molto formale"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"generation_id": "<uuid>",
|
||||||
|
"markdown": "...output MD...",
|
||||||
|
"model": "anthropic/claude-sonnet-4",
|
||||||
|
"tokens": {"in": 1234, "out": 2345},
|
||||||
|
"cost_usd": 0.0123,
|
||||||
|
"ephemeral_assets_urls": [
|
||||||
|
"https://mcp.vps/generated/<uuid>/foto_prodotto.png"
|
||||||
|
],
|
||||||
|
"ephemeral_expires_at": "2026-05-21T10:33:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Dataflow `document_generate`
|
||||||
|
|
||||||
|
```
|
||||||
|
Client MCP ──► document_generate(...)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
auth: valida API key (401 se KO)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Pydantic: valida shape input
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
renderer.generate():
|
||||||
|
1. template_store.get(name)
|
||||||
|
2. validate variables (strict)
|
||||||
|
3. gen_id = uuid4()
|
||||||
|
4. per ogni var image:
|
||||||
|
- decode base64, valida mime/size
|
||||||
|
- salva /data/generated/<gen_id>/<var>.<ext>
|
||||||
|
- insert ephemeral_assets (expires_at)
|
||||||
|
- sostituisce var con URL assoluto
|
||||||
|
5. rewrite asset paths del template
|
||||||
|
6. assembla prompt finale
|
||||||
|
7. llm_client.chat(prompt, model)
|
||||||
|
8. generation_store.record(success|error)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Ritorna GenerationResult al client
|
||||||
|
```
|
||||||
|
|
||||||
|
**Flusso secondario — accesso asset:**
|
||||||
|
|
||||||
|
Quando il client (o il renderer downstream) dereferenzia ``:
|
||||||
|
|
||||||
|
- `GET /assets/<template>/<file>` → auth check → serve file da `{DATA_DIR}/templates/<template>/assets/`.
|
||||||
|
- `GET /generated/<gen_id>/<file>` → auth check → serve file. 410 Gone se record scaduto.
|
||||||
|
|
||||||
|
Anche gli asset richiedono API key: il Markdown non è pubblico.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Gestione errori
|
||||||
|
|
||||||
|
### Errori di validazione (pre-LLM, nessuna registrazione)
|
||||||
|
|
||||||
|
| Caso | Errore |
|
||||||
|
|------|--------|
|
||||||
|
| API key mancante/errata | HTTP 401 `invalid_api_key` |
|
||||||
|
| `template_name` non esiste | `TemplateNotFound` |
|
||||||
|
| Nome template invalido | `InvalidTemplateName` |
|
||||||
|
| Frontmatter YAML malformato | `InvalidFrontmatter` (con dettagli Pydantic) |
|
||||||
|
| `required_variables` mancanti | `MissingVariables: [lista]` |
|
||||||
|
| Variabile con tipo sbagliato | `InvalidVariableType: <var> expected <type>` |
|
||||||
|
| Immagine base64 non decodificabile | `InvalidImageEncoding: <var>` |
|
||||||
|
| Mime image non supportato | `UnsupportedImageMime` |
|
||||||
|
| Immagine > `MAX_IMAGE_SIZE_MB` | `ImageTooLarge` |
|
||||||
|
|
||||||
|
### Errori di generazione (post-validazione)
|
||||||
|
|
||||||
|
| Caso | Comportamento |
|
||||||
|
|------|---------------|
|
||||||
|
| I/O salvataggio asset | Rollback file già scritti, `AssetStorageError` |
|
||||||
|
| OpenRouter timeout | Retry 3x backoff (1s, 2s, 4s), poi `LLMTimeout` |
|
||||||
|
| OpenRouter 5xx | Retry 3x, poi `LLMUpstreamError` |
|
||||||
|
| OpenRouter 401/403 (config server) | No retry, `LLMAuthError` |
|
||||||
|
| OpenRouter 429 | Retry backoff lungo (5s, 10s, 20s), poi `LLMRateLimit` |
|
||||||
|
| Risposta non parseable | `LLMInvalidResponse` |
|
||||||
|
| Risposta vuota | `LLMEmptyResponse` |
|
||||||
|
|
||||||
|
Ogni fallimento post-validazione → record `generations` con `success=0`. Asset efimeri non rimossi (scadranno col TTL).
|
||||||
|
|
||||||
|
### Errori tool CRUD template
|
||||||
|
|
||||||
|
| Caso | Errore |
|
||||||
|
|------|--------|
|
||||||
|
| Create su nome esistente | `TemplateAlreadyExists` |
|
||||||
|
| Update/delete su nome inesistente | `TemplateNotFound` |
|
||||||
|
| I/O filesystem | `TemplateStorageError` |
|
||||||
|
|
||||||
|
### Errori serving asset HTTP
|
||||||
|
|
||||||
|
| Caso | Codice |
|
||||||
|
|------|--------|
|
||||||
|
| Missing/invalid API key | 401 |
|
||||||
|
| Path traversal | 400 |
|
||||||
|
| `/assets/<template>/<file>` non trovato | 404 |
|
||||||
|
| `/generated/<gen_id>/<file>`: record `ephemeral_assets` scaduto o `gen_id`/file assente | 410 Gone |
|
||||||
|
|
||||||
|
Semantica: gli asset dei template sono persistenti, la loro assenza è 404. Gli asset generati sono effimeri: assenti = già esistiti o mai esistiti → 410 Gone in entrambi i casi (il client riconosce che è scaduto o invalido, non deve riprovare con path diverso).
|
||||||
|
|
||||||
|
### Filosofia
|
||||||
|
|
||||||
|
- Nessuna fallback silenziosa. Validazione strict → errore esplicito, niente chiamata LLM sprecata.
|
||||||
|
- Retry solo su errori transitori upstream. Mai su 4xx.
|
||||||
|
- Codici errore sono stringhe ben definite: il client sa cosa correggere.
|
||||||
|
- Errori di config server (LLM auth, fs non scrivibile) → log ERROR, 500 con codice.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Strategia di test
|
||||||
|
|
||||||
|
### Piramide
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────┐
|
||||||
|
│ E2E (1-2)│ Container Docker reale
|
||||||
|
├────────────┤
|
||||||
|
│ Integration│ FastMCP test client + mock OpenRouter
|
||||||
|
├────────────┤
|
||||||
|
│ Unit │ Moduli isolati
|
||||||
|
└────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Unit
|
||||||
|
|
||||||
|
- `test_template_store.py` — CRUD, nomi invalidi, frontmatter malformato, gestione asset dir.
|
||||||
|
- `test_renderer.py` — validazione strict, shape image, rewrite asset paths, assemblaggio prompt, LLM mock per verifica modello scelto.
|
||||||
|
- `test_auth.py` — header presente/assente, `/health` senza auth.
|
||||||
|
- `test_generation_store.py` — insert, query, TTL cleanup con `expires_at` forzato.
|
||||||
|
- `test_llm_client.py` — retry su 5xx/timeout/429, no retry su 4xx, parsing usage.
|
||||||
|
|
||||||
|
### Integration
|
||||||
|
|
||||||
|
- `test_mcp_tools.py` — tool call end-to-end via FastMCP test client, ogni tool happy path + error paths.
|
||||||
|
- `test_http_routes.py` — assets auth, generated fresco vs scaduto (410), health sempre 200.
|
||||||
|
- `test_document_generate_flow.py` — create template → generate → verifica record + file + asset URL 200.
|
||||||
|
|
||||||
|
### E2E
|
||||||
|
|
||||||
|
- `test_e2e_docker.py` — build container, compose up, client MCP reale, OpenRouter mockato via env var URL.
|
||||||
|
|
||||||
|
### Copertura
|
||||||
|
|
||||||
|
Target 85%+ sui moduli logici. `main.py` esente.
|
||||||
|
|
||||||
|
### Cosa NON testiamo
|
||||||
|
|
||||||
|
- OpenRouter reale (costi, flaky).
|
||||||
|
- Qualità output LLM (responsabilità del modello).
|
||||||
|
- Performance/load (inserire se nascerà bisogno).
|
||||||
|
|
||||||
|
### Fixture principali
|
||||||
|
|
||||||
|
- `tmp_data_dir` — DATA_DIR pulito per test.
|
||||||
|
- `fake_template` — factory template valido.
|
||||||
|
- `mock_openrouter` — respx con risposta + usage.
|
||||||
|
- `client` — FastMCP test client autenticato.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Deploy e operatività
|
||||||
|
|
||||||
|
### Container
|
||||||
|
|
||||||
|
- Dockerfile basato su `python:3.11-slim`.
|
||||||
|
- `uv` per installazione dipendenze.
|
||||||
|
- Utente non-root.
|
||||||
|
- Porta interna esposta: 8000.
|
||||||
|
- Volume: `/data`.
|
||||||
|
|
||||||
|
### docker-compose
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
docugen-mcp:
|
||||||
|
build: .
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
env_file: .env
|
||||||
|
volumes:
|
||||||
|
- ./data:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
||||||
|
interval: 30s
|
||||||
|
```
|
||||||
|
|
||||||
|
### TLS / reverse proxy
|
||||||
|
|
||||||
|
Fuori scope del container. Esempi:
|
||||||
|
- Traefik con label Docker.
|
||||||
|
- Nginx con Let's Encrypt.
|
||||||
|
- Caddy (TLS automatico).
|
||||||
|
|
||||||
|
Il reverse proxy termina HTTPS e inoltra a `docugen-mcp:8000`. `PUBLIC_BASE_URL` nell'env var deve puntare al dominio esterno (`https://mcp.tuavps.com`).
|
||||||
|
|
||||||
|
### Configurazione client MCP
|
||||||
|
|
||||||
|
Claude Code:
|
||||||
|
```bash
|
||||||
|
claude mcp add --transport http docugen-mcp https://mcp.tuavps.com/mcp \
|
||||||
|
--header "Authorization: Bearer <api-key>"
|
||||||
|
```
|
||||||
|
|
||||||
|
Claude Desktop: entry in `claude_desktop_config.json` con URL + header.
|
||||||
|
|
||||||
|
### Backup
|
||||||
|
|
||||||
|
- Cartella `{DATA_DIR}/templates/` → rsync periodico o copia nel git.
|
||||||
|
- `docugen_mcp.db` → dump SQLite regolare.
|
||||||
|
- `{DATA_DIR}/generated/` → effimero, non critico.
|
||||||
|
|
||||||
|
### Osservabilità
|
||||||
|
|
||||||
|
- Log strutturati (JSON) su stdout. Tracciamento via docker logs.
|
||||||
|
- Metriche dai record `generations`: spesa mensile, success rate, latenza media per modello. Query SQL ad-hoc o piccolo endpoint admin in fasi successive.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Stack tecnologico
|
||||||
|
|
||||||
|
| Layer | Tecnologia |
|
||||||
|
|-------|------------|
|
||||||
|
| Runtime | Python 3.11+ |
|
||||||
|
| Package manager | uv |
|
||||||
|
| Validazione | Pydantic v2, Pydantic Settings |
|
||||||
|
| MCP | `mcp` SDK (FastMCP) |
|
||||||
|
| HTTP (sub-app) | FastAPI |
|
||||||
|
| Client HTTP | httpx (async) |
|
||||||
|
| Retry | httpx + wrapper custom (retry exponential backoff) |
|
||||||
|
| DB | SQLite via aiosqlite |
|
||||||
|
| Immagini | Pillow (validazione mime/dimensione) |
|
||||||
|
| Test | pytest, pytest-asyncio, respx |
|
||||||
|
| Deploy | Docker, docker-compose |
|
||||||
|
| Reverse proxy | Nginx/Traefik/Caddy (fuori container) |
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,885 @@
|
|||||||
|
# Sistema Documentale — Design Master
|
||||||
|
|
||||||
|
> Sistema documentale multi-wiki (personale, aziendale, scalabile a N) come
|
||||||
|
> vault Git di markdown, sincronizzato su Gitea (VPS), pubblicato in sola
|
||||||
|
> lettura via Quartz, accessibile da desktop (Obsidian + Claude Code),
|
||||||
|
> Android (Obsidian) e browser mobile, integrato con `mcp-docgen` per
|
||||||
|
> documenti formali e predisposto per pipeline di ingest futura
|
||||||
|
> (STT di riunioni, bot Telegram).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Indice
|
||||||
|
|
||||||
|
1. [Panoramica e scopo](#1-panoramica-e-scopo)
|
||||||
|
2. [Componenti del sistema](#2-componenti-del-sistema)
|
||||||
|
3. [Struttura di un singolo wiki](#3-struttura-di-un-singolo-wiki)
|
||||||
|
4. [Configurazione multi-wiki](#4-configurazione-multi-wiki)
|
||||||
|
5. [Flusso Git end-to-end](#5-flusso-git-end-to-end)
|
||||||
|
6. [Integrazione con `mcp-docgen`](#6-integrazione-con-mcp-docgen)
|
||||||
|
7. [Pubblicazione web — Quartz multi-site](#7-pubblicazione-web--quartz-multi-site)
|
||||||
|
8. [Accesso da Android](#8-accesso-da-android)
|
||||||
|
9. [Storage degli asset binari](#9-storage-degli-asset-binari)
|
||||||
|
10. [Autenticazione e sicurezza](#10-autenticazione-e-sicurezza)
|
||||||
|
11. [Backup e disaster recovery](#11-backup-e-disaster-recovery)
|
||||||
|
12. [Setup iniziale — fasi implementative](#12-setup-iniziale--fasi-implementative)
|
||||||
|
13. [Estensioni future pianificate](#13-estensioni-future-pianificate)
|
||||||
|
14. [Decisioni e rationale](#14-decisioni-e-rationale)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Panoramica e scopo
|
||||||
|
|
||||||
|
### Cosa stiamo costruendo
|
||||||
|
|
||||||
|
Un sistema documentale che:
|
||||||
|
|
||||||
|
- **Archivia** documenti (relazioni, preventivi, note commercialista, idee, verbali riunione, ecc.) in più **wiki separati per dominio** (personale, aziendale, altri futuri) — tutti come file Markdown
|
||||||
|
- **Versiona** ogni wiki in un **repo Git separato** su Gitea self-hosted (VPS)
|
||||||
|
- **Pubblica** ogni wiki in sola lettura su **un sotto-dominio dedicato** (Quartz statico), con auth unificata
|
||||||
|
- **Integra** un generatore di documenti formali (`mcp-docgen`, già progettato) condiviso da tutti i wiki
|
||||||
|
- **Resta editabile** principalmente da desktop (Obsidian + Claude Code) e occasionalmente da Android (Obsidian mobile + Git plugin)
|
||||||
|
- **È predisposto** per estensioni future di ingest automatico: trascrizione riunioni via STT, bot Telegram per note vocali/testuali inviate da mobile
|
||||||
|
|
||||||
|
### Principi cardine
|
||||||
|
|
||||||
|
**Un wiki, un dominio, un repo, un ciclo di vita.** Wiki personale e wiki aziendale sono **fisicamente separati**: repo diversi, vault Obsidian diversi, siti Quartz diversi, asset separati, backup indipendenti. Una compromissione o un errore su uno non tocca l'altro. Un eventuale hand-off di un wiki (es. condivisione di `aziendale` con un collaboratore o commercialista) è possibile senza sforzo.
|
||||||
|
|
||||||
|
**Pattern riusabile.** Tutti i wiki seguono la stessa struttura interna (sezione 3), la stessa convenzione di frontmatter, lo stesso workflow Git. Aggiungere un terzo wiki (`wiki-<nome>`) è una replica meccanica.
|
||||||
|
|
||||||
|
**Infrastruttura condivisa.** Gitea, Quartz, nginx, Authelia, `mcp-docgen` sono servizi orizzontali usati da tutti i wiki. Una sola istanza di ciascuno.
|
||||||
|
|
||||||
|
**Flat-file + Git + Claude Code + Obsidian.** Niente database applicativo nel percorso di produzione del contenuto.
|
||||||
|
|
||||||
|
### Cosa NON fa
|
||||||
|
|
||||||
|
- Non è un DMS con workflow di approvazione / firma digitale integrata
|
||||||
|
- Non sostituisce il gestionale contabile
|
||||||
|
- Non fa OCR né ricerca semantica (eventualmente integrabili in futuro)
|
||||||
|
- Non supporta edit concorrente multi-utente (un operatore, Adriano)
|
||||||
|
- Non espone interfaccia di editing da browser (edit solo da Obsidian/Claude Code)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Componenti del sistema
|
||||||
|
|
||||||
|
### Mappa macro-componenti
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ DESKTOP (questa macchina, Linux Ubuntu) │
|
||||||
|
│ │
|
||||||
|
│ /home/adriano/Documenti/Git_XYZ/Documentale/ │
|
||||||
|
│ │
|
||||||
|
│ ├─ wiki-personale/ ← vault locale (clone git Gitea) │
|
||||||
|
│ ├─ wiki-aziendale/ ← vault locale (clone git Gitea) │
|
||||||
|
│ ├─ wiki-<altro>/ ← futuri wiki, stessa struttura │
|
||||||
|
│ │ │
|
||||||
|
│ ├─ wiki-personale-assets/ ← binari fuori git, rsync → VPS │
|
||||||
|
│ ├─ wiki-aziendale-assets/ ← idem │
|
||||||
|
│ │ │
|
||||||
|
│ └─ mcp-docgen/ ← sorgente MCP server (repo separato) │
|
||||||
|
│ │
|
||||||
|
│ Strumenti: │
|
||||||
|
│ • Obsidian: un vault per ogni wiki (switch-vault funzione nativa) │
|
||||||
|
│ • Claude Code: opera su cartelle del wiki selezionato │
|
||||||
|
│ • Obsidian Git plugin: auto-push per ciascun vault │
|
||||||
|
│ • systemd timer: rsync asset → VPS │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────┘
|
||||||
|
│ ▲ │ ▲ │ ▲
|
||||||
|
│ git │ │ git │ │ rsync │
|
||||||
|
▼ │ ▼ │ ▼ │
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ VPS Hostinger │
|
||||||
|
│ │
|
||||||
|
│ ┌───────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Gitea (un'istanza, più repo) │ │
|
||||||
|
│ │ • adriano/wiki-personale.git + post-receive hook │ │
|
||||||
|
│ │ • adriano/wiki-aziendale.git + post-receive hook │ │
|
||||||
|
│ │ ↓ (hook → quartz build per ciascun wiki) │ │
|
||||||
|
│ └───────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌───────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Quartz build pipeline (una config per wiki) │ │
|
||||||
|
│ │ /opt/quartz-personale/ → build → /var/www/personale/ │ │
|
||||||
|
│ │ /opt/quartz-aziendale/ → build → /var/www/aziendale/ │ │
|
||||||
|
│ └───────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────┐ ┌─────────────────────┐ │
|
||||||
|
│ │ /var/www/assets/ │ │ mcp-docgen (Docker) │ │
|
||||||
|
│ │ ├─ personale/ │ │ condiviso tra wiki │ │
|
||||||
|
│ │ └─ aziendale/ │ │ FastMCP HTTP │ │
|
||||||
|
│ └─────────────────────┘ │ OpenRouter LLM │ │
|
||||||
|
│ └─────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌───────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ nginx + Authelia (SSO unico) │ │
|
||||||
|
│ │ personale.<dominio> → /var/www/personale/ │ │
|
||||||
|
│ │ aziendale.<dominio> → /var/www/aziendale/ │ │
|
||||||
|
│ │ <w>.<dominio>/assets/ → /var/www/assets/<w>/ │ │
|
||||||
|
│ │ gitea.<dominio> → Gitea web UI │ │
|
||||||
|
│ │ docgen.<dominio> → mcp-docgen (solo API key, no SSO) │ │
|
||||||
|
│ └───────────────────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────┘
|
||||||
|
▲
|
||||||
|
│ HTTPS + SSO (TOTP 2FA)
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ MOBILE Android │
|
||||||
|
│ • Obsidian app + Git plugin → un vault per ciascun wiki │
|
||||||
|
│ • Chrome/Firefox → consultazione siti sotto-dominio │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ruoli sintetici
|
||||||
|
|
||||||
|
| Componente | Dove vive | Ruolo | Molteplicità | Stato |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `wiki-<nome>/` | Desktop + Gitea VPS | Vault markdown versionato | **Per wiki** | Da creare |
|
||||||
|
| `wiki-<nome>-assets/` | Desktop + `/var/www/assets/<nome>/` | Binari | **Per wiki** | Da creare |
|
||||||
|
| `mcp-docgen` | VPS Docker | Generazione documenti formali | **Condiviso** | Design pronto |
|
||||||
|
| Gitea | VPS | Git remote | Unica istanza, più repo | Da installare |
|
||||||
|
| Quartz | VPS | SSG per wiki | **Un'istanza per wiki** (config + output separati) | Da configurare |
|
||||||
|
| nginx | VPS | Reverse proxy | Unico | Da configurare |
|
||||||
|
| Authelia | VPS | SSO 2FA | Unico | Da configurare |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Struttura di un singolo wiki
|
||||||
|
|
||||||
|
Tutti i wiki (personale, aziendale, futuri) seguono **identica struttura interna**. Schema isomorfico a `Cerbero_Brain` (raw immutabile + wiki LLM-managed), esteso con `documents/` per output prodotti e `areas/` come MOC tematiche.
|
||||||
|
|
||||||
|
```
|
||||||
|
wiki-<nome>/
|
||||||
|
├── CLAUDE.md # schema e regole del vault
|
||||||
|
├── README.md # intro human-readable
|
||||||
|
├── index.md # catalogo content-oriented
|
||||||
|
├── log.md # timeline append-only
|
||||||
|
│
|
||||||
|
├── raw/ # IMMUTABILE
|
||||||
|
│ ├── comunicazioni_in/ # mail ricevute, PEC, lettere
|
||||||
|
│ ├── documenti_ricevuti/ # markdown ricevuti, allegati testuali
|
||||||
|
│ ├── notes/ # appunti grezzi, voice-to-text (manual)
|
||||||
|
│ └── riunioni/ # trascrizioni riunioni grezze (STT futuro)
|
||||||
|
│
|
||||||
|
└── wiki/ # LLM-managed
|
||||||
|
├── documents/ # documenti prodotti da Adriano
|
||||||
|
│ ├── preventivi/ # (prevalente in wiki-aziendale)
|
||||||
|
│ ├── relazioni/
|
||||||
|
│ ├── verbali_riunione/ # note sintetiche strutturate da raw/riunioni/
|
||||||
|
│ └── comunicazioni_out/
|
||||||
|
│
|
||||||
|
├── entities/ # clienti, fornitori, persone, studi
|
||||||
|
│
|
||||||
|
├── concepts/ # procedure, template, framework
|
||||||
|
│
|
||||||
|
├── areas/ # MOC tematici
|
||||||
|
│
|
||||||
|
└── syntheses/ # analisi trasversali
|
||||||
|
```
|
||||||
|
|
||||||
|
### Contenuto tipico per wiki
|
||||||
|
|
||||||
|
| Wiki | Contenuto prevalente |
|
||||||
|
|---|---|
|
||||||
|
| **wiki-personale** | Idee, note personali, journal, documentazione privata (salute, famiglia, viaggi), inventari personali, consapevolezza finanziaria personale (distinta da Milito/Cerbero) |
|
||||||
|
| **wiki-aziendale** | Clienti, preventivi, relazioni, fatturazione, commercialista, fornitori, procedure aziendali, contratti, verbali riunione con clienti |
|
||||||
|
|
||||||
|
### Convenzioni comuni
|
||||||
|
|
||||||
|
Frontmatter, naming, wikilink, log.md append-only, index content-oriented: **identici** per tutti i wiki. Vedi esempi completi in §5 e §6.
|
||||||
|
|
||||||
|
Ogni wiki ha il **proprio `CLAUDE.md`** locale al suo root, che specifica:
|
||||||
|
- Identità del wiki (es. "wiki personale di Adriano")
|
||||||
|
- Dominio applicativo (cosa ci va, cosa non ci va)
|
||||||
|
- Eventuali regole peculiari (es. wiki-aziendale richiede che ogni `documents/preventivi/` menzioni un `entities/cliente`)
|
||||||
|
|
||||||
|
Claude Code, quando opera dentro un wiki, carica il suo `CLAUDE.md` contestuale (comportamento nativo).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Configurazione multi-wiki
|
||||||
|
|
||||||
|
### Registro dei wiki
|
||||||
|
|
||||||
|
File di riferimento: `/home/adriano/Documenti/Git_XYZ/Documentale/wikis.yaml`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
wikis:
|
||||||
|
- name: personale
|
||||||
|
local_path: wiki-personale
|
||||||
|
assets_path: wiki-personale-assets
|
||||||
|
remote: git@gitea.<dominio>:adriano/wiki-personale.git
|
||||||
|
publish_domain: personale.<dominio>
|
||||||
|
quartz_source: /opt/quartz-personale
|
||||||
|
quartz_output: /var/www/personale
|
||||||
|
vps_assets_path: /var/www/assets/personale
|
||||||
|
|
||||||
|
- name: aziendale
|
||||||
|
local_path: wiki-aziendale
|
||||||
|
assets_path: wiki-aziendale-assets
|
||||||
|
remote: git@gitea.<dominio>:adriano/wiki-aziendale.git
|
||||||
|
publish_domain: aziendale.<dominio>
|
||||||
|
quartz_source: /opt/quartz-aziendale
|
||||||
|
quartz_output: /var/www/aziendale
|
||||||
|
vps_assets_path: /var/www/assets/aziendale
|
||||||
|
```
|
||||||
|
|
||||||
|
Questo file è la **single source of truth** per scripting (sync, backup, provisioning nuovo wiki).
|
||||||
|
|
||||||
|
### Aggiungere un terzo wiki
|
||||||
|
|
||||||
|
Procedura meccanica:
|
||||||
|
|
||||||
|
1. Aggiungi voce a `wikis.yaml`
|
||||||
|
2. `gitea` web UI → crea repo privato `adriano/wiki-<nuovo>`
|
||||||
|
3. Script di provisioning (da scrivere in Fase 2): `./tools/new-wiki.sh <nome>` che:
|
||||||
|
- Crea struttura cartelle locale
|
||||||
|
- Inizializza git, primo commit, push
|
||||||
|
- Installa plugin Obsidian Git nel nuovo vault
|
||||||
|
- Crea config Quartz per il nuovo sotto-dominio
|
||||||
|
- Aggiunge virtual host nginx + cert Let's Encrypt
|
||||||
|
- Registra hook post-receive Gitea
|
||||||
|
|
||||||
|
### Scripting condiviso
|
||||||
|
|
||||||
|
Gli script operativi (backup, sync asset, provisioning) iterano su `wikis.yaml`, zero duplicazione.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Flusso Git end-to-end
|
||||||
|
|
||||||
|
### Remote e branch
|
||||||
|
|
||||||
|
- **Gitea self-hosted**: un server, più repo (uno per wiki)
|
||||||
|
- **Branch unico `main`** per repo (no PR, uso personale)
|
||||||
|
- **Commit strategy**: piccoli e frequenti, messaggi descrittivi (`doc(acme): preventivo v1`, `ingest: PEC 2026-04-15`, `refactor: split concepts/procedura-x`)
|
||||||
|
|
||||||
|
### Flusso desktop → Gitea (per ciascun wiki)
|
||||||
|
|
||||||
|
1. Edit markdown (Obsidian o Claude Code) nella cartella del wiki
|
||||||
|
2. **Obsidian Git plugin** in quel vault (configurato per-wiki):
|
||||||
|
- Auto-commit + push ogni 10 min
|
||||||
|
- Messaggio commit: `auto: {{date}}`
|
||||||
|
3. Gitea riceve push sul repo del wiki
|
||||||
|
4. Hook `post-receive` triggera **`quartz build` specifico di quel wiki** → `/var/www/<wiki>/`
|
||||||
|
5. nginx serve la nuova versione di `<wiki>.<dominio>`
|
||||||
|
|
||||||
|
### Flusso mobile Android → Gitea
|
||||||
|
|
||||||
|
Per ciascun vault (un vault per wiki) su Obsidian mobile:
|
||||||
|
|
||||||
|
1. Apri vault → pull manuale (bottone Git plugin)
|
||||||
|
2. Edita
|
||||||
|
3. Commit & Push manuale
|
||||||
|
|
||||||
|
### Fallback systemd timer (desktop)
|
||||||
|
|
||||||
|
Sotto `~/.config/systemd/user/`, un timer + service generici parametrizzati per-wiki:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# vault-sync@.service (template unit)
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
WorkingDirectory=/home/adriano/Documenti/Git_XYZ/Documentale/%i
|
||||||
|
ExecStart=/usr/bin/git add -A
|
||||||
|
ExecStart=/bin/sh -c 'git diff --cached --quiet || git commit -m "auto: $(date -Iseconds)"'
|
||||||
|
ExecStart=/usr/bin/git pull --rebase --autostash
|
||||||
|
ExecStart=/usr/bin/git push
|
||||||
|
```
|
||||||
|
|
||||||
|
Abilita per wiki: `systemctl --user enable vault-sync@wiki-personale.timer`, `vault-sync@wiki-aziendale.timer`.
|
||||||
|
|
||||||
|
### Asset sync (per wiki)
|
||||||
|
|
||||||
|
Un timer rsync per wiki:
|
||||||
|
```bash
|
||||||
|
rsync -az --delete \
|
||||||
|
/home/adriano/Documenti/Git_XYZ/Documentale/wiki-<nome>-assets/ \
|
||||||
|
adriano@vps:/var/www/assets/<nome>/
|
||||||
|
```
|
||||||
|
|
||||||
|
Script unico che itera `wikis.yaml` e lancia rsync per ciascuno.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Integrazione con `mcp-docgen`
|
||||||
|
|
||||||
|
`mcp-docgen` è **condiviso** tra tutti i wiki. Il template selezionato determina il formato; la destinazione di archiviazione la scegli (o la suggerisce Claude Code) al momento della generazione.
|
||||||
|
|
||||||
|
### Tool MCP
|
||||||
|
|
||||||
|
| Tool | Uso |
|
||||||
|
|---|---|
|
||||||
|
| `template_create/update/delete/list/get` | Gestione template |
|
||||||
|
| `document_generate` | Genera markdown formale |
|
||||||
|
|
||||||
|
### Configurazione Claude Code
|
||||||
|
|
||||||
|
`~/.claude/.mcp.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"docgen": {
|
||||||
|
"type": "http",
|
||||||
|
"url": "https://docgen.<dominio>/mcp",
|
||||||
|
"headers": { "Authorization": "Bearer <API_KEY>" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Template iniziali (condivisi, taggati per wiki di provenienza tipica)
|
||||||
|
|
||||||
|
| Template | Wiki tipico | Uso |
|
||||||
|
|---|---|---|
|
||||||
|
| `preventivo-commerciale` | aziendale | Preventivi clienti |
|
||||||
|
| `relazione-audit` | aziendale | Relazioni tecniche |
|
||||||
|
| `lettera-formale` | aziendale o personale | Comunicazioni ufficiali |
|
||||||
|
| `nota-commercialista` | aziendale | Comunicazioni al commercialista |
|
||||||
|
| `verbale-riunione` | aziendale | Verbali post-riunione (anche da STT futuro) |
|
||||||
|
| `journal-entry` | personale | Entry diario personale strutturato |
|
||||||
|
|
||||||
|
I template hanno `tags: [wiki:aziendale]` o `[wiki:personale]` nel frontmatter per filtrarli in `template_list`, ma nulla impedisce di usarli cross-wiki se serve.
|
||||||
|
|
||||||
|
### Flusso "draft → formal → archivio in wiki corretto"
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Adriano: "Prepara preventivo per Acme in wiki-aziendale"
|
||||||
|
|
||||||
|
2. Claude Code (contesto wiki-aziendale):
|
||||||
|
a) Legge wiki-aziendale/wiki/entities/Acme_SRL.md
|
||||||
|
b) Legge wiki-aziendale/wiki/concepts/Template_Preventivo.md
|
||||||
|
c) Prepara bozza grezza
|
||||||
|
d) Invoca mcp__docgen__document_generate:
|
||||||
|
template = "preventivo-commerciale"
|
||||||
|
content = bozza
|
||||||
|
variables = {cliente: "Acme SRL", piva: "...", ...}
|
||||||
|
e) Salva output in wiki-aziendale/wiki/documents/preventivi/YYYY-MM-DD_Acme_v1.md
|
||||||
|
f) Aggiorna wiki-aziendale/wiki/entities/Acme_SRL.md (storico preventivi)
|
||||||
|
g) Aggiorna wiki-aziendale/index.md e log.md
|
||||||
|
|
||||||
|
3. Adriano rivede in Obsidian (vault aziendale)
|
||||||
|
|
||||||
|
4. Finale → export PDF in wiki-aziendale-assets/clienti/Acme_SRL/
|
||||||
|
→ aggiorna status: sent
|
||||||
|
→ rsync asset → VPS
|
||||||
|
→ Git auto-push → Quartz rebuild → visibile su aziendale.<dominio>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Quando NON usare `mcp-docgen`
|
||||||
|
|
||||||
|
- Note grezze, appunti, idee → markdown diretto
|
||||||
|
- Ingest di materiale ricevuto (va in `raw/` as-is)
|
||||||
|
- Template operativi interni (scritti manualmente in `concepts/`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Pubblicazione web — Quartz multi-site
|
||||||
|
|
||||||
|
### Approccio
|
||||||
|
|
||||||
|
**Un'istanza Quartz per wiki.** Stessa versione del codice sorgente Quartz, configurazione separata, output separato, sotto-dominio separato.
|
||||||
|
|
||||||
|
### Layout VPS
|
||||||
|
|
||||||
|
```
|
||||||
|
/opt/quartz-personale/
|
||||||
|
├── quartz.config.ts # title: "Wiki Personale", baseUrl: "personale.<dominio>"
|
||||||
|
├── content/ # git checkout di wiki-personale (pull nel hook)
|
||||||
|
└── ...
|
||||||
|
|
||||||
|
/opt/quartz-aziendale/
|
||||||
|
├── quartz.config.ts # title: "Wiki Aziendale", baseUrl: "aziendale.<dominio>"
|
||||||
|
├── content/
|
||||||
|
└── ...
|
||||||
|
|
||||||
|
/var/www/personale/ # output Quartz personale
|
||||||
|
/var/www/aziendale/ # output Quartz aziendale
|
||||||
|
/var/www/assets/
|
||||||
|
├── personale/
|
||||||
|
└── aziendale/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hook `post-receive` (per repo)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# hooks/post-receive di wiki-<nome>.git
|
||||||
|
set -e
|
||||||
|
WIKI_NAME=<nome>
|
||||||
|
WORKDIR=/opt/quartz-$WIKI_NAME
|
||||||
|
|
||||||
|
cd $WORKDIR/content
|
||||||
|
git pull --rebase
|
||||||
|
|
||||||
|
cd $WORKDIR
|
||||||
|
npx quartz build --output /var/www/$WIKI_NAME/
|
||||||
|
|
||||||
|
nginx -t && systemctl reload nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
### nginx per wiki
|
||||||
|
|
||||||
|
Un virtual host per sotto-dominio:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
server_name personale.<dominio>;
|
||||||
|
|
||||||
|
include /etc/nginx/authelia-location.conf;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
include /etc/nginx/authelia-authrequest.conf;
|
||||||
|
root /var/www/personale;
|
||||||
|
try_files $uri $uri/ $uri.html =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /assets/ {
|
||||||
|
include /etc/nginx/authelia-authrequest.conf;
|
||||||
|
alias /var/www/assets/personale/;
|
||||||
|
}
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/<dominio>/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/<dominio>/privkey.pem;
|
||||||
|
}
|
||||||
|
# Stesso identico schema per aziendale.<dominio>, sostituendo "personale" con "aziendale"
|
||||||
|
```
|
||||||
|
|
||||||
|
### CSS print condiviso
|
||||||
|
|
||||||
|
Custom media-query `@media print` in un file `print.scss` copiato in entrambe le config Quartz, o mantenuto in un submodule Git condiviso.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Accesso da Android
|
||||||
|
|
||||||
|
Consultazione (90%) via browser sui sotto-domini. Edit occasionale via Obsidian mobile con **un vault separato per wiki**.
|
||||||
|
|
||||||
|
### Obsidian mobile multi-vault
|
||||||
|
|
||||||
|
Obsidian mobile supporta N vault. Ciascun vault è una cartella separata (che clona il repo Gitea del wiki corrispondente).
|
||||||
|
|
||||||
|
Setup per wiki:
|
||||||
|
1. Aggiungi vault in Obsidian Android (crea cartella locale, es. `/storage/emulated/0/Obsidian/wiki-personale/`)
|
||||||
|
2. Installa community plugin "Obsidian Git"
|
||||||
|
3. Clone via plugin: URL `https://gitea.<dominio>/adriano/wiki-personale.git` + **token personale Gitea** (scope: repo read/write)
|
||||||
|
4. Configurazione plugin:
|
||||||
|
- Auto-pull on load: on
|
||||||
|
- Auto-push: off
|
||||||
|
- Commit message: `mobile: {{date}}`
|
||||||
|
|
||||||
|
Ripeti per `wiki-aziendale`.
|
||||||
|
|
||||||
|
### Switch vault
|
||||||
|
|
||||||
|
Dalla home di Obsidian Android, icona a destra → lista vault → tap per switchare. Comodo e veloce.
|
||||||
|
|
||||||
|
### Limiti
|
||||||
|
|
||||||
|
- Android non supporta sync in background → azione manuale pull/commit/push
|
||||||
|
- Asset binari: via browser sotto-dominio, non Obsidian
|
||||||
|
- Evitare edit contemporaneo mobile vs desktop (pull sempre prima di editare)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Storage degli asset binari
|
||||||
|
|
||||||
|
### Struttura per wiki (identica)
|
||||||
|
|
||||||
|
```
|
||||||
|
wiki-<nome>-assets/
|
||||||
|
├── clienti/ # (prevalente in aziendale)
|
||||||
|
├── commercialista/ # (prevalente in aziendale)
|
||||||
|
├── personale/ # (prevalente in personale)
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sync
|
||||||
|
|
||||||
|
Systemd timer desktop, uno script, itera `wikis.yaml`:
|
||||||
|
```bash
|
||||||
|
for wiki in $(yq e '.wikis[].name' wikis.yaml); do
|
||||||
|
src="$HOME/Documenti/Git_XYZ/Documentale/wiki-$wiki-assets/"
|
||||||
|
dst="adriano@vps:/var/www/assets/$wiki/"
|
||||||
|
rsync -az --delete "$src" "$dst"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
### Referenziazione dal markdown
|
||||||
|
|
||||||
|
Link assoluto al sotto-dominio del wiki:
|
||||||
|
```markdown
|
||||||
|
PDF firmato: [preventivo v1](https://aziendale.<dominio>/assets/clienti/Acme_SRL/2026-04-22_preventivo_v1_firmato.pdf)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Autenticazione e sicurezza
|
||||||
|
|
||||||
|
### Authelia SSO condiviso
|
||||||
|
|
||||||
|
**Una sola istanza Authelia** protegge tutti i sotto-domini dei wiki e gli asset:
|
||||||
|
|
||||||
|
- `personale.<dominio>`, `aziendale.<dominio>`, `<nuovo>.<dominio>`
|
||||||
|
- Session cookie valido cross-subdomain (`Domain=<dominio>`)
|
||||||
|
- Utente unico `adriano` con TOTP 2FA (Aegis/Google Authenticator)
|
||||||
|
- Una login per aprire qualsiasi wiki
|
||||||
|
|
||||||
|
### Eccezioni
|
||||||
|
|
||||||
|
- `gitea.<dominio>` → login Gitea separato (con 2FA)
|
||||||
|
- `docgen.<dominio>` → solo API key header, non per browser umano
|
||||||
|
|
||||||
|
### ACL per wiki
|
||||||
|
|
||||||
|
Se in futuro vuoi dare accesso a `aziendale.<dominio>` a un collaboratore senza dare accesso a `personale.<dominio>`:
|
||||||
|
|
||||||
|
Authelia configurable ACL:
|
||||||
|
```yaml
|
||||||
|
access_control:
|
||||||
|
rules:
|
||||||
|
- domain: personale.<dominio>
|
||||||
|
policy: two_factor
|
||||||
|
subject: "user:adriano"
|
||||||
|
- domain: aziendale.<dominio>
|
||||||
|
policy: two_factor
|
||||||
|
subject: ["user:adriano", "user:collaboratore"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Nessun lavoro extra nell'architettura — feature already-there.
|
||||||
|
|
||||||
|
### TLS
|
||||||
|
|
||||||
|
Let's Encrypt con cert SAN per tutti i sotto-domini, rinnovo automatico `certbot` + `systemd timer`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Backup e disaster recovery
|
||||||
|
|
||||||
|
### Cosa salvare (per wiki)
|
||||||
|
|
||||||
|
| Asset | Criticità | Strategia |
|
||||||
|
|---|---|---|
|
||||||
|
| Repo Git Gitea (per wiki) | **Alta** | Clone desktop + Gitea VPS + snapshot settimanale |
|
||||||
|
| Asset binari (per wiki) | **Alta** (legale in aziendale) | Desktop + VPS + backup offsite weekly |
|
||||||
|
| `mcp-docgen` template + SQLite | Media | Snapshot volume Docker |
|
||||||
|
| Config VPS (nginx, Authelia, Quartz, Gitea) | Media | Repo `ops-vps` versionato |
|
||||||
|
|
||||||
|
### Strategia 3-2-1 (per wiki)
|
||||||
|
|
||||||
|
Ogni wiki ha la sua catena 3-2-1 indipendente. Script che itera `wikis.yaml` su VPS:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# systemd timer offsite-backup.timer, weekly
|
||||||
|
for wiki in $(yq e '.wikis[].name' /etc/documentale/wikis.yaml); do
|
||||||
|
rsync -az --delete \
|
||||||
|
/var/www/$wiki/ /var/www/assets/$wiki/ \
|
||||||
|
/var/opt/gitea/repositories/adriano/wiki-$wiki.git/ \
|
||||||
|
offsite:backups/documentale/$wiki/$(date +%Y-W%V)/
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
### Retention differenziabile per wiki
|
||||||
|
|
||||||
|
Potenzialmente (configurabile in `wikis.yaml`):
|
||||||
|
- `aziendale`: retention 10+ anni (documentazione legale)
|
||||||
|
- `personale`: retention 3 anni rotating (ok da revisione)
|
||||||
|
|
||||||
|
Implementabile con policy distinte per destinazione offsite (non critico al MVP).
|
||||||
|
|
||||||
|
### DR procedure
|
||||||
|
|
||||||
|
Documentata per ciascun wiki in `wiki-<nome>/wiki/concepts/DR_Procedure.md`. Procedura standard:
|
||||||
|
1. Repo perduto → clone da Gitea, riparti
|
||||||
|
2. Gitea VPS perduta → restore volume da offsite, repush da desktop
|
||||||
|
3. Tutto perduto → restore da offsite (perdita max 1 settimana)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Setup iniziale — fasi implementative
|
||||||
|
|
||||||
|
**Strategia:** porta **un solo wiki** fino in fondo (pipeline completa funzionante) prima di clonare la configurazione per il secondo. Così validi il modello end-to-end, poi replichi.
|
||||||
|
|
||||||
|
**Scelta del wiki pilota:** raccomando `wiki-aziendale` perché:
|
||||||
|
- Ha esigenze più concrete e immediate (preventivi, commercialista)
|
||||||
|
- Forza a testare `mcp-docgen` (template formali)
|
||||||
|
- Il pattern è più impegnativo, quindi se funziona lì il personale è banale
|
||||||
|
|
||||||
|
### Fase 1 — `wiki-aziendale` locale funzionante
|
||||||
|
|
||||||
|
**Deliverable:** vault locale usabile, Claude Code e Obsidian felici, commit git a repo locale.
|
||||||
|
|
||||||
|
1. Crea `Documentale/wikis.yaml` con solo `aziendale`
|
||||||
|
2. Crea struttura `wiki-aziendale/` (raw/ + wiki/{documents,entities,concepts,areas,syntheses})
|
||||||
|
3. Scrivi `wiki-aziendale/CLAUDE.md` (regole wiki aziendale)
|
||||||
|
4. `README.md`, `index.md`, `log.md` iniziali
|
||||||
|
5. `git init` + primo commit
|
||||||
|
6. Crea `wiki-aziendale-assets/`
|
||||||
|
7. Apri in Obsidian come vault, verifica rendering
|
||||||
|
8. Documento di prova (cliente fittizio + preventivo prova)
|
||||||
|
|
||||||
|
### Fase 2 — Gitea su VPS + sync wiki-aziendale
|
||||||
|
|
||||||
|
**Deliverable:** vault sincronizzato desktop ↔ Gitea.
|
||||||
|
|
||||||
|
1. Docker Gitea su VPS, `gitea.<dominio>` (TLS)
|
||||||
|
2. Crea repo `adriano/wiki-aziendale` (privato)
|
||||||
|
3. SSH key desktop → Gitea, token mobile Gitea
|
||||||
|
4. `git remote add` dal desktop, primo push
|
||||||
|
5. Plugin Obsidian Git → configura auto-push 10 min
|
||||||
|
6. Test round-trip
|
||||||
|
7. Systemd timer fallback
|
||||||
|
|
||||||
|
### Fase 3 — `mcp-docgen` su VPS + integrazione Claude Code
|
||||||
|
|
||||||
|
**Deliverable:** workflow "bozza → formale → archivio" in wiki-aziendale.
|
||||||
|
|
||||||
|
1. Implementa `mcp-docgen` (vedi suo design doc in `mcp-docgen/`)
|
||||||
|
2. Deploy Docker su `docgen.<dominio>`
|
||||||
|
3. Aggiungi a `~/.claude/.mcp.json`
|
||||||
|
4. Crea template: `preventivo-commerciale`, `relazione-audit`, `lettera-formale`, `nota-commercialista`, `verbale-riunione`
|
||||||
|
5. Test end-to-end dal wiki-aziendale
|
||||||
|
|
||||||
|
### Fase 4 — Quartz + nginx + TLS per wiki-aziendale
|
||||||
|
|
||||||
|
**Deliverable:** `aziendale.<dominio>` consultabile via HTTPS (senza auth stringente ancora, o basic auth temporanea).
|
||||||
|
|
||||||
|
1. Node su VPS, clone Quartz in `/opt/quartz-aziendale/`
|
||||||
|
2. Config Quartz (title, baseUrl)
|
||||||
|
3. CSS print
|
||||||
|
4. Hook `post-receive` in `wiki-aziendale.git` → `quartz build`
|
||||||
|
5. nginx virtual host + cert Let's Encrypt
|
||||||
|
6. Test da browser desktop + mobile
|
||||||
|
|
||||||
|
### Fase 5 — Authelia SSO + protezione
|
||||||
|
|
||||||
|
**Deliverable:** `aziendale.<dominio>` privato, login con TOTP.
|
||||||
|
|
||||||
|
1. Docker Authelia su VPS
|
||||||
|
2. Utente `adriano` con password + TOTP
|
||||||
|
3. nginx `auth_request` per dominio wiki e `/assets/`
|
||||||
|
4. Verifica login desktop + mobile
|
||||||
|
|
||||||
|
### Fase 6 — Asset sync + backup offsite (wiki-aziendale)
|
||||||
|
|
||||||
|
**Deliverable:** asset sync, backup 3-2-1 attivo.
|
||||||
|
|
||||||
|
1. Systemd timer rsync asset → VPS
|
||||||
|
2. nginx serve `/var/www/assets/aziendale/` (con auth)
|
||||||
|
3. Test link dai markdown
|
||||||
|
4. Backup offsite weekly (Hetzner Storagebox / B2)
|
||||||
|
|
||||||
|
### Fase 7 — Obsidian Android per wiki-aziendale
|
||||||
|
|
||||||
|
**Deliverable:** edit mobile funzionante.
|
||||||
|
|
||||||
|
1. Obsidian app + plugin Obsidian Git
|
||||||
|
2. Clone repo via plugin, token Gitea
|
||||||
|
3. Test edit mobile → push → verifica desktop
|
||||||
|
|
||||||
|
### Fase 8 — Replica per `wiki-personale`
|
||||||
|
|
||||||
|
**Deliverable:** secondo wiki operativo end-to-end.
|
||||||
|
|
||||||
|
Usando script `tools/new-wiki.sh` (scritto in Fase 2 o qui):
|
||||||
|
1. Aggiungi `personale` a `wikis.yaml`
|
||||||
|
2. Crea struttura locale
|
||||||
|
3. Crea repo Gitea
|
||||||
|
4. Provisioning Quartz + nginx + cert (wildcard o nuova SAN)
|
||||||
|
5. Clone vault Obsidian desktop + mobile
|
||||||
|
6. Testa ciclo completo
|
||||||
|
|
||||||
|
Replica banale — se Fasi 1-7 hanno validato il modello.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Estensioni future pianificate
|
||||||
|
|
||||||
|
Funzionalità **non** implementate in Fasi 1-8 ma già predisposte nell'architettura. Vengono sviluppate in fasi successive con i propri design + implementation doc.
|
||||||
|
|
||||||
|
### 13.1 Pipeline STT — trascrizione riunioni
|
||||||
|
|
||||||
|
**Scenario:** hai una riunione con cliente Acme, registri audio (Zoom, dittafono, registratore telefono). Vuoi che l'audio diventi automaticamente una bozza di verbale nel `wiki-aziendale`, pronta per revisione.
|
||||||
|
|
||||||
|
**Architettura prevista:**
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐ ┌─────────────────┐ ┌──────────────────┐
|
||||||
|
│ Registrazione │ │ Upload a VPS │ │ STT service VPS │
|
||||||
|
│ (mobile, │─────▶│ (curl / web │─────▶│ (Whisper.cpp │
|
||||||
|
│ registratore, │ │ form / SFTP) │ │ faster-whisper │
|
||||||
|
│ Zoom export) │ │ │ │ Docker) │
|
||||||
|
└─────────────────┘ └─────────────────┘ └────────┬─────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────┐
|
||||||
|
│ Inbox Service (nuovo microservizio VPS)│
|
||||||
|
│ │
|
||||||
|
│ ├─ Riceve trascrizione grezza │
|
||||||
|
│ ├─ Normalizza a markdown + frontmatter │
|
||||||
|
│ │ (source: stt, target_wiki: ?) │
|
||||||
|
│ ├─ Salva in /var/lib/inbox/drafts/ │
|
||||||
|
│ └─ Espone API: │
|
||||||
|
│ GET /drafts │
|
||||||
|
│ GET /drafts/{id} │
|
||||||
|
│ DELETE /drafts/{id} (consume) │
|
||||||
|
└────────────────┬────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌────────────────────────────────────────┐
|
||||||
|
│ Claude Code desktop │
|
||||||
|
│ (comando /ingest-inbox o cron locale) │
|
||||||
|
│ │
|
||||||
|
│ ├─ GET /drafts │
|
||||||
|
│ ├─ Per ciascun draft: │
|
||||||
|
│ │ • Identifica target wiki │
|
||||||
|
│ │ (euristica + conferma Adriano) │
|
||||||
|
│ │ • Eventuale mcp-docgen format │
|
||||||
|
│ │ con template verbale-riunione │
|
||||||
|
│ │ • Commit in wiki/ │
|
||||||
|
│ │ documents/verbali_riunione/ │
|
||||||
|
│ │ • DELETE /drafts/{id} │
|
||||||
|
│ └─ Push git │
|
||||||
|
└────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Componenti nuovi da creare:**
|
||||||
|
|
||||||
|
- **`stt-service`**: container Docker con `faster-whisper` o `whisper.cpp`, API FastAPI minimale (`POST /transcribe` con audio → text). Modello `large-v3` per italiano.
|
||||||
|
- **`inbox-service`**: microservizio FastAPI che:
|
||||||
|
- Riceve audio (multipart) o testo
|
||||||
|
- Orchestra STT se audio
|
||||||
|
- Salva draft markdown in `/var/lib/inbox/drafts/<uuid>.md`
|
||||||
|
- Espone API REST protetta da API key
|
||||||
|
- **Claude Code skill `/ingest-inbox`**: comando che pollicia l'inbox, ingeriste nel wiki corretto con conferma utente
|
||||||
|
|
||||||
|
**Predisposizione nell'architettura corrente:**
|
||||||
|
|
||||||
|
- La cartella `raw/riunioni/` è **già prevista** nella struttura wiki (§3)
|
||||||
|
- Il template `verbale-riunione` è **già previsto** tra i template iniziali `mcp-docgen` (§6)
|
||||||
|
- nginx può ospitare `inbox.<dominio>` a un sottodomino aggiuntivo (Authelia o API key)
|
||||||
|
- Nessuna ristrutturazione necessaria quando arriva — è un'aggiunta pulita
|
||||||
|
|
||||||
|
**Fase stimata:** Fase 9 dopo completamento di Fase 8 e uso reale per qualche settimana.
|
||||||
|
|
||||||
|
### 13.2 Bot Telegram — ingest da mobile
|
||||||
|
|
||||||
|
**Scenario:** sei in giro, hai un'idea o vuoi registrare una nota vocale. Mandi messaggio (audio o testo) al tuo bot Telegram → finisce nel wiki corretto.
|
||||||
|
|
||||||
|
**Architettura prevista:**
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────┐ ┌─────────────────┐ ┌───────────────┐
|
||||||
|
│ Telegram │────────▶│ Bot Telegram │──────▶│ Inbox Service │
|
||||||
|
│ (tu, mobile│ message │ (python-tele- │ POST │ (stessa API │
|
||||||
|
│ vocale o │ │ gram-bot su │ │ di §13.1) │
|
||||||
|
│ testo) │ │ VPS Docker) │ └───────────────┘
|
||||||
|
└─────────────┘ └─────────────────┘ │
|
||||||
|
▼
|
||||||
|
(stessa pipeline
|
||||||
|
STT se audio,
|
||||||
|
poi drafts/,
|
||||||
|
poi Claude Code
|
||||||
|
desktop ingerisce)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Logica minima del bot:**
|
||||||
|
|
||||||
|
- Riceve messaggio da chat autorizzata (whitelist per telegram ID)
|
||||||
|
- Se audio → forward a `stt-service`, poi `inbox-service`
|
||||||
|
- Se testo → forward diretto a `inbox-service` come markdown con `source: telegram`
|
||||||
|
- Risponde a Adriano: "Draft creato: ID xxx, target wiki non specificato"
|
||||||
|
- Opzionale: comandi slash tipo `/wiki aziendale` prima del messaggio per hint di targeting
|
||||||
|
|
||||||
|
**Predisposizione nell'architettura corrente:**
|
||||||
|
|
||||||
|
- È già disponibile il server MCP `cerbero-telegram` (vedi `~/.claude/.mcp.json` nel progetto Cerbero). Può essere riutilizzato o duplicato per questo uso, o si scrive un bot dedicato
|
||||||
|
- `inbox-service` previsto in §13.1 è la colonna portante di entrambe le integrazioni
|
||||||
|
- Nessuna ristrutturazione necessaria
|
||||||
|
|
||||||
|
**Fase stimata:** Fase 10 dopo §13.1.
|
||||||
|
|
||||||
|
### 13.3 Principio comune
|
||||||
|
|
||||||
|
Entrambe le estensioni seguono il pattern **"sorgente esterna → inbox VPS → Claude Code desktop ingerisce"**. Questo mantiene:
|
||||||
|
|
||||||
|
- **Single point of ingest** — `inbox-service` è l'unico che parla col VPS per contenuti nuovi
|
||||||
|
- **Claude Code resta autorevole** — decide target wiki, applica format, scrive il commit, mantiene coerenza
|
||||||
|
- **Evita commit diretti al wiki da VPS** — la VPS non ha mai bisogno di scrivere nei repo (solo leggerli per Quartz build e Gitea)
|
||||||
|
|
||||||
|
### 13.4 Altre estensioni meno urgenti
|
||||||
|
|
||||||
|
Tracciato qui per non dimenticarle:
|
||||||
|
|
||||||
|
- **OCR automatico** di scansioni ricevute (Tesseract + preprocessing, spinge output in inbox)
|
||||||
|
- **Ricerca semantica** del vault (integrazione con `claude-mem` o sistema embedding dedicato)
|
||||||
|
- **Auto-export PDF** per ogni `wiki/documents/` con status `final` (tramite pandoc + template LaTeX/typst)
|
||||||
|
- **Firma digitale** integrata in workflow preventivi (via CADES o firma semplice qualificata)
|
||||||
|
- **Collaboratore** su `wiki-aziendale` (già supportato da ACL Authelia §10)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Decisioni e rationale
|
||||||
|
|
||||||
|
### Perché separazione forte (repo per wiki)
|
||||||
|
|
||||||
|
- Separazione netta di ciclo di vita, backup, retention, ACL
|
||||||
|
- Possibilità di hand-off di un wiki senza esporre l'altro
|
||||||
|
- Un wiki rotto / corrotto non tocca gli altri
|
||||||
|
- Tradeoff accettato: niente wikilink cross-wiki — si duplica volentieri quando un soggetto è davvero rilevante per entrambi
|
||||||
|
|
||||||
|
### Perché Quartz e non SilverBullet/Wiki.js
|
||||||
|
|
||||||
|
- Nessun runtime app su VPS per il publish → zero superficie d'attacco extra
|
||||||
|
- Wikilink Obsidian nativi + backlinks + graph
|
||||||
|
- Velocità: pagine statiche
|
||||||
|
- Tradeoff: no edit da browser — accettato perché edit è da Obsidian/Claude Code
|
||||||
|
|
||||||
|
### Perché asset fuori git
|
||||||
|
|
||||||
|
- Git si gonfia con binari senza delta utili
|
||||||
|
- rsync separato → repo leggero per sempre
|
||||||
|
|
||||||
|
### Perché Authelia SSO unificato
|
||||||
|
|
||||||
|
- Una login per tutti i sotto-domini (cookie cross-subdomain)
|
||||||
|
- 2FA nativo
|
||||||
|
- ACL configurabili future (multi-utente parziale)
|
||||||
|
|
||||||
|
### Perché portare un wiki fino in fondo prima di replicare
|
||||||
|
|
||||||
|
- Valida l'intero stack end-to-end (meno rischio)
|
||||||
|
- Script di provisioning (`new-wiki.sh`) nasce dall'esperienza del primo setup
|
||||||
|
- Replica in Fase 8 è meccanica
|
||||||
|
|
||||||
|
### Perché `inbox-service` come hub per STT e Telegram
|
||||||
|
|
||||||
|
- Un solo endpoint normalizza l'ingest (API, storage, retention)
|
||||||
|
- Ogni sorgente futura (email-to-wiki, web-clipper, ecc.) riutilizza lo stesso hub
|
||||||
|
- Claude Code desktop resta il solo autorizzato a scrivere nei repo wiki
|
||||||
|
|
||||||
|
### Cose lasciate fuori scope (consapevolmente, MVP)
|
||||||
|
|
||||||
|
- OCR automatico
|
||||||
|
- Ricerca semantica / embeddings
|
||||||
|
- Auto-export PDF
|
||||||
|
- Firma digitale integrata
|
||||||
|
- Workflow di approvazione multi-step
|
||||||
|
- Multi-utente pieno
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Note finali
|
||||||
|
|
||||||
|
Design **incrementale** e **decomponibile**:
|
||||||
|
|
||||||
|
- Fase 1 sola → vault locale usabile (valore immediato)
|
||||||
|
- Fasi 1-4 → vault sincronizzato + pubblicato
|
||||||
|
- Fasi 1-7 → wiki-aziendale completo end-to-end
|
||||||
|
- Fase 8 → secondo wiki operativo
|
||||||
|
- Fase 9-10 → STT + Telegram (estensioni future)
|
||||||
|
|
||||||
|
Ogni fase è stop accettabile. Ogni fase avrà il proprio implementation plan prodotto da **writing-plans** quando ci arriveremo.
|
||||||
|
|
||||||
|
Cuore concettuale invariato: flat markdown + git + Claude Code + Obsidian + Quartz. Multi-wiki è replica di questo cuore, isolata per dominio. Estensioni future (STT, Telegram) sono aggiunte senza riarchitettura.
|
||||||
Reference in New Issue
Block a user