La cartella aveva dentro solo rules/ e i settings: nessun posto dove mettere una skill, un hook o un piano. Ora ha lo scheletro completo e un .claude/CLAUDE.md che spiega cosa va dove — non duplica CLAUDE.md di progetto, che resta quello che comanda. Due skill locali (le altre restano globali in ~/.claude/skills/): - /preventivo — la catena agent.ts → schema.ts → assemble.ts → ProposalDeck e i tre modi di romperla, di cui uno solo fa rumore. Nessun prompt di generazione qui dentro: quello vive in agent.ts ed e' l'unico. Porta check-profilo.sh. - /audit — guida scripts/audit-fonti.ts, nuovo, che mette in moto le cinque fonti di src/lib/audit/sources/, in prod dal 2026-08-19 ma mai chiamate da nessuno. Provate su giojello.com: 5 su 5, 42,7 s, PageSpeed mobile 58 / desktop 93. Due hook, provati a mano (6 casi il primo, 5 il secondo): - guardia-migration.sh BLOCCA l'SQL distruttivo sulle entita' protette — il vincolo Data Safety (LOCKED) fatto rispettare dalla macchina invece che dalla memoria. - guardia-token.sh AVVISA sulle classi Tailwind grezze. Non blocca: con ~450 occorrenze di debito, bloccare lo renderebbe un ostacolo da disattivare. I tre piani di v2.5 entrano nel repo: stavano solo in ~/.claude/plans/ e STATE.md avvertiva che senza quelli la milestone non era ricostruibile. Passati al setaccio per credenziali prima di committarli. Corretta in rules/memory-discipline.md la chiave della memoria persistente: e' …-Vault-IAMCAVALLI-hub, non quella del workspace. Sedici file stavano nella prima, la regola indicava la seconda. Impeccable resta abilitato solo a livello globale: fuori da settings.json locale. Nessun tocco al prodotto. Build e lint verdi, lint identico al baseline. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
16 KiB
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
generale40 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:
- Creare un progetto Google Cloud, abilitare PageSpeed Insights API e Chrome UX Report API, generare una chiave (gratuita, nessuna carta). In
.env.locale in Coolify comePAGESPEED_API_KEY. - Una chiamata a mano, e verificare che esistano davvero:
lighthouseResult.fullPageScreenshot.screenshot.data→data:image/jpeg;base64,…a pagina interalighthouseResult.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:
- Validazione Zod di ogni output di modello. Lo spike fa
as Record<string, unknown>e stampa.proposal/schema.tsè il precedente:safeParse, fallimento duro, nessun loop di riparazione. - 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.
- Heartbeat a ogni passo.
audit_runs.heartbeat_at; una run senza battito da N minuti va inerror, "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 <AuditVisitTracker> montata nella pagina pubblica chiama una server action fire-and-forget:
- all'mount → evento
view - su
window.print()e sull'eventobeforeprint→ eventoprint
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] = nulle 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
generaleha 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:
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..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:
npm run buildpulito- Migration applicata in prod prima del push; le sette tabelle esistono
- 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
- Ogni numero nel documento consegnato ha una fonte rintracciabile in
audit_runs.raw. Nessuna percentuale inventata - Un sito senza dati CrUX: il documento lo dice, non lascia un buco
- Riavviare il container a metà analisi: la run va in
error, "Rilancia" riparte senza rifare i passi completati - Upload di un'immagine → redeploy → l'immagine è ancora lì (prova del volume)
- Aprire il documento consegnato in incognito: compare una riga in
audit_visits. Aprirlo da admin loggato: non compare. Premere "Scarica PDF": compare una rigaprint - 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
- Rete disattivata dopo il primo caricamento: i font restano quelli giusti (prova che sono self-hostati)
- 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.