From 817a8cd5d1805864cb4cc25d00464715ac9c9e7f Mon Sep 17 00:00:00 2001 From: Simone Cavalli Date: Wed, 26 Aug 2026 16:09:14 +0200 Subject: [PATCH] =?UTF-8?q?chore(claude):=20architettura=20base=20.claude?= =?UTF-8?q?=20=E2=80=94=20skill=20preventivo=20e=20audit,=20hook=20di=20gu?= =?UTF-8?q?ardia,=20piani=20nel=20repo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .claude/CLAUDE.md | 77 ++++ .claude/agents/.gitkeep | 0 .claude/commands/.gitkeep | 0 .claude/hooks/guardia-migration.sh | 61 +++ .claude/hooks/guardia-token.sh | 59 +++ .claude/memory/.gitkeep | 0 .claude/plans/README.md | 17 + .claude/plans/v2.5-audit-documento.md | 413 +++++++++++++++++++++ .claude/plans/v2.5-audit-motore.md | 200 ++++++++++ .claude/plans/v2.5-modifiche-hub.md | 319 ++++++++++++++++ .claude/projects/.gitkeep | 0 .claude/rules/memory-discipline.md | 4 +- .claude/settings.json | 33 +- .claude/skills/audit/SKILL.md | 77 ++++ .claude/skills/preventivo/SKILL.md | 89 +++++ .claude/skills/preventivo/check-profilo.sh | 49 +++ .claude/workflows/.gitkeep | 0 .gitignore | 3 + .planning/STATE.md | 14 +- CLAUDE.md | 13 +- STATUS.md | 89 ++++- scripts/audit-fonti.ts | 108 ++++++ 22 files changed, 1601 insertions(+), 24 deletions(-) create mode 100644 .claude/CLAUDE.md create mode 100644 .claude/agents/.gitkeep create mode 100644 .claude/commands/.gitkeep create mode 100755 .claude/hooks/guardia-migration.sh create mode 100755 .claude/hooks/guardia-token.sh create mode 100644 .claude/memory/.gitkeep create mode 100644 .claude/plans/README.md create mode 100644 .claude/plans/v2.5-audit-documento.md create mode 100644 .claude/plans/v2.5-audit-motore.md create mode 100644 .claude/plans/v2.5-modifiche-hub.md create mode 100644 .claude/projects/.gitkeep create mode 100644 .claude/skills/audit/SKILL.md create mode 100644 .claude/skills/preventivo/SKILL.md create mode 100755 .claude/skills/preventivo/check-profilo.sh create mode 100644 .claude/workflows/.gitkeep create mode 100644 scripts/audit-fonti.ts diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 0000000..798728d --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,77 @@ +# `.claude/` — la cartella di configurazione di ClientHub + +Questo file si carica quando si lavora **dentro `.claude/`**. Spiega cosa va dove, e basta. + +**Per il progetto comanda [`../CLAUDE.md`](../CLAUDE.md)**: stack, vincoli LOCKED, procedura +di deploy, accesso al DB, design system. Qui non si duplica niente di quello — una seconda +copia è il modo più veloce per averne due che si contraddicono. + +## Cosa va in ogni cartella + +| Cartella | Cosa ci va | Cosa **non** ci va | +|---|---|---| +| `rules/` | Regole caricate per contesto. Oggi: `memory-discipline.md`, importata da `../CLAUDE.md` | Documentazione di feature — quella sta in `STATUS.md` | +| `skills/` | **Solo** skill specifiche di ClientHub: `preventivo/`, `audit/` | Le skill globali (`/seo-audit`, `/copywriting`, `/docx`…) — stanno in `~/.claude/skills/` | +| `agents/` | **Solo** agenti specifici di ClientHub. Oggi vuota: quello dell'audit nasce col motore | I 18 agenti globali di `~/.claude/agents/` | +| `hooks/` | Script eseguibili richiamati da `settings.json` | Logica di prodotto | +| `plans/` | I piani delle milestone, versionati | Piani usa-e-getta di una singola sessione | +| `commands/`, `workflows/`, `projects/` | Vuote per ora, sono lo scheletro | — | +| `memory/` | Appunti di lavoro versionati nel repo | **La memoria persistente. Non è qui** → vedi sotto | +| `worktrees/` | Generata dagli strumenti | — | + +### Agenti e skill globali non si copiano qui + +Regola già fissata in [`../../CLAUDE.md`](../../CLAUDE.md): agenti e skill che valgono per +tutti i progetti vivono in `~/.claude/` e si invocano da qualunque cartella. Copiarne uno qui +crea due file destinati a divergere, e il primo a cambiare vince a caso. + +In locale ci va solo ciò che **senza questo repo non ha senso**: le due skill qui sotto. + +### `.claude/memory/` ≠ memoria persistente + +Due posti diversi con lo stesso nome, e confonderli fa perdere il lavoro: + +- **`.claude/memory/`** (questa cartella) — appunti versionati nel repo, li vede chiunque + faccia clone. +- **`~/.claude/projects/-Users-simonecavalli-Vault-IAMCAVALLI-hub/memory/`** — la memoria + persistente vera, un file per fatto più `MEMORY.md` come indice. Sta fuori dal repo, non + si committa, e viene iniettata in automatico a inizio sessione. + +Ci va quello che **non si deduce dal repo**: perché una decisione è stata presa, un vincolo +operativo, una cosa provata che non funziona. Regole complete in +[`rules/memory-discipline.md`](rules/memory-discipline.md). + +## Le skill del progetto + +- **`/preventivo`** — l'attrezzo per lavorare sulla pipeline che genera i preventivi + (`src/lib/proposal/`) senza romperla. Non contiene un prompt di generazione: quello vive + in `agent.ts` ed è l'unico. +- **`/audit`** — fa girare le cinque fonti di `src/lib/audit/sources/` su un URL e dice cosa + è stato **misurato** e cosa no. Le fonti sono in produzione ma inerti: nessuna route le + chiama ancora. + +## Gli hook attivi + +Tutti e tre in [`settings.json`](settings.json). Si provano a mano prima di fidarsi. + +| Hook | Quando | Cosa fa | +|---|---|---| +| Promemoria memoria | `Stop` | Se `src/` o `.planning/` hanno modifiche non committate, ricorda di aggiornare `STATE.md`. Non blocca | +| [`guardia-migration.sh`](hooks/guardia-migration.sh) | `PreToolUse` su Write/Edit in `src/db/migrations/` | **Blocca** l'SQL che cancella dati dalle entità protette (`clients`, `projects`, `payments`, `phases`). È il vincolo Data Safety LOCKED fatto rispettare dalla macchina | +| [`guardia-token.sh`](hooks/guardia-token.sh) | `PostToolUse` su `.tsx`/`.css` | **Avvisa** se compaiono classi Tailwind grezze o hex letterali. Non blocca: le eccezioni sanzionate esistono e stanno nella whitelist dello script | + +Per provarli senza passare da Claude: + +```bash +echo '{"tool_input":{"file_path":"src/db/migrations/9999_x.sql","content":"DROP TABLE payments;"}}' \ + | .claude/hooks/guardia-migration.sh; echo "exit=$?" # atteso: 2 + +echo '{"tool_input":{"file_path":"src/x.tsx","content":"
"}}' \ + | .claude/hooks/guardia-token.sh; echo "exit=$?" # atteso: 0 + avviso +``` + +## I piani + +`plans/` contiene i piani della milestone v2.5, portati dentro il repo il 2026-08-26 perché +stavano solo in `~/.claude/plans/` e `STATE.md` avvertiva che senza quelli la milestone non +era ricostruibile. Dettaglio in [`plans/README.md`](plans/README.md). diff --git a/.claude/agents/.gitkeep b/.claude/agents/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/.claude/commands/.gitkeep b/.claude/commands/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/.claude/hooks/guardia-migration.sh b/.claude/hooks/guardia-migration.sh new file mode 100755 index 0000000..45369f4 --- /dev/null +++ b/.claude/hooks/guardia-migration.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# PreToolUse su Write/Edit — vincolo Data Safety (LOCKED) di ../../CLAUDE.md. +# +# Blocca la scrittura di una migration che cancella dati dalle entita' protette. +# Non e' una prova: e' una rete. Un DROP scritto in modo esotico puo' passare — +# la revisione a occhio dell'SQL resta obbligatoria. +# +# exit 0 = passa · exit 2 = bloccato, il messaggio su stderr torna a Claude. +set -uo pipefail + +INPUT=$(cat) +FILE=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty') + +# Fuori da src/db/migrations/ questo hook non ha voce in capitolo. +case "$FILE" in + *src/db/migrations/*) ;; + *) exit 0 ;; +esac + +# Write porta `content`, Edit porta `new_string`. MultiEdit porta un array. +SQL=$(printf '%s' "$INPUT" | jq -r ' + [ .tool_input.content?, + .tool_input.new_string?, + (.tool_input.edits? // [] | .[].new_string?) + ] | map(select(. != null)) | join("\n") +') +[ -z "$SQL" ] && exit 0 + +PROTETTE='clients|projects|payments|phases' + +# Via i commenti, tutto minuscolo, una riga per statement: cosi' "DROP" e il nome +# della tabella devono stare nella STESSA istruzione per far scattare il blocco. +PULITO=$(printf '%s' "$SQL" \ + | sed -E 's/--.*$//' \ + | tr '\n' ' ' \ + | sed -E 's;/\*[^*]*\*+([^/*][^*]*\*+)*/; ;g' \ + | tr '[:upper:]' '[:lower:]' \ + | tr ';' '\n') + +COLPEVOLI=$(printf '%s\n' "$PULITO" \ + | grep -E 'drop[[:space:]]+table|drop[[:space:]]+column|truncate|delete[[:space:]]+from' \ + | grep -E "\\b($PROTETTE)\\b" || true) + +if [ -n "$COLPEVOLI" ]; then + { + echo "BLOCCATO — Data Safety (LOCKED, ../CLAUDE.md)." + echo + echo "Questa migration cancella dati da un'entita' protetta (clients, projects," + echo "payments, phases). Le migration sono additive: si aggiungono colonne e" + echo "tabelle, non si tolgono righe." + echo + echo "Istruzioni incriminate:" + printf '%s\n' "$COLPEVOLI" | sed 's/^[[:space:]]*/ · /' + echo + echo "Se la rimozione serve davvero, e' una decisione da confermare a voce con" + echo "l'utente prima di scriverla — non da aggirare qui." + } >&2 + exit 2 +fi + +exit 0 diff --git a/.claude/hooks/guardia-token.sh b/.claude/hooks/guardia-token.sh new file mode 100755 index 0000000..7bc0a4c --- /dev/null +++ b/.claude/hooks/guardia-token.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# PostToolUse su .tsx/.css — regola cardinale del design system: +# solo token semantici (bg-card, text-muted-foreground, border-border). +# +# AVVISA, non blocca: le eccezioni sanzionate esistono (vedi WHITELIST) e il +# debito storico e' gia' di ~450 occorrenze. Bloccare renderebbe l'hook un +# ostacolo da disattivare invece di un promemoria da leggere. +# +# exit 0 sempre. Il messaggio su stderr arriva a Claude come contesto. +set -uo pipefail + +INPUT=$(cat) +FILE=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty') + +case "$FILE" in + *.tsx|*.css) ;; + *) exit 0 ;; +esac + +# Eccezioni sanzionate da ../CLAUDE.md § Design System: +# - StatusBadge: i colori di stato usano la palette con variante dark: esplicita +# - AdminShell: il verde brand della sidebar +# - mailer.ts / .html: l'HTML delle email non puo' usare variabili CSS +# - globals.css: e' il posto dove i token vengono *definiti* +# - design-reference/: i mock precedono la regola, si traducono non si copiano +case "$FILE" in + *StatusBadge*|*AdminShell*|*mailer*|*globals.css|*design-reference/*) exit 0 ;; +esac + +TESTO=$(printf '%s' "$INPUT" | jq -r ' + [ .tool_input.content?, + .tool_input.new_string?, + (.tool_input.edits? // [] | .[].new_string?) + ] | map(select(. != null)) | join("\n") +') +[ -z "$TESTO" ] && exit 0 + +PALETTE='slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose' +GREZZE=$(printf '%s\n' "$TESTO" \ + | grep -oE "\\b(bg|text|border|ring|from|to|via|fill|stroke|divide|outline|shadow|accent|decoration|placeholder)-($PALETTE)-[0-9]{2,3}\\b" \ + | sort -u | head -12 || true) +HEX=$(printf '%s\n' "$TESTO" \ + | grep -oE '#[0-9a-fA-F]{3,8}\b' | sort -u | head -6 || true) + +if [ -n "$GREZZE" ] || [ -n "$HEX" ]; then + { + echo "AVVISO design system — token semantici, non palette grezza." + echo "File: $FILE" + [ -n "$GREZZE" ] && { echo " classi grezze: $(printf '%s ' $GREZZE)"; } + [ -n "$HEX" ] && { echo " hex letterali: $(printf '%s ' $HEX)"; } + echo + echo "Usa bg-card / text-muted-foreground / border-border: e' quello che fa" + echo "funzionare chiaro e scuro sul solo toggle della classe .dark." + echo "Riferimento: design-reference/DESIGN-SYSTEM.md" + echo "Eccezione sanzionata? Aggiungi il file alla WHITELIST di questo hook." + } >&2 +fi + +exit 0 diff --git a/.claude/memory/.gitkeep b/.claude/memory/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/.claude/plans/README.md b/.claude/plans/README.md new file mode 100644 index 0000000..cf4787b --- /dev/null +++ b/.claude/plans/README.md @@ -0,0 +1,17 @@ +# Piani + +I piani delle milestone, **dentro il repo**. Ci sono arrivati il 2026-08-26: prima +stavano solo in `~/.claude/plans/`, con nomi generati a caso, e `STATE.md` avvertiva +che senza quei file v2.5 non era ricostruibile. Un piano che vive solo sul portatile +di chi l'ha scritto non e' documentazione, e' un ricordo. + +| File | Cosa contiene | Origine | +|---|---|---| +| `v2.5-audit-documento.md` | Il documento di audit: tre livelli (Radiografia / Prima-Dopo / Rotta) come configurazioni di un unico documento su `/audit/[slug]` | `dovremmo-fare-una-cosa-woolly-puddle.md` | +| `v2.5-audit-motore.md` | Il motore: raccolta in parallelo, quattro sub-agent, sintetizzatore. Il vincolo che regge tutto — **un numero entra solo se misurato** | `vorrei-solo-farti-capire-radiant-valley.md` | +| `v2.5-modifiche-hub.md` | I blocchi A/B/C delle modifiche all'hub chieste il 2026-08-18 | `sei-arrivato-qua-search-recursive-kettle.md` | + +Sono **piani, non stato**: dicono cosa era stato deciso di fare, non cosa e' fatto. +Per quello ci sono `STATUS.md` (narrativa) e `.planning/STATE.md` (digest). +Passati al setaccio per credenziali prima del commit: dentro compaiono nomi di +variabili d'ambiente, mai i loro valori. diff --git a/.claude/plans/v2.5-audit-documento.md b/.claude/plans/v2.5-audit-documento.md new file mode 100644 index 0000000..1505cdd --- /dev/null +++ b/.claude/plans/v2.5-audit-documento.md @@ -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 ``** — 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 ``. + +**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 `` **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. diff --git a/.claude/plans/v2.5-audit-motore.md b/.claude/plans/v2.5-audit-motore.md new file mode 100644 index 0000000..b75d26b --- /dev/null +++ b/.claude/plans/v2.5-audit-motore.md @@ -0,0 +1,200 @@ +# 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. diff --git a/.claude/plans/v2.5-modifiche-hub.md b/.claude/plans/v2.5-modifiche-hub.md new file mode 100644 index 0000000..ebf4118 --- /dev/null +++ b/.claude/plans/v2.5-modifiche-hub.md @@ -0,0 +1,319 @@ +# Modifiche all'hub — dashboard, progetti, pipeline + +## Contesto + +Il 2026-08-18 hai elencato una serie di modifiche all'hub divise per sezione +(dashboard, progetti, pipeline). La sessione si è fermata sul limite settimanale +subito dopo aver lanciato l'esplorazione, e quel lavoro non è mai stato ripreso. +Questo piano lo raccoglie. + +Il punto di partenza è che **quasi tutto quello che chiedi è calcolabile con i +dati che l'hub già ha**: le categorie di offerta esistono, le fasi e i task hanno +uno stato, i pagamenti hanno una data di incasso, le conversazioni sono già +aggregate. Serve poco schema nuovo — una sola migration additiva — e molta +query + UI. + +L'eccezione è l'ingresso da Whop per gli audit: dipende dal motore di analisi +della v2.5, che è fermo alle sole fonti. Hai scelto di fare **prima le modifiche +hub e poi il motore**, quindi quel blocco resta documentato ma non costruito. + +### Decisioni prese + +| Domanda | Scelta | +|---|---| +| Sequenza | Hub prima, motore audit dopo | +| Fonte "Audit" negli analytics | È la tua **Entry Offer** — si legge da `offer_macros.category`, come Signature e Retainer | +| Data di consegna attesa | **Derivata** da offerta + durata, con **override manuale** opzionale | +| Ingresso lead | TidyCal + form sito — vedi il blocco C, dove c'è un vincolo che cambia le carte | + +--- + +## Blocco A — Progetti + +Il blocco a rischio più basso e a resa più immediata. Da fare per primo. + +### A1 · Togliere il tab Commenti e il timer dalla lista + +Due rimozioni chieste esplicitamente. + +- **Tab "Commenti"** in [page.tsx:75](src/app/admin/projects/[id]/page.tsx#L75) e + [page.tsx:118-120](src/app/admin/projects/[id]/page.tsx#L118-L120), più l'import a riga 8. + **Non si perde niente**: ho verificato che `buildEntityMap()` in + [conversations-queries.ts:57](src/lib/conversations-queries.ts#L57) cammina + clienti → progetti → fasi → task → deliverable e raccoglie *tutti* i commenti + con la loro etichetta d'entità. `/admin/conversazioni` è un sovrainsieme + stretto di quel tab. Il campo `comments` in `ProjectFullDetail` diventa morto: + va tolto anche da [admin-queries.ts:567](src/lib/admin-queries.ts#L567) e dalla query. +- **Colonna "Timer"** dalla lista progetti: `` in + [ProjectRow.tsx:85](src/components/admin/ProjectRow.tsx#L85) e l'intestazione in + [projects/page.tsx:47](src/app/admin/projects/page.tsx#L47). Il timer resta dove + ha senso, dentro il progetto. + +### A2 · Mini-dashboard nel singolo progetto + +Una striscia sopra i tab, prima di `` in +[projects/[id]/page.tsx:69](src/app/admin/projects/[id]/page.tsx#L69). **Nessuna +query nuova**: `getProjectFullDetail` restituisce già tutto. + +**Finance** — incassato / contrattualizzato / residuo, da `payments` + +`offersAcceptedTotal`; ore tracciate e €/h reale da `totalTrackedSeconds` e +`targetHourlyRate` (già passato alla pagina a riga 24). + +**Avanzamento** — task `done` su totale, con barra. Stessa aritmetica del +`progress_pct` già usato per le fasi. + +Riusa il pattern `MetricCard` di [admin/page.tsx:18](src/app/admin/page.tsx#L18): +va estratto in `src/components/admin/MetricCard.tsx` e importato da entrambe. +Scrivere la striscia **a token semantici** (`bg-card`, `text-muted-foreground`) — +questa route è il cluster peggiore di DEBT-01 (~182 occorrenze di palette raw) e +non va peggiorata. + +### A3 · Timer per task e fase + +Oggi `time_entries` ha solo `project_id` +([schema.ts:245](src/db/schema.ts#L245)). Servono due colonne nullable. + +- **Migration `0018`** (dettagli in fondo): `phase_id`, `task_id` su + `time_entries`, entrambe `ON DELETE SET NULL`. Cancellare un task **non deve** + cancellare il tempo tracciato: è storico fatturabile, l'entry ricade a livello + progetto. `project_id` resta obbligatoria, quindi ogni entry è sempre + attribuita. +- **Server actions** in [timer-actions.ts](src/app/admin/timer-actions.ts): + `startTimer` prende `phaseId`/`taskId` opzionali; stessa cosa per + `addManualTimeEntry`. La regola "un solo timer attivo alla volta" (righe 19-38) + resta com'è — vale globalmente, non per task. +- **UI**: `` su ogni riga task dentro + [PhasesTab.tsx](src/components/admin/tabs/PhasesTab.tsx), con il totale della + fase come somma dei suoi task. `TimerTab` continua a mostrare il totale progetto. + +--- + +## Blocco B — Dashboard + +### B1 · Analytics per linea di prodotto + +Tre righe — **Entry (Audit), Signature, Retainer** — lette da +`offer_macros.category`, che è già una taxonomy editabile da +`/admin/impostazioni` ([taxonomy.ts:37](src/lib/taxonomy.ts#L37)). In produzione +oggi ci sono esattamente quelle tre categorie. + +Per categoria: + +- **Iniziati questo mese** — `count(project_offers)` con `start_date` nel mese corrente. +- **Totale anno** — `count` + somma `accepted_total` con `start_date` nell'anno. +- **Incassato** — `payments` in stato `saldato` con `paid_at` nell'anno. + +L'incasso ha un problema di attribuzione da risolvere esplicitamente: i pagamenti +stanno sul **progetto**, non sull'offerta. La regola: + +1. progetto con **una** offerta → tutto l'incasso va a quella categoria; +2. progetto con **più** offerte → ripartito in proporzione ai rispettivi `accepted_total`; +3. progetto **senza** offerta → riga separata **"Senza offerta"**, mostrata a video. + +Il terzo caso non va nascosto. Oggi in prod ci sono 5 progetti e 2 righe in +`project_offers`: la maggior parte dell'incassato finirebbe lì, e vederlo è il +modo per accorgersene. + +Nuovo modulo `src/lib/product-analytics.ts` (non gonfiare `analytics-queries.ts`), +nuovo componente `src/components/admin/dashboard/ProductBreakdown.tsx`. + +> **Nota da verificare al primo giro:** l'unica Entry Offer in produzione si +> chiama *"Sblocca Business"*. Se l'audit è un prodotto diverso, va creata la sua +> `offer_macro` con categoria `Entry Offer` — è configurazione, non codice. + +### B2 · Timeline delle consegne + +Riguarda i progetti che **si consegnano** — Entry e Signature. I retainer sono +continuativi e non hanno una consegna: restano fuori, filtrando su +`offer_macros.offer_type = 'una_tantum'`. + +Per ogni progetto non archiviato: + +- **Consegna attesa** = `projects.due_date` se valorizzata, altrimenti + `project_offers.start_date + offer_micros.duration_months`. La colonna + `due_date` arriva con la migration 0018 ed è l'override manuale che hai chiesto. +- **% completamento** = task `done` / task totali. +- **% tempo trascorso** = `(oggi − inizio) / (consegna − inizio)`. +- **Semaforo** — soglia ±10 punti: `in linea` dentro la banda, `in ritardo` se il + completamento è sotto, `in anticipo` se è sopra. Un progetto oltre la data di + consegna e non completo è `in ritardo` a prescindere. + +I progetti senza scadenza calcolabile vanno elencati sotto come **"senza +scadenza"** invece di sparire — altrimenti un progetto senza offerta assegnata +diventa invisibile proprio nella vista che dovrebbe segnalarlo. + +Query in `src/lib/delivery-queries.ts`, componente +`src/components/admin/dashboard/DeliveryTimeline.tsx`. `StatusBadge` per il +semaforo, Geist Mono per date e percentuali. + +### B3 · Inbox + +**Questo esiste già.** [MessagesWidget.tsx](src/components/admin/dashboard/MessagesWidget.tsx) +mostra i clienti con messaggi non letti, l'anteprima dell'ultimo messaggio e il +link "Rispondi →" verso `/admin/conversazioni`. È il widget stretto in colonna +1/3, in fondo: probabilmente non l'hai visto perché sta sotto la piega. + +Quindi non si costruisce, si promuove: + +- fascia **a piena larghezza in cima** alla dashboard, sopra i KPI, e solo quando + c'è almeno un non letto; +- 6 righe invece di 4, con **data relativa** e **etichetta dell'entità** + (`lastMessageAt` e `lastEntityLabel` sono già in `ConversationSummary`, + [conversations-queries.ts:8](src/lib/conversations-queries.ts#L8) — oggi non + vengono usati); +- anteprima più lunga del messaggio. + +Nuovo layout in [admin/page.tsx](src/app/admin/page.tsx): Inbox → KPI → +Timeline consegne → Prodotti → il resto. + +--- + +## Blocco C — Pipeline: ingresso lead + +### Il vincolo che cambia le carte + +**TidyCal non ha webhook.** È scritto nella loro FAQ: nessun supporto nativo, e +la strada suggerita è passare da Zapier / Make / Pabbly. Quello che TidyCal *ha* +è una REST API con OAuth 2.0 e **Personal Access Token disponibile su tutti i +piani** (si crea da `tidycal.com/integrations/oauth`), con endpoint sulle +prenotazioni. + +Elementor Pro, invece, ha un'azione **Webhook** nativa in "Actions After Submit": +si incolla un URL e lui fa POST dei campi del form. Se invece andate su Astro, +il POST lo scrivete voi. In entrambi i casi è una chiamata in ingresso. + +Da qui la forma della soluzione: **un endpoint solo, sia per il form che per +TidyCal**, e per TidyCal un pezzo in più che va a prendersi le prenotazioni. + +### C1 · Endpoint di ingresso — `POST /api/webhooks/lead` + +Un solo endpoint, indipendente da chi chiama. + +- **Guardia**: header `x-webhook-secret` confrontato con una env nuova + (`LEAD_WEBHOOK_SECRET`), stesso pattern di + [validate-slug/route.ts:7](src/app/api/internal/validate-slug/route.ts#L7) — + ma **senza** la scorciatoia "se non è configurato passa": questa route è + esposta a internet, secret assente deve voler dire 403. +- **Rate limit** con `rateLimit()` da [rate-limit.ts](src/lib/rate-limit.ts), + bucket per IP. `src/proxy.ts` **non intercetta** `/api/*` (matcher a riga 120), + quindi la difesa è tutta dentro la route. +- **Payload** validato con Zod: `name` obbligatorio, `email`/`phone`/`company`/ + `notes` opzionali, più un `source` libero. Mappatura campi in un modulo a + parte così aggiungere una sorgente non tocca la route. +- **Effetto**: crea un `lead` in stato `contacted` riusando le funzioni di + [lead-service.ts](src/lib/lead-service.ts). Se l'email esiste già, aggiorna + `last_contact_date` invece di duplicare. + +Questo è tutto ciò che serve per Elementor, ed è pronto in anticipo per Astro: +la decisione Elementor-vs-Astro **non blocca niente**, perché il contratto è +un POST JSON in entrambi i casi. + +### C2 · TidyCal — sincronizzazione a polling + +Senza webhook, l'unico modo pulito senza terze parti è andare a leggere. + +- `src/lib/tidycal.ts` — client con Personal Access Token in + `TIDYCAL_ACCESS_TOKEN`, che chiede le prenotazioni create dall'ultimo giro. +- `GET /api/internal/tidycal-sync` — protetta da `INTERNAL_SECRET`, come le due + internal esistenti. Per ogni prenotazione nuova crea un lead con nome, email e + la data della call in `next_action_date`, e registra un'`activity` di tipo + `meeting`. Idempotente sull'id TidyCal, così rilanciarla non duplica. +- **Schedulazione**: Coolify ha gli Scheduled Tasks. Un `curl` ogni 15 minuti + verso quella route. Non serve infrastruttura cron nell'app — e infatti nel + repo non ce n'è. + +**Prima di scrivere il client va guardata la documentazione vera dell'API**, che +è dietro login (`tidycal.com/integrations` → API Keys) e non è indicizzata: forma +esatta dell'endpoint prenotazioni, filtro per data, paginazione. È il primo passo +del blocco, non un dettaglio: vale la regola già scritta in memoria — *ogni +estrazione da API di terzi va vista funzionare, non dedotta dai docs*. + +Se l'API risultasse inadatta, il ripiego è Zapier/Make che POSTa su C1, che a +quel punto esiste già. + +### C3 · "Alleggerire l'hub" + +Questa richiesta non ha ancora un perimetro. Non la pianifico al buio: quando i +blocchi A e B sono in produzione ti porto l'elenco di cosa è davvero poco usato +(`/admin/quotes` e `/admin/preventivi` convivono, `service_catalog` e +`offer_services` sono legacy da DEBT-02) e decidi tu cosa togliere. Cancellare +route è irreversibile e va fatto guardando, non indovinando. + +--- + +## Blocco D — Whop → audit *(rinviato, dipende dal motore)* + +L'ingresso Whop è già predisposto nello schema: `audits.origin` accetta +`'whop'` e `audits.external_ref` tiene il riferimento esterno +([schema.ts:688-690](src/db/schema.ts#L688-L690)). + +Ma "partono gli agent e fanno il lavoro" oggi **non è possibile**: in +`src/lib/audit/` ci sono solo le cinque fonti; agent, sintetizzatore e pipeline +(AUD-06 → AUD-11) non sono scritti. Costruire il webhook adesso significa +costruire un innesco che non innesca niente. + +Quando il motore c'è, il blocco è: `POST /api/webhooks/whop` con verifica firma → +crea cliente + progetto con la Entry Offer assegnata (così l'audit compare +automaticamente in B1 e B2) → crea l'`audit` con `origin='whop'` → accoda la run. + +--- + +## Migration 0018 — additiva + +`src/db/migrations/0018_timer_scope_and_due_date.sql`, in parallelo alle stesse +modifiche in [schema.ts](src/db/schema.ts) (drizzle-kit generate è rotto, i due +file si tengono in pari a mano). + +``` +ALTER TABLE time_entries ADD COLUMN phase_id text REFERENCES phases(id) ON DELETE SET NULL; +ALTER TABLE time_entries ADD COLUMN task_id text REFERENCES tasks(id) ON DELETE SET NULL; +CREATE INDEX time_entries_phase_idx ON time_entries(phase_id); +CREATE INDEX time_entries_task_idx ON time_entries(task_id); +ALTER TABLE projects ADD COLUMN due_date timestamptz; +``` + +Solo `ADD COLUMN` e `CREATE INDEX`: nessun DROP, nessun TRUNCATE, nessuna colonna +rimossa — conforme a Data Safety (LOCKED). Le righe esistenti di `time_entries` +restano valide con le due colonne a NULL, cioè "tempo di progetto". + +**Ordine di applicazione**, come da CLAUDE.md: prima a produzione via +`ssh root@178.104.27.55` + `docker exec`, **poi** il push del codice che le usa. + +--- + +## Ordine di esecuzione + +1. **A1** — le due rimozioni (nessuna dipendenza, effetto immediato) +2. **Migration 0018** in produzione +3. **A3** timer su task/fase · **A2** mini-dashboard progetto +4. **B3** inbox (piccola) → **B1** analytics prodotto → **B2** timeline consegne +5. **C1** endpoint lead → **C2** TidyCal, dopo aver letto l'API vera +6. **C3** e **D** dopo, con le informazioni che oggi non abbiamo + +Commit per unità logica, come negli ultimi quattro. + +## Verifica + +Non c'è suite di test in questo progetto: **`npm run build` è la verifica di +riferimento** (fa il typecheck), più `npm run lint`. + +Controlli manuali, in ordine: + +- **A1** — il progetto non mostra più il tab Commenti; gli stessi commenti si + vedono ancora in `/admin/conversazioni` con la loro etichetta d'entità; la + lista progetti non ha più la colonna Timer. +- **A3** — timer avviato su un task: `time_entries` ha `task_id` valorizzato; il + totale della fase è la somma dei suoi task; il totale progetto non cambia. + Poi cancellare quel task e verificare che **l'entry sopravviva** con + `task_id` a NULL. +- **A2 / B1 / B2** — confrontare ogni numero a video con la stessa query lanciata + a mano su prod via `psql`. Su un dataset di 5 progetti si controllano tutti a + occhio; è l'unico momento in cui questo è ancora possibile. +- **B2** — mettere una `due_date` manuale su un progetto e verificare che vinca + sulla data derivata dall'offerta. +- **C1** — `curl` con secret giusto (lead creato), con secret sbagliato (403), + senza secret (403), e ripetuto per verificare che non duplichi. +- Dual light/dark su ogni schermata nuova, e nessuna classe palette raw. + +Il portale cliente non va toccato da nessuna di queste modifiche: `client-view.ts` +non si tocca, quindi il vincolo LOCKED #2 non è in gioco. diff --git a/.claude/projects/.gitkeep b/.claude/projects/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/.claude/rules/memory-discipline.md b/.claude/rules/memory-discipline.md index 7eab54c..255daed 100644 --- a/.claude/rules/memory-discipline.md +++ b/.claude/rules/memory-discipline.md @@ -16,7 +16,9 @@ Dopo **ogni** unità di lavoro conclusa — una fase, una migration applicata, u ## Cosa scrivere nella memoria persistente -`~/.claude/projects/-Users-simonecavalli-Vault-IAMCAVALLI/memory/` — un file per fatto, più la riga di indice in `MEMORY.md`. +`~/.claude/projects/-Users-simonecavalli-Vault-IAMCAVALLI-hub/memory/` — un file per fatto, più la riga di indice in `MEMORY.md`. + +⚠️ **La chiave finisce in `-hub`.** Quella senza suffisso (`…-Vault-IAMCAVALLI/memory/`) è la memoria del *workspace*, un altro posto con altri file: scriverci un fatto di ClientHub significa non ritrovarlo più, perché a inizio sessione qui viene iniettata solo quella con `-hub`. Da non confondere nemmeno con `.claude/memory/`, che è versionata nel repo — la distinzione sta in [`../CLAUDE.md`](../CLAUDE.md). Ci va quello che **non si deduce dal repo**: decisioni e il loro perché, vincoli operativi, cose che sono state provate e non funzionano. Non ci va quello che il codice già dice: struttura, cronologia dei fix, contenuto di `CLAUDE.md`. diff --git a/.claude/settings.json b/.claude/settings.json index 8506990..2a80c20 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -18,6 +18,28 @@ ] }, "hooks": { + "PreToolUse": [ + { + "matcher": "Write|Edit|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/guardia-migration.sh" + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Write|Edit|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/guardia-token.sh" + } + ] + } + ], "Stop": [ { "hooks": [ @@ -28,16 +50,5 @@ ] } ] - }, - "enabledPlugins": { - "impeccable@impeccable": true - }, - "extraKnownMarketplaces": { - "impeccable": { - "source": { - "source": "github", - "repo": "pbakaus/impeccable" - } - } } } diff --git a/.claude/skills/audit/SKILL.md b/.claude/skills/audit/SKILL.md new file mode 100644 index 0000000..1d09361 --- /dev/null +++ b/.claude/skills/audit/SKILL.md @@ -0,0 +1,77 @@ +--- +name: audit +description: Far girare le fonti di rilevazione dell'audit sito di ClientHub su un URL e capire cosa e' stato misurato e cosa no. Da usare quando si lavora al motore audit v2.5, quando serve una rilevazione su un sito reale, quando si vuole sapere se una fonte risponde, o prima di scrivere qualsiasi pezzo del documento di audit. +--- + +# Audit — le rilevazioni, e cosa vale come misura + +Il motore v2.5 **non e' scritto**. Quello che esiste, e che questa skill mette in moto, +sono le fonti di rilevazione: nessun LLM, nessun database, nessun browser headless. + +## Due attrezzi, due domande diverse + +| Comando | Cosa fa | Quando | +|---|---|---| +| `npx tsx scripts/audit-fonti.ts ` | Le **cinque fonti** di `src/lib/audit/sources/` — pagine, CrUX, PageSpeed, storico Wayback, segnali. Puramente meccanico | «Cosa si riesce a misurare su questo sito?» | +| `npx tsx scripts/spike-audit.ts ` | Verifica le **264 voci di checklist** con Sonnet 5, poi sintetizza con Opus 5. Volutamente **isolato** da `src/`: non importa nulla, non tocca il DB | «Cosa c'e' che non va, in parole?» | + +Opzioni comuni: `--profilo=servizi|ecommerce`, `--no-psi` (salta PageSpeed, che da solo +vale meta' del tempo). Riferimento misurato il 2026-08-26 su `giojello.com`: cinque fonti +su cinque, **42,7 s** in tutto, PageSpeed 34,2 s. Se una fonte tace, **quella e' la notizia** — +va riportata, non aggirata. + +`PAGESPEED_API_KEY` viene letta dall'ambiente o da `.env.local`. Senza, PageSpeed e CrUX +rispondono a vuoto **senza errore**: lo script lo dice in testa, leggerlo. + +Il grezzo finisce in `audit-fonti-.json` (gitignorato). E' li' che ogni numero del +documento dovra' essere rintracciabile. + +## Le tre regole che il motore erediterà + +Non sono preferenze. Sono gia' costate, e stanno per esteso in `STATUS.md` § Lezioni operative. + +1. **Un numero entra solo se e' stato misurato.** Rintracciabile nel grezzo, e nel motore in + `audit_runs.raw`. Nessun numero dedotto, arrotondato o ricordato. + +2. **Laboratorio e campo non si fondono — la differenza *e'* il risultato.** Su giojello.com + Lighthouse dava `server-response-time` **7 ms** e CrUX TTFB p75 **3.553 ms con l'1% nel + verde**: il server risponde in fretta al datacenter Google e lento a tutti gli altri. Due + numeri con lo stesso nome verrebbero fusi in uno solo — per questo il campo si chiama + `risposta_server_ms` e non `ttfb_ms`. + +3. **Un punteggio va sempre con la sua data.** Performance mobile 52, poi 64 sullo stesso sito + mezz'ora dopo. Mai presentato come una costante del sito. + +E una regola di metodo che vale per ogni fonte nuova: **ogni estrazione da un'API di terzi va +vista funzionare, non dedotta dai docs.** Scrivendo `sources/`, due estrazioni prese dalla +documentazione hanno restituito valori vuoti *senza errore* — `largest-contentful-paint-element` +non esiste piu' e `configSettings.screenEmulation` non esiste affatto nelle risposte pubbliche. +Trovate solo perche' le fonti sono state fatte girare su un sito vero. + +## Perche' niente browser headless + +Misurato il 2026-08-17 sul VPS: **1,5 GB di RAM liberi su 3,8**, Chromium ne prende 500 MB–1 GB +→ OOM kill sui servizi con dati reali. Verificato il 2026-08-18 che non serve comunque: 153 voci +si verificano sul DOM renderizzato che PageSpeed restituisce, e lo screenshot buono e' +`final-screenshot`, non `fullPageScreenshot`. + +## Lo stato di v2.5, per non ripartire dal posto sbagliato + +**In pausa dal 2026-08-19** per scelta: prima le modifiche all'hub, poi il motore. + +| Pezzo | Dove | Stato | +|---|---|---| +| Schema, 7 tabelle + rubrica 264 voci | `0017_audits.sql`, `checklist_items` | in produzione | +| Le cinque fonti | `src/lib/audit/sources/` | **in prod ma inerti**: nessuna route le chiama | +| Agent, sintetizzatore, pipeline, editor, pagina | `src/lib/audit/`, `src/app/{admin/audit,audit}` | **da scrivere** | + +I piani stanno in [`../../plans/`](../../plans/): `v2.5-audit-documento.md` (i tre livelli +Radiografia / Prima-Dopo / Rotta come configurazioni di **un unico** documento) e +`v2.5-audit-motore.md` (raccolta in parallelo → quattro sub-agent → sintetizzatore che +**incrocia** le osservazioni in massimo 10 finding). + +## L'agente, quando lo progetteremo + +Va in `.claude/agents/audit-*.md`, non qui. Non esiste ancora e non va abbozzato: il design +dei quattro sub-agent cambiera' scrivendo il motore. Quando nascera' eredita le tre regole +qui sopra — sono il perimetro, non il contorno. diff --git a/.claude/skills/preventivo/SKILL.md b/.claude/skills/preventivo/SKILL.md new file mode 100644 index 0000000..f4a7cc8 --- /dev/null +++ b/.claude/skills/preventivo/SKILL.md @@ -0,0 +1,89 @@ +--- +name: preventivo +description: Lavorare sulla pipeline che genera i preventivi di ClientHub (src/lib/proposal/) senza romperla. Da usare quando si modifica il prompt, lo schema Zod, il montaggio o la resa di un preventivo, quando una generazione fallisce con "Contenuto AI non valido", quando una sezione del documento esce vuota, o prima di pubblicare una proposta a un cliente. +--- + +# Preventivo — la catena e come non spezzarla + +Il preventivo lo genera **l'app**, da `/admin/preventivi/genera`. Questa skill non contiene +un prompt di generazione e non ne va aggiunto uno: il prompt vive in `agent.ts` ed e' l'unico. +Un secondo prompt qui resterebbe indietro rispetto a quello vero senza che nessuno se ne accorga. + +## La catena: quattro file che devono dire la stessa cosa + +``` +src/lib/proposal/agent.ts il prompt chiede N campi ─┐ +src/lib/proposal/schema.ts lo Zod pretende quegli N campi │ se non +src/lib/proposal/assemble.ts li impacchetta con prezzi+profilo │ coincidono, +src/components/public/proposal/ ProposalDeck + sections/ li resa ─┘ si rompe +``` + +**Si tocca il prompt, si riapre lo schema. Sempre.** I due fallimenti hanno forma diversa e +solo uno si vede subito: + +| Cosa hai fatto | Cosa succede | +|---|---| +| Campo aggiunto allo **schema**, non al prompt | `ProposalContentSchema.safeParse` fallisce → **ogni** generazione muore con «Contenuto AI non valido». Rumoroso, si scopre subito | +| Campo aggiunto al **prompt**, non allo schema | Zod lo scarta in silenzio. Il documento esce senza quella parte, e te ne accorgi davanti al cliente | +| Campo nello schema, nessuna `
` che lo legge | Generazione verde, sezione assente. Il piu' subdolo: niente segnala l'errore | + +Quando aggiungi un campo, il giro completo e' **quattro file**: prompt in `buildUserPrompt`, +schema Zod, eventuale passaggio in `assembleProposal`, e la sezione in +`src/components/public/proposal/sections/` piu' la riga in `ProposalDeck.tsx` che la monta. +I vincoli di cardinalita' stanno nello schema (`.min(3).max(5)` sui problemi, `.length(5)` sui +nodi del diagramma, `.min(4).max(10)` sulla matrice): se li cambi li' e non nel prompt, il +modello continua a produrre il numero vecchio e Zod lo rifiuta. + +## Prezzi: cosa puo' vedere il cliente + +Vincolo **LOCKED #2** (`../../CLAUDE.md`): al cliente vanno i totali, mai le righe di prezzo. + +`PricingSection.tsx` renderizza `tier.publicPrice ?? tier.servicesTotal` e il **nome** dei +servizi — corretto. Ma `assembleProposal` mette in `content.offer.tiers[].services[].unitPrice` +anche il prezzo unitario, e `ProposalDeck` e' `"use client"`: riceve l'intero `proposal` come +prop da un server component, quindi **tutto** l'oggetto finisce serializzato nel payload RSC +della pagina, renderizzato o no. + +E' la stessa trappola del gate OTP annotata in `STATUS.md` — *sparire a schermo non e' sparire*. +Prima di toccare la pagina pubblica, e prima di mandare un preventivo a un cliente che potrebbe +aprire il sorgente: + +```bash +curl -s https:///preventivo/ > /tmp/p.html +LC_ALL=C grep -c unitPrice /tmp/p.html # atteso a regime: 0 +``` + +Se e' > 0, la correzione non e' nascondere la sezione: e' una proiezione client-safe in +`assemble.ts` o al confine del componente, come gia' fa `src/lib/client-view.ts` per il portale. + +## Prima di pubblicare + +1. **Preflight sui dati finti** — `./check-profilo.sh` da questa cartella. `profile.ts` e' uno + **snapshot**: quello che c'e' dentro al momento della generazione finisce in + `proposals.content` e ci resta anche se poi correggi il file. +2. **Le citazioni sono verbatim o non sono.** Il prompt lo impone; verificarlo a campione contro + il transcript e' il controllo che nessuna macchina fa al posto tuo. Una citazione inventata + in un preventivo e' peggio di un preventivo senza citazioni. +3. **Copy** — italiano, registro di `../../../../brand/voce.md`, e **nessun numero che non stia + in `../../../../brand/prove.md` marcato divulgabile** (`../../../../.claude/rules/lingua-e-tono.md`). + ⚠️ `brand/` sta **fuori dal repo hub**: su un clone senza il workspace questo passo si salta + dichiarandolo, non si finge di averlo fatto. +4. **Stato** — `draft` non e' visibile (la pagina risponde «non ancora disponibile»). + Pubblicare = `publishProposal` in `src/app/admin/preventivi/actions.ts`. + +## Come si prova davvero + +- `npm run build` e' **la verifica di riferimento**: non esiste test suite, e il build fa il + typecheck. Verde qui significa «i tipi tornano», non «il documento e' giusto». +- Per vedere l'output serve una **generazione vera** dall'admin, con un'offerta e almeno un + transcript. In locale oggi **non si puo'**: `.env.local` non autentica piu' contro il DB + (dal 2026-08-21, vedi `STATUS.md`). Si guarda in produzione. +- Un preventivo si legge a schermo prima di mandarlo. «Buildato» non e' «funziona», e + «generato» non e' «verificato». + +## Note sulla chiamata al modello + +`agent.ts` usa `max_tokens: 8192` per uno schema che chiede fino a 5 problemi + 5 soluzioni + +matrice. Se la risposta viene troncata, il fallimento **non** dice «troncata»: dice +«L'AI ha prodotto JSON non valido», e manda a cercare nel posto sbagliato. Se capita, +controllare `message.stop_reason === "max_tokens"` prima di dare la colpa al parsing. diff --git a/.claude/skills/preventivo/check-profilo.sh b/.claude/skills/preventivo/check-profilo.sh new file mode 100755 index 0000000..09a4668 --- /dev/null +++ b/.claude/skills/preventivo/check-profilo.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# Preflight: src/lib/proposal/profile.ts contiene dati placeholder? +# +# profile.ts e' uno SNAPSHOT: finisce dentro proposals.content al momento della +# generazione e ci resta. Un placeholder spedito una volta non si corregge piu' +# modificando il file — va rigenerato il preventivo. +# +# exit 0 = pulito · exit 1 = placeholder trovati +set -uo pipefail + +ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd) +F="$ROOT/src/lib/proposal/profile.ts" +[ -f "$F" ] || { echo "profile.ts non trovato in $F" >&2; exit 1; } + +# Ogni riga: etichetta|regex. Sono i segni concreti di "da riempire". +PATTERNS=( + 'testimonianza segnaposto|→ Aggiungere' + 'nome cliente finto|"Cliente [0-9]"' + 'ruolo generico|role: "Professione"' + 'campo da compilare|// → ' + 'dominio sbagliato (il brand e iamcavalli.net)|iamcavalli\.com' +) + +TROVATI=0 +for p in "${PATTERNS[@]}"; do + ETICHETTA="${p%%|*}"; RE="${p#*|}" + N=$(grep -cE "$RE" "$F" || true) + if [ "$N" -gt 0 ]; then + printf ' ✗ %-48s %s occorrenz%s\n' "$ETICHETTA" "$N" "$([ "$N" = 1 ] && echo a || echo e)" + TROVATI=$((TROVATI + N)) + fi +done + +if [ "$TROVATI" -gt 0 ]; then + cat >&2 <<'MSG' + +BLOCCO — profile.ts spedisce dati placeholder in ogni preventivo generato. + +Non si riempiono a occhio: i numeri divulgabili stanno in brand/prove.md, che oggi +e' ancora `stato: scheletro-intervista`. Finche' non sono verificati, la mossa giusta +non e' inventare — e' non renderizzare il blocco. + +Regola: ../../../.claude/rules/lingua-e-tono.md — «un numero affermato con sicurezza e +mai verificato e' il modo piu' veloce per bruciare la credibilita' che il tono costruisce». +MSG + exit 1 +fi + +echo " ✓ profile.ts pulito" diff --git a/.claude/workflows/.gitkeep b/.claude/workflows/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/.gitignore b/.gitignore index f7f1e32..7077307 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,6 @@ next-env.d.ts # Output degli spike audit (dati di siti di clienti, non vanno committati) spike-audit-*.json + +# Grezzo delle rilevazioni audit (npx tsx scripts/audit-fonti.ts) +audit-fonti-*.json diff --git a/.planning/STATE.md b/.planning/STATE.md index e2ec626..32c87bc 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -4,8 +4,8 @@ milestone: v2.5 milestone_name: Audit status: executing stopped_at: "v2.5 in PAUSA. Modifiche hub: A, B, C1, rifiniture, chat a canali (0021), modifica messaggi + firma (0022), stati task e date pagamenti (0023) in prod; C2 (TidyCal) bloccato sulle credenziali API. Nulla del portale e' stato visto a schermo: .env.local non autentica piu', serve ?preview=1 in prod. Il riquadro 'Prossimo pagamento' non compare finche' nessuna rata ha una due_date." -last_updated: "2026-08-22T15:20:00.000Z" -last_activity: 2026-08-22 -- stato task "cancellata" + scadenza e data di incasso dei pagamenti nel portale +last_updated: "2026-08-26T16:10:00.000Z" +last_activity: 2026-08-26 -- architettura .claude di hub: skill /preventivo e /audit, due hook di guardia, piani portati nel repo progress: total_phases: 4 completed_phases: 0 @@ -47,9 +47,7 @@ Progress: [███░░░░░░░] 25% (v2.5) ## Dove sta cosa -**Fuori dal repo, e senza questi niente è ricostruibile:** i piani in `~/.claude/plans/` -— `…woolly-puddle.md` (documento audit), `…radiant-valley.md` (motore), -`sei-arrivato-qua-search-recursive-kettle.md` (modifiche hub). +I piani della milestone sono **nel repo dal 2026-08-26**: `.claude/plans/v2.5-*.md`. | Cosa (audit) | Dove | Stato | |---|---|---| @@ -63,7 +61,7 @@ Progress: [███░░░░░░░] 25% (v2.5) Raccolta in parallelo (nessun LLM, nessun browser headless) → quattro sub-agent → sintetizzatore che **incrocia** le osservazioni in massimo 10 finding. Vincolo che regge tutto: **un numero entra solo se misurato**, rintracciabile in `audit_runs.raw`. -Passo per passo in `STATUS.md` e in `…radiant-valley.md`. +Passo per passo in `STATUS.md` e in `.claude/plans/v2.5-audit-motore.md`. ## Accumulated Context @@ -94,7 +92,9 @@ Log completo in `PROJECT.md`. Vive per il lavoro corrente: ## Session Continuity -Last session: 2026-08-22 +Last session: 2026-08-26 Stopped at: **stato task «Cancellata» + date dei pagamenti nel portale** (`fe76789`, migration `0023` applicata a prod prima del push). Un task tolto dal lavoro ora ha dove stare: **X nel cerchio, titolo barrato**, pill «Cancellata» — l'unico stato chiuso con la pill, perché «Fatto» e «Cancellata» sono entrambi barrati e scambiarli significa credere consegnato ciò che non esiste. Esce da **tutti** i denominatori via `countsTowardProgress()`. Nel Kanban cliente la colonna compare solo se piena (mai nascosta se ha dentro qualcosa); nell'admin c'è sempre, ed è così che si cancella un task. Lato pagamenti: `payments.due_date` (nullable, più indice parziale per il futuro promemoria email), riquadro «Prossimo pagamento» con conto alla rovescia in parole e rosso se scaduto, «Scade il…» / «Pagato il…» su ogni riga, **zero importi**. Admin: campo Scadenza per rata, «Incassato nel mese» → «Incassato il» (giorno; le analytics raggruppano per mese e non se ne accorgono). Build, typecheck e lint verdi. +**2026-08-26 — architettura `.claude/`**: skill `/preventivo` e `/audit`, due hook di guardia, piani nel repo. Nessun tocco al prodotto. Le due cose trovate e non risolte sul preventivo → `STATUS.md`. + Next: (1) verificare in prod con `?preview=1` **entrambe** le cose: stati task su **Caruso Speaker, fase «3 - Esecuzione»** (l'unica con «In corso» e «In revisione» insieme, 4 + 2 su 11) e box pagamenti — ma prima **inserire una scadenza** dal tab Pagamenti, altrimenti il riquadro non compare per definizione; (2) due `paid_at` storici valgono il primo del mese (2026-03-01, 2026-01-01, scritti dal vecchio selettore a mese) e il cliente ora li legge come «Pagato il 1 mar 2026»: correggerli se il giorno vero era un altro; (3) provare la chat in prod: modificare un messaggio admin e vederlo cambiare da solo entro ~20s senza duplicarsi; (4) sbloccare TidyCal con token o documentazione; (5) `LEAD_WEBHOOK_SECRET` su Coolify, senza cui `/api/webhooks/lead` risponde 403 a tutti; (6) poi v2.5 da `src/lib/audit/schema.ts` + `agents/`. Resume file: None diff --git a/CLAUDE.md b/CLAUDE.md index ac90017..57381d5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,6 +4,10 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co # ClientHub +> Progetto del workspace [IAMCAVALLI](../CLAUDE.md). Questa e' la cartella del **codice**: sito, social e Meta Ads sono progetti separati accanto a questo. Per il codice comanda questo file, non quello del workspace. +> +> **Per i testi che legge un cliente** — etichette di stato, pulsanti, messaggi di errore, email transazionali — vale [`../brand/voce.md`](../brand/voce.md) e la regola in `../.claude/rules/lingua-e-tono.md`. Italiano, registro premium e assertivo, e nessun numero affermato che non stia in `../brand/prove.md`. + Portale clienti per consulente di personal branding. Admin area + dashboard cliente via link segreto. ## Stack @@ -50,7 +54,14 @@ Single source of truth: **`design-reference/DESIGN-SYSTEM.md`** ("Quiet Luxury" - Reuse the existing primitives before building new ones: `StatusBadge`, `SearchInput`, `SegmentedToggle`, `editable-cell`, `option-select`/`option-multi-select` in `src/components/ui/`, and the shell in `src/components/admin/AdminShell.tsx`. - Status/semantic colours (lead stages in `StatusBadge`) are the one sanctioned exception to the token rule — they use the Tailwind palette directly, each with an explicit `dark:` variant. So is the sidebar's brand green, and `src/lib/mailer.ts` (email HTML can't use CSS vars). -Other docs: `STATUS.md` — **the single narrative document**: current status, backlog, and the operational lessons worth re-reading · `.planning/STATE.md` (GSD digest, kept under 100 lines, milestone v2.4) · `.planning/REQUIREMENTS.md` (current backlog) · `.planning/security/` (2026-07 audit, closed) · `.planning/milestones/` (closed-milestone archives). +Other docs: `STATUS.md` — **the single narrative document**: current status, backlog, and the operational lessons worth re-reading · `.planning/STATE.md` (GSD digest, kept under 100 lines, milestone v2.4) · `.planning/REQUIREMENTS.md` (current backlog) · `.planning/security/` (2026-07 audit, closed) · `.planning/milestones/` (closed-milestone archives) · `.claude/CLAUDE.md` (guida della cartella di configurazione: skill, hook, dove sta la memoria) · `.claude/plans/` (i piani delle milestone). + +## Project Skills +Due skill locali, in `.claude/skills/` (le altre sono globali in `~/.claude/skills/`): +- **`/preventivo`** — la catena `agent.ts → schema.ts → assemble.ts → ProposalDeck` e come non spezzarla; preflight sui dati placeholder di `profile.ts`. +- **`/audit`** — `npx tsx scripts/audit-fonti.ts ` mette in moto le cinque fonti di `src/lib/audit/sources/` (oggi inerti in prod) e dice cosa e' stato misurato. + +Due hook di guardia in `.claude/hooks/`: `guardia-migration.sh` **blocca** l'SQL distruttivo sulle entita' protette (Data Safety LOCKED), `guardia-token.sh` **avvisa** sulle classi Tailwind grezze. Dettaglio e comandi di prova in `.claude/CLAUDE.md`. ## GSD Workflow Planning in `.planning/`. Use `/gsd-plan-phase N` → `/gsd-execute-phase N`. State in `.planning/STATE.md`. diff --git a/STATUS.md b/STATUS.md index d39f653..f74e439 100644 --- a/STATUS.md +++ b/STATUS.md @@ -30,7 +30,7 @@ schema dell'audit, nessuna pagina. La pausa è una scelta del 2026-08-19 — pri ### Modifiche hub (richieste 2026-08-18) Tre aree: dashboard, progetti, pipeline. Piano in -`~/.claude/plans/sei-arrivato-qua-search-recursive-kettle.md`, **fuori dal repo**. +[`.claude/plans/v2.5-modifiche-hub.md`](.claude/plans/v2.5-modifiche-hub.md). **In produzione dal 2026-08-19** (commit `4b135ce` → `19ed377`): @@ -377,6 +377,85 @@ tab Pagamenti per vederlo. (2) Due `paid_at` storici valgono il **primo del mese li legge come «Pagato il 1 mar 2026». Se il giorno vero era un altro, si corregge dal nuovo campo. +### Architettura `.claude/` (2026-08-26) + +Configurazione, non prodotto: l'unico file di codice che nasce qui è `scripts/audit-fonti.ts`. + +`hub/.claude/` aveva tre cose dentro — `rules/`, `settings.json`, `settings.local.json` — e +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 è «il CLAUDE.md che mancava»: quello di +progetto sta alla radice ed è lui a comandare. Questo è la guida della cartella di +configurazione, e si carica quando ci lavori dentro. + +**Due skill locali** (le altre restano globali in `~/.claude/skills/`, e copiarle qui +significherebbe averne due destinate a divergere): + +- **`/preventivo`** — non contiene un prompt di generazione, e non deve averlo: quello vive + in `agent.ts` ed è l'unico. Contiene la mappa della catena `agent.ts → schema.ts → + assemble.ts → ProposalDeck + sections/`, con la tabella dei tre modi di romperla — e solo + uno fa rumore. Campo nello schema e non nel prompt: **ogni** generazione muore con + «Contenuto AI non valido». Campo nel prompt e non nello schema: Zod lo scarta in silenzio. + Campo nello schema senza sezione che lo legge: generazione verde, sezione assente. + Porta anche `check-profilo.sh`, il preflight sui dati finti. +- **`/audit`** — guida `scripts/audit-fonti.ts`, nuovo, che mette in moto le cinque fonti di + `src/lib/audit/sources/`, in produzione dal 2026-08-19 ma **mai chiamate da nessuno**. + Provate su giojello.com il 2026-08-26: cinque su cinque, **42,7 s** (PageSpeed 34,2), tre + pagine raccolte, CrUX disponibile a livello URL, PageSpeed mobile 58 / desktop 93. Il + grezzo finisce in `audit-fonti-.json`, gitignorato: è lì che ogni numero del + documento dovrà essere rintracciabile. Distinto da `spike-audit.ts`, che è un altro + attrezzo e risponde a un'altra domanda. + +**Due hook di guardia**, provati a mano prima di fidarsene (6 casi il primo, 5 il secondo): + +- `guardia-migration.sh` **blocca** — è il vincolo Data Safety (LOCKED) fatto rispettare dalla + macchina invece che dalla memoria. Toglie i commenti e spezza per `;` prima di guardare, così + «non fare mai DROP TABLE payments» dentro un commento non fa scattare nulla, e + `ALTER TABLE clients DROP COLUMN token` sì. Fuori da `src/db/migrations/` non ha voce. +- `guardia-token.sh` **avvisa e basta**. Bloccare, con ~450 occorrenze di debito già in casa, + lo renderebbe un ostacolo da disattivare invece di un promemoria da leggere. Whitelist per + le eccezioni sanzionate: `StatusBadge`, `AdminShell`, `mailer`, `globals.css`, + `design-reference/`. + +**I tre piani della milestone sono entrati nel repo.** Stavano solo in `~/.claude/plans/` con +nomi generati a caso, e STATE.md avvertiva che senza quelli v2.5 non era ricostruibile: un +piano che vive sul portatile di chi l'ha scritto non è documentazione, è un ricordo. Passati +al setaccio per credenziali prima di committarli — dentro ci sono nomi di variabili +d'ambiente, mai i loro valori. + +Corretta anche una riga di `rules/memory-discipline.md` che mandava nel posto sbagliato: la +memoria persistente di hub sta sotto la chiave `…-Vault-IAMCAVALLI-hub`, non quella del +workspace senza suffisso. Sedici file erano nella prima, la regola indicava la seconda. + +#### Due cose trovate sul preventivo, e non risolte + +Nessuna delle due è stata toccata: sono decisioni, non refusi. + +**`profile.ts` spedisce dati placeholder in ogni preventivo generato.** Tre testimonianze sono +letteralmente `{ name: "Cliente 1", quote: "→ Aggiungere testimonianza reale." }`; accanto, +email `simone@iamcavalli.com` e sito `iamcavalli.com` quando il dominio è **iamcavalli.net**, e +titolo «Strategist · Brand & Business» quando il posizionamento è **Full Stack Designer**. Il +file è uno **snapshot**: finisce in `proposals.content` al momento della generazione e ci resta +anche correggendo il file dopo. Non è riempibile a occhio — `brand/prove.md` è ancora +`stato: scheletro-intervista`, con i numeri marcati «da verificare». Quindi la mossa non è +inventare: è **non renderizzare il blocco** finché il dato non c'è, allineare i campi già +verificati, e portare la domanda a chi ha le testimonianze vere. `check-profilo.sh` oggi +fallisce con 19 occorrenze, ed è il comportamento giusto. + +**`unitPrice` viaggia nel payload RSC di `/preventivo/[slug]`.** `PricingSection` renderizza +solo `publicPrice ?? servicesTotal` e il nome dei servizi — corretto. Ma `assembleProposal` +mette in `content.offer.tiers[].services[].unitPrice` anche il prezzo unitario, e +`ProposalDeck` è `"use client"`: riceve l'intero `proposal` come prop da un server component, +quindi tutto l'oggetto viene serializzato nella pagina, renderizzato o no. È la stessa trappola +del gate OTP: **sparire a schermo non è sparire**. Verificato leggendo il codice, **non ancora +a schermo** — serve `curl` su un preventivo pubblicato e `LC_ALL=C grep -c unitPrice`. Se +conferma, la correzione è una proiezione client-safe come già fa `client-view.ts` per il +portale, non nascondere una sezione. + +Terza, minore: `agent.ts` chiama il modello con `max_tokens: 8192` per uno schema che chiede +fino a 5 problemi + 5 soluzioni + matrice. Se la risposta viene troncata l'errore non dice +«troncata», dice «L'AI ha prodotto JSON non valido» — e manda a cercare nel posto sbagliato. +Il controllo di `stop_reason` costa tre righe. + ### v2.5 — Audit (Phases 27–30), in pausa Il servizio di analisi sito (tre livelli: **Radiografia / Prima-Dopo / Rotta**) diventa @@ -397,9 +476,11 @@ Manca: agent + sintetizzatore + pipeline, storage immagini, editor admin, pagina pubblica. Dettaglio in [`.planning/ROADMAP.md`](.planning/ROADMAP.md) e [`.planning/REQUIREMENTS.md`](.planning/REQUIREMENTS.md). -⚠️ **I due piani della milestone stanno in `~/.claude/plans/`, fuori dal repo** -(`…woolly-puddle.md` per il documento, `…radiant-valley.md` per il motore). Senza quei -file la milestone non è ricostruibile. +I due piani della milestone sono **nel repo dal 2026-08-26**: +[`.claude/plans/v2.5-audit-documento.md`](.claude/plans/v2.5-audit-documento.md) per il +documento, [`.claude/plans/v2.5-audit-motore.md`](.claude/plans/v2.5-audit-motore.md) per il +motore. Prima stavano solo in `~/.claude/plans/`, e senza quei file la milestone non era +ricostruibile: un piano che vive sul portatile di chi l'ha scritto non è documentazione. ## Fatto diff --git a/scripts/audit-fonti.ts b/scripts/audit-fonti.ts new file mode 100644 index 0000000..1627532 --- /dev/null +++ b/scripts/audit-fonti.ts @@ -0,0 +1,108 @@ +/** + * Mette in moto le cinque fonti di `src/lib/audit/sources/` su un URL. + * + * Le fonti sono in produzione dal 2026-08-19 ma INERTI: nessuna route le chiama. + * Questo script e' l'unico modo di vederle funzionare, e serve a rispondere a una + * domanda sola: **cosa e' stato misurato davvero, e cosa no.** Un buco qui e' il + * risultato, non un errore da nascondere — il motore dovra' saperlo dire al cliente. + * + * Nessun LLM, nessun database, nessun browser headless (il VPS non lo regge). + * + * npx tsx scripts/audit-fonti.ts https://esempio.it + * npx tsx scripts/audit-fonti.ts https://esempio.it --profilo=ecommerce + * npx tsx scripts/audit-fonti.ts https://esempio.it --no-psi + * + * PAGESPEED_API_KEY (per PageSpeed e CrUX) viene letta dall'ambiente o da .env.local. + */ +import { readFileSync, writeFileSync, existsSync } from "node:fs"; +import { resolve } from "node:path"; +import { raccogliPagine } from "../src/lib/audit/sources/fetch"; +import { rilevaCrux } from "../src/lib/audit/sources/crux"; +import { rilevaPagespeedCompleto } from "../src/lib/audit/sources/pagespeed"; +import { rilevaStorico } from "../src/lib/audit/sources/history"; +import { rilevaSegnali } from "../src/lib/audit/sources/signals"; + +function caricaEnv() { + if (process.env.PAGESPEED_API_KEY) return; + const f = resolve(process.cwd(), ".env.local"); + if (!existsSync(f)) return; + for (const riga of readFileSync(f, "utf8").split("\n")) { + const m = riga.match(/^\s*(?:export\s+)?([A-Z0-9_]+)\s*=\s*(.*)$/); + if (m && !process.env[m[1]]) process.env[m[1]] = m[2].trim().replace(/^["']|["']$/g, ""); + } +} + +/** Esegue una fonte senza far cadere le altre: un buco e' un dato, non un crash. */ +async function fonte(nome: string, f: () => Promise) { + const t0 = Date.now(); + try { + const dati = await f(); + return { nome, ok: true as const, ms: Date.now() - t0, dati }; + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + return { nome, ok: false as const, ms: Date.now() - t0, errore: msg }; + } +} + +async function main() { + caricaEnv(); + + const argv = process.argv.slice(2); + const url = argv.find((a) => !a.startsWith("--")); + if (!url) { + console.error("uso: npx tsx scripts/audit-fonti.ts [--profilo=servizi] [--no-psi]"); + process.exit(1); + } + const profilo = argv.find((a) => a.startsWith("--profilo="))?.split("=")[1] ?? "servizi"; + const psi = !argv.includes("--no-psi"); + const inizio = Date.now(); + + console.log(`\n ${url} · profilo ${profilo}${psi ? "" : " · PageSpeed saltato"}`); + if (!process.env.PAGESPEED_API_KEY) { + console.log(" ⚠ PAGESPEED_API_KEY assente: PageSpeed e CrUX risponderanno a vuoto."); + } + console.log(""); + + // La home serve alle altre due fonti: si scarica una volta sola. + const raccolta = await fonte("pagine", () => raccogliPagine(url, profilo)); + if (!raccolta.ok) { + console.error(` ✗ pagine — ${raccolta.errore}\n\n Senza la home non parte nient'altro.`); + process.exit(1); + } + const { home, homeHtml, homeHeaders, pagine, errori } = raccolta.dati; + + const risultati = [ + raccolta, + ...(await Promise.all([ + fonte("crux", () => rilevaCrux(home)), + ...(psi ? [fonte("pagespeed", () => rilevaPagespeedCompleto(home))] : []), + fonte("storico", () => rilevaStorico(home, homeHtml)), + fonte("segnali", () => rilevaSegnali(home, homeHtml, homeHeaders)), + ])), + ]; + + console.log(" FONTE ESITO TEMPO"); + for (const r of risultati) { + const esito = r.ok ? "ok " : "MUTA "; + console.log(` ${r.nome.padEnd(11)} ${esito} ${(r.ms / 1000).toFixed(1)}s${r.ok ? "" : ` ${r.errore}`}`); + } + + console.log(`\n Pagine raccolte: ${pagine.length} (${pagine.map((p) => p.ruolo).join(", ")})`); + if (errori.length) console.log(` Non scaricate: ${errori.join(", ")}`); + if (home !== url) console.log(` Canonico: ${home} (redirect dal valore passato)`); + + // Il grezzo e' il posto dove ogni numero del documento dovra' essere rintracciabile. + const dominio = new URL(home).hostname.replace(/^www\./, ""); + const file = resolve(process.cwd(), `audit-fonti-${dominio}.json`); + writeFileSync(file, JSON.stringify({ url: home, profilo, quando: new Date().toISOString(), risultati }, null, 2)); + + console.log(`\n Grezzo: ${file}`); + console.log(` Totale: ${((Date.now() - inizio) / 1000).toFixed(1)}s\n`); + console.log(" Un numero entra in un documento solo se lo ritrovi qui dentro."); + console.log(" Un punteggio PageSpeed va sempre con la sua data: ballano di 12 punti in mezz'ora.\n"); +} + +main().catch((e) => { + console.error("\nERRORE:", e instanceof Error ? e.message : e); + process.exit(1); +});