# Fase 10 — Sinottico Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Aggiungere a Toto la generazione automatica di sinottici (diagrammi rete ottica) per ogni armadio importato, con visualizzazione interattiva (pan/zoom, click→dettaglio, ricerca, filtri), editing tipo PTE persistente, report errori topologia, ed export PDF stile FiberCop.

**Architecture:** parser di topologia da `Foglio / Prestazione`, persistito in tabelle `nodes`/`edges`/`node_metadata`/`import_errors`. Layout ad albero verticale con Dagre, render SVG nativo Svelte, pan/zoom con `@panzoom/panzoom`. Export PDF via stylesheet `@media print` (fallback server-side Playwright se serve).

**Tech Stack additions:** `@dagrejs/dagre`, `@panzoom/panzoom`. Nessun cambio infrastrutturale.

**Riferimento design:** `docs/plans/2026-05-02-fase10-sinottico-design.md`.
**Template PDF:** `docs/05518L_11_SINOTTICO.pdf`.

---

## Convenzioni

- **Cwd:** `/Users/paolo/Server/Siti Web/_private/Toto`. Path con spazi → quotare.
- **Git:** repo già su GitHub (`61Toto/gestione-sinottici`), main branch. Push autonomo dopo ogni fase.
- **TDD** dove possibile (parser, builder, layout). Per UI Svelte: light tests + visual E2E.
- **Reuse** delle 5 repository già esistenti, dei pattern di form actions, del DataTable per le righe Excel del nodo.

---

## Fase 10.1 — Migrazione DB e dipendenze

### Task 10.1.1: Migration `002_sinottico.sql`

**Files:**
- Create: `migrations/002_sinottico.sql`

Copia integralmente lo schema dalla **sezione 4 del design** (4 tabelle: `nodes`, `edges`, `node_metadata`, `import_errors`, con relativi index). Tutti i `CREATE` con `IF NOT EXISTS` per idempotenza.

**Step 1: Esegui contro DB locale**

```bash
TURSO_DB_URL=file:/tmp/toto-test-fase10.db INITIAL_USER_EMAIL=test@test INITIAL_USER_PASSWORD=test npm run migrate
sqlite3 /tmp/toto-test-fase10.db ".schema" | head -80
rm /tmp/toto-test-fase10.db
```

Atteso: tutte le 9 tabelle (5 originali + 4 nuove).

**Step 2: Commit**

```bash
git add migrations/002_sinottico.sql
git commit -m "feat(db): nodes, edges, node_metadata, import_errors tables"
```

### Task 10.1.2: Install dipendenze

```bash
npm install @dagrejs/dagre @panzoom/panzoom
npm install -D @types/dagre   # se esiste; altrimenti skip e usiamo any
```

Commit: `chore: install dagre and panzoom for sinottico`.

---

## Fase 10.2 — Topology parser (TDD)

### Task 10.2.1: Pattern matching

**Files:**
- Create: `src/lib/server/topology/parser.ts`, `parser.test.ts`

```ts
export type ParsedElement =
  | { kind: 'giunto'; label: string; from: number; to: number }
  | { kind: 'pte'; label: string; n: number }
  | { kind: 'pte_group'; label: string; from: number; to: number }
  | { kind: 'ao'; label: string }
  | { kind: 'spillato'; label: string; from: string; to: string }   // edge, NON nodo
  | { kind: 'unknown'; raw: string; reason: string };

export function parseElement(elementStr: string): ParsedElement;
```

**Test (≥10 casi):**
1. `'G57:61'` → giunto from=57 to=61
2. `'PTE_4'` → pte n=4
3. `'PTE_20:22'` → pte_group from=20 to=22
4. `'AO'` → ao
5. `'G19:24-G19:39'` → spillato from='G19:24' to='G19:39'
6. `'G.19:39-G. 19:24'` → spillato (tolleranza punti+spazi)
7. `'XYZ'` → unknown, reason='pattern non riconosciuto'
8. `''` → unknown
9. `'G:'` → unknown (range invalido)
10. `'PTE_'` → unknown (numero mancante)

Implementazione: regex con tolleranza spazi/punti per spillati.

