# HANDOFF — Fix feedback cliente round 2 (2026-07-07)

> Continuazione dopo che la generazione PDF DVR è andata **LIVE + seedata (806/806)** il 2026-06-25.
> Il cliente ha testato e mandato feedback. Questo doc cattura lo stato per riprendere dopo `/compact`.
> Contesto della feature base: `docs/superpowers/HANDOFF-2026-06-24-pdf-generation.md`, memoria `pdf_generation_2026_06_24.md` + `infrastruttura_alleanza.md`.

## ⏱️ AGGIORNAMENTO 2026-07-07 (sera) — TUTTO FATTO, RE-SEED IN CORSO

Round feedback pomeriggio COMPLETATO e DEPLOYATO. Commit **`9be3a2a`** pushato su main (deploy Vercel). Dati prod applicati e verificati con PDF di prova SOVERATO (Datore=Andrea Polliot, RSPP=Stefano Boffi **senza mansioni**, didascalia luogo sicuro, no codice immobile, SOVERATO ok). **Re-seed massivo 806 PDF in corso** (run GitHub Actions `28863458473`, ~2h). Restano solo: verifica live a fine re-seed (dashboard tasto ZIP per regione + qualche PDF) + salvataggio memoria.

- **#10** tasto "Scarica tutti i DVR" → popup 12 regioni + lock (un download alla volta), endpoint `api/dvr/zip/[regione]` (archiver **v7**, STORE; scarica ogni PDF in buffer). ⚠️ `npm run build` è gate obbligatorio: archiver v8 rompe l'interop ESM (`import ... from 'archiver'` "no default export"), risolto pinnando **archiver@^7.0.1**.
- **#11** didascalia luogo sicuro: 328 righe su prod (script `import-tools/scripts/import_luogo_sicuro_descr.py` + `didascalie.json`), campo editabile nel tab inquadramento, resa nel PDF sotto la foto. DOCX (197) restano vuoti (didascalia inesistente all'origine).
- **Nomi Datore/RSPP**: ora `Andrea Polliot` / `Stefano Boffi`, qualifiche VUOTE.
- **#12 CACHE ZIP per regione** (proposta Paolo, round successivo): tabella `dvr_zip` (fingerprint = set PDF ready + data_hash + TEMPLATE_VERSION); endpoint `api/dvr/zip/[regione]` ora risponde `{ url }` con **presigned URL R2** → download diretto da Cloudflare, Vercel NON trasferisce i 180MB (ottimizzazione richiesta da Paolo per Vercel Hobby). Modulo `$lib/server/zip.ts` (regionZipState), `r2.ts` +putStream (lib-storage) +presignedGetUrl (s3-request-presigner). Modale: mappa 14 etichette leggibili (validate Paolo) + no truncate + `max-w-2xl`; download via `{url}` (lock solo sulla preparazione). **Review adversariale** (wf 3 lenti, 11 confirmed) → FIXATI: cache-poisoning se PDF mancante su R2 (conto missing → NON aggiorno cache + console.error, self-heal), `export const config={maxDuration:300}`, appended===0→500. Deps aggiunte: `@aws-sdk/lib-storage`, `@aws-sdk/s3-request-presigner` (archiver già a v7). **Schema prod `dvr_zip`**: creare via `app/setup_dvr_zip.mjs` (classifier blocca l'auto-mode → esegue Paolo con `!`). Limitazioni note (segnalate, non fixate): concorrenza multi-utente stessa regione (raro), lifecycle rule R2 AbortIncompleteMultipartUpload (config Cloudflare).
- **NOTA DATI da segnalare a Paolo**: 3 `source_file` risultano su 2 righe `dvr` in prod (`ALATRI_TECCHIENA`, `MARSALA_TABACCARO`, `ROMANO LOMBARDIA_COLOGNO AL SERIO`, tutti Rev00_30112021) — possibile duplicato d'import o gemelle pre-migrazione. Non impatta le didascalie (stesso testo su entrambe). Fuori perimetro di oggi.

---

## STATO SINTETICO

Feature PDF **completa, live, seedata** (deploy `ee1f0d1`). Ora si lavora su un **round di feedback** del cliente. Le modifiche di codice per i 7 punti originali sono **fatte ma NON committate** (working tree sporco su `main`). #8 (mansioni) e #9 (firma) chiusi senza codice (firma già identica al prototipo; mansioni prima confermate poi RITRATTATE). **Round pomeriggio 2026-07-07**: (a) nomi Datore/RSPP di nuovo cambiati → `Andrea Polliot` / `Stefano Boffi`, **senza mansioni/titoli**; (b) 2 NUOVI requisiti che richiedono CODICE — **#10** tasto "scarica tutti i PDF" nella card dashboard "DVR COMPILATI", **#11** didascalia luogo sicuro (import ~800 file + campo editabile + resa nel PDF). Restano: implementare #10 e #11, fix dati prod (`app_settings` + typo SOVERATO), **un solo** commit+deploy, **un solo re-seed massivo degli 806 PDF** (che includerà nomi nuovi + didascalia).

