Con un cliente «Gian Luca Caruso / Caruso Speaker» il menu proponeva tre voci
per un destinatario solo. Non era un difetto di resa: al menu era stata passata
la stessa lista che il parser usa per RICONOSCERE un tag rileggendo il testo.
Sono due domande diverse, e il codice le aveva confuse:
- come lo si puo' scrivere -> tutti gli alias, invisibili, solo in lettura
- chi si puo' scegliere -> una riga per destinatario
`MentionTarget` le separa: `label` per il menu, `aliases` per la rilettura.
Scrivere «@Gian» a mano continua a valere come tag e a far partire la mail --
cambia solo cosa viene offerto, non cosa viene accettato.
Il filtro del menu ora passa da `normalizeForSearch`, la stessa
normalizzazione del parser: con un toLowerCase() a parte, digitare «nicolo»
non avrebbe trovato «Nicolò» nel menu mentre nel messaggio sarebbe stato
riconosciuto -- due regole per la stessa domanda, e la seconda si scopre solo
quando la mail non parte.
Il difetto scalava peggio del valore: con tre contatti per cliente sarebbero
diventate nove righe. `mentionTargets()` ritorna gia' un array per questo, ma
il tag per singola persona resta impossibile finche' `client_emails` ha gli
indirizzi e non i nomi: serve una migration, ed e' un lavoro a se'.
Verificato: build e lint puliti, 23 test su parser e menu. Mai visto a schermo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Il bloccante sul push va tolto, non lasciato: una memoria sbagliata e' peggio
di una assente, e "push 403" avrebbe fatto ripartire la prossima sessione da
un problema gia' risolto.
Resta scritto COME si e' risolto -- l'accesso a Gitea si recupera dalla CLI
admin dentro il container, non dal web -- perche' quello sara' ancora vero la
prossima volta che il portachiavi scade.
In cima ai Next: rigenerare il token, che e' stato incollato in chat.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Il 403 va scritto come bloccante e non come nota di passaggio: finche' dura,
NESSUN commit arriva in produzione -- Coolify deploya sul push a `main`.
La credenziale nel portachiavi legge ma non scrive, e la chiave SSH locale non
e' registrata su Gitea, quindi non c'e' via alternativa.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La cartella aveva dentro solo rules/ e i settings: nessun posto dove mettere una
skill, un hook o un piano. Ora ha lo scheletro completo e un .claude/CLAUDE.md che
spiega cosa va dove — non duplica CLAUDE.md di progetto, che resta quello che comanda.
Due skill locali (le altre restano globali in ~/.claude/skills/):
- /preventivo — la catena agent.ts → schema.ts → assemble.ts → ProposalDeck e i tre
modi di romperla, di cui uno solo fa rumore. Nessun prompt di generazione qui
dentro: quello vive in agent.ts ed e' l'unico. Porta check-profilo.sh.
- /audit — guida scripts/audit-fonti.ts, nuovo, che mette in moto le cinque fonti di
src/lib/audit/sources/, in prod dal 2026-08-19 ma mai chiamate da nessuno. Provate
su giojello.com: 5 su 5, 42,7 s, PageSpeed mobile 58 / desktop 93.
Due hook, provati a mano (6 casi il primo, 5 il secondo):
- guardia-migration.sh BLOCCA l'SQL distruttivo sulle entita' protette — il vincolo
Data Safety (LOCKED) fatto rispettare dalla macchina invece che dalla memoria.
- guardia-token.sh AVVISA sulle classi Tailwind grezze. Non blocca: con ~450
occorrenze di debito, bloccare lo renderebbe un ostacolo da disattivare.
I tre piani di v2.5 entrano nel repo: stavano solo in ~/.claude/plans/ e STATE.md
avvertiva che senza quelli la milestone non era ricostruibile. Passati al setaccio
per credenziali prima di committarli.
Corretta in rules/memory-discipline.md la chiave della memoria persistente: e'
…-Vault-IAMCAVALLI-hub, non quella del workspace. Sedici file stavano nella prima,
la regola indicava la seconda.
Impeccable resta abilitato solo a livello globale: fuori da settings.json locale.
Nessun tocco al prodotto. Build e lint verdi, lint identico al baseline.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
STATUS.md: perché la pill sta su "Cancellata" e non su "Fatto" (sono entrambi
barrati, e scambiarli significa credere consegnato ciò che non esiste), perché
`due_date` è nullable, e le due verifiche che restano a mano — il riquadro
"Prossimo pagamento" non compare finché nessuna rata ha una scadenza, e due
`paid_at` storici valgono il primo del mese perché li ha scritti il vecchio
selettore a mese.
DESIGN-SYSTEM.md: la quinta forma, la regola della colonna Kanban che si
nasconde solo da vuota, e la spec del box pagamenti.
STATE.md: due decisioni nuove, e resta sotto le 100 righe accorpando le righe
di tabella della stessa settimana.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Il punto che vale la pena scrivere non e' la UI ma la semantica: "in revisione" significa
"aspetta l'OK del cliente", non controllo qualita' interno come dice 5547e55. Quel commit
resta nel repo a raccontare la versione sbagliata, quindi la correzione deve stare dove
qualcuno la trova — STATUS.md, non solo nel messaggio di questo commit.
Annotato anche cosa NON e' stato verificato: il bundle in produzione contiene il codice
nuovo (controllato dentro il container), ma nessuno l'ha ancora visto a schermo, perche'
il portale sta dietro il gate OTP e l'anteprima vuole una sessione admin.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nella Timeline — la vista di default — lo stato di un task era solo un cerchietto
colorato: ambra col puntino da 1.5px per "in corso", violetto col puntino da 2px per
"in revisione". A 20px sono lo stesso oggetto. Solo "in revisione" aveva un title; gli
altri tre stati non avevano ne' testo, ne' tooltip, ne' aria-label — l'opposto della
regola UX 3 del design system, che chiede colore + testo, mai colore da solo.
Il punto pero' non era la somiglianza fra i due colori. "In revisione" non significa
quello che dice 5547e55, che lo descriveva come controllo qualita' interno: nel flusso
reale vuol dire "consegnato, aspetta l'OK del cliente". E' uno stato che richiede
un'azione, e per questo un cerchietto muto era il problema vero.
- Le quattro icone si distinguono per forma, non solo per colore, quindi reggono anche
in bianco e nero e con un daltonismo: vuoto -> punto -> spunta vuota -> spunta piena.
La spunta compare a lavoro finito e si riempie a lavoro confermato.
- Pill di testo solo su "in corso" e "in revisione". "Da fare" e' un cerchio vuoto e
"fatto" ha il titolo barrato: etichettarli aggiungeva rumore, non informazione.
- Il conteggio dei task in attesa sta nell'header della fase, quindi si legge anche a
card chiusa. Serve davvero: l'admin puo' forzare una fase su "completata" dalla
select, e in quel caso la card parte collassata con dentro la richiesta.
- Legenda + micro-copy sopra entrambe le viste: chi revisiona, e che ogni pagina include
un giro di revisione — l'informazione commerciale che il cliente non aveva da nessuna
parte. Rimanda alla chat della fase, che esiste gia'.
- Colonne del kanban cliente colorate con le stesse tinte dell'icona in Timeline: prima
erano quattro colonne grigie identiche.
- Icona aria-hidden + sr-only, cosi' lo screen reader annuncia lo stato anche sui due
che non portano la pill.
La barra di avanzamento non e' stata toccata: un task in revisione conta come uno in
corso, cioe' zero. Cambia il tipo di lavoro, non l'avanzamento — gonfiare la percentuale
le avrebbe fatto dire una cosa che il contatore "x di y task" smentiva una riga sotto.
Visual e copy in un solo file, e la legenda si genera da TASK_STATUSES: un quinto stato
non lascera' indietro una lista scritta a mano. Stessa disciplina di 5547e55, nata
proprio perche' due liste fisse avevano fatto sparire dei task senza un errore.
Nessuna migration: e' solo UI.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La cosa da rileggere fra sei mesi: propagare una modifica richiede
insieme il filtro allargato a edited_at e il watermark del client sul
massimo dei due timestamp. Una sola delle due e o la modifica non
arriva, o arriva a ogni giro per sempre.
Più il perché di due scelte che sembrano sviste: il non-letto resta su
created_at, e la foto è un URL esterno perché l'upload su volume non
esiste (la deroga LOCKED #5 è scritta ma mai costruita).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Il pattern che vale la pena rileggere: la chiave del canale è derivata,
non è una colonna, e sta in un modulo condiviso perché le due sponde
devono concordare al carattere. Più il perché del letto/non-letto
asimmetrico (tabella per il cliente, timestamp per l'admin).
Chiude anche il caveat in STATUS.md sulla risposta admin che finiva
sempre sul thread generale.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Guardando il DB: Caruso Speaker / B ha offer_value_override = 7500 a
fronte di un calcolo di 20.250. La 0020 non fa backfill, quindi quel
numero è stato messo a mano dal tab Offerte — updateOfferValueOverride è
provata sul campo, non più solo buildata.
Resta scoperta Rossi Inc / B: venduta a 7.000, il portale mostra 20.250.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cosa è stato verificato davvero: build e lint verdi, migration 0020
applicata a prod prima del push con la colonna riletta a conferma,
deploy atterrato (immagine taggata 44be190 = HEAD).
Cosa no, e ora è scritto: le pagine non sono state rese a runtime.
.env.local è scaduto su tre fronti — ADMIN_PASSWORD, NEXTAUTH_SECRET e
la password del DB — e l'host che dichiara (…:5432) è chiuso dal
firewall; il DB vero sta su 127.0.0.1:54321. Estrarre la password viva
dal container è bloccato dal classifier e non è stato aggirato. La nota
tecnica che dava per buone quelle credenziali diceva il falso.
Aggiunto come vedere se un deploy è atterrato: il tag dell'immagine in
docker ps è lo SHA del commit.
REQUIREMENTS: HUB-14 e HUB-15 registrati come fatti, HUB-16 apre la
verifica a mano che resta da fare, e il riallineamento di .env.local
entra fra le cose aperte.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Barra di avanzamento a tutta larghezza (via il max-w-[1200px]).
Card offerta: rimosso l'accordion "Cosa è compreso". La lista servizi non
arriva più al client — client-view.ts legge dai servizi i soli prezzi — e
OffersSection perde il suo unico useState, quindi anche il "use client".
"Valore incluso" diventa "Valore dell'offerta" e accetta un override manuale
(migration 0020, project_offers.offer_value_override). Prima era sempre la
somma dei prezzi di catalogo del tier: in produzione mostrava €20.250 su
offerte vendute a 7.000 e 5.500. NULL torna al calcolo, 0 nasconde la riga.
Si gestisce dal tab Offerte del progetto, che affianca la cifra calcolata
per far vedere cosa si sta sostituendo.
La somma calcolata vive ora in src/lib/offer-value.ts, letta sia dal portale
sia dall'admin: duplicarla avrebbe fatto divergere le due viste.
"Prezzo finale" diventa "Investimento finale" (il ricorrente resta "Canone
mensile").
Migration 0020 applicata a prod prima del push. Override tutti NULL, quindi
il comportamento resta invariato finché non se ne imposta uno.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
STATE.md diceva "Phase 27, nessun bloccante" mentre v2.5 e' ferma per scelta e
in produzione e' andato altro. Ora dice cosa e' vero: v2.5 in pausa, modifiche
hub in corso, blocchi A/B/C1 in produzione, e due bloccanti scritti con cosa
manca e chi li sblocca — le credenziali API di TidyCal e LEAD_WEBHOOK_SECRET
su Coolify, senza la quale la route rifiuta tutti (fallimento chiuso voluto).
Sta di nuovo sotto le 100 righe: ci e' rientrato togliendo cio' che STATUS.md
gia' racconta per esteso, non accorciando i bloccanti.
REQUIREMENTS.md guadagna HUB-01..13, con lo stato reale: otto fatti, cinque no.
Fra quelli aperti c'e' anche la conferma del payload Elementor, che oggi e'
gestito in modo difensivo e non verificato sul campo — distinguere "scritto" da
"visto funzionare" e' il punto della regola.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Il push era rimasto bloccato e la riga delle fonti diceva ancora "non in prod".
Ora e' vero il contrario, ma "in produzione" da solo sarebbe fuorviante: i cinque
moduli sono deployati e nessuna route li chiama. La riga dice entrambe le cose,
perche' confondere *deployato* con *funzionante* e' il modo piu' rapido per
credere di avere una feature che non esiste.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La roadmap era ferma al 2026-08-08 e diceva ancora "nessuna fase aperta" mentre
v2.5 era gia' partita e Phase 27 era a meta'; REQUIREMENTS.md era ancora quello
di v2.4. STATE.md invece era corretto — segno che aggiornare solo quello non
basta. Da qui la divisione dei ruoli, ora esplicita in testa a ogni file:
- STATE.md orientamento breve (99 righe): dove sta cosa, come funziona il
motore, i blocchi vivi. Niente narrativa.
- ROADMAP.md tutte le fasi 1->30, con lo stato di ciascuna
- REQUIREMENTS.md i 25 requisiti di v2.5 (AUD-01..25) e il backlog
- STATUS.md l'unica narrativa lunga: lezioni e note tecniche
v2.4 chiusa e archiviata in milestones/v2.4-REQUIREMENTS.md.
Decisione nuova: il documento di restituzione usa il design system dell'area
admin ("Quiet Luxury"), non una tipografia sua — token semantici, Plus Jakarta
Sans, Geist Mono per metriche e date, StatusBadge per gli impatti. Sostituisce
la deroga tipografica prevista dal piano. I font sono gia' self-hostati da
next/font/google, quindi la CSP font-src 'self' e' soddisfatta senza lavoro, e
il documento non aggiunge debito a DEBT-01 perche' nasce gia' a token.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
STATUS.md e .planning/STATE.md raccontavano la stessa storia in due posti
con date diverse, e STATE.md si contraddiceva: frontmatter fermo a
"milestone v2.3 / executing" quando v2.3 e shipped, l'anteprima admin data
per "NON committata" mentre e il commit 187550f deployato l'8 agosto,
Session Continuity ferma al 29/07 e la tabella Performance Metrics spezzata
a meta.
Il template GSD dice esplicitamente che STATE.md deve stare sotto le 100
righe ("a DIGEST, not an archive"): ne aveva 177, quasi tutte narrativa.
- STATUS.md assorbe la narrativa e diventa l'unico posto dove si racconta
il progetto. Nuova sezione "Lezioni operative" per le trappole in cui si
ricasca: il gate OTP non va nel layout App Router (il payload RSC
trapela), ricreare il dominio Resend rigenera la chiave DKIM, .env.local
non e allineato a produzione dal 28/07, Playwright non funziona contro
npm run dev
- STATE.md sceso a 98 righe, con i campi che state.cjs legge davvero.
Frontmatter corretto a v2.4, blocchi gia risolti (DKIM, env Coolify)
rimossi, nulla risulta piu "non committato"
- il debito design era sottostimato: non 11 pagine ma ~40 file e ~450
occorrenze. Esclusi perche legittimi AdminSidebar (eccezione brand),
mailer.ts (HTML email) e i colori di stato di StatusBadge
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Quando un cliente segnalava "non trovo una cosa" non c'era modo di
guardare il portale con i suoi occhi: il gate OTP lascia entrare solo
lui. Dall'elenco clienti ora un'icona apre /client/<slug>?preview=1.
getClientGate() accetta { previewRequested } e salta il gate solo se il
query param c'è E getServerSession(authOptions) è valida. Senza param
anche un admin vede il gate OTP, così il gate resta testabile dal vivo.
Ritorna preview: true senza sintetizzare una ClientSession: un admin in
anteprima non è un cliente autenticato, e confondere i due stati li
renderebbe indistinguibili proprio dove serve distinguerli.
Sola lettura perché il portale scrive davvero: /api/client/approve e
/api/client/comment autenticano sul token nel body, non sulla sessione,
e deliverables.approved_at è immutabile una volta impostato (LOCKED #3).
La protezione è a livello di UI, non di API — impedisce l'incidente, non
difende da sé stessi. Il flag passa da PreviewProvider e non per prop
drilling: ApproveButton sta quattro livelli sotto la dashboard.
Deviazione consapevole dal vincolo LOCKED #4: una route client ora legge
anche la sessione Auth.js. CLAUDE.md non è aggiornato, la sezione LOCKED
richiede approvazione esplicita.
Verificato col build di produzione contro il DB reale (sole letture):
gate OTP senza sessione admin, con cookie contraffatto e con preview=0/
abc/vuoto; portale con banner e composer disattivato con sessione valida,
sia a progetto singolo sia a due progetti. Il ramo ApproveButton non è
esercitabile dal vivo: in produzione deliverables è vuota.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Gate OTP live su hub.iamcavalli.net. Verificato: gate senza cookie con zero
dati di progetto nell'HTML, no-enumeration, codice sbagliato/corretto,
cookie Secure+HttpOnly+SameSite 90 giorni, rientro col cookie, isolamento
fra clienti. Dati di test rimossi, tabelle protette invariate.
Resta da popolare la whitelist dei 3 clienti reali.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Il dominio e stato ricreato su Resend (nuovo id) e i DNS rimessi: DKIM,
SPF TXT e MX tutti verified. Invio da no-reply@iamcavalli.net verso un
indirizzo esterno confermato riuscito.
Annotato che ricreare il dominio su Resend rigenera la chiave DKIM, quindi
i valori DKIM annotati in passato non sono affidabili.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- RESEND_API_KEY e RESEND_FROM create su Coolify (production + preview)
- sendEmail() verificato con il template OTP reale: {ok:true}
- unico blocco residuo: TXT resend._domainkey.iamcavalli.net ha una chiave
vecchia, dominio Resend status=failed, invio dal dominio rifiutato 403
- valore DKIM corretto e sequenza di ripresa annotati in STATE.md
- note API Coolify: envs non accetta is_build_time (422)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Il portale /client/<slug> era protetto dal solo token in URL: chiunque
ricevesse o intercettasse il link entrava, per sempre, senza identificarsi.
Ora l'admin registra le email autorizzate per cliente e il cliente si
identifica con un codice usa-e-getta prima di vedere qualsiasi dato.
- Resend 6.18.1 + src/lib/mailer.ts (Result tipizzato, mai catch silenzioso)
- migration 0015 (gia applicata a prod): client_emails, otp_codes,
clients.sessions_valid_from. Additiva pura, conteggi verificati pre/post
- admin: sezione "Accessi al portale" in /admin/clients/[id] con whitelist
e revoca sessioni in blocco
- gate: codice 6 cifre CSPRNG, hash SHA-256 (mai il codice in chiaro),
TTL 15 min, max 5 tentativi, rate limit su entrambi gli endpoint,
risposta identica per email in whitelist e non (no enumeration)
- sessione: cookie HMAC per-cliente, 90 giorni, httpOnly/secure/lax
Il gate sta in cima alla page, NON nel layout: nell'App Router il segmento
page viene renderizzato in parallelo al layout, quindi gattare nel layout
nascondeva la dashboard a schermo ma lasciava fasi, task e pagamenti nel
payload RSC dell'HTML (46907 byte -> 17594 dopo il fix). Verificato.
Verifica: build OK, 9/9 test E2E in locale contro il DB di produzione.
NON DEPLOYARE prima di: RESEND_API_KEY+RESEND_FROM su Coolify e whitelist
popolata per i 3 clienti reali (oggi vuota) - altrimenti il gate li chiude
fuori dal loro portale. Checklist in .planning/STATE.md.
SEND-01/02 (invio preventivo via email) rinviati a v2.4 su richiesta.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
UI-SPEC approved (5/6 PASS, 1 non-blocking flag on focal point).
Locks reuse of Phase 11 primitives (EditableCell, OptionSelect,
OptionMultiSelect) for the leads table, plus scope for the four
CRM bug fixes (i18n, type-safety, dead code).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tags table + legacy consolidation applied via SSH tunnel to Coolify Postgres.
Both validators: ALL CHECKS PASSED. Protected tables (clients/projects/payments/phases) unchanged.
Phase 11 verification: passed (OFFER-07/08/09/10/13 all Complete).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- add 11-03-SUMMARY.md documenting EditableCell/TagMultiSelect delivery
and the react-hooks lint-rule fix (Rule 1 deviation)
- update STATE.md position (3 of 4 -> 4 of 4), progress 86% -> 89%,
performance metrics, and decisions log
- log gsd-sdk unavailability and OFFER-07/08 status timing as deferred
items for Phase 11
- SUMMARY.md for Plan 02 (ServiceWithTags + 4 new server actions)
- STATE.md: advance to Plan 3/4, record metrics and decision
- REQUIREMENTS.md: mark OFFER-07/08/09 backend foundation complete
- Advance STATE.md to plan 2/4, record metrics/decisions/blocker for 11-01
- Fix Status/Progress fields regressed by state advance-plan (status was
reset to "Ready to execute" / 0% despite phase being mid-execution)
- Note .planning/ROADMAP.md is an empty PLACEHOLDER (pre-existing v2.1
planning gap) — roadmap update-plan-progress is a no-op until populated