**Commit:** `feat(topology): element parser with regex patterns`.

### Task 10.2.2: Topology builder

**Files:**
- Create: `src/lib/server/topology/builder.ts`, `builder.test.ts`

```ts
import type { RowRecord } from '$lib/server/repositories/rows';
import type { ParsedElement } from './parser';

export interface BuiltNode {
  kind: 'armadio' | 'giunto' | 'pte' | 'pte_group' | 'ao';
  label: string;
  pteFrom?: number;
  pteTo?: number;
  parentLabel: string | null;       // null = root
  parseWarning?: string;
  sourceRowId?: number;
}
export interface BuiltEdge {
  fromLabel: string;
  toLabel: string;
  kind: 'tree' | 'spillato';
  cableLabel?: string;
  lengthM?: number;
}
export interface ParseError {
  severity: 'error' | 'warning';
  category: 'overlap' | 'orphan' | 'pattern_unknown' | 'spillato_not_found';
  message: string;
  contextJson: string;     // serialized context: row id, raw element, etc.
}

export interface TopologyResult {
  nodes: BuiltNode[];
  edges: BuiltEdge[];
  errors: ParseError[];
}

export function buildTopology(
  rowsT: RowRecord[],
  rowsM: RowRecord[],
  armadio: string
): TopologyResult;
```

Algoritmo: vedi design sezione 5. In sintesi:
1. Crea root armadio.
2. Per ogni riga T: parse element. Se valido nodo → push. Se spillato → tieni da parte.
3. Per ogni PTE: trova giunto contenente. 0 → orphan (parent=armadio, warning). 1 → ok. ≥2 → error overlap (parent=primo).
4. AO → parent=armadio.
5. Genera edge tree per ogni nodo non root.
6. Per ogni spillato: lookup giunti. Trovati → edge spillato. Mancanti → error.
7. Per ogni edge tree: aggrega righe M figlie del foglio T → calcola cable_label e length_m.
8. Returns.

**Test (≥12 casi):**
1. Solo armadio (no T) → 1 nodo (armadio), 0 edge, 0 errori.
2. Armadio + 1 giunto + 2 PTE in range → 4 nodi, 3 edge tree, 0 errori.
3. PTE orfano (no giunto lo contiene) → parent = armadio, warning orphan.
4. Overlap range: 2 giunti che condividono PTE → 1 errore, PTE assegnato deterministicamente al primo.
5. AO → parent armadio, edge dedicato.
6. Spillato risolto → edge kind='spillato'.
7. Spillato giunto inesistente → error.
8. Pattern unknown → error 'pattern_unknown'.
9. PTE_group dentro un giunto → child del giunto.
10. Cable label calcolata: M con cod 779381 (ROE 16 UI) e quantità lunghezza → cable_label="16/X#TOL*Ym".
11. Cable label non calcolabile (no M con codici riconosciuti) → null.
12. Test su un mini-fixture realistico (3 giunti, 6 PTE, 1 AO, 1 spillato).

**Commit:** `feat(topology): builder with tree+spillato+errors`.

### Task 10.2.3: Persistenza topology

**Files:**
- Create: `src/lib/server/repositories/nodes.ts`, `edges.ts`, `nodeMetadata.ts`, `importErrors.ts`
- Modify: `src/lib/server/importService.ts` per chiamare builder dopo insert rows

Repository methods (vitest test ognuno con ≥3 casi):

`NodesRepo`: `replaceForArmadio(comuneId, importId, armadio, nodes)`, `listByArmadio(comuneId, armadio)`, `findByLabel(comuneId, armadio, label)`.

`EdgesRepo`: `replaceForArmadio(...)`, `listByArmadio(...)`.

`NodeMetadataRepo`: `upsert(comuneId, armadio, label, patch)`, `listByArmadio(comuneId, armadio)` (mappa label→meta).

`ImportErrorsRepo`: `replaceForImport(importId, errors)`, `listByImport(importId)`, `listByComune(comuneId)`, `countByImport(importId)`.

**Modify `importService.ts`:**