## I 9 REQUISITI (7 originali + 2 nuovi)

1. ✅ (codice) PDF: rimuovere ogni "Codice immobile" (copertina + tabella §1) — tenere solo codice fisico.
2. ✅ (codice) Nascondere codice immobile nel gestionale (lista, scheda, form) SENZA azzerarlo in DB alle modifiche.
3. ✅ (codice) Lista agenzie, colonna CODICE → codice **fisico** (era immobile).
4. ✅ (codice) Scheda agenzia, codice sotto il nome → codice **fisico**.
5. ✅ (codice) Lista agenzie, colonna DVR: mostrava "vuoto" per tutte → ora link diretto **"Scarica PDF"** quando `dvr_pdf.stato='ready'` (LEFT JOIN). *(Vedi nota "vuoto" sotto.)*
6. ⏳ (DATI) PDF: Datore di Lavoro + RSPP mancavano ("—") → causa: `app_settings` globale VUOTO. Da popolare: `datore_lavoro_nome='Dott. Davide Passero'`, `rspp_nome='Ing. Antonino Abate'` (verificato: costanti di fatto, la variabilità era rumore d'import docx). Il template li renderizza già.
7. ✅ (codice) Bump `TEMPLATE_VERSION` v11→v12 (invalida cache PDF vecchi).
8. ❌ **ANNULLATO 2026-07-07 (pomeriggio)**: il cliente RITRATTA le mansioni. Vuole **solo i nomi, niente qualifiche/titoli** ("senza mettere altro"). → `datore_lavoro_qualifica=''`, `rspp_qualifica=''`. Il template gestisce già il caso vuoto (`cover-firma-sub` diventa un div vuoto, innocuo). Nessuna modifica di codice.
9. ✅ (DATI, nessun codice) **"uniformare anche la Firma" — RISOLTO senza codice**: confronto FATTO (2026-07-07) → la sezione firma di `app/scripts/pdf/template.ts` (markup 158-169, CSS 660-666) è **identica bit-a-bit** al prototipo approvato `pdf-prototype/template.js` (markup 164-175, CSS 666-672). I PDF reali seedati mostravano `—` nei riquadri firma solo perché `app_settings` era VUOTO. Popolando i 4 campi (nomi+qualifiche), i riquadri firma diventano identici al PDF di prova. **Stessa causa radice di #6 e #8.**

## NUOVI REQUISITI 2026-07-07 (round pomeriggio) — DA FARE, richiedono CODICE

10. ⏳ **NUOVO — "Scarica tutti i PDF in una volta"**: pulsante nella **dashboard**, dentro la card "DVR COMPILATI 800" (indicazione esplicita del cliente). Serve un endpoint che produca uno **ZIP di tutti gli 806 PDF** letti da R2 (chiavi `pdf/{codice_fisico}_DVR.pdf`). ⚠️ DA VALUTARE: peso totale ZIP (806 PDF) e timeout Vercel (streaming ZIP consigliato, es. `archiver` in streaming lato server; oppure generare lo ZIP su R2 e servire un link). Progettare bene: è la parte più "infrastrutturale" del round. Individuare la card dashboard in `app/src/routes/+page.svelte` (o dove sta la dashboard) e aggiungere un `<a>`/`<button>` verso il nuovo endpoint `GET /api/dvr/zip` (o simile).

11. ⏳ **NUOVO — Didascalia luogo sicuro (foglio Conclusione, cella riga 54)**: sotto la foto del luogo sicuro c'è una **didascalia testuale** (es. "LUOGO SICURO ESTERNO - Marciapiede antistante Via Solferino…"). Il cliente la vuole **editabile nel gestionale** + **mostrata nel PDF come didascalia sotto la foto**.
    - **VERIFICATO 2026-07-07 (dev.db `import-tools/db/dev.db`)**: il campo `dvr.luogo_sicuro_esterno_descr` ESISTE in schema (`schema.ts:329`) ma è **VUOTO per ~tutti**: su 800 righe solo **1** popolata (residuo del sync 4-DVR `gen_sync_4dvr.py`). `conclusione_testo` = 0 popolate. Il parser `parse_conclusione_xlsx` (import_dvr.py:410) cattura solo testo >80 char e salta le righe `ALL*` → **NON legge la cella riga 54**.
    - **QUINDI: la didascalia NON è importata. Va importata da zero per gli ~800 file.** Lavoro in 3 parti: (a) **IMPORT** — nuovo passo che rilegge la cella riga 54 del foglio Conclusione di ogni file (⚠️ 603 xlsx via openpyxl leggono la cella; 197 **docx** hanno struttura tabellare diversa → gestire a parte o accettare copertura parziale); popola `luogo_sicuro_esterno_descr`. (b) **GESTIONALE** — campo editabile nel form/compilazione DVR (tab Conclusione). (c) **PDF** — renderizzare la didascalia sotto l'immagine del luogo sicuro in `template.ts` (individuare dove si stampa l'immagine `luogo_sicuro_esterno` nella sezione Conclusione/allegati). Poi il re-seed la includerà.
    - ⚠️ Chiedere a Paolo: per i 197 docx la cella "riga 54" non esiste come tale — decidere se importare solo dagli xlsx e lasciare i docx editabili a mano, o cercare la didascalia anche nel docx.

