# Audit — motore multi-agente, rilevazioni e tracciamento ## Context Il piano approvato il 2026-08-16 (`dovremmo-fare-una-cosa-woolly-puddle.md`) resta in vigore per struttura del documento, versionamento del template, rubrica checklist, editor admin, pagina pubblica e print CSS. **Questo file ne sostituisce tre parti** — §6 (motore), §7 (immagini) e il tracciamento in §5/§8 — e non tocca il resto. Cosa è successo da allora: - **Lo spike è girato su giojello.com** e ha prodotto un audit di buona qualità. Ma `psi: {}` — le rilevazioni PageSpeed sono fallite tutte per quota anonima esaurita. Quell'audit è stato scritto **con zero misurazioni**, solo HTML statico. - **Il 52% della checklist non è verificabile da HTML statico** (204 voci → 107 non verificabili; lo step `generale` 40 su 50). Barra di ricerca, widget carrello, hover, animazioni, banner cookie esistono solo a runtime. - **Il VPS non può ospitare Chromium**: 1.492 MB di RAM disponibili su 3.819, 736 MB già in swap, 2 vCPU, con Coolify/n8n/Gitea/Vaultwarden sopra. Il disco invece è libero (21 GB su 38). Immagine `node:20-alpine`, non supportata da Playwright. - **Il processo bersaglio è: incolli un URL, il motore fa ricerche incrociate con le API gratuite, esce un documento pronto da inoltrare.** Tu carichi a mano solo due immagini: hero prima e hero dopo, JPG ≤ 0,5 MB. ### Decisioni | Punto | Decisione | Data | |---|---|---| | Renderer headless | **Nessuno, da nessuna parte.** Motore 100% in-app, self-service | 2026-08-18 | | Buco a runtime | Coperto da audit Lighthouse completi + screenshot renderizzato letto in visione + CrUX + Wayback | 2026-08-18 | | Immagini manuali | Solo le due del redesign, JPG ≤ 0,5 MB | 2026-08-18 | | Screenshot stato di fatto | Automatici, da PageSpeed | 2026-08-18 | | Tracciamento | Registro delle singole visite + evento stampa | 2026-08-18 | **Due decisioni del 2026-08-17 sono superate da questa revisione** e vanno corrette prima di eseguire (§8): il renderer locale sul Mac e la frase "la copia master è la cartella di cattura locale" scritta in `CLAUDE.md` — non esiste più nessuna cattura locale. --- ## 1 · Prerequisito bloccante — chiave PageSpeed e verifica dell'ipotesi Tutto il resto poggia su un'ipotesi **non ancora verificata**: la quota anonima risponde `429` e senza chiave non si prova. Primo passo, prima di scrivere codice: 1. Creare un progetto Google Cloud, abilitare *PageSpeed Insights API* e *Chrome UX Report API*, generare una chiave (gratuita, nessuna carta). In `.env.local` e in Coolify come `PAGESPEED_API_KEY`. 2. Una chiamata a mano, e **verificare che esistano davvero**: - `lighthouseResult.fullPageScreenshot.screenshot.data` → `data:image/jpeg;base64,…` a pagina intera - `lighthouseResult.audits` → l'insieme completo (~150 voci), non i 10 scalari che lo spike estrae oggi **Se lo screenshot a pagina intera non c'è**, ripiego su `lighthouseResult.audits["final-screenshot"]` (solo viewport, quindi di fatto l'hero — che è comunque il blocco più importante). **Se non c'è nemmeno quello**, l'analisi visiva esce dalla fase 1 e gli screenshot dello stato di fatto tornano manuali: cambia §5 di questo piano, non il resto. > ⚠️ Non costruire il sub-agent visivo prima di aver visto quel campo con i tuoi occhi. --- ## 2 · Le fonti — "tutte le api gratuite che servono" | Fonte | Cosa dà | Chiave | Alimenta | |---|---|---|---| | **PageSpeed Insights v5** (mobile + desktop) | ~150 audit Lighthouse, punteggi, screenshot renderizzato a pagina intera | sì, gratuita | blocco 3, finding tecnici, sub-agent visivo | | **CrUX API** | LCP/INP/CLS **p75 di utenti reali**, per origin e per URL | stessa chiave | blocco 3 — la distinzione lab/campo | | **Wayback CDX** | storico degli snapshot, diff del testo hero rispetto a 1/3/5 anni fa | no | la tesi "l'azienda è cresciuta, il sito no" | | **RDAP** (`rdap.org`) | età del dominio | no | contesto in blocco 3 | | **Mozilla HTTP Observatory** | voto sugli header di sicurezza | no | segnali di fiducia | | **Fetch diretto** | `robots.txt`, `sitemap.xml` (→ `pagine_indicizzate`), JSON-LD, `hreflang`, impronta della piattaforma | no | blocco 3, finding tecnici | | **Le pagine del sito** | già nello spike: home + fino a 3 pagine per profilo | no | verifica checklist | **Perché questo elenco e non altro:** ognuna di queste è una **rilevazione**, non una stima. È esattamente ciò che impone la disciplina sui numeri del piano approvato — un numero entra nel documento solo se misurato. CrUX in particolare porta dati di utenti veri, la cosa più difendibile che si possa scrivere in un audit. **Il guadagno più grande è già in casa e viene buttato via:** lo spike chiama PageSpeed e ne estrae 10 numeri. Gli audit Lighthouse coprono da soli una fetta consistente del 52% non verificabile — `color-contrast`, `tap-targets`, `font-size`, `image-alt`, `link-text`, `crawlable-anchors`, `structured-data`, `unsized-images`, `errors-in-console`, `viewport`, `canonical`. Sono verifiche fatte **sul DOM renderizzato**, cioè proprio quello che l'HTML statico non vede. --- ## 3 · Il motore `src/lib/audit/`, modellato su `src/lib/proposal/` ma multi-step e con fan-out: ``` src/lib/audit/ pipeline.ts orchestratore: (auditId) => risultato. Non sa chi lo chiama sources/ raccolta dati, nessun LLM fetch.ts pagine + estrazione testo (dallo spike, quasi invariato) pagespeed.ts PSI: audit completi + screenshot + estrazione scalari crux.ts dati di campo history.ts Wayback CDX + diff signals.ts RDAP, header, robots/sitemap/JSON-LD agents/ un file per sub-agent, ognuno con prompt e schema Zod propri checklist.ts verificatore, Sonnet, batch da 12 per step di funnel visual.ts analisi visiva, Opus con input immagine history.ts scarto fra il sito di allora e l'azienda di oggi, Sonnet technical.ts audit Lighthouse + header + dati strutturati, Sonnet synthesis.ts sintetizzatore, Opus schema.ts Zod per ogni output di sub-agent + per la sintesi ``` **Sequenza.** Raccolta in parallelo (I/O di rete puro) → quattro sub-agent in parallelo, ognuno sul proprio materiale → sintetizzatore che riceve i quattro output e **incrocia**. **Cosa vuol dire "incrociate", in concreto.** I sub-agent producono osservazioni, non finding. È il sintetizzatore che le fonde: > checklist: "nessun segnale di fiducia sulla scheda prodotto" + tecnico: "nessun dato strutturato `Product`/`Review`" + storico: "il testo dell'hero non cambia dal 2021" + visivo: "le recensioni sono sotto tre schermate di scroll" > → **un solo finding**, con quattro evidenze indipendenti, invece di quattro finding deboli. Questo serve la regola di selezione del piano approvato: il documento promette "non un elenco di quaranta punti", e la forza sta nella selezione. Massimo 10 problemi, ordinati per impatto. **Tre cose che lo spike non fa e vanno fatte:** 1. **Validazione Zod di ogni output di modello.** Lo spike fa `as Record` e stampa. `proposal/schema.ts` è il precedente: `safeParse`, fallimento duro, nessun loop di riparazione. 2. **Concorrenza limitata e retry.** Fan-out sì, ma con un tetto (4 chiamate contemporanee) e un retry con backoff sulle 429/529 di Anthropic. Lo spike non ha né l'uno né l'altro. 3. **Heartbeat a ogni passo.** `audit_runs.heartbeat_at`; una run senza battito da N minuti va in `error`, "Rilancia" riparte dall'ultimo passo completato. È la mitigazione già prevista per il redeploy che uccide il job. **Da riusare invariati dallo spike:** i blocchi `SICUREZZA` e `NUMERI`, la funzione `fence()` che neutralizza i tag di chiusura, e l'estrazione del primo blocco `text` con guardia su `stop_reason === "max_tokens"` (più robusta di `content[0]` in `proposal/agent.ts`). **Impronta sul VPS:** I/O di rete e JSON. Nessun processo pesante, nessun Chromium. Il picco è la dimensione dello screenshot in memoria prima dell'invio ad Anthropic — vedi §7. --- ## 4 · Schema — differenze rispetto a §5 del piano approvato La migration resta `0017_audits.sql`, additiva e idempotente, applicata in prod **prima** del push. Rispetto all'elenco già approvato: **Nuova tabella `audit_visits`** — il registro delle singole visite: `id`, `audit_id` CASCADE, `occurred_at` default now, `event` CHECK `('view','print')`, `referrer`, `user_agent`, `ip_hash`. Indice su `(audit_id, occurred_at desc)`. L'IP **non si salva in chiaro**: SHA-256 di `ip + NEXTAUTH_SECRET` via Web Crypto, come già si fa per il digest del gate admin in `src/lib/admin-gate.ts`. Serve a distinguere due aperture dello stesso lettore da due lettori diversi, non a identificare qualcuno. **Campi aggiunti su `audits`** — separare laboratorio e campo, perché è la distinzione che rende credibile il blocco 3: `lcp_field`, `inp_field`, `cls_field` (numeric, da CrUX, nullable — un sito senza traffico sufficiente non ha dati di campo, ed è un'informazione anch'essa). **Restano come da piano approvato** le colonne di roll-up `first_viewed_at` / `last_viewed_at` / `view_count`: sono la lettura veloce per la lista admin, aggiornate insieme alla riga di `audit_visits`. **`screenshot_desktop_url` / `screenshot_mobile_url`** ora li scrive la pipeline, non tu. --- ## 5 · Immagini **Due upload manuali per audit**, non di più: `redesign_prima_url` e `redesign_dopo_url`. JPG, limite **512 KB**. Il componente `ImageUploadField` accetta `image/jpeg|png|webp` — non ha senso rifiutare un PNG per principio — ma il limite di dimensione è quello. **Due file scritti dalla pipeline**: gli screenshot PageSpeed mobile e desktop, decodificati da base64 e salvati come JPG sullo stesso volume. Quattro file per audit, circa 1,5 MB. Il volume Coolify su `/app/uploads` e la lettura via `src/app/api/uploads/[...path]/route.ts` con guardia sul path traversal restano come da §7 del piano approvato — con un margine molto più comodo di quanto si era dimensionato. > ⚠️ **Checkpoint bloccante.** Volume creato in Coolify **prima** del deploy, altrimenti gli upload si perdono a ogni redeploy. --- ## 6 · Tracciamento Un'isola client `` montata nella pagina pubblica chiama una server action fire-and-forget: - **all'mount** → evento `view` - **su `window.print()`** e sull'evento `beforeprint` → evento `print` La server action, lato server, chiama `getServerSession`: **se c'è una sessione admin non scrive niente.** È così che l'anteprima admin non inquina i numeri — requisito già scritto nella verifica del piano approvato (test 11). Perché un'isola client e non una scrittura nel render RSC: la pagina ha `revalidate = 0` e quindi ri-esegue a ogni richiesta, ma scrivere sul DB dentro un render è un anti-pattern Next e conterebbe anche i prefetch e i bot. La convenzione del progetto è comunque "le mutazioni sono Server Actions". Lato admin: il registro visite compare nell'editor dell'audit, sotto forma di elenco cronologico. --- ## 7 · Rischi da mettere in conto - **Lo screenshot a pagina intera può essere altissimo** (una home lunga arriva a 10-15.000 px). Va ridimensionato prima di mandarlo a Opus, o mandato a fette. Non risolto: da misurare sul primo audit vero. - **Latenza PageSpeed**: 30-60 s per strategia, due strategie. Il job in background lo regge, ma incide sul tempo totale — stimare il totale della pipeline dopo il primo giro completo, non prima. - **Wayback e Observatory sono lenti e ballerini.** Ogni fonte deve fallire in modo non fatale, come già fa PageSpeed nello spike (`psi[s] = null` e si prosegue). - **CrUX non risponde per i siti a basso traffico.** Non è un errore: è un dato. Il documento deve saperlo dire ("non ci sono abbastanza visitatori perché Google raccolga dati di campo") invece di lasciare un buco. - **La ripetibilità è già stata vista traballare**: lo step `generale` ha dato 0 non conformi nel giro completo e 2 in quello isolato, a parità di sito e voci. Con il fan-out il rischio non diminuisce. Da tenere sotto osservazione al primo audit di prova. --- ## 8 · Da correggere prima di eseguire Due cose scritte ieri sono ora sbagliate: 1. **`CLAUDE.md`, deroga a LOCKED #5** — dice "la copia master è la cartella di cattura locale". Non esiste più nessuna cattura locale. Sostituire con la formulazione di §11 del piano approvato, che è già corretta e non parla di copie locali. 2. **`.planning/STATE.md`** — la decisione "il renderer headless gira in LOCALE sul Mac" va riscritta come "nessun renderer headless: il buco a runtime è coperto dagli audit Lighthouse e dallo screenshot PageSpeed". Il *perché* il VPS non regge Chromium resta valido e va tenuto: è la ragione per cui l'opzione non tornerà. Stessa correzione nel file di memoria persistente `project_clienthub_vps_no_headless.md`. --- ## 9 · File toccati **Nuovi** — `src/db/migrations/0017_audits.sql` · `src/lib/audit/pipeline.ts` · `src/lib/audit/sources/{fetch,pagespeed,crux,history,signals}.ts` · `src/lib/audit/agents/{checklist,visual,history,technical,synthesis}.ts` · `src/lib/audit/schema.ts` · `src/lib/audit/template/{v1,index}.ts` · `src/lib/audit-queries.ts` · `src/app/audit/[slug]/page.tsx` · `src/components/public/audit/*` (uno per blocco, più `AuditVisitTracker`) · `src/app/admin/audit/**` · `src/components/admin/audit/AuditEditor.tsx` · `src/components/ui/ImageUploadField.tsx` · `src/app/api/uploads/[...path]/route.ts` **Modificati** — `src/db/schema.ts` · `src/proxy.ts` (aggiungere `/audit` al matcher, con `rateLimit`) · `next.config.ts` (`X-Robots-Tag: noindex, nofollow`) · `src/app/globals.css` (blocco `@media print`) · `src/components/admin/AdminSidebar.tsx` · `CLAUDE.md` · `.planning/STATE.md` · Dockerfile + volume Coolify **Riusati senza modifiche** — `src/lib/rate-limit.ts` · `src/components/public/proposal/RichText.tsx` · `src/lib/admin-gate.ts` (per l'hash) · `src/app/admin/offers/actions.ts` come modello di editor a payload intero · `@dnd-kit/sortable` per il riordino --- ## 10 · Verifica Non c'è test suite: **`npm run build` è la verifica di riferimento** (fa il typecheck). **Prima di tutto** — la chiave PageSpeed funziona e `fullPageScreenshot` esiste davvero (§1). Se non esiste, fermarsi e rivedere §5. Poi, nell'ordine: 1. `npm run build` pulito 2. Migration applicata in prod **prima** del push; le sette tabelle esistono 3. Un audit completo su giojello.com: **confrontare la quota di non verificabili con il 52% dello spike**. È la misura che dice se l'approccio senza Chromium ha funzionato 4. Ogni numero nel documento consegnato ha una fonte rintracciabile in `audit_runs.raw`. Nessuna percentuale inventata 5. Un sito senza dati CrUX: il documento lo dice, non lascia un buco 6. Riavviare il container a metà analisi: la run va in `error`, "Rilancia" riparte senza rifare i passi completati 7. Upload di un'immagine → redeploy → l'immagine è ancora lì (prova del volume) 8. Aprire il documento consegnato in incognito: compare una riga in `audit_visits`. Aprirlo da admin loggato: **non** compare. Premere "Scarica PDF": compare una riga `print` 9. Cmd+P sul documento: interruzioni di pagina corrette, slider impilato in due immagini, nessun marcatore di lavorazione sopravvissuto, e in **scala di grigi** impatti e metriche restano distinguibili 10. Rete disattivata dopo il primo caricamento: i font restano quelli giusti (prova che sono self-hostati) 11. I test 3, 4, 5, 8, 12 e 13 del piano approvato — blocchi condizionali per livello, salvataggio a metà, re-sync degli id dei figli, riordino, congelamento del template, e nessuna voce `registro='volume'` su un brand premium ⚠️ Playwright non funziona contro `npm run dev` (la CSP blocca `eval`). Vale solo se un giorno servirà un E2E sul portale: usare il build di produzione.