Dopo l'insert delle rows:
```ts
// Per ogni armadio nei dati appena importati:
const armadiSet = new Set(parsedRows.filter(r => r.fm === 'T' && r.desc_armadio).map(r => r.desc_armadio!));
for (const armadio of armadiSet) {
  const rowsForArmadio = await rowsRepo.list({comuneId, limit: 100000, offset: 0, filters: {/* desc_armadio */}});
  // (oppure: filtra in memoria parsedRows)
  const rowsT = rowsForArmadio.filter(r => r.fm === 'T');
  const rowsM = rowsForArmadio.filter(r => r.fm === 'M');
  const result = buildTopology(rowsT, rowsM, armadio);
  await nodesRepo.replaceForArmadio(comuneId, importId, armadio, result.nodes);
  await edgesRepo.replaceForArmadio(comuneId, importId, armadio, result.edges);
  await importErrorsRepo.replaceForImport(importId, result.errors);   // tutti gli errori per import
}
```

Aggiorna l'interfaccia `ImportResult` per esporre `errorCount`.

**Test:** estendi `importService.test.ts` con un caso che importa il file Excel reale e verifica `nodes/edges/import_errors` popolati. Verifica idempotenza (importa due volte → sostituisce, non duplica).

**Commit:** `feat(import): persist nodes/edges/errors after row import`.

---

## Fase 10.3 — Layout grafo

### Task 10.3.1: Dagre layout

**Files:**
- Create: `src/lib/sinottico/layout.ts`, `layout.test.ts`

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

export interface PositionedNode { id: string; x: number; y: number; width: number; height: number; data: BuiltNode; }
export interface RoutedEdge { id: string; points: { x: number; y: number }[]; data: BuiltEdge; }
export interface LayoutResult { nodes: PositionedNode[]; edges: RoutedEdge[]; viewBox: string; }

export function layoutGraph(nodes: BuiltNode[], edges: BuiltEdge[]): LayoutResult;
```

Config dagre: `rankdir: 'BT'`, `nodesep: 30`, `ranksep: 60`, `marginx: 40`, `marginy: 40`. Node size: width=80, height=60 default; armadio 100×80; giunto 70×50; pte 50×50; ao 100×80; pte_group 80×60.

ViewBox calcolato da bounding box nodi+edge.

**Test (≥4 casi):**
1. 3 nodi in catena → posizioni con `x` centrate, `y` decrescenti (BT = bottom-top).
2. Albero con 1 root + 3 figli → root in basso, figli sopra equidistanti.
3. Edge spillato fra 2 giunti → polilinea con ≥2 punti.
4. ViewBox include tutti i nodi con margine.

**Commit:** `feat(sinottico): graph layout with dagre`.

---

## Fase 10.4 — Componenti SVG

### Task 10.4.1: Simboli SVG

**Files:**
- Create: `src/lib/sinottico/Symbols.svelte`

Componente che renderizza un singolo `<defs>` SVG con tutti i simboli (replicano la legenda del PDF FiberCop):

- `sym-armadio` — semicerchio rosso "F.O." (taglio piano in basso)
- `sym-giunto` — rettangolo orizzontale rosso pieno
- `sym-pte-default` — rettangolo verticale diviso in due metà (sx vuota, dx vuota), bordo rosso
- `sym-pte-colonnina` — rettangolo verticale con divisione obliqua, vuoto
- `sym-pte-muro_esterno` — rettangolo verticale, divisione obliqua, vuoto + ombra
- `sym-pte-muro_interno` — rettangolo verticale, mezzo pieno mezzo vuoto
- `sym-pte-interno_edificio` — rettangolo verticale tutto pieno
- `sym-pte-sotterraneo` — rettangolo verticale (orizzontale?) con linea di terra
- `sym-pte-business` — cerchio pieno
- `sym-pte-passed` — linea diagonale attraverso il rettangolo
- `sym-ao` — semicerchio (come armadio) ma più piccolo
- `sym-pte_group` — rettangolo allungato a doppio scomparto

Ogni `<symbol>` con `viewBox` proprio, dimensione default 60×40 (si riscala con `<use width=... height=...>`).

Inserisco anche le icone della legenda (muffole) anche se nel MVP non vengono usate per l'auto-detection — restano disponibili per editing manuale futuro.

**Stile:** stroke `#c00` (rosso FiberCop), fill `#c00` o `#fff`. Tema chiaro = sfondo bianco. Possibile tema scuro per visualizzazione a schermo (sfondo nero, simboli rossi/arancio come yetisis).

