# Generazione PDF dei DVR nel portale — Design

**Data**: 2026-06-24
**Stato**: approvato (design), in attesa di piano di implementazione

## Contesto e obiettivo

Il design grafico del PDF del DVR è stato **approvato dal cliente** (prototipo `pdf-prototype/`, reso da Chromium/Puppeteer su HTML+CSS, "v11"). Va portato dentro il portale SvelteKit: l'utente deve poter scaricare il PDF di un DVR, **sempre allineato ai dati attuali**.

## Vincoli (decisi con Paolo)

- **Fedeltà al design approvato = priorità.** pdfmake è stato valutato e **scartato**: pur veloce e Hobby-compatibile, perde dettagli che rendono il documento curato (angoli arrotondati di pill/legende, gradiente copertina, font esatto). Si resta su **Chromium/Puppeteer** (HTML del prototipo, 1:1 col design approvato).
- **Hosting = Vercel Hobby gratuito.** Niente Vercel Pro. Conseguenza chiave: **Chromium NON può girare su Vercel** (limite funzione 10s; un DVR pesante ci mette ~7s in locale a caldo → su Hobby a freddo sfora). La generazione gira **fuori da Vercel**.
- **Costo zero.** Motore di generazione remoto = **GitHub Actions** (gratis, 2000 min/mese su repo privato, runner Linux con Chromium pieno e affidabile).

## Architettura

### Componenti
1. **Modulo di generazione** (Node + Puppeteer) — evoluzione di `pdf-prototype/` (`load.js`, `template.js`, `generate.js`). Differenza chiave: legge i dati da **Turso** (live) e le immagini allegati da **Cloudflare R2**, non più da file locali. È l'unico motore, usato sia per il seed iniziale sia per le rigenerazioni.
2. **R2** — archivio dei PDF generati. Chiave oggetto: `pdf/{codice}_DVR.pdf` (`codice` = `codice_fisico` reale della sede).
3. **Tabella `dvr_pdf`** (nuova) — stato per agenzia: `agenzia_id` (PK, FK agenzie.id), `r2_key`, `data_hash` (la "fotografia" dei dati con cui è stato generato il PDF in R2), `stato` (`ready` | `generating` | `error`), `generato_il`, `error_msg`, `updated_at`.
4. **GitHub Action** (`.github/workflows/generate-pdf.yml`) — il motore Chromium remoto. Innescata via `repository_dispatch` con `client_payload.agenzia_id` (o `"all"` per il seed). Genera, carica su R2, aggiorna `dvr_pdf`. Secrets repo: `DATABASE_URL`, `DATABASE_AUTH_TOKEN`, credenziali R2.
5. **Endpoint/azione nel portale** — calcola l'hash corrente, confronta con `dvr_pdf.data_hash`, serve da R2 o innesca la rigenerazione; espone lo stato per la UI.

### Flusso dati

**Seed iniziale (una tantum)**: si lancia la generazione per tutte le ~806 sedi (in locale puntando a Turso+R2, oppure via Action `agenzia_id="all"`). R2 popolato, `dvr_pdf` valorizzato. Dal giorno 1 ogni agenzia ha il suo PDF fedele, scaricabile all'istante.

**Click sull'icona PDF**:
1. L'app calcola l'**hash dei dati attuali** del DVR e lo confronta con `dvr_pdf.data_hash`.
2. **Hash uguale + stato `ready`** → download immediato del PDF da R2 (caso più frequente: i DVR cambiano di rado).
3. **Hash diverso / record mancante** → se non già `generating`, l'app:
   - imposta `dvr_pdf.stato = 'generating'`,
   - innesca la Action (`repository_dispatch`),
   - mostra "PDF in aggiornamento, pronto tra ~1 min", **lasciando scaricabile la versione precedente** (se esiste).
4. La pagina **fa polling** dello stato (ogni ~10-15s); quando la Action finisce e `dvr_pdf` torna `ready` con il nuovo hash, l'icona si abilita al download.