## MODIFICHE DI CODICE GIÀ FATTE (uncommitted, working tree su `main`)

6 file, `npm run check` = 0 errori, `npm run build` = OK. Diff salvato in `.superpowers/sdd/feedback-fixes.diff`.
- `app/scripts/pdf/template.ts` — rimosso codice_immobile da copertina (cover-sede-codice) + riga tabella §1.
- `app/src/lib/server/pdf/fingerprint.ts` — `TEMPLATE_VERSION` v11→v12.
- `app/src/routes/agenzie/+page.server.ts` — import `dvrPdf`; rimosso `like(codiceImmobile)` dalla ricerca; rimosso `codiceImmobile` dalla select; aggiunto `pdfStato: dvrPdf.stato` + `.leftJoin(dvrPdf, eq(dvrPdf.agenziaId, agenzie.id))` SOLO sulla query principale.
- `app/src/routes/agenzie/+page.svelte` — CODICE col → `{a.codiceFisico}`; colonna DVR riscritta 4-way (ready→link `/api/dvr/{id}/pdf`, generating→ambra, hasDvr→"DVR compilato", else→"—").
- `app/src/routes/agenzie/[id]/+page.svelte` — codice sotto nome → `{a.codiceFisico}`.
- `app/src/lib/components/AgenziaForm.svelte` — campo Codice Immobile → `<input type="hidden">` (nasconde ma preserva il valore; NON azzera in DB).

**Verifica adversariale FATTA** (workflow `wf_f50133dd-554`, 3 lenti). Esito:
- 🔴 IMPORTANTE (TROVATO E GIÀ FIXATO): il link "Scarica PDF" della lista usava `dvr_pdf.stato` grezzo (non hash-aware) ma l'endpoint GET è hash-aware → col bump v12 tutte le righe diventano `stale` → 409 fino a fine re-seed (e in generale a ogni modifica DVR). **Fix applicato**: il link lista ora passa `?allowStale=1` (`+page.svelte`, serve comunque il PDF precedente; il dettaglio ha il flusso "Aggiorna"). ⚠️ Ri-lanciare `npm run check` (fix banale su URL, non dovrebbe rompere) e rigenerare il diff.
- 🟡 MINOR (opzionali, NON fixati): (a) `fingerprint.ts:53` la SELECT include ancora `codice_immobile` nell'hash — innocuo (campo non più editabile) ma residuo; volendo rimuovere per coerenza. (b) `+page.svelte` colonna DVR: `stato='error'` cade nel branch "DVR compilato" senza segnalare l'errore — dopo il re-seed dovrebbero essere 0 error, bassa priorità; volendo aggiungere un branch error.

## COSA MANCA (in ordine)

1. **Leggere esito workflow** `wf_f50133dd-554` → fixare eventuali bloccanti.
2. ~~**Nuovi requisiti**~~ ✅ CHIUSI 2026-07-07 senza codice — confluiscono nel fix-dati del punto 3 (mansioni confermate + firma già identica al prototipo).
3. **Fix DATI su prod** (via libsql client, vedi gotcha):
   - `UPDATE app_settings SET datore_lavoro_nome='Andrea Polliot', datore_lavoro_qualifica='', rspp_nome='Stefano Boffi', rspp_qualifica='', updated_at=<now> WHERE id=1` — **⚠️ NOMI AGGIORNATI dal cliente 2026-07-07 (pomeriggio), SOSTITUISCONO Passero/Abate di stamattina.** Il cliente vuole **SOLO i nomi, SENZA titoli né mansioni** ("senza mettere altro") → qualifiche VUOTE. (Nomi esatti come scritti dal cliente: `Andrea Polliot`, `Stefano Boffi`, nessun "Dott."/"Ing.".)
   - `UPDATE agenzie SET nome_agenzia='SOVERATO', updated_at=<now> WHERE id=698` (era "SOVERTAO", typo).