**Commit:** `feat(sinottico): SVG symbols library (FiberCop legend)`.

### Task 10.4.2: Componente `Sinottico.svelte`

**Files:**
- Create: `src/lib/components/Sinottico.svelte`

Riceve `{nodes, edges, selectedId, onSelect, theme}`. Renderizza:

```svelte
<svg viewBox={vb} class="sinottico" class:dark={theme==='dark'}>
  <Symbols />
  <g class="edges">
    {#each edges as e}
      <polyline points={pts(e.points)} class="edge edge-{e.data.kind}" />
      {#if e.data.cableLabel}
        <text class="cable-label" x={mid(e).x} y={mid(e).y}>{e.data.cableLabel}</text>
      {/if}
    {/each}
  </g>
  <g class="nodes">
    {#each nodes as n}
      <g transform="translate({n.x},{n.y})" class:selected={n.id===selectedId}
         on:click={() => onSelect(n)} role="button" tabindex="0">
        <use href={`#sym-${symbolFor(n)}`} width={n.width} height={n.height} x={-n.width/2} y={-n.height/2} />
        <text class="node-label" y={n.height/2 + 12} text-anchor="middle">{n.data.label}</text>
      </g>
    {/each}
  </g>
</svg>
```

`symbolFor(n)`: per `kind=pte` → `pte-${subtype ?? 'default'}` (subtype viene dal join con node_metadata).

Stile: stylesheet inline, classi `dark` per tema notte, hover/selected highlight.

**Commit:** `feat(sinottico): Sinottico SVG component`.

### Task 10.4.3: Pan/zoom wrapper

**Files:**
- Create: `src/lib/components/SinotticoCanvas.svelte`

Wrapper che:
- Inserisce `<Sinottico/>` dentro un `<div class="pz-container">`
- Inizializza `@panzoom/panzoom` su mount (browser-only)
- Espone bottoni: ⟲ Fit, + Zoom in, - Zoom out, ⌂ Reset, ☀/🌙 toggle tema (state locale + persist localStorage)

Disponi i controlli sticky in alto a destra del container (responsive: collassabili su mobile).

**Commit:** `feat(sinottico): canvas wrapper with pan/zoom`.

---

## Fase 10.5 — Pagina sinottico

### Task 10.5.1: Loader + endpoint update tipo PTE

**Files:**
- Create: `src/routes/comune/[slug]/sinottico/[armadio]/+page.server.ts`

`load`:
- Parent comune.
- Decode armadio dall'URL (URL-encoded).
- `nodes = nodesRepo.listByArmadio(comune.id, armadio)`
- `edges = edgesRepo.listByArmadio(comune.id, armadio)`
- `metadata = nodeMetadataRepo.listByArmadio(comune.id, armadio)` → mappa label→meta
- `errors = importErrorsRepo.listByArmadio(comune.id, armadio)` (oppure listByImport del latest)
- Costruisce `BuiltNode[]` arricchiti con metadata (subtype) e li mappa.
- Layout via `layoutGraph()` (server-side è OK, dagre puro JS).
- Returns `{ comune, armadio, nodes, edges, viewBox, errors, metadata }`.

`actions.updatePteSubtype`:
- Riceve `label`, `subtype`.
- Whitelist subtype (uno dei 7 valori validi o '').
- `nodeMetadataRepo.upsert(...)`.
- Returns success.

`actions.updateNotes`:
- Riceve `label`, `notes`.
- Upsert su node_metadata.

**Commit:** `feat(sinottico): page loader and metadata actions`.

### Task 10.5.2: Pagina svelte

**Files:**
- Create: `src/routes/comune/[slug]/sinottico/[armadio]/+page.svelte`
- Create: `src/lib/components/SinotticoNodeDrawer.svelte`

Layout:
- Header sticky: torna ad `/comune/[slug]/sinottici`, titolo `Sinottico — <armadio>`, bottoni "Esporta PDF" e "Errori (N)" (apre modale o link a tab errori).
- Filtri (collassabili su mobile): cerca, periodo (range date), week (multi-select), tipo elemento, tipo cavo. Comunicano via store reattivo a Sinottico per visibility.
- `<SinotticoCanvas {nodes} {edges} {viewBox} bind:selectedId />`
- `<SinotticoNodeDrawer node={selected} {metadata} {rowsForNode} on:save />` (drawer laterale).

`SinotticoNodeDrawer`:
- Header: `<icon> <label>` + close.
- Per nodo PTE: select "Tipo PTE" (7 opzioni + "Non specificato"). Submit form action.
- Per tutti: campo Note libero, salva con debounce.
- Tabella delle righe Excel `M` figlie del foglio T del nodo (riusa `DataTable` con visibleColumns minimal: cod_prestazione, desc_trec, quantita, um, valore_record, data_prod, squadra, assistente).

Responsive: drawer fullscreen su mobile, sidebar 480px su desktop. Filtri orizzontali scroll su mobile.

**Commit:** `feat(sinottico): main page with drawer and filters`.

### Task 10.5.3: Indice sinottici per comune

**Files:**
- Create: `src/routes/comune/[slug]/sinottici/+page.server.ts`, `+page.svelte`

`load`: query `SELECT armadio, COUNT(*) FROM nodes WHERE comune_id=? GROUP BY armadio` + `errors_count` da import_errors per armadio.

Pagina: lista cards/tabella di armadi con numero nodi/edge/errori, link "Apri sinottico". Empty state se nessun import.

**Commit:** `feat(sinottico): comune armadi index page`.

### Task 10.5.4: Pagina errori import

**Files:**
- Create: `src/routes/comune/[slug]/sinottici/errori/+page.server.ts`, `+page.svelte`

Tabella errori: severity, category, armadio, label, message, link a riga Excel (se contextJson contiene rowId), pulsante "Vai al sinottico armadio".

**Commit:** `feat(sinottico): import errors report page`.

### Task 10.5.5: Link da dashboard comune

**Files:**
- Modify: `src/routes/comune/[slug]/+page.svelte`

Aggiungi terzo bottone "🗺 Sinottici" che linka a `/comune/[slug]/sinottici`. Mostra anche badge n.errori se >0.

**Commit:** `feat(comuni): link sinottici from comune dashboard`.

---

## Fase 10.6 — Export PDF

### Task 10.6.1: Print stylesheet

**Files:**
- Create: `src/lib/styles/sinottico-print.css`
- Modify: `src/routes/comune/[slug]/sinottico/[armadio]/+page.svelte`

CSS:
```css
@page { size: A3 landscape; margin: 10mm; }
@media print {
  /* Hide chrome */
  .site-header, .toolbar, .filters, .drawer, .pz-controls, button, .toast-stack { display: none !important; }
  /* Layout sinottico+legenda+cartiglio */
  .print-root { display: grid; grid-template-columns: 1fr 220mm; }
  .print-svg-wrap { /* full page svg */ }
  .print-side { display: flex; flex-direction: column; }
  .print-legend { /* simboli legenda */ }
  .print-cartiglio { /* box footer FiberCop */ }
}
```

Bottone "Esporta PDF" nella toolbar → `window.print()`.

Cartiglio HTML/SVG renderizzato sempre (display:none a schermo, display:block in print). Contiene:
- Logo FiberCop (placeholder se asset assente)
- Titolo "PROGETTAZIONE DELLA RETE OTTICA DI DISTRIBUZIONE"
- Sottotitolo "SECONDARIA TIM"
- "SCHEMA DI GIUNZIONE", comune, data
- Codice progetto = armadio
- Nome file = "<armadio>_SINOTTICO"
- Tavola = "<armadio>"

Test manuale: aprire pagina, click Esporta PDF, verificare anteprima dialog stampa.

**Commit:** `feat(sinottico): PDF export via print stylesheet`.

### Task 10.6.2 (opzionale, fallback): Server-side render Playwright

Solo se 10.6.1 non produce risultato accettabile.

**Files:**
- Create: `src/routes/comune/[slug]/sinottico/[armadio]/pdf/+server.ts`

Endpoint che:
1. Avvia browser headless (Playwright chromium).
2. Apre la pagina sinottico passando un cookie di sessione (impersonate user).
3. Aspetta layout completo.
4. Esegue `page.pdf({ format: 'A3', landscape: true, ... })`.
5. Restituisce il PDF al client come download.

⚠️ Su Vercel serverless: Playwright pesa molto. Soluzioni: `@sparticuz/chromium-min` oppure servizio esterno (Browserless). Per MVP è scope-creep. Preferiamo 10.6.1.

**Commit (se attuato):** `feat(sinottico): server-side PDF export via Playwright`.

---

## Fase 10.7 — Validazione + E2E

### Task 10.7.1: Test E2E sinottico

**Files:**
- Create: `e2e/sinottico-flow.spec.ts`

Scenario:
1. Login + cambia password (riusa setup esistente).
2. Crea comune "Test Sinottico".
3. Importa `docs/Estrazioen totale.xlsx`.
4. Vai a `/comune/<slug>/sinottici` → screenshot lista armadi.
5. Click su armadio `57400D_02` → screenshot sinottico aperto.
6. Verifica almeno 5 nodi visibili (`<g class="node">` count ≥ 5).
7. Click su un nodo PTE → drawer aperto, screenshot.
8. Cambia tipo PTE in dropdown → submit. Verifica simbolo cambiato (riload o invalidate).
9. Verifica badge errori se presente.
10. Click "Esporta PDF" → cattura il print preview (skip — Playwright non gestisce nativamente print dialog).

Run desktop + tablet + mobile (riusa pattern già esistente in visual-*.spec.ts).

**Commit:** `test(e2e): sinottico flow desktop/tablet/mobile`.

### Task 10.7.2: Visual regression

**Files:**
- Modify: `e2e/visual-desktop.spec.ts` etc.

Aggiungi step: dopo import, naviga a `/comune/<slug>/sinottici/<armadio>`, screenshot. Garantisce che il rendering sinottico sia stabile a ogni release.

**Commit:** `test(e2e): visual regression for sinottico render`.

---

## Fase 10.8 — Polish

### Task 10.8.1: README + design notes

Aggiungi sezione "Sinottico" al `README.md` con:
- Cos'è (1 paragrafo)
- Come si genera (auto a ogni import)
- Come si edita il tipo PTE (click + dropdown)
- Come si esporta in PDF (browser print, A3 landscape)
- Errori di import: dove vederli, come correggere

Commit: `docs: README sezione sinottico`.

---

## Criteri di accettazione finali

- [ ] Importando `docs/Estrazioen totale.xlsx`, il sistema genera 11 sinottici (uno per armadio del file).
- [ ] Per ogni armadio, i nodi e gli edge sono coerenti con la regola di parsing (verificabile manualmente su `57400D_02`: 1 armadio + 11 giunti + 35 pte + 4 pte_group + 1 ao + edge spillato `G19:39 ↔ G19:24`).
- [ ] Salvatore può cliccare un PTE, scegliere il tipo da dropdown, e il simbolo si aggiorna. Re-importando l'Excel la scelta persiste.
- [ ] Errori di topologia mostrati nella pagina dedicata e nel badge dell'armadio.
- [ ] Pan/zoom funziona desktop+touch.
- [ ] Filtri (cerca, periodo, week, tipo elemento, tipo cavo) nascondono/mostrano nodi.
- [ ] "Esporta PDF" apre la stampa con layout A3 landscape: SVG sinottico + legenda + cartiglio FiberCop.
- [ ] Test E2E pass su desktop+tablet+mobile.
- [ ] Tutti i test unitari (`npm test`) passano.

## Cosa serve da Salvatore prima della deploy

- Un **PNG/SVG ufficiale del logo FiberCop** da mettere in `static/fibercop-logo.png` (se vuole il logo nel cartiglio del PDF). Senza, mettiamo un placeholder testuale.
- Conferma visiva di un sinottico generato vs un sinottico "vero" yetisis per l'armadio `57400D_02`: stessi nodi? stessi edge? Se discrepanze, le correggiamo (probabilmente regole di parsing edge case).