La staleness si rileva **pigramente** dal confronto hash: nessuna modifica alle azioni di salvataggio del DVR. Cattura anche cambi indiretti (config globale datore/RSPP, allegati).

### Hash dei dati

Requisito: l'hash deve cambiare **se e solo se** cambia qualcosa che incide sul PDF. Composto da una "fingerprint" deterministica di:
- `dvr` (campi + `updated_at`) e relative figlie (pericoli, valutazione, misure, incendio, piano, attività),
- anagrafica dell'agenzia mostrata nel PDF,
- allegati (id + versione/sha o `updated_at`),
- config globale (`app_settings`: datore di lavoro, RSPP),
- **`TEMPLATE_VERSION`** (costante: bumpata quando cambia il template → forza rigenerazione di tutti).

Approccio di default: SHA-256 di una stringa-fingerprint costruita da `updated_at`/conteggi delle entità sopra + `TEMPLATE_VERSION`. Da garantire in fase di piano: ogni percorso di mutazione del DVR (inclusi gli autosave `/api/dvr/*`) aggiorni un `updated_at` intercettato dalla fingerprint (es. bump di `dvr.updated_at`). Alternativa più robusta ma più pesante: hash dell'intero oggetto-dati serializzato.

### Sedi gemelle

Il PDF di una gemella usa il **contenuto DVR della compagna** (`resolveDvrAgenziaId`, già implementato) ma **codice, intestazione e nome-file della gemella**. Conseguenza: quando cambia il DVR della compagna, diventano stale **entrambi** i PDF (gemella + compagna) — la fingerprint della gemella include l'hash del DVR risolto (compagna) + la propria anagrafica.

### Nome file

`{codice}_DVR.pdf` (es. `22399_DVR.pdf`), per l'ingestione nel software di firma del cliente.

## UX

- **Stati dell'icona**: `ready` ("Scarica PDF" + data generazione) · `stale` ("Aggiorna PDF") · `generating` (spinner "in aggiornamento ~1 min" + "scarica versione precedente") · `error` ("errore, riprova") · `missing` ("Genera PDF").
- **Posizione**: dettaglio agenzia + elenco agenzie (download rapido).
- **Progress bar**: reale **X/806** per il seed e per un eventuale "rigenera tutti"; per il singolo PDF, stato asincrono (Chromium è un'unica fase opaca, non c'è % reale).

## Gestione errori

- Action fallita → `dvr_pdf.stato = 'error'` + `error_msg`; UI mostra "errore, riprova"; resta scaricabile l'ultima versione valida in R2.
- Guard di concorrenza: non innescare una nuova Action se già `generating` (con timeout di sicurezza per stati appesi).
- Trigger fallito (GitHub API) → messaggio chiaro, nessuno stato `generating` appeso.

## Trigger model (decisione)

**Pigro on-demand**: la rigenerazione parte al click sull'icona quando l'hash è cambiato. Un **cron notturno** (Action schedulata) che pre-rigenera gli stale è una **migliora futura** opzionale (riduce le attese), non nell'MVP.

## Fuori scope (MVP)

- Cron di pre-rigenerazione (futuro).
- Firma elettronica (software del cliente, già fuori perimetro).
- Anteprima PDF in-browser senza download (futuro).
- Allegati propri della gemella (oggi gli allegati restano per-sede; la gemella eredita il DVR ma non gli allegati della compagna — da valutare se serve).

## Punti da definire nel piano

- Esatta composizione della fingerprint e quali `updated_at` bumpare.
- Productizzazione del modulo di generazione (lettura Turso + R2, TypeScript vs script Node a parte richiamato dalla Action).
- Forma esatta dell'endpoint di stato/serve + polling lato client.
- Setup secrets GitHub + token di dispatch su Vercel.
- Strategia per il seed iniziale (Action "all" vs script locale).
