# Fase 10 — Sinottico per armadio + export PDF

**Data:** 2026-05-02
**Stato:** approvato, pronto per planning implementativo
**Riferimenti:** `2026-05-02-toto-design.md` (architettura base), `docs/05518L_11_SINOTTICO.pdf` (template export FiberCop), screenshot `yetisis.com/sinottico/57400D_7964` (esempio interattivo).

## 1. Obiettivo

Per ogni **armadio** importato, generare automaticamente un **sinottico** (diagramma ad albero della rete ottica) renderizzato in SVG, navigabile (pan/zoom, click su nodo per dettagli, ricerca, filtri) e **esportabile in PDF** con il template grafico ufficiale FiberCop.

Il sinottico replica visivamente quanto OPTIMVS/yetisis mostra oggi a Salvatore, ma è generato esclusivamente dai dati Excel già presenti in Toto. Salvatore non dipenderà più dal portale esterno per visualizzare/stampare la rete.

## 2. Scope

**In MVP:**
- Una pagina `/comune/[slug]/sinottico/[armadio]` per ogni armadio dei dati importati.
- Layout ad albero verticale (radice = armadio, fronde = giunti → PTE), stile FiberCop (template PDF).
- Simboli: 7 tipi PTE + giunto + armadio ottico + (in futuro) muffola/dirramatore.
- Edge = cavi tra nodi, con etichetta `<fibre>/<altro>#<tipo>*<lunghezza>m` se calcolabile.
- Pan/zoom + fit-to-screen.
- Click su nodo → pannello laterale con: tipo (editabile per i PTE), elenco righe Excel del nodo.
- Ricerca testuale nodo.
- Filtri: Periodo, Week, Tipo elemento, Tipo cavo.
- Export PDF (A3 landscape, layout simile a `05518L_11_SINOTTICO.pdf`: diagramma + legenda a destra + cartiglio FiberCop in basso a destra).
- Report errori di import topologia (overlap, pattern sconosciuti, orfani).

**Esplicitamente fuori MVP:**
- Stati colorati (Collaudato/Giuntato/Posato/...) — Salvatore ha confermato che non interessano.
- "Foto" su doppio click (richiede integrazione XME).
- Editor manuale di topologia (drag&drop di nodi/archi).
- Algoritmi di layout custom — usiamo libreria standard.
- Stampa diretta da browser (preferiamo PDF server-side per layout fisso).

## 3. Stack aggiuntivo

| Cosa | Libreria | Motivazione |
|---|---|---|
| Layout grafo | **`@dagrejs/dagre`** | Tree/DAG layout deterministico, controllo verticale, robust. |
| Rendering | **SVG nativo Svelte** | Ottimo per stampa, vettoriale, debuggabile, nessuna dep grafica esterna. |
| Pan/zoom | **`@panzoom/panzoom`** o custom (~50 LoC) | Leggero, no dep React. |
| PDF export | **server-side via Playwright/Chromium** già presente come dev-dep, oppure `@sparticuz/chromium-min` su Vercel | Garantisce identità tra preview a schermo e stampa. |

Alternativa per PDF: **client-side via `window.print()`** con CSS `@page A3 landscape` + `@media print`. Più semplice ma layout meno controllabile. **Decisione:** parto da client-side print → PDF (browser nativo); se il risultato non rispecchia il template, passo a server-side Playwright.

## 4. Schema DB esteso

Aggiungo 3 tabelle senza toccare le esistenti.

