817a8cd5d1
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>
201 lines
16 KiB
Markdown
201 lines
16 KiB
Markdown
# 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<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
|
|
|
|
**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.
|