Files
clienthub/.claude/plans/v2.5-audit-motore.md
simone 817a8cd5d1 chore(claude): architettura base .claude — skill preventivo e audit, hook di guardia, piani nel repo
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>
2026-08-26 16:09:14 +02:00

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 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.datadata: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<string, unknown> 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 <AuditVisitTracker> 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

Nuovisrc/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

Modificatisrc/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 modifichesrc/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.