```sql
-- Nodi del grafo, ricostruiti a ogni import
CREATE TABLE IF NOT EXISTS nodes (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  comune_id INTEGER NOT NULL REFERENCES comuni(id) ON DELETE CASCADE,
  import_id INTEGER NOT NULL REFERENCES imports(id) ON DELETE CASCADE,
  armadio TEXT NOT NULL,             -- es. '57400D_02'
  kind TEXT NOT NULL,                -- 'armadio' | 'giunto' | 'pte' | 'pte_group' | 'ao' | 'unknown'
  label TEXT NOT NULL,               -- es. 'G57:61', 'PTE_4', 'PTE_20:22', 'AO'
  pte_from INTEGER,                  -- per pte/pte_group: numero (o range start)
  pte_to INTEGER,                    -- per pte_group/giunto: range end
  parent_id INTEGER REFERENCES nodes(id) ON DELETE CASCADE,
  parse_warning TEXT                 -- es. 'PTE_57 ambiguo: in G56:59 e G57:61'
);
CREATE INDEX IF NOT EXISTS idx_nodes_armadio ON nodes(comune_id, armadio);
CREATE INDEX IF NOT EXISTS idx_nodes_label ON nodes(comune_id, armadio, label);

-- Edge espliciti (per "giunti spillati" e simili)
CREATE TABLE IF NOT EXISTS edges (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  comune_id INTEGER NOT NULL REFERENCES comuni(id) ON DELETE CASCADE,
  import_id INTEGER NOT NULL REFERENCES imports(id) ON DELETE CASCADE,
  armadio TEXT NOT NULL,
  from_node_id INTEGER NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
  to_node_id INTEGER NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
  kind TEXT NOT NULL,                -- 'tree' (parent→child) | 'spillato' (giunto-giunto)
  cable_label TEXT,                  -- es. '24/40#MC*120' (best-effort, può essere null)
  length_m REAL                      -- lunghezza in metri (best-effort)
);
CREATE INDEX IF NOT EXISTS idx_edges_armadio ON edges(comune_id, armadio);

-- Metadati editabili manualmente da Toto (es. Tipo PTE)
-- Persistono tra re-import (la chiave include label per lookup stabile)
CREATE TABLE IF NOT EXISTS node_metadata (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  comune_id INTEGER NOT NULL REFERENCES comuni(id) ON DELETE CASCADE,
  armadio TEXT NOT NULL,
  label TEXT NOT NULL,               -- es. 'PTE_57'
  pte_subtype TEXT,                  -- 'colonnina' | 'muro_esterno' | 'muro_interno' | 'interno_edificio' | 'sotterraneo' | 'business' | 'passed' | NULL
  notes TEXT,
  updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
  UNIQUE(comune_id, armadio, label)
);

-- Errori e warning del parsing topologia
CREATE TABLE IF NOT EXISTS import_errors (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  import_id INTEGER NOT NULL REFERENCES imports(id) ON DELETE CASCADE,
  comune_id INTEGER NOT NULL REFERENCES comuni(id) ON DELETE CASCADE,
  armadio TEXT,
  severity TEXT NOT NULL,            -- 'error' | 'warning'
  category TEXT NOT NULL,            -- 'overlap' | 'orphan' | 'pattern_unknown' | 'ambiguous' | ...
  message TEXT NOT NULL,
  context_json TEXT                  -- JSON con dettagli (riga Excel coinvolta, ecc.)
);
CREATE INDEX IF NOT EXISTS idx_import_errors_import ON import_errors(import_id);
```

Migration: `migrations/002_sinottico.sql`.

## 5. Algoritmo topology builder

`src/lib/server/topology.ts` esporta:

```ts
function buildTopology(rowsT: RowRecord[], armadio: string): {
  nodes: NodeData[];
  edges: EdgeData[];
  errors: ParseError[];
};
```

Pseudo-codice:

```
1. Filtra rowsT: tutte le righe T del comune con `desc_armadio == armadio`.
2. Per ogni riga, parse del campo `foglio_prestazione`:
   - Strip prefisso `<armadio>/`. Se non parte con armadio → error 'pattern_unknown'.
   - Resto = `element`. Pattern recognition:
     a) /^G\d+:\d+$/ → giunto (range a..b)
     b) /^PTE_\d+$/ → pte singolo (number n)
     c) /^PTE_\d+:\d+$/ → pte_group (range a..b)
     d) /^AO$/ → ao
     e) /^G[\d.\s]+:[\d.\s]+\s*-\s*G[\d.\s]+:[\d.\s]+$/ → spillato (edge non nodo)
     f) altro → error 'pattern_unknown'
3. Crea nodo armadio (root, kind='armadio').
4. Inserisci tutti i nodi giunto/pte/pte_group/ao.
5. Per ogni giunto, calcola child PTE: pte (n) o pte_group (a:b) il cui range è contenuto/intersect range giunto.
   - Se PTE rientra in 0 giunti → orphan: parent = armadio (root), warning 'orphan'.
   - Se PTE rientra in >=2 giunti → error 'overlap', parent = primo per id ordine inserimento (deterministico, ma comunque errore segnalato).
6. AO: parent = armadio.
7. Per ogni edge spillato (G a:b - G c:d):
   - Risolvi i due giunti per label (es. 'G19:24', 'G19:39'). Tolleranza al formato (strip punti, spazi).
   - Se entrambi trovati → edge.kind = 'spillato'.
   - Altrimenti → error.
8. Per ogni nodo che non è root, crea edge tree (parent → child).
9. Per ogni edge tree: calcolare cable_label e length_m da aggregazione righe M del foglio T figlio
   (somma quantità di voci con UM='M' nel cod prestazione = scavo/posa/minicavo).
   Se non calcolabile, lascia null.
10. Ritorna {nodes, edges, errors}.
```

