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>
This commit is contained in:
@@ -0,0 +1,413 @@
|
||||
# Audit — documento di restituzione
|
||||
|
||||
## Context
|
||||
|
||||
iamcavalli vende un servizio di analisi sito in tre livelli (La Radiografia / Il Prima-Dopo / La Rotta). Il deliverable è un documento di restituzione presentato in una call da 40 minuti. Oggi si fa a mano fuori dall'hub.
|
||||
|
||||
Il servizio si chiama **audit** → rotta `/audit/[slug]`. Il nome del livello acquistato compare **solo in copertina**.
|
||||
|
||||
```
|
||||
acquisto Whop ──┐
|
||||
├─► audit creato ─► intake dati ─► agent: analisi profonda
|
||||
creazione manuale ┘ │
|
||||
▼
|
||||
call (fissata a mano) ◄── consegna ◄── revisione + redesign (manuale)
|
||||
```
|
||||
|
||||
Il redesign lo prepari tu mentre gli agent lavorano. **Deve essere possibile creare un cliente e far partire l'audit in manuale, senza acquisto.**
|
||||
|
||||
Volume: ~50 audit/anno. Overhead non-analitico sotto i 10 minuti per audit. Regola guida: *tu scrivi solo analisi e redesign, tutto il resto si popola*.
|
||||
|
||||
### Documento canonico
|
||||
|
||||
La **Spec V1** (blocchi, copy fisso, campi) è il documento canonico. L'Excel *Ecommerce Growthlist* è **rubrica interna del motore di analisi**, non struttura del documento — vedi §4.
|
||||
|
||||
### Verdetto di fattibilità
|
||||
|
||||
Fattibile. Tre pezzi da costruire da zero: **motore di analisi**, **PDF**, **hosting immagini**. Il resto ha precedenti diretti in casa.
|
||||
|
||||
### Conflitti risolti a favore del progetto
|
||||
|
||||
| Spec V1 | Qui | Perché |
|
||||
|---|---|---|
|
||||
| Supabase | Neon + Drizzle | Lo stack è quello |
|
||||
| `uuid` | `text` + `nanoid()` | Convenzione di tutte le tabelle in `schema.ts` |
|
||||
| PDF serverless headless | Print CSS | Deploy Docker su Coolify, non serverless. Stesso risultato, zero infra |
|
||||
| `/r/[slug]?k=[token]`, slug = nome cliente | `/audit/[slug]`, slug nanoid nel path | **Sicurezza** (sotto) |
|
||||
|
||||
**Sullo slug.** `teckell-2026` è indovinabile: la segretezza si sposterebbe tutta sul token in query string, e i parametri di query finiscono nei log d'accesso e nei referrer molto più facilmente di un path. Il `Referrer-Policy: strict-origin-when-cross-origin` già presente mitiga in parte, ma la convenzione collaudata qui è **nanoid non indovinabile nel path, niente query** (`/preventivo/[slug]`, `/quote/[token]`). Un audit nomina un'azienda reale e ne elenca le debolezze: è il contenuto più sensibile che il portale pubblicherà.
|
||||
|
||||
**Due correzioni ad altre premesse:**
|
||||
|
||||
1. **I `gsd-audit-*` non c'entrano.** Verificato: `gsd-audit-milestone` verifica una milestone GSD, `gsd-audit-uat` raccoglie i test in sospeso, `gsd-audit-fix` sistema i finding nel *tuo* codebase. Auditano il progetto, non il sito del cliente. Motore custom; il precedente è `src/lib/proposal/agent.ts`.
|
||||
2. **Il deck dei preventivi non è riusabile come layout.** `ProposalDeck.tsx:151` monta solo la slide corrente e `:133` imposta `body.overflow = "hidden"`: stamparlo produce una pagina sola. Il documento è **a scorrimento**. Si riusa schema e macchina a stati, non il guscio visivo.
|
||||
|
||||
### Decisioni prese
|
||||
|
||||
| Punto | Decisione |
|
||||
|---|---|
|
||||
| Motore | In-app, background, polling dall'admin (§5) |
|
||||
| "La direzione" (blocco 8) | Manuale, foglio bianco, **con il materiale grezzo a fianco** |
|
||||
| PDF | Print CSS — stesso DOM, vincolo "una sola fonte" per costruzione |
|
||||
| Immagini | Volume persistente. **Modifica il vincolo LOCKED #5** (§9) |
|
||||
| Redesign | Immagini caricate **e** link Figma, con ruoli distinti (§6) |
|
||||
| Miglioramento nel tempo | Template versionato: tocca gli audit futuri, mai i consegnati (§3) |
|
||||
| Copertina | Titolo = nome del livello acquistato |
|
||||
| Scadenza | Nessuna. Depubblicazione manuale reversibile |
|
||||
| Tracking | Prima apertura, ultima apertura, conteggio |
|
||||
|
||||
---
|
||||
|
||||
## 1 · Spike sul motore — si parte da qui
|
||||
|
||||
È la parte più incerta e quella su cui si regge tutto il resto. Se la qualità dell'analisi non regge, meglio scoprirlo prima di costruirci sopra schema, editor e documento.
|
||||
|
||||
**Non tocca il database, non tocca l'hub.** Uno script isolato, `scripts/spike-audit.ts`, sul sito di un cliente attuale, che stampa l'output grezzo.
|
||||
|
||||
Deve dimostrare che:
|
||||
- Il fetch delle pagine chiave e l'estrazione del testo reggono su un sito reale
|
||||
- La verifica delle voci di checklist è **affidabile e ripetibile** (§4)
|
||||
- I problemi sono **concreti, non generici** — "il messaggio non è chiaro" non vale niente, "l'headline non nomina il destinatario" sì
|
||||
- La conseguenza per il business è specifica
|
||||
|
||||
Da riusare subito: la protezione da prompt injection di `src/lib/proposal/agent.ts:32-35` (blocco SICUREZZA che dichiara il contenuto come dati, non istruzioni) e `:49` (neutralizzazione dei tag di chiusura). Con HTML scrapato da un sito esterno serve **più** che con le trascrizioni: è contenuto di terzi e può contenere istruzioni ostili.
|
||||
|
||||
> Il resto si esegue **dopo** esito positivo dello spike.
|
||||
|
||||
---
|
||||
|
||||
## 2 · Il documento
|
||||
|
||||
Un solo documento ben progettato; i tre livelli sono **configurazioni** di quello.
|
||||
|
||||
| # | Blocco | Radiografia | Prima/Dopo | Rotta |
|
||||
|---|---|:--:|:--:|:--:|
|
||||
| 1 | Copertina | ● | ● | ● |
|
||||
| 2 | Sintesi | ● | ● | ● |
|
||||
| 2b | **Cosa funziona già** | ● | ● | ● |
|
||||
| 3 | Stato di fatto | ● | ● | ● |
|
||||
| 4 | I problemi, per impatto | ● | ● | ● |
|
||||
| 5 | Analisi per area | ● | ● | ● |
|
||||
| 6 | Il redesign | — | ● | ● |
|
||||
| 6b | **Cosa il redesign non risolve** | — | ● | ● |
|
||||
| 7 | Le ottimizzazioni | — | — | ● |
|
||||
| 8 | La direzione | ● | ● | ● |
|
||||
| 9 | Come si prosegue | ● | ● | ● |
|
||||
|
||||
I blocchi non pertinenti **non esistono nel DOM**, non sono nascosti via CSS.
|
||||
|
||||
Il **copy fisso** di ogni blocco è quello della Spec V1, riportato integralmente in `template/v1.ts` (§3). Nel blocco 9 le *fasi già completate* si derivano dal livello acquistato, e il credito riconosciuto è `importo_pagato`.
|
||||
|
||||
**I due blocchi 2b e 6b sono aggiunte del prototipo Giojello, non della Spec V1**, e vanno tenute:
|
||||
- *Cosa funziona già* costruisce credibilità prima di criticare, e dichiara cosa non va toccato
|
||||
- *Cosa il redesign non risolve* è onesto e commercialmente più efficace della vendita: apre al progetto completo senza promettere
|
||||
|
||||
Campi: `punti_forza` (lista) e `redesign_limiti` (testo).
|
||||
|
||||
### Il prototipo Giojello è il riferimento del template v1
|
||||
|
||||
Il file HTML prodotto per Giojello **non è il documento: è la fonte di `template/v1.ts`** — copy, tipografia, gerarchia. Va spacchettato in componenti alimentati dal record. Quattro difetti da NON portarsi dietro:
|
||||
|
||||
1. **Contenuto cablato nel markup** (699€, 8,0s, 500€, le date). Viola il vincolo "un record → una pagina → un PDF": tutto viene dal DB
|
||||
2. **Google Fonts via `<link>`** — la CSP del progetto è `font-src 'self' data:` e `style-src 'self' 'unsafe-inline'`: verrebbe **bloccato** e il documento cadrebbe sui font di sistema. **Font self-hostati** (Instrument Serif / Inter / IBM Plex Mono, se si conferma quel trio in luogo di Plus Jakarta Sans — è una deroga consapevole a DESIGN-SYSTEM.md, giustificabile perché è un documento pubblico, non la UI admin)
|
||||
3. **Print CSS di sei righe.** Nasconde la legenda interna ma **non i colori che la legenda spiega**: i bordi e i testi rossi/blu dei blocchi di lavorazione finiscono stampati senza più nulla che li spieghi. I marcatori di lavorazione non devono esistere nel documento consegnato
|
||||
4. **Colore come unico portatore di informazione nelle metriche** — `3,4 s` rosso e `1,0 s` verde diventano identici in scala di grigi. Serve un secondo canale (glifo, peso, etichetta)
|
||||
|
||||
⚠️ **Tassonomia degli impatti.** Il prototipo usa cinque valori ("medio-alto", "basso-medio"…), lo schema ne prevede tre. Con cinque l'ordinamento automatico su tre non funziona. Decisione: si resta a **tre valori**, e la sfumatura sta nell'ordine dentro il gruppo (`sort_order`).
|
||||
|
||||
### Regola di collocazione: finding vs analisi
|
||||
|
||||
Il prototipo v2 ha aggiunto una sezione discorsiva "Interfaccia" con otto paragrafi che contengono almeno quattro **veri finding** — contrasto insufficiente sulla CTA della hero, riflesso che mangia il 40% dell'immagine prodotto, titoli troncati, e soprattutto il carrello (spedizione assicurata 39,90€ contro 6,13€ con la non assicurata preselezionata). Quest'ultimo è da primi cinque ed era in coda a una sezione discorsiva.
|
||||
|
||||
**Regola per il motore e per l'editor:** se una cosa ha un impatto e una conseguenza, è un finding — va nel blocco 4, prende un numero e viene ordinata. Il blocco 5 resta discorsivo e resta a **tre aree** (struttura / messaggio / conversione). Nessuna quarta area.
|
||||
|
||||
Motivo: il documento promette "non un elenco di quaranta punti", e una seconda lista non ordinata dopo quella ordinata annulla la promessa. La forza del documento è la **selezione**.
|
||||
|
||||
### Disciplina sui numeri — vincolo del motore
|
||||
|
||||
Confronto con un audit parallelo dello stesso sito prodotto da un altro modello: conteneva "~65% abbandono stimato" e "+60% velocità mobile immediata". **Nessuno dei due è misurato**: sono congetture presentate come rilevazioni.
|
||||
|
||||
Il prototipo Claude è disciplinato su questo — dice "una parte importante del traffico" proprio perché non può quantificarla. **Quella disciplina va imposta nel prompt di sistema del motore**: un numero compare nel documento solo se proviene da una rilevazione (PageSpeed, conteggio DOM, peso pagina, dati del sito). Mai stime di conversione o di guadagno percentuale. In un deliverable premium basta un cliente che verifichi per bruciare tutta la credibilità.
|
||||
|
||||
Voce di verifica: nessun numero nel documento consegnato è privo di fonte in `audit_runs.raw`.
|
||||
|
||||
### Contributi da assorbire dall'audit parallelo
|
||||
|
||||
- **La misura/taglia nella griglia prodotti** — su pezzi unici rigenerati è criterio di scelta primario e non compare da nessuna parte. Si aggancia al finding sull'unicità e apre a un servizio ("messa a misura disponibile"). Va nella checklist profilo `ecommerce` come voce
|
||||
- **Conteggio dei nodi DOM** — quantifica il problema di peso meglio dei MB. Da aggiungere alle rilevazioni di `fetch.ts`
|
||||
- **Stime in giornate** — alimentano la colonna *Impegno* del blocco 7
|
||||
|
||||
### Estensione opzionale — allegato tecnico (NON in fase 1)
|
||||
|
||||
Il blocco 9 promette *"il documento resta tuo e puoi darlo a chiunque lavorerà sul sito"*, ma consegna prosa da imprenditore a uno sviluppatore.
|
||||
|
||||
Un **allegato tecnico generato dallo stesso record** — stessi finding, registro da sviluppatore, con selettori, file coinvolti e stime — renderebbe quella promessa molto più preziosa. È una seconda vista sugli stessi dati, quindi non viola il vincolo della fonte unica e non richiede contenuto aggiuntivo. Da valutare dopo il primo audit consegnato.
|
||||
|
||||
---
|
||||
|
||||
## 3 · Versionamento del template
|
||||
|
||||
Requisito: migliorare il documento deve toccare gli audit **successivi**, mai quelli già consegnati.
|
||||
|
||||
Precedente in casa: `proposals.content` congela profilo e prezzi alla generazione, così modificarli dopo non retro-cambia un preventivo pubblicato.
|
||||
|
||||
Copy fisso e configurazione dei blocchi vivono in **moduli TS versionati**, non nel database:
|
||||
|
||||
```
|
||||
src/lib/audit/template/
|
||||
v1.ts copy fisso di ogni blocco + ordine + mappa livello→blocchi
|
||||
index.ts registry versione→modulo + LATEST_TEMPLATE_VERSION
|
||||
```
|
||||
|
||||
- `audits.template_version` impostata alla versione corrente **alla creazione**
|
||||
- Un audit **consegnato resta congelato** sulla sua versione, per sempre
|
||||
- Migliorare il documento = aggiungere `v2.ts` e alzare `LATEST_TEMPLATE_VERSION`. Nessuna migration, nessun backfill, storia completa in git
|
||||
- Le **bozze** si portano all'ultima versione con un bottone esplicito, mai in automatico
|
||||
|
||||
---
|
||||
|
||||
## 4 · La checklist come rubrica del motore
|
||||
|
||||
L'Excel ha 264 voci su 7 step di funnel, con scoring ICE e peso di importanza per sezione. Serve al **motore**, non al documento.
|
||||
|
||||
**Perché è preziosa:** ogni voce è un'asserzione binaria e verificabile ("Il checkout consente l'acquisto come ospite"). Verificare 264 affermazioni falsificabili è molto più affidabile che chiedere a un modello "analizza questo sito": trasforma l'analisi da generativa a **verificativa**, controllabile voce per voce. E lo scoring ICE risolve gratis l'ordinamento del blocco 4 e le priorità del blocco 7.
|
||||
|
||||
**Tre vincoli emersi dalla lettura del file:**
|
||||
|
||||
1. **È al 73% ecommerce.** Generale (50) e Homepage (21) valgono per qualsiasi sito; Categoria, Scheda Prodotto, Carrello, Checkout, Ringraziamento — **193 voci su 264** — presuppongono un carrello. Servono **due profili**: `ecommerce` e `servizi`. Il secondo va scritto, non esiste nel file.
|
||||
2. **Misura la conformità, non l'adeguatezza.** Un sito può fare 250/264 e continuare a descrivere un'azienda che non esiste più. Nessuna voce chiede se il posizionamento dichiarato corrisponde a quello che vendi oggi, o se le prove sono della fascia di cliente giusta. La checklist è il **pavimento** (guasti meccanici, li trova l'agent); lo scarto strategico è il **soffitto** e resta analisi tua. Se il documento diventa il rendering della checklist, torna a sembrare un audit automatico gratuito.
|
||||
3. **Molte voci sono tattiche da ecommerce a volume** — scarsità, urgenza, countdown, popup di social proof. Su un brand premium **danneggiano**: abbassano il segnale di prezzo mentre tu vendi il contrario. Ogni voce porta quindi un campo `registro` (`volume` / `premium` / `neutro`) e l'audit di un brand premium non propone mai i trigger da discount.
|
||||
|
||||
⚠️ Note tecniche sul file: il foglio *Algoritmo* ha errori **`#REF!`**, e la colonna *Facilità* è **1.0 su tutte le righe** — non compilata, quindi lo Score attuale è di fatto solo Impatto × Confidenza. Da sistemare prima di seminarne i default.
|
||||
|
||||
Il foglio *Best Ecommerce List* (88 siti per settore) alimenta confronti concreti: "il tuo checkout chiede 11 campi, i riferimenti del settore ne chiedono 6".
|
||||
|
||||
**Nel documento** la checklist non compare come elenco. Il blocco 3 mostra al massimo il grado di conformità **per step di funnel** (una barra per step), accanto alle metriche di performance. I problemi del blocco 4 sono una **selezione curata** — voci non conformi ad alto impatto più lo scarto strategico — non il dump delle non conformità.
|
||||
|
||||
---
|
||||
|
||||
## 5 · Migration `0017_audits.sql`
|
||||
|
||||
Additiva e idempotente. Template di stile: `src/db/migrations/0016_retainer_lifecycle.sql` (header in italiano che spiega il *perché*, `ADD COLUMN IF NOT EXISTS`, `CHECK ... NOT VALID` dentro `DO $$ ... pg_constraint`, `CREATE INDEX IF NOT EXISTS`).
|
||||
|
||||
Convenzioni da `src/db/schema.ts`: id `text` con `$defaultFn(() => nanoid())`, **nessun `pgEnum`** (text + CHECK in SQL + tupla `as const` in TS + Zod nell'action), timestamp `withTimezone: true`, `updated_at` bumpato a mano nell'action.
|
||||
|
||||
**`audits`** — colonne scalari, non jsonb: non c'è snapshot da congelare (ci pensa `template_version`) e l'editor mappa 1:1.
|
||||
|
||||
- Identità: `id`, `slug` unique nanoid, `lead_id` → `leads` SET NULL, `client_id` → `clients` SET NULL (nullable entrambi, come `proposals`)
|
||||
- Config: `livello` CHECK `('radiografia','prima_dopo','rotta')`, `template_version`, `profilo` CHECK `('ecommerce','servizi')`, `cliente_nome`, `cliente_referente`, `sito_url`, `importo_pagato numeric(10,2)`, `data_consegna date`
|
||||
- Origine: `origin` CHECK `('manuale','whop')` default `'manuale'`, `external_ref` — predispone Whop senza costruirlo
|
||||
- Stato: `state` CHECK `('draft','published')` default `'draft'` (etichettati *Bozza* / *Consegnata* nella UI), `published_at`
|
||||
- Tracking: `first_viewed_at`, `last_viewed_at`, `view_count integer default 0`
|
||||
- Rilevazioni (blocco 3): `perf_mobile`, `perf_desktop`, `lcp numeric(6,2)`, `cls numeric(5,3)`, `inp`, `pagine_indicizzate`, `screenshot_desktop_url`, `screenshot_mobile_url`, `measured_at`
|
||||
- Contenuto (blocchi 2/2b/5/8): `sintesi`, `punti_forza jsonb` (lista, blocco 2b), `analisi_struttura`, `analisi_messaggio`, `analisi_conversione`, `direzione`
|
||||
- Redesign (blocchi 6/6b): `redesign_sezione`, `redesign_prima_url`, `redesign_dopo_url`, `redesign_razionale`, `redesign_limiti`, `redesign_figma_url`
|
||||
- Intake: `intake jsonb` — i dati che il cliente condivide, forma ancora da definire
|
||||
|
||||
**Tutti i campi di contenuto sono nullable.** È ciò che rende possibile "si salva sempre, anche a metà": la validazione di completezza scatta solo alla consegna.
|
||||
|
||||
**`audit_findings`** (blocco 4) — `audit_id` CASCADE, `titolo`, `impatto` CHECK `('alto','medio','basso')`, `area` CHECK `('struttura','messaggio','conversione','performance')`, `descrizione`, `conseguenza`, `screenshot_url`, `sort_order integer default 0`, `origin` CHECK `('agent','manuale')`
|
||||
|
||||
**`audit_optimizations`** (blocco 7) — `audit_id` CASCADE, `intervento`, `priorita` CHECK `('alta','media','bassa')`, `motivazione`, `impegno`, `sort_order integer default 0`
|
||||
|
||||
**`checklist_items`** (rubrica, versionata come il template) — `profilo`, `step`, `focus`, `testo`, `impatto_default`, `confidenza_default`, `registro` CHECK `('volume','premium','neutro')`, `sort_order`
|
||||
|
||||
**`audit_checklist_results`** — `audit_id` CASCADE, `item_id`, `esito` CHECK `('conforme','non_conforme','non_rilevante')`, `note`, `evidenza`, `origin` CHECK `('agent','manuale')`
|
||||
|
||||
**`audit_runs`** (§6) — `audit_id` CASCADE, `status` CHECK `('queued','running','done','error')`, `step`, `started_at`, `finished_at`, `heartbeat_at`, `error`, `raw jsonb` (output grezzo: materiale per il blocco 8)
|
||||
|
||||
Indici: unique su `slug`; index su `client_id`, `lead_id`, `(audit_id, sort_order)` per le figlie, `(audit_id, started_at desc)` per le run, `(audit_id, item_id)` unique per i risultati.
|
||||
|
||||
**Ordinamento automatico** (blocchi 4 e 7): la query ordina per rango di impatto/priorità con `sort_order` come spareggio *dentro* il gruppo. Tu non ordini niente.
|
||||
|
||||
> ⚠️ **Checkpoint bloccante.** Migration applicata in produzione **prima** di pushare il codice schema-dipendente (regola CLAUDE.md "Ordering"), via SSH/docker-exec. Aggiornare `src/db/schema.ts` a mano in parallelo — `drizzle-kit generate` è rotto.
|
||||
|
||||
---
|
||||
|
||||
## 6 · Motore di analisi
|
||||
|
||||
`src/lib/audit/` modellato su `src/lib/proposal/` ma multi-step:
|
||||
|
||||
```
|
||||
src/lib/audit/
|
||||
pipeline.ts funzione pura: (auditId) => risultato. Non conosce chi la chiama
|
||||
agent.ts chiamate Anthropic, una per step di funnel
|
||||
schema.ts validazione Zod dell'output
|
||||
fetch.ts recupero pagine + estrazione testo + PageSpeed
|
||||
```
|
||||
|
||||
**Passi:** recupero pagine chiave → metriche PageSpeed → verifica delle voci di checklist del profilo → sintesi dei problemi con impatto+area+descrizione+conseguenza.
|
||||
|
||||
Tutto l'output è **bozza**: finisce in `audit_checklist_results` e in `audit_findings` con `origin='agent'`. Tu rivedi e correggi prima di consegnare. Si automatizza il meccanico, **il giudizio resta tuo**.
|
||||
|
||||
**Il blocco 8 "La direzione" resta manuale, foglio bianco.** L'editor mostra a lato il materiale grezzo — problemi ad alto impatto e temi ricorrenti, da `audit_runs.raw`. Nessun testo proposto, nessun template. È il blocco che giustifica il prezzo: se diventa formula, il cliente lo sente.
|
||||
|
||||
**Esecuzione: in-app, background, con polling.**
|
||||
- "Avvia analisi" → riga `audit_runs` con `status='queued'`, lancia la pipeline
|
||||
- L'admin fa polling e mostra `step` corrente e avanzamento
|
||||
- Progresso scritto a ogni passo: un'interruzione non perde il lavoro fatto
|
||||
|
||||
> ⚠️ **Rischio da mettere in conto.** Con `output: "standalone"` su singolo container Coolify, un redeploy **uccide un job in corso** e lascia una riga bloccata su `running`. Mitigazione: `heartbeat_at` aggiornato a ogni passo, le run senza heartbeat da N minuti vanno in `error`, e "Rilancia" riparte dall'ultimo passo completato. Non è un sistema a code: è deliberatamente il minimo che regge 50 audit/anno.
|
||||
|
||||
`pipeline.ts` è una **funzione pura riusabile**: oggi la chiama il bottone, domani il webhook Whop. Nessuna riscrittura.
|
||||
|
||||
---
|
||||
|
||||
## 7 · Storage immagini e redesign
|
||||
|
||||
Servono: 2 screenshot home, N screenshot dei problemi, 2 immagini redesign. Con URL esterni incollati a mano il budget dei 10 minuti non regge, e un URL morto uccide il documento del cliente.
|
||||
|
||||
**Volume persistente, non servizio esterno.** Nessuna dipendenza npm nuova, nessun account terzo, nessuna credenziale.
|
||||
|
||||
- Volume Coolify montato su `/app/uploads`
|
||||
- Upload via **server action** che riceve `File` da `FormData`. Niente upload diretto dal browser: la CSP ha `connect-src 'self'`, un POST verso un host esterno sarebbe bloccato — passare dal nostro origin è l'unica strada e va bene così
|
||||
- Validazione: whitelist MIME `image/png|jpeg|webp`, max 5MB, nome file `nanoid()` (mai l'originale)
|
||||
- Lettura via `src/app/api/uploads/[...path]/route.ts` con guardia sul path traversal e `Cache-Control` lungo
|
||||
- CSP invariata: serviamo dal nostro origin, coperto da `img-src 'self'`
|
||||
|
||||
> ⚠️ **Checkpoint bloccante.** Volume creato in Coolify prima del deploy, altrimenti gli upload si perdono a ogni redeploy.
|
||||
|
||||
Nuovo componente `ImageUploadField` — in tutto `src/` non c'è un solo `<input type="file">`.
|
||||
|
||||
**Il redesign usa entrambi i formati, con ruoli distinti:**
|
||||
- **Immagini caricate** — rappresentazione canonica, quelle che si vedono nello slider e **le uniche che finiscono nel PDF**
|
||||
- **Link Figma** — opzionale, "apri il redesign interattivo", nuova scheda
|
||||
|
||||
Perché non embeddare Figma: la CSP ha `default-src 'self'` senza `frame-src`, quindi l'iframe sarebbe bloccato; e un iframe **in stampa non produce nulla**, rompendo il vincolo "una sola fonte" proprio sul blocco che vale di più.
|
||||
|
||||
---
|
||||
|
||||
## 8 · Pagina pubblica e PDF
|
||||
|
||||
`/audit/[slug]`, modellata su `src/app/preventivo/[slug]/page.tsx`: `export const revalidate = 0`, fetch by slug, `notFound()` se assente.
|
||||
|
||||
**Differenze deliberate dal preventivo:**
|
||||
- **Documento a scorrimento.** Nessun `h-screen`, nessun `overflow-hidden`, nessuna manipolazione di `body.style`
|
||||
- **Rate limit**: aggiungere `/audit` al matcher di `src/proxy.ts` riusando `src/lib/rate-limit.ts`. Il preventivo non ce l'ha — non ereditare quell'omissione
|
||||
- **noindex**: `X-Robots-Tag: noindex, nofollow` in `next.config.ts` più `metadata.robots`
|
||||
- **Anteprima admin**: con `state === 'draft'` mostrare comunque il documento se `getServerSession` restituisce una sessione admin, altrimenti il placeholder. Non tocca LOCKED #4, che riguarda `/client/*`
|
||||
- **Tracking**: alla prima render non-admin aggiornare `first_viewed_at` / `last_viewed_at` / `view_count`
|
||||
|
||||
Nuovi componenti in `src/components/public/audit/`, uno per blocco, server components salvo slider e bottone stampa. **Non riusare** le 20 sezioni di `public/proposal/sections/`: sono saldate a `ProposalContent` con copy hard-coded.
|
||||
|
||||
Da riusare: **`RichText.tsx`** per ogni testo libero (obbligatorio su output del modello — mai `dangerouslySetInnerHTML`), e le convenzioni visive esistenti (eyebrow `text-xs font-mono tracking-widest uppercase`, headline `text-5xl font-light`, card `border border-border rounded-xl p-8`).
|
||||
|
||||
⚠️ Il deck preventivi è light-only (`bg-white` hard-coded in `ProposalDeck.tsx:139,141,156`, `PricingSection.tsx:46`) e viola la regola dei token in due punti (`TimelineSection.tsx:5-9`, `ClosingSection.tsx:20,34`). **Non replicare quei difetti**: solo token semantici, dual light/dark.
|
||||
|
||||
**Leggibilità in B/N**: il livello di impatto non può dipendere dal colore. Etichetta + glifo — `●●● alto` / `●●○ medio` / `●○○ basso`.
|
||||
|
||||
### PDF via print CSS
|
||||
|
||||
Stessa pagina, stesso DOM, stesso record: il vincolo "una sola fonte" è soddisfatto per costruzione.
|
||||
|
||||
In `src/app/globals.css` — oggi **zero regole print** in 218 righe — un blocco `@media print`:
|
||||
- Palette chiara forzata, `print-color-adjust: exact` dove serve
|
||||
- `break-inside: avoid` su ogni card problema e riga della tabella; `break-before: page` sui blocchi maggiori
|
||||
- Nascosti: bottone stampa, nav, controllo dello slider
|
||||
- **Slider prima/dopo**: a schermo interattivo, in stampa due immagini impilate con etichetta. Tecnica: entrambe le `<img>` **sempre** nel DOM, clippate in overlay via CSS a schermo; in `@media print` si toglie il clip e si impila. Nessun ramo JS, nessuna divergenza possibile
|
||||
- Bottone "Scarica PDF" sticky in alto a destra = client island con `window.print()`
|
||||
|
||||
---
|
||||
|
||||
## 9 · Admin
|
||||
|
||||
Mutazioni come Server Actions colocate in `actions.ts` (niente REST per l'admin).
|
||||
|
||||
- `src/app/admin/audit/page.tsx` — lista. Riusare `PageHeader`, `SearchInput`, badge di stato
|
||||
- `src/app/admin/audit/nuovo/` — **creazione manuale**: cliente o lead, livello, profilo, URL, importo → redirect all'editor
|
||||
- `src/app/admin/audit/[id]/edit/page.tsx` — RSC sottile + client component
|
||||
- `src/components/admin/audit/AuditEditor.tsx`
|
||||
- `src/app/admin/audit/actions.ts`
|
||||
- Voce in `AdminSidebar.tsx`
|
||||
|
||||
**Pattern editor: `saveOfferEditor` + `OfferEditorClient`** (`src/app/admin/offers/actions.ts:177-274`, `src/components/admin/offers/OfferEditorClient.tsx`) — unico precedente di salvataggio a payload intero con array di figli:
|
||||
|
||||
- Zod annidato con `z.array(findingSchema)` / `z.array(optimizationSchema)`, `id` opzionale sui figli
|
||||
- Nell'action: update degli scalari, loop sui figli con **upsert per presenza di `id`**, delete delle righe sparite, `sort_order: index` assegnato server-side
|
||||
- Client: array in `useState`, update immutabili via `.map`, `startTransition` + try/catch → `setSaveError`
|
||||
- ⚠️ **Il re-sync degli id dopo il primo salvataggio** (`OfferEditorClient.tsx:93-98`): senza, i figli appena creati vengono re-inseriti invece che aggiornati al salvataggio successivo. È il bug più facile da introdurre qui
|
||||
|
||||
**Aggiungi / rimuovi / riordina righe**: non esiste nel codebase — `sort_order` oggi è write-once. `@dnd-kit/sortable` è già in `package.json` e usato in `KanbanBoard.tsx`: riusarlo, con l'indice dell'array che diventa `sort_order` al salvataggio.
|
||||
|
||||
**Campi condizionali**: redesign e ottimizzazioni non si renderizzano se il livello non li prevede.
|
||||
|
||||
**Bozza**: niente autosave (non esiste nel codebase, complessità sproporzionata). "Salva bozza" esplicito + avviso all'uscita con modifiche pendenti. Tutti i campi nullable. La **validazione di completezza scatta solo su "Consegna"**, sui soli blocchi pertinenti.
|
||||
|
||||
**Consegna / Ritira**: transizioni guardate `WHERE id = ? AND state = ?` come `publishProposal` (`src/app/admin/preventivi/actions.ts:117-127`). Entrambe reversibili.
|
||||
|
||||
**Automatismi** (budget 10 minuti): credito = `importo_pagato`, fasi già completate derivate dal livello, date da `published_at`/`measured_at`, ordinamento dei blocchi 4 e 7, analisi draftata dagli agent.
|
||||
|
||||
---
|
||||
|
||||
## 10 · Ingresso: manuale ora, Whop dopo
|
||||
|
||||
Fase 1 = **solo creazione manuale**, che resta comunque un requisito permanente.
|
||||
|
||||
Predisposto senza costruirlo: `origin` + `external_ref` sulla tabella, e `pipeline.ts` chiamabile da un webhook. L'intake del cliente è **ancora da definire** — la colonna `intake jsonb` accoglie qualunque forma prenderà, senza migration aggiuntive.
|
||||
|
||||
---
|
||||
|
||||
## 11 · Modifica al vincolo LOCKED #5 — richiede approvazione
|
||||
|
||||
`CLAUDE.md` oggi: `5. No file hosting v1 — documenti come URL esterni`
|
||||
|
||||
Nuova versione proposta:
|
||||
|
||||
```
|
||||
5. No file hosting per i documenti — restano URL esterni.
|
||||
Deroga (Phase 27, 2026-08-16): le immagini dell'audit (screenshot e
|
||||
redesign) sono caricate su volume persistente e servite da
|
||||
/api/uploads/[...path], con whitelist MIME e limite di dimensione.
|
||||
Non estendere l'upload ad altre entità senza modificare questo vincolo.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12 · GSD e memoria di progetto
|
||||
|
||||
**Non c'è niente da costruire**: state file, roadmap e checkpoint automatico esistono già. `.planning/STATE.md` è il digest che comanda i comandi `/gsd-*`, `.planning/ROADMAP.md` traccia la posizione, `execute-plan.md` aggiorna STATE.md **dopo ogni piano**, e `.claude/rules/memory-discipline.md` è la regola scritta.
|
||||
|
||||
Due difetti reali:
|
||||
|
||||
1. **`.planning/config.json` non ha la chiave `hooks`.** Verificato: entrambi gli hook globali (`gsd-session-state.sh`, `gsd-phase-boundary.sh`) escono a vuoto senza `"hooks": { "community": true }`. Aggiungerla riattiva l'iniezione di STATE.md all'avvio sessione e il promemoria a ogni scrittura in `.planning/`. Una riga.
|
||||
2. **`gsd-sdk` e `gsd-tools` non sono nel PATH** (verificato: `not found`). Le mutazioni automatiche di STATE.md in `execute-plan.md` non hanno backend e ricadono su edit manuali — è il motivo per cui le fasi 13 e 26 hanno SUMMARY ricostruiti a posteriori.
|
||||
|
||||
L'hook `Stop` in `.claude/settings.json` è solo advisory (`echo`) e guarda solo `src` e `.planning`: non nota modifiche a `STATUS.md` o `CLAUDE.md`.
|
||||
|
||||
**Inquadramento**: nuovo milestone **v2.5 — Audit** via `/gsd-new-milestone`. Numerazione progressiva e mai riusata (l'ultima è 26 ⇒ si parte da **27**), come impone `.planning/PROJECT.md`.
|
||||
|
||||
Da aggiornare a fine lavoro: `.planning/STATE.md`, `.planning/ROADMAP.md`, `STATUS.md`, `CLAUDE.md`.
|
||||
|
||||
---
|
||||
|
||||
## 13 · File toccati
|
||||
|
||||
**Nuovi** — `scripts/spike-audit.ts` · `src/db/migrations/0017_audits.sql` · `src/lib/audit/{pipeline,agent,schema,fetch}.ts` · `src/lib/audit/template/{v1,index}.ts` · `src/lib/audit-queries.ts` · `src/lib/audit-view.ts` · `src/app/audit/[slug]/page.tsx` · `src/components/public/audit/*` · `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` · `next.config.ts` · `src/app/globals.css` · `src/components/admin/AdminSidebar.tsx` · `CLAUDE.md` · `.planning/config.json` · `Dockerfile` + volume Coolify
|
||||
|
||||
---
|
||||
|
||||
## 14 · Verifica
|
||||
|
||||
Non esiste test suite (nessun vitest/jest/playwright, nessuno script `test`). **`npm run build` è la verifica di riferimento** — fa il typecheck. `npm run lint` è `eslint` nudo.
|
||||
|
||||
**Fase 1 (spike)** — gira su un sito reale, i problemi sono concreti e verificabili, la valutazione delle voci di checklist è ripetibile. È un giudizio tuo, non un test automatico.
|
||||
|
||||
**Dopo:**
|
||||
1. `npm run build` — typecheck e compilazione
|
||||
2. Migration applicata in prod **prima** del push; verificare che le sei tabelle esistano
|
||||
3. Un audit di prova per ciascuno dei tre livelli, su entrambi i profili: i blocchi condizionali compaiono e spariscono correttamente
|
||||
4. Salvare a metà, ricaricare, nulla si perde. Salvare due volte di fila: i figli si aggiornano e **non si duplicano** (test del re-sync degli id)
|
||||
5. Riordinare i problemi, salvare, ricaricare: l'ordine regge
|
||||
6. Avviare un'analisi e **riavviare il container a metà**: la run va in `error` e "Rilancia" riparte senza perdere i passi completati
|
||||
7. Upload di un'immagine → redeploy → l'immagine è ancora lì (prova del volume)
|
||||
8. Slug consegnato in incognito: si apre. Bozza in incognito: placeholder. Bozza da admin loggato: si vede
|
||||
9. `X-Robots-Tag: noindex, nofollow` presente nella risposta
|
||||
10. **Stampa**: Cmd+P sul documento consegnato → interruzioni di pagina corrette, slider impilato in due immagini, nessun marcatore di lavorazione (bordi/testi colorati) sopravvissuto, e in **scala di grigi** sia i livelli di impatto sia i valori delle metriche restano distinguibili
|
||||
10b. **Rete disattivata dopo il primo caricamento**: i font restano quelli giusti (prova che sono self-hostati e che nessuna risorsa esterna è rimasta)
|
||||
11. Il tracking apertura si incrementa da visitatore e **non** dalla preview admin
|
||||
12. Consegnato un audit su `v1`, alzare `LATEST_TEMPLATE_VERSION` a `v2`: il consegnato continua a rendere `v1`, uno nuovo nasce `v2`
|
||||
13. Un audit su brand premium **non** propone mai voci con `registro='volume'`
|
||||
|
||||
⚠️ Playwright non funziona contro `npm run dev` (la CSP blocca `eval`, i client component non si idratano). Per E2E usare il build di produzione.
|
||||
Reference in New Issue
Block a user