4. **Smoke locale**: `cd app; set -a; source .env.pdf.local; set +a; npx tsx scripts/pdf/generate.ts 698` (usa codice LOCALE + DB/R2 PROD). Poi verificare il PDF: scaricare da R2 (`pdf/25702_DVR.pdf`, codice fisico di 698) e controllare con `pdftotext` che: NON contenga "immobile", contenga i nomi + mansioni DL/RSPP, "SOVERATO" (non SOVERTAO), spazio firma uniforme.
5. **Commit** tutte le modifiche di codice + **push** su `main` (= deploy Vercel automatico). *(Chiedere GO a Paolo prima del deploy — è la prassi.)*
6. **Re-seed massivo**: `gh workflow run generate-pdf.yml -f agenzia_id=all --repo paoloalby/alleanza-dvr`. Usa il codice DEPLOYATO → push PRIMA. ~2h. Monitor con ScheduleWakeup ogni ~25 min: `gh run view <id>` + `SELECT stato, COUNT(*) FROM dvr_pdf GROUP BY stato`. Atteso 806 ready.
7. **Verifica live** (Playwright/claude-in-chrome): lista mostra "Scarica PDF"; scheda mostra codice fisico; PDF scaricato ha DL/RSPP+mansioni, niente codice immobile, 698=SOVERATO.

## COORDINATE / GOTCHA (IMPORTANTISSIMO)

- **Credenziali prod**: `app/.env.pdf.local` (gitignored) ha tutte 6 le vars: `DATABASE_URL`, `DATABASE_AUTH_TOKEN` (token Turso coniato), `R2_ACCOUNT_ID`, `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`, `R2_BUCKET`. Sourcing: `set -a; source app/.env.pdf.local; set +a`.
- **Turso CLI è SLOGGATA** (scade in fretta). NON usare `turso db shell`. Usare il **client libsql** via node da `app/`:
  `node --input-type=module -e 'import {createClient} from "@libsql/client"; const db=createClient({url:process.env.DATABASE_URL,authToken:process.env.DATABASE_AUTH_TOKEN}); ...'`
  ⚠️ Per LIKE/stringhe usare query PARAMETRIZZATE `{sql:"... LIKE ?", args:["%x%"]}` — le doppie virgolette in SQL sono IDENTIFICATORI in SQLite (errore "no such column").
- **gh CLI autenticata** (scope repo+workflow). Repo `paoloalby/alleanza-dvr`. 6 GitHub secrets impostati. Vercel env: `GITHUB_DISPATCH_TOKEN` + `PUPPETEER_SKIP_DOWNLOAD` impostati. Prod servito = `ee1f0d1`.
- **Fingerprint/cache**: `app_settings` è GLOBALE e incluso nel fingerprint di OGNI agenzia → popolarlo cambia tutti gli hash → tutti stale → il re-seed rigenera tutti. Anche il bump v12 invalida tutti. Coerente col re-seed massivo.
- **NOTA "vuoto"**: perché la colonna DVR mostrasse "vuoto" per TUTTE non è stato spiegato (il codice `hasDvr` deployato è corretto e in query diretta torna 1). Sidesteppato: la nuova colonna si basa su `dvr_pdf.stato` (LEFT JOIN), quindi dopo il re-seed mostra "Scarica PDF" per tutte. **Verificare dal vivo dopo il deploy** che non sia più "vuoto".
- **Datore/RSPP per-agenzia**: `agenzie.datore_lavoro`/`rspp` esistono ancora (per-sede, incoerenti: 206/209 null=docx, qualche typo). NON usati più dal PDF (usa il globale). La scheda agenzia "Note tecniche" mostra ancora il per-agenzia — valutare se uniformare anche lì al globale (fuori scope attuale, segnalare a Paolo).
- **Generazione**: locale (1 agenzia, codice locale + prod) = `npx tsx scripts/pdf/generate.ts <id>`. Massiva = Action `agenzia_id=all` (codice deployato). Chiave R2 = `pdf/{codice_fisico}_DVR.pdf`.

## REGOLE DI LAVORO
Ultracode ON (xhigh + workflow). Paolo: piccoli step, mostra output, GO esplicito prima di deploy/azioni prod. Cliente non tecnico. Firma commit come da template sessione.