Test (vitest, `:memory:` SQLite + fixture data):
- Caso base: armadio + 1 giunto + 2 PTE → tree corretto.
- Overlap range: 2 giunti che condividono PTE → errore generato, PTE assegnato deterministicamente.
- Pattern sconosciuto → errore.
- Spillato: edge giunto-giunto creato.
- Orphan PTE: warning, parent = armadio.
- Test su dato reale di `57400D_02` (51 righe T): 11 giunti, 35+4 PTE, 1 AO, edges generati.

## 6. Persistenza topology in DB

Estendo `ImportService.run` (Phase 5):

```
Dopo aver inserito le righe in tabella `rows`:
  - Per ogni `desc_armadio` distinto presente nelle righe T appena inserite:
      const result = buildTopology(rowsT_for_armadio, armadio)
      INSERT INTO nodes (...)
      INSERT INTO edges (...)
      INSERT INTO import_errors (...) per ogni errors[]
  - Cancella prima i vecchi nodes/edges/import_errors di quell'armadio
    (cascade via FK su import_id non basta perché il vecchio import potrebbe
    essere lo stesso comune ma armadio diverso — meglio scope per armadio
    + comune_id).
```

## 7. Layout grafo

`src/lib/sinottico/layout.ts`:

```ts
import dagre from '@dagrejs/dagre';

function layoutGraph(nodes, edges): { nodes: PositionedNode[]; edges: RoutedEdge[] }
```

Configurazione dagre:
- `rankdir: 'BT'` (radice in basso, fronde verso l'alto — coerente col PDF FiberCop)
- `nodesep: 30`
- `ranksep: 60`
- `marginx: 40, marginy: 40`

Output: ogni nodo riceve `x, y, width, height`; ogni edge riceve `points[]` (polilinea).

## 8. Componente SVG

`src/lib/components/Sinottico.svelte` — componente puro (riceve nodes+edges già posizionati, renderizza SVG).

```svelte
<svg viewBox={viewBox} preserveAspectRatio="xMidYMid meet">
  <!-- background pattern hex (opzionale, solo screen) -->
  <g class="edges">
    {#each edges as e}
      <polyline points={e.points} class="edge edge-{e.kind}" />
      {#if e.cable_label}
        <text class="edge-label" ...>{e.cable_label}</text>
      {/if}
    {/each}
  </g>
  <g class="nodes">
    {#each nodes as n}
      <g transform="translate({n.x},{n.y})" class="node node-{n.kind}" 
         on:click={() => onSelect(n)} role="button" tabindex="0">
        <use href="#sym-{n.kind}-{n.subtype ?? 'default'}" />
        <text class="node-label">{n.label}</text>
      </g>
    {/each}
  </g>
</svg>
```

I simboli sono `<symbol id="sym-pte-colonnina">…</symbol>`, ecc., definiti in un `<defs>` condiviso (`src/lib/sinottico/symbols.svelte` o file SVG inline). Replicano la legenda del PDF FiberCop:
- Pte colonnina, muro esterno, muro interno, interno edificio, sotterraneo, business, passed
- Muffola secondaria, aerea compatta, di linea
- Armadio ottico large, small
- Giunto (rettangolo orizzontale)
- Dirramatore (icona biforcazione)

## 9. Pan / zoom

Uso `@panzoom/panzoom` (libreria leggera, no React, supporta touch). Wrappo l'SVG in un `<div class="pz-container">` e inizializzo dopo mount.

Bottoni dedicati: ⟲ Fit, +/- Zoom, ⌂ Reset.

## 10. Pannello dettagli

Pannello laterale (drawer) attivato al click su nodo. Contenuto:
- Header: tipo nodo + label
- Per i Pte: select "Tipo PTE" (legenda). Submit = `node_metadata` upsert.
- Tabella delle righe Excel filtrate per `foglio_prestazione = "<armadio>/<label>"` (M figlie del T del nodo).
- Note libere (campo `node_metadata.notes`).

## 11. Pagina sinottico

Route: `/comune/[slug]/sinottico/[armadio]`.

`+page.server.ts`:
- Carica nodes, edges, node_metadata, import_errors per armadio.
- Carica righe Excel pertinenti (tutti i T + i loro M per quell'armadio).

`+page.svelte`: layout
```
+----------------------------------------------+
| Header: armadio | toolbar (Fit, +, -, PDF)   |
+--------+-------------------------------------+
| Filtri | SVG Sinottico (pan/zoom)            |
| Cerca  |                                     |
| Errori |                                     |
+--------+-------------------------------------+
| (drawer dettagli, slide-in da destra)        |
+----------------------------------------------+
```

Sub-pagina elenco armadi: `/comune/[slug]/sinottici` (lista degli armadi disponibili per il comune con n.nodi/edges/errori).

## 12. Export PDF

Strategia: **stylesheet `@media print`** + bottone "Esporta PDF" che apre il dialog di stampa (`window.print()`).

CSS print:
- `@page { size: A3 landscape; margin: 10mm }`
- Nasconde toolbar, header app, drawer, navigazione browser.
- SVG full-page a sinistra (~70% larghezza).
- Box "LEGENDA" in alto a destra con i simboli dalla `defs`.
- Cartiglio FiberCop in basso a destra: titolo, codice armadio, comune, data, scala (es. "Scala 1:1000"), nome file, logo.

Il logo FiberCop: per legalità non posso embedderlo se è asset proprietario. Lascio un placeholder text "FiberCop" in stile + spazio per immagine — Salvatore allega il PNG/SVG ufficiale dopo (carica su `/static/fibercop.png`, lo includiamo via CSS).

Se il risultato non basta (font, layout, qualità), **fallback B**: server-side render via Playwright headless → screenshot/PDF. Già abbiamo Playwright dev-dep. Aggiungo solo una route `+server.ts` che lancia un browser, naviga alla pagina sinottico in modalità "stampa", restituisce PDF.

## 13. Report errori import

Pagina o sezione dedicata `/comune/[slug]/sinottici/errori` (o badge nel header armadio) che lista:
- Pattern sconosciuti (non pattern G/PTE/AO/spillato)
- Overlap range tra giunti
- PTE orfani (non rientrano in nessun giunto)
- Riferimenti a giunti inesistenti (es. spillato che cita giunto non importato)

Per ogni errore: armadio, label problematica, descrizione, pulsante "Apri riga Excel" che porta a `/comune/[slug]/data?id=<row_id>`.

## 14. Editing tipo PTE (persistenza)

- Click su PTE in SVG → drawer apre → utente sceglie tipo da dropdown → submit (form action) → `INSERT OR REPLACE` su `node_metadata` (chiave: comune_id, armadio, label).
- Al rendering successivo, il join `nodes ⋈ node_metadata` popola `pte_subtype` per scegliere il simbolo.
- Reimport non cancella `node_metadata` (FK è su `comune_id`, non `import_id`). Salvatore non perde le sue assegnazioni.

## 15. Filtri

- **Periodo**: range `data_prod` di righe M.
- **Week**: distinct `Ultimo Update` settimana (o calcolata).
- **Tipo elemento**: kind del nodo.
- **Tipo cavo**: dedotto da edge.cable_label.

I filtri agiscono sulla **visibilità** dei nodi (e edge collegati), non sul ricalcolo del layout. Nodi nascosti = grayed-out o opacity 0 (configurabile).

## 16. Performance

- 11 armadi × ~50 nodi ciascuno = ~500 nodi totali per il dato reale. Trascurabile.
- Nessuna paginazione necessaria — l'intero grafo cape in una pagina.
- PDF: A3 landscape, vettoriale, file <500KB atteso.

## 17. Out-of-scope (espliciti)

- Mappa geografica (lat/long per Pte) — non disponibile nei dati.
- Animazioni di rete (status real-time).
- Multi-armadio in unica vista.
- Versioning topologia tra import (snapshot storici).
- Modifica struttura topologia direttamente dal sinottico (drag/drop/aggiunta nodi). Salvatore corregge l'Excel sorgente, non il grafico.

## 18. Prossimo passo

Invocare `superpowers:writing-plans` per produrre il piano implementativo dettagliato (`2026-05-02-fase10-sinottico-implementation.md`) con task TDD-ready in stile delle fasi precedenti.
