Commit Graph

331 Commits

Author SHA1 Message Date
simone 330749a883 docs: STATE.md rientra sotto le 100 righe
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 16:12:02 +02:00
simone 9a57e450fc docs: registra rinomina tassonomie e stato task "In revisione"
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 16:11:47 +02:00
simone 5547e555bd feat(tasks): stato "In revisione", con gli stati finalmente in un posto solo
Mancava il modo di dire "finito, ma da controllare prima di consegnarlo".
Il nuovo stato sta fra "In corso" e "Fatto" ed e' visibile anche al cliente:
il lavoro c'e' ed e' in controllo qualita', non e' fermo.

Il costo non era la logica ma la dispersione: tre letterali ricopiati a mano in
otto file, in tre forme diverse (allow-list a runtime, union TS, colonne kanban,
opzioni della select) e nessun CHECK in DB a tenerli insieme. Invece di
modificarne quattordici occorrenze, tutto deriva da TASK_STATUSES in
src/lib/task-status.ts: la prossima aggiunta costa una riga.

Due punti perdevano dati in silenzio, ed erano il vero motivo per centralizzare:

- recomputePhaseStatus considerava "iniziato" solo in_progress|done, come lista.
  Una fase con tutti i task in revisione non rientrava ne' in allDone ne' in
  anyActive e retrocedeva a "upcoming": si leggeva "non iniziata" quando era
  quasi finita. Ora e' la negazione di "todo", e regge anche il prossimo stato.
- ClientKanban ripartiva i task con un oggetto a tre chiavi fisse, non derivato
  dalle colonne: un task fuori da quelle spariva da ogni colonna e da ogni
  contatore, e il cliente ne vedeva meno di quanti ce n'erano, senza errore.

Chiuso anche il cast non verificato al confine del portale (page.tsx), che era
la causa a monte di entrambi: ora ci passa normalizeTaskStatus.

Nessuna migration: tasks.status e' text senza CHECK, le righe esistenti valgono.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 16:10:52 +02:00
simone 3fcb10dac6 feat(impostazioni): rinomina di un valore di tassonomia, fasi dei progetti incluse
Dalle impostazioni si poteva solo aggiungere o eliminare un valore: per
cambiargli nome bisognava cancellarlo — strappandolo via da ogni servizio che
lo usava — e ricrearlo a mano. La matita nel chip fa il rename in un passo.

renamePoolValue esisteva gia' e propagava ovunque, tranne in un punto:
importOfferIntoProject copia services.fase dentro phases.title e poi ritrova
la fase confrontando i titoli (phases.offer_phase_id non viene mai popolata,
quindi il titolo e' l'unico legame). Un rename fermo al catalogo lasciava le
fasi dei progetti col vecchio nome e al re-import ne nasceva una duplicata.
Ora propaga anche li', con lo stesso match trim+lowercase del merge.

E' l'unico rename di tassonomia che scrive fuori dal dominio catalogo/offerte,
quindi e' l'unico che chiede conferma, dicendo quante fasi e quanti progetti
sta per toccare.

Tolte anche le UPDATE manuali in renameServiceOption/renameOfferOption: erano
la stessa scrittura che renamePoolValue faceva subito dopo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 16:08:20 +02:00
simone 97cc6460a0 docs: registra le modifiche hub e mette v2.5 in pausa dichiarata
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>
2026-08-20 10:13:50 +02:00
simone 19ed377214 feat(pipeline): endpoint di ingresso lead, indipendente dalla sorgente
POST /api/webhooks/lead con header x-webhook-secret. Un endpoint solo per il
form del sito e per qualunque bridge: il contratto e' un POST, e chi lo manda
non cambia la route.

La normalizzazione dei campi sta in lead-intake.ts, non nella route, perche' e'
la parte che cambia quando si aggiunge una sorgente. Regge tre forme senza
doverle distinguere: payload piatto, `fields` annidati con {value} (Elementor
Pro), e urlencoded per i form che non mandano JSON. Riconosce i nomi italiani
(nome, telefono, azienda, messaggio), che e' come li chiama un form Elementor
scritto in italiano.

Chi compila due volte non diventa due lead. Il riconoscimento e' sull'email: il
secondo invio aggiorna last_contact_date e lascia un'attivita' con quello che
ha scritto, cosi' il messaggio non si perde ma la scheda resta una. Senza email
non si puo' dedurre nulla e si crea.

Due scelte di sicurezza, entrambe diverse dalle route /api/internal:

- Segreto assente in ambiente = 403, non "passa". Le internal possono
  permetterselo perche' sono raggiungibili solo da localhost; questa e' esposta
  a internet, e un deploy con la variabile dimenticata deve smettere di
  accettare lead, non accettarli da chiunque.
- Il rate limit viene PRIMA del confronto sul segreto, altrimenti tentare
  segreti a raffica costerebbe zero. Confronto a tempo costante con safeEqual,
  lo stesso del gate admin.

src/proxy.ts non intercetta /api/*, quindi da monte non arriva nessuna
protezione: sta tutto dentro la route.

Provato contro il DB di produzione via tunnel SSH, poi ripulito (2 lead e 2
attivita' prima, 2 e 2 dopo): senza segreto 403, segreto sbagliato 403, nome
mancante 422, payload piatto 201, ripetuto 200 "updated" senza duplicare,
forma Elementor 201 con nome/telefono/messaggio mappati, urlencoded 201, e con
starts_at valorizzato il lead nasce con la data della call e un'attivita'
"meeting".

Resta da confermare con un invio VERO da Elementor la forma esatta del suo
payload: qui e' gestita in modo difensivo, non verificata sul campo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 22:57:42 +02:00
simone d6d3be00f4 feat(dashboard): timeline delle consegne con semaforo ritardo/anticipo
Quali progetti vanno consegnati, a che punto sono e se il ritmo regge.

La data attesa non esisteva nello schema. Si deduce da start_date +
duration_months dell'offerta, e projects.due_date (migration 0018) la
sovrascrive quando la durata a catalogo non descrive quel progetto li'.

Entrano solo le offerte una_tantum. Un retainer e' continuativo e una consegna
non ce l'ha per costruzione: in questa lista risulterebbe in ritardo per
sempre. E' lo stesso discrimine che gia' regge il forecast. Teckell, che ha
solo un "Mantenimento", sparisce dalla vista — e sparisce del tutto, non
finisce fra i "senza scadenza" dove sembrerebbe una dimenticanza.

Il semaforo confronta due percentuali, task chiusi e tempo trascorso, con dieci
punti di tolleranza: sotto, la differenza e' rumore, e un semaforo che vira al
rosso ogni settimana storta smette di essere guardato. Oltre la data di
consegna e' rosso e basta.

La barra le mostra entrambe: pieno = fatto, tacca = tempo passato. La distanza
fra le due E' il ritardo, e si legge senza doversi fidare del semaforo.

I progetti senza scadenza calcolabile restano elencati sotto invece di
sparire: sono quelli a cui non e' assegnata un'offerta, cioe' esattamente il
problema che la vista dovrebbe far notare.

StatusBadge guadagna i toni. Serviva perche' i quattro stati di consegna non
sono stadi di lead e cadevano tutti nel grigio di fallback: quattro pillole
grigie non sono un semaforo. I lead continuano a usarlo come prima.

Atteso e verificato sul DB: una sola riga, Rossi Inc in ritardo (22 giu + 1
mese = 22 lug, oggi 19 ago, 3 task su 28), piu' tre progetti senza scadenza.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 22:48:42 +02:00
simone 8f2b3255ab feat(dashboard): analytics per linea di prodotto, con il residuo in chiaro
Entry (l'Audit), Signature e Retainer a confronto: quante offerte sono partite
questo mese, quante nell'anno, quanto valgono e quanto e' stato incassato. Le
categorie si leggono da offer_macros.category, cioe' dalla stessa tassonomia
che si edita da /admin/impostazioni: aggiungerne una la fa comparire, senza
toccare il codice. Le categorie configurate compaiono sempre, anche a zero —
"questo mese non e' partito nessun audit" e' una risposta, una riga mancante no.

Il punto delicato e' l'attribuzione dell'incassato. I pagamenti stanno sul
PROGETTO, non sull'offerta: per dire quanto ha incassato una linea di prodotto
bisogna ridiscendere dal progetto alle sue offerte. Un'offerta sola prende
tutto; piu' offerte si spartiscono in proporzione all'accepted_total; nessuna
offerta finisce in una riga "Senza offerta", separata e visibile.

Quella riga separata non e' prudenza teorica. Sui dati di oggi vale il 100%
dell'incassato: 5.300 EUR su 5.300. I due progetti che hanno incassato (Caruso
Speaker, Protocollo Estetico) non hanno nessuna offerta assegnata; i due che
l'hanno (Rossi Inc, Teckell) non hanno ancora incassato. Spalmando quei soldi
sulle tre categorie la dashboard avrebbe mostrato numeri inventati con un
totale che quadra. Cosi' invece si vede che c'e' da assegnare le offerte.

Lo stato dell'offerta non filtra niente: un retainer disdetto oggi ha comunque
incassato quello che ha incassato. Stessa ragione gia' scritta in
getOffersSoldBreakdown — il ciclo di vita riguarda il forecast, non lo storico.

Attesi per il 2026, verificati a mano sul DB: Entry 0, Retainer 1 offerta /
200 EUR, Signature 1 / 7.000 EUR, Senza offerta 5.300 EUR incassati. Nessuna
partenza ad agosto.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 22:45:23 +02:00
simone 1115bb2265 feat(dashboard): l'inbox sale in cima e dice da quanto aspettano
Il widget dei messaggi esisteva gia', ma era il terzo riquadro della colonna
stretta: sotto la piega, cioe' invisibile. Un messaggio non letto e' la cosa
piu' urgente della giornata e ora sta in testa alla dashboard, a piena
larghezza, due colonne.

Due dati erano gia' in ConversationSummary e non venivano mostrati: da quanto
aspetta il messaggio e su cosa e' stato scritto (fase, task, deliverable o
generale). Sono esattamente i due che dicono con che fretta rispondere. Il
tempo relativo si ferma alla settimana e poi passa alla data: oltre, "23 giorni
fa" non aiuta piu' nessuno.

La fascia sparisce del tutto quando non c'e' niente da leggere, invece di
lasciare a video un riquadro vuoto che si impara a saltare — e con lui si
imparerebbe a saltare anche quello pieno.

relativeTime va in src/lib/dates.ts perche' serve anche altrove. FollowUpWidget
ha ancora la sua copia locale con il prefisso "Contattato": la si unifica
quando la si tocca, non oggi.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 22:42:52 +02:00
simone d6e95ef66a feat(progetti): riepilogo soldi e avanzamento in testa al progetto
Per sapere a che punto era un progetto bisognava aprire tre tab e sommare a
mente. Ora la risposta e' sopra i tab: contrattualizzato, incassato, da
incassare, redditivita' oraria, e una barra con i task fatti sul totale.

Nessuna query nuova: sono tutti dati che getProjectFullDetail restituisce gia'
per i tab sottostanti. E' aritmetica su quello che c'e'.

Una scelta: l'incassato si legge dai `payments`, non da `accepted_total`. Il
contratto dice quanto vale il progetto, le rate dicono quanto e' entrato
davvero, e quando i due non tornano e' un'informazione — non un errore di
calcolo da nascondere pareggiando i conti.

MetricCard esce da admin/page.tsx e diventa un componente condiviso: due copie
della stessa card avrebbero iniziato a divergere alla prima modifica.

Numeri verificati contro il DB di produzione, progetto per progetto. Rossi Inc:
7.000 EUR su 30h tracciate = 233 EUR/h sopra il target di 100, 3 task su 28,
1 fase su 4. Teckell, che ha ore ma nessun contratto, mostra "—" e non uno
zero travestito da dato.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 22:40:48 +02:00
simone a9358da96f feat(timer): il tempo si imputa a fase e task, non solo al progetto
"Quanto e' costata questa fase" non era una domanda che si potesse fare:
time_entries aveva la sola project_id.

Migration 0018 (gia' applicata a prod): phase_id e task_id su time_entries,
piu' due indici, piu' projects.due_date che serve al blocco successivo. Solo
ADD COLUMN e CREATE INDEX.

Due scelte che vale la pena spiegare:

- ON DELETE SET NULL, non CASCADE. Cancellare un task NON deve cancellare le
  ore lavorate su di esso: sono storico fatturabile. L'entry ricade a livello
  progetto e il totale del progetto non cambia mai. Con CASCADE, ripulire una
  fase avrebbe silenziosamente abbassato il fatturato tracciato. Verificato
  sul DB di produzione dentro una transazione con ROLLBACK: cancellato il
  task, l'entry sopravvive con task_id NULL, phase_id intatto e i secondi
  invariati.
- Il timer su un task scrive ENTRAMBE le colonne. Cosi' il totale di una fase
  e' un group-by diretto su phase_id, senza risalire dai task, e comprende
  anche il tempo imputato alla fase ma a nessun task in particolare.

Resta un solo timer attivo in tutto l'hub. Da qui una conseguenza in UI: se
sta girando su un task, il timer del tab "Timer" NON si mostra acceso —
mostrarlo acceso farebbe credere che siano due cronometri diversi. Il tab lo
dice a parole e avvisa che avviarlo fermerebbe l'altro.

Le 8 entry esistenti restano valide con entrambe le colonne a NULL, cioe'
"tempo di progetto": e' esattamente cio' che sono.

PhasesTab passa ai token semantici mentre lo si tocca. Non e' zelo: ci si
infila dentro una TimerCell che i token li usa gia', e in dark mode un badge a
token dentro una card bg-white si vede. Un pezzo di DEBT-01 in meno.

Cade startTimerForClient, senza chiamanti da quando la lista progetti non ha
piu' il timer.

Build pulito. L'avvio/arresto dal browser non e' ancora stato provato: si
verifica in produzione, che e' l'unico posto dove esiste il DB.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 22:38:06 +02:00
simone 4b135ce67f refactor(progetti): via il tab Commenti e il timer dalla lista
Due rimozioni chieste esplicitamente, che tolgono due doppioni.

Il tab "Commenti" del progetto duplicava /admin/conversazioni. Non era una
scorciatoia: era la seconda copia. `buildEntityMap()` in conversations-queries
cammina clienti -> progetti -> fasi -> task -> deliverable e raccoglie TUTTI i
commenti con l'etichetta dell'entita' di origine, quindi l'inbox e' un
sovrainsieme stretto di quel tab. La lettura non perde niente.

Una cosa la perde, e va detta: dal tab si poteva rispondere sulla singola
entita', mentre `replyToConversation` salva sempre sul thread generale. Non e'
una regressione introdotta qui — e' una scelta di prodotto gia' presa e gia'
annotata in conversazioni/actions.ts — ma da oggi e' l'unica via, e il commento
la' sopra ora lo dice.

Il timer nella lista progetti era l'altro doppione: si avvia e si ferma dentro
il progetto, dove c'e' il contesto per sapere su cosa stai lavorando. Toglierlo
elimina anche una query per pagina (la scansione delle entry aperte).

Cade di conseguenza il codice rimasto senza chiamanti: CommentsTab.tsx,
`postAdminComment`, il campo `comments` di ProjectFullDetail con la sua query, e
i due campi activeTimer* di ProjectWithPayments. `totalTrackedSeconds` resta:
serve al calcolo del EUR/h, che in lista ci sta ancora.

Build e lint puliti.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 22:30:58 +02:00
simone 571f58bff8 docs: STATE.md registra il deploy di v2.5 e distingue "in prod" da "in uso"
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>
2026-08-19 20:01:52 +02:00
simone 8000d562dc docs: apre v2.5 "Audit" e rimette in pari roadmap e requisiti
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>
2026-08-18 17:48:46 +02:00
simone 24213e7251 chore(audit): spike del motore e seed della rubrica
- scripts/spike-audit.ts        lo spike che ha risposto alla domanda "la
                                verifica delle voci su un sito reale e'
                                affidabile e produce problemi concreti?".
                                Deliberatamente ISOLATO: non importa da src/,
                                non tocca il database, non tocca l'hub.
- scripts/seed-checklist.ts     emette SQL su stdout, cosi' il popolamento della
                                rubrica passa dalla stessa procedura SSH+docker
                                exec delle migration invece che da uno script
                                usa e getta puntato al DB di produzione.
- scripts/data/checklist.json   le 264 voci, 71 delle quali valgono anche per i
                                siti non-ecommerce.

.gitignore esclude spike-audit-*.json: sono i dati del sito di un cliente.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 17:48:34 +02:00
simone 08b0a60bae feat(audit): le fonti del motore di analisi
src/lib/audit/sources/ — raccolta dati, nessun LLM. Cinque moduli:

- fetch.ts      home + fino a 3 pagine interne per profilo, piu' gli helper di
                rete condivisi dalle altre fonti (ritentativi su 429/5xx e
                timeout, tetto di concorrenza, navigazione JSON difensiva)
- pagespeed.ts  153 audit Lighthouse fatti sul DOM renderizzato, falliti
                ordinati per gravita' con elementi concreti, per_id per la
                checklist, fasi LCP, screenshot
- crux.ts       dati di utenti reali, con scala di ripiego a quattro gradini
- history.ts    Wayback CDX, istantanee a 1/3/5 anni, confronto con la home
- signals.ts    RDAP, robots/sitemap, JSON-LD, hreflang, piattaforma, header

Regola comune: nessuna fonte puo' uccidere la pipeline. Chi fallisce restituisce
un risultato con `errore` valorizzato — e "non ha risposto" resta distinto da
"ha risposto che non ci sono dati", perche' il documento deve poterlo dire.

Provate sul campo su giojello.com prima di costruirci sopra, e il giro ha
trovato quattro cose che il typecheck non poteva vedere:

- fasi_lcp usciva vuoto: largest-contentful-paint-element non esiste piu'
  nell'API pubblica, ora e' lcp-breakdown-insight con subpart/duration e senza
  percentuali (si calcolano). Dice che il 91% dell'LCP e' ritardo nel *trovare*
  la risorsa, non peso dell'immagine: comprimere le foto non toccherebbe nulla.
- ttfb_ms era un nome pericoloso. Lighthouse da' 7 ms, CrUX da' 3.553 ms di p75:
  il server risponde in fretta al datacenter Google e lento a tutti gli altri.
  Con lo stesso nome il sintetizzatore li tratterebbe come un numero solo, da
  qui risposta_server_ms.
- le dimensioni dello screenshot erano sempre null: configSettings.screenEmulation
  non esiste. Ora si leggono dai byte dell'immagine — 250x498, leggibile.
- Wayback andava in timeout a 30 s e la fonte usciva vuota.

Nessun renderer headless, da nessuna parte: il VPS non regge Chromium e non
serve, gli audit Lighthouse arrivano gia' fatti sul DOM renderizzato.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 17:48:27 +02:00
simone 31237da11c feat(audit): schema del documento di restituzione (migration 0017)
Sette tabelle additive per la milestone v2.5 "Audit": audits, audit_findings,
audit_optimizations, checklist_items, audit_checklist_results, audit_runs e
audit_visits. Nessun DROP, nessun TRUNCATE, nessuna colonna rimossa.

Tre scelte che vale la pena spiegare:

- Colonne scalari su audits, non un jsonb unico. A differenza di proposals non
  c'e' snapshot da congelare: il copy fisso sta in moduli TS versionati e
  l'editor mappa 1:1 sui campi. Tutti i campi di contenuto sono NULLABLE — e'
  cio' che rende possibile "si salva sempre, anche a meta'".
- checklist_items.profili e' jsonb: 71 voci su 264 valgono per entrambi i
  profili, una colonna singola costringerebbe a duplicarle.
- audit_checklist_results.esito ammette 'non_verificabile'. E' l'esito piu'
  frequente misurato sullo spike (107 su 204) e serve a sapere quanto il motore
  NON riesce a vedere: buttarlo via renderebbe impossibile misurare se le
  rilevazioni Lighthouse stanno recuperando terreno.

Migration gia' applicata in produzione il 2026-08-18, dati esistenti intatti.
CLAUDE.md annota la deroga al vincolo LOCKED #5, limitata agli asset di audit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 17:48:11 +02:00
simone ee47f35e97 docs: allinea CLAUDE.md e i mock design-reference
I mock per pagina erano file HTML senza estensione, mentre CLAUDE.md li
descriveva come cartelle design-reference/pagina-*/ - un path che non
esisteva. Rinominati in pagina-*.html e aggiornati i riferimenti anche in
DESIGN-SYSTEM.md.

CLAUDE.md:
- puntatori corretti dopo il riordino di .planning/ (security/, STATE.md a
  digest, REQUIREMENTS.md come backlog corrente)
- rimosso il paragrafo sui doc superseded: i file non esistono piu
- annotato che i mock sono scritti in slate-* raw perche precedono la
  regola dei token: vanno tradotti, non copiati
- rese esplicite le eccezioni sanzionate alla regola dei token (colori di
  stato di StatusBadge, verde brand della sidebar, HTML delle email)
- vincolo LOCKED #4: annotata l'unica deroga, getClientGate() legge
  getServerSession per l'anteprima admin in sola lettura e solo con
  ?preview=1 (Phase 26). Testo approvato dall'utente

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 22:38:58 +02:00
simone 0aad9caf46 docs: STATUS.md unico documento narrativo, STATE.md a digest
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>
2026-08-08 22:38:49 +02:00
simone f7eb7eec23 docs(planning): archivia v2.1/v2.2/v2.3 e documenta v2.4
.planning/ documentava in dettaglio cio che era vecchio e per niente cio
che e in produzione: le fasi 11-22 (v2.1 e v2.2, chiuse a giugno) erano
ancora in phases/ mentre v1.0 e v2.0 stavano gia in milestones/, e il
lavoro degli ultimi due mesi - gate OTP e ciclo di vita dei retainer, cioe
quello che gira su hub.iamcavalli.net - non aveva nessuna cartella.

- phases/{11,12,14} -> milestones/v2.1-phases/, phases/{18..22} ->
  milestones/v2.2-phases/. Ora phases/ contiene solo la milestone in
  corso, che e quello che state.cjs conta per il progresso
- v2.1-ROADMAP.md ricostruito: era l'unica milestone senza archivio,
  interrotta dal reset del 19/06 e mai chiusa formalmente
- v2.3-ROADMAP.md + v2.3-REQUIREMENTS.md: v2.3 e stata eseguita fuori dal
  ciclo GSD, non esistono PLAN/SUMMARY per fase. L'archivio E la doc
- REQUIREMENTS.md riscritto per v2.4 con il backlog reale
- phases/13 e phases/26: SUMMARY ricostruiti da commit, migration e
  STATUS.md. 26 e il primo numero libero
- research/: cancellate 4 varianti dello stesso PITFALLS e FEATURES/
  SUMMARY, superati da PROJECT.md. Diverse anti-feature erano ormai
  contraddette dai fatti (il Kanban e stato costruito in Phase 19,
  l'email in v2.3, il time tracking esiste)
- cancellati UI-RULES.md e DESIGN-SYSTEM.md (CLAUDE.md li dichiara
  superseded: impongono l'inverso della regola attuale) e HANDOFF.md,
  fermo al 13/06
- SECURITY-*.md -> security/: audit chiuso, ma i report restano la doc di
  cosa e stato ruotato e perche
- PROJECT.md/MILESTONES.md/ROADMAP.md allineati: milestone corrente v2.4,
  sessione OTP 90gg non 30, migrazioni fino alla 0016

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 22:38:36 +02:00
simone 187550fedf feat(client): anteprima admin in sola lettura del portale cliente
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>
2026-08-08 14:19:22 +02:00
simone 09a5b1ff4f feat(auth): toggle mostra/nascondi password sul login admin
Il campo password non offriva modo di rileggere quanto digitato, quindi
un accesso fallito era indistinguibile da un errore di battitura.

Toggle inline e non nuovo primitivo in ui/: `type="password"` compare una
sola volta in tutto il codebase. type="button" perché dentro un <form> il
default è submit, e tabIndex -1 per tenere il Tab sulla sequenza campo →
Accedi. Classi a token semantici; gli hex literal preesistenti di questa
pagina restano da migrare a parte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 14:19:09 +02:00
simone 5177a3700a feat(offers): ciclo di vita dei servizi ricorrenti (v2.4 Phase 13)
Un retainer, una volta assegnato, non si poteva fermare: project_offers
aveva solo start_date e il forecast sommava il canone a ogni mese
dell'orizzonte da li in poi, per sempre. Un cliente che disdiceva
continuava a gonfiare il forecast a 12 mesi e a vedersi l'abbonamento
attivo nel portale.

- migration 0016 (gia applicata a prod): project_offers.status
  (attivo|sospeso|cessato, CHECK) + end_date. Additiva pura, default
  'attivo' cosi le righe esistenti conservano il comportamento di prima
- forecast: i retainer si fermano a end_date, sospesi e cessati escono.
  getOffersSoldBreakdown NON filtra per stato: e uno storico di vendita,
  escludere le cessate riscriverebbe il passato
- offersAcceptedTotal esclude le cessate (default del piano pagamenti)
- admin: comandi Sospendi/Riattiva/Cessa + data fine nella tab Offerte,
  solo per i ricorrenti. setProjectOfferLifecycle valida con Zod e filtra
  anche per project_id, cosi un id arbitrario non tocca altri progetti
- portale: "Attivo dal", "fino al", badge In pausa, "Canone mensile"
  invece di "Prezzo finale"; le cessate non arrivano al client
- fix: un retainer sospeso continuava a intestare i pagamenti "Totale
  Pagamento Mensile" e a sovrascrivere l'importo

Igiene nello stesso giro:
- STATUS.md riscritto: era fermo al 22 giugno e diceva che node/docker non
  sono disponibili sul server e che le migrazioni si applicano da locale
  con uno script postgres.js — il contrario della procedura reale
- rimossi ChatSection/CommentList/CommentForm, senza importatori (308 righe)
- overrides postcss>=8.5.18 e sharp>=0.35.0: 3 CVE high transitive di Next
  senza fix upstream. npm audit ora pulito, build verde

Verifica: forecast controllato sui dati veri in 5 scenari (baseline
invariata, end_date, sospeso, cessato, ripristino); portale verificato nei
4 stati; tab admin verificata con Playwright sul build di produzione
(i comandi non compaiono sulle una tantum). Dati di test ripuliti,
tabelle protette invariate 4/5/11/10.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 18:18:28 +02:00
simone d57b0f3e04 docs(state): v2.3 in produzione e verificata end-to-end
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>
2026-07-30 11:36:31 +02:00
simone 27da969963 docs(state): dominio Resend verificato, v2.3 pronta al deploy
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>
2026-07-29 23:32:35 +02:00
simone c9b5cd7451 docs(state): Coolify configurato + mailer verificato; deploy fermo sul record DKIM
- 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>
2026-07-29 23:20:47 +02:00
simone 8158038145 feat(auth): gate OTP email sul portale cliente (v2.3 Phases 23-25)
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>
2026-07-29 12:10:33 +02:00
simone b27b9d07ac chore: svuota il cestino e allinea CLAUDE.md alla nuova struttura
Verificato prima di cancellare: le 10 cartelle di fasi in
planning-fasi-duplicate/ erano byte-per-byte identiche alle copie in
.planning/milestones/ (diff -rq su ognuna), i 13 script one-off erano
gia eseguiti su fasi chiuse, e CLAUDE-SECURITY-*/ conteneva solo i
metadati di una run interrotta. Tutto resta comunque in git fino a
94b3b2f^.

CLAUDE.md citava ancora gli scripts/push-*.ts come vecchio metodo per
le migrazioni: quei file non esistono piu, quindi il riferimento
puntava a fantasmi. Ora la procedura SSH+docker exec e l'unica indicata.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 14:30:19 +02:00
simone 94b3b2f766 chore: riorganizzazione e pulizia della cartella di progetto
Rimossi i doppioni e gli artefatti accumulati, senza cancellare nulla di
definitivo: tutto cio' che serviva una revisione e' parcheggiato in cestino/
(gitignored), documentato in cestino/LEGGIMI.md.

- .planning/phases/01-10: 10 cartelle identiche byte-per-byte alle copie in
  .planning/milestones/v1.0-phases e v2.0-phases. HANDOFF.md:33 documentava che
  furono copiate e non spostate, lasciando la pulizia 'facoltativa in futuro'.
  Verificata l'identita' con diff -rq prima di spostare ciascuna.
- scripts/: 13 script one-off gia' eseguiti (push-*, migrate-*, validate-*,
  verify-12-03-*) piu' reset-and-import-services.ts, che cancella dati.
  Restano i 3 riutilizzabili: seed, import-services-notion, import-service-offer-tags.
- CLAUDE-SECURITY-20260727-210226/: cartella di lavoro della run interrotta.

Cancellati subito, senza revisione: 6 .DS_Store, le due cache .impeccable/
(una era dentro src/) e tsconfig.tsbuildinfo.

.gitignore: aggiunti cestino/, .impeccable/ e CLAUDE-SECURITY-*/ per evitare
che si riformino.

src/ non e' stato toccato: la struttura e' dettata dall'App Router di Next.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 14:13:32 +02:00
simone fb6ab92fd0 docs(security): correzione severita finding 1 + chiusura punti Coolify
CORREZIONE: la password Postgres trovata in git NON era attiva. La verifica
iniziale si limitava a constatare che la stringa comparisse in .env.local e ne
deduceva che fosse quella viva. Il confronto del verifier SCRAM-SHA-256 di
pg_authid contro i due candidati mostra che quella committata non combacia:
era gia stata ruotata. La voce DATABASE_URL porta 5432 di .env.local e' stale.
Severita' reale: BASSA, non CRITICA. Nessuna rotazione necessaria.

Chiusi via API Coolify: INTERNAL_SECRET creata (le route /api/internal/*
rispondono ora 403 invece di 404, oracolo di enumerazione token chiuso) e
ADMIN_PASSWORD portata da 15 a 32 caratteri. Redeploy verificato.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 11:06:48 +02:00
simone bcff4aad48 docs(security): slug cliente ruotati in produzione
I 4 slug esistenti avevano suffissi da 4 caratteri generati con Math.random();
ora 12 caratteri CSPRNG. Lo slug risolve prima del token, quindi era il vero
anello debole dell'accesso alla dashboard cliente (C-2).

Lo script SQL NON e' committato di proposito: contiene gli slug in chiaro, che
sono credenziali bearer verso /client/<slug> — committarlo ripeterebbe la fuga
di credenziali trovata al punto 1 dell'audit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 10:55:38 +02:00
simone e2bd1d95ed fix(security): audit completo — secondo gate admin, hardening slug, XSS, CSP/HSTS, update CVE
Audit di sicurezza su tutta l'app. Report in .planning/SECURITY-SCAN.md (codice),
.planning/SECURITY-AUDIT-INFRA.md (dipendenze/segreti/deploy) e piano in
.planning/SECURITY-REMEDIATION-PLAN.md.

CRITICO — l'autorizzazione admin era un unico punto di rottura: nessuna delle 21
pagine /admin controllava la sessione e admin/layout.tsx renderizzava comunque i
figli quando mancava. L'unico guard era proxy.ts, su un Next.js affetto da
GHSA-6gpp-xcg3-4w24 (proxy bypass). Ora il layout è un secondo gate indipendente;
proxy.ts marca il path con un token derivato da NEXTAUTH_SECRET, così il gate non
è aggirabile forgiando header e fallisce chiuso se il proxy non gira.

ALTO — gli slug cliente avevano 4 caratteri casuali da Math.random() (~20 bit,
1.7M tentativi) e risolvono prima del token: ora 12 caratteri via nanoid
(CSPRNG, ~62 bit). Aggiunto rate limit al ramo /client/, che ne era privo.

ALTO — src/lib/quote-actions.ts esponeva due server action pubbliche senza
autenticazione, una delle quali scriveva su DB. Codice morto, zero chiamanti:
rimosso.

MEDIO — i quattro dangerouslySetInnerHTML nelle sezioni proposta rendevano output
AI come HTML grezzo su pagina pubblica, alimentato da transcript di terzi. Sostituiti
con RichText (whitelist di emphasis, nessun HTML al DOM). I transcript ora sono
recintati in tag che il system prompt dichiara essere dati, non istruzioni.

Inoltre: next 16.2.6 -> 16.2.12 e next-auth 4.24.14 -> 4.24.15 (chiude 9 CVE Next
piu GHSA-xmf8-cvqr-rfgj su getToken, raggiungibile dal proxy); HSTS e CSP;
potatura della Map di rate-limit.ts, che cresceva senza limite; espunta la password
Postgres di produzione dai due 07-01-SUMMARY.md.

Verificato: tsc pulito, build OK, smoke test su login/redirect/header forgiati.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 23:35:46 +02:00
simone dd2d148457 feat: pagina Impostazioni redesign Quiet Luxury — sezioni tokenizzate + PoolManager tassonomie dual-mode
- impostazioni/page.tsx: sezione Analytics con input tariffa bordato (€/h, font-mono) + header uppercase
- TaxonomyManager: card sezione tokenizzata, griglie Offerte (2col) / Catalogo (3col)
- PoolManager: box bg-muted, badge conteggio mono, chip valori, add su Enter, delete cascade confermata
- DESIGN-SYSTEM.md: note pagina Impostazioni

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 12:14:52 +02:00
simone d444bd6064 feat: pagina Progetti redesign Quiet Luxury — tabella tokenizzata + badge pagamento/timer dual-mode
- projects/page.tsx: tabella in contenitore bg-card/shadow-card, header uppercase muted
- ProjectRow: badge pagamento pill rounded-full dual light/dark (saldato/da_saldare/inviata)
- TimerCell: pill rounded-full mono, idle neutro + running emerald con contatore live
- ConversationsView: bordo item attivo 3px per coerenza
- DESIGN-SYSTEM.md: note pagina Progetti

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 12:14:31 +02:00
simone dd4ae42542 feat: pagina Preventivi redesign Quiet Luxury — tabella tokenizzata + badge stato dual-mode
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 11:53:41 +02:00
simone c29fab8975 feat: pagina Catalogo redesign Quiet Luxury — griglia servizi tokenizzata + footer conteggio
- ServiceTable: contenitore bg-card/shadow-card, header uppercase muted, righe hover:bg-muted/40, footer "Visualizzazione di N servizi"
- CatalogSearch: usa il primitivo SearchInput
- page: sottotitolo sezione
- tokenizza OptionSelect ed EditableCell (accent-primary) — erano rimasti con hex hardcoded

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 11:37:09 +02:00
simone 1fc0650dcf feat: pagina Offerte redesign Quiet Luxury — grid card tokenizzata + badge categoria
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 16:36:34 +02:00
simone c1ff5a7e67 feat: pagina Clienti redesign Quiet Luxury — tabella tokenizzata + copia link
- /admin/clients replica design-reference/pagina-clienti (dual light/dark via token)
- ClientRow: colonne numeriche font-mono right-aligned, incassato come pill emerald,
  cella "Link profilo" con pill mono troncata + CopyLinkButton
- nuovo primitivo CopyLinkButton (copia URL assoluto, feedback check emerald)
- PageHeader con sottotitolo + footer conteggio clienti
- DESIGN-SYSTEM.md: inventory (CopyLinkButton, ClientRow) + note pagina Clienti

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 16:32:34 +02:00
simone a1d8d18902 feat: dashboard box "Messaggi Clienti" — chat clienti in attesa di risposta
Nuovo MessagesWidget nella colonna 1/3 della dashboard: riusa
getConversations() filtrando le conversazioni unread, mostra le prime 4
con anteprima e deep-link "Rispondi" a /admin/conversazioni?c=<clientId>.
Pill emerald "N Nuovi" + pallino pulsante, empty state quando non c'è nulla
in attesa. Stessa fonte di verità (clients.admin_last_read_at) del badge
sidebar. Aggiornato DESIGN-SYSTEM.md (inventory + note Conversazioni/dashboard)
e il mock design-reference/pagina-dashboard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 16:23:43 +02:00
simone c1cc13a99a feat: pagina Conversazioni — inbox unificata messaggi clienti
Nuova pagina admin /admin/conversazioni: vista WhatsApp-style (lista
conversazioni a sinistra, thread + risposta a destra) che aggrega i
messaggi di tutti i clienti dalla tabella comments, cross-cliente.

- Migration additiva 0014: clients.admin_last_read_at per tracciare
  letto/non-letto (pallini + badge in sidebar). Applicata a prod.
- conversations-queries.ts: aggregazione entity_id → cliente
  (general/phase/task/deliverable), getConversations /
  getConversationThread / getUnreadConversationsCount.
- Risposte admin salvate come commento "general" (visibili anche nella
  chat cliente e nel CommentsTab della scheda).
- Voce di menu + badge non-letti (AdminSidebar/AdminShell/layout).
- Token semantici per dual light/dark; refresh manuale (no polling).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 16:10:14 +02:00
simone a20a9de2d7 style: rifinitura Quiet Luxury dashboard — bordi soft, badge, colori
Allinea la dashboard alla rifinitura del mock (finezza bordi/colori/badge):

- Card su border-border-light (slate-100) per il bordo sottile
- KPI: valore verde su Incassato Reale, delta verde "+N questo mese" su
  Clienti Attivi (nuova metrica getDashboardStats.clientiNuoviMese)
- Redditività: badge "Ottimo" con tint emerald soft (non brand primary)
- Follow-up: righe come box bordati (no avatar), link "Apri →" primary
- Forecast: selettore anno (pill interattiva) spostato nell'header della card,
  barre non-picco grigie (bg-border), rimosso il numero "mese prossimo"
- Offerte Più Richieste: rimossa la pill totale nell'header

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 12:18:10 +02:00
simone 08aadc1d97 feat: Dashboard admin redesign to Quiet Luxury + client profitability widget
Replica il mock design-reference/pagina-dashboard: layout condensato a
schermata singola (4 KPI + griglia 2/3-1/3), token semantici per dual-theme.

- Nuova query getClientProfitability(year): valore orario reale per cliente
  (contrattualizzato / ore tracciate) con badge margine, target 100 €/h
- Nuovo widget ClientProfitability "Analisi Oraria & Redditività Clienti"
- Rewrite /admin: KPI strip + Cashflow + Redditività | Follow-up + Offerte
  Più Richieste. Rimossi chart mensile, card extra, barre ore/cliente
- Tokenizzati ForecastChart, OffersSoldChart, FollowUpWidget, YearSelector
  (ora pill interattiva); rimosso MonthlyChart e prop availableYears inutile
- DESIGN-SYSTEM.md: inventario + note dashboard (applied 2026-07-11)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 11:57:10 +02:00
simone e5fa07bba3 feat: Lead Details redesign to Quiet Luxury + unified interaction timeline
Rewrite /admin/pipeline/[id] LeadDetail with token-based dual-theme layout:
bespoke header with inline StatusBadge, asymmetric 1/3 profile + 2/3 content
columns, and a unified reverse-chronological timeline merging activities and
transcripts (transcript node highlighted + expandable + deletable).

Restyle modal triggers (Registra Attività, Aggiungi Transcript, Modifica-as-icon)
to the mock outline style. Remove the confusing "Invia Preventivo" button (it only
linked a token + advanced stage, no email). Keep Converti in cliente for won leads.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 11:33:18 +02:00
simone c110689b6a fix: chat drawer above header + vertical "Chiudi" tab on left edge
Panel z-index raised to z-[60] so it sits above the sticky page header
(z-50) that was covering the close control. Replaced the header X with a
notebook-divider-style vertical "Chiudi" tab protruding from the left edge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 08:34:25 +02:00
simone 11870e15d3 feat: Client Portal redesign to Quiet Luxury + milestone stepper
- New MilestoneStepper primitive (horizontal per-phase progress)
- Token-migrate portal shell, phase cards, sidebar cards, kanban to dual-theme
- Soft-tint status pills (emerald/amber/muted) replacing solid badges
- embedded prop on ClientDashboard fixes double-header in multi-project tabs

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 22:54:58 +02:00
simone 6d5e04bf9c style: refine Pipeline fidelity to Quiet Luxury reference
- Sidebar: revert to white palette (drop emerald pass), remove right border
- Table: softer slate-100 borders, remove min-height gap, "Mostrando N di M
  lead" footer, empty Tag cell = dashed-circle add button
- SegmentedToggle: more visible track (slate-200/60), neutral active text
- Kanban: roomy fixed-width columns (280px, horizontal scroll), sober column
  headers (no colored dots), airy cards matching mock (name+badge/email/
  company+"Tag +"); 6 stages + @dnd-kit intact

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 22:33:26 +02:00
simone 26d752892a style: align light palette to Quiet Luxury reference
- Page background now #F8F9FA (was white); tokens matched to reference:
  foreground slate-900, muted slate-50/slate-500, border slate-200,
  card pure white, tertiary slate-400, border-light slate-100
- Sidebar colors matched to mock: emerald-200/70 inactive, white/8 active
  with emerald-400 active icon, emerald-50 logo, red-300 logout, right border
- Content wrapped in max-w-[1400px] centered container (px-8 lg:px-10)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:33:00 +02:00
simone 9eb2d45c67 fix: sidebar width/centering + kanban card badge & table details
- Sidebar: expanded 256->200px, collapsed 80->64px; collapsed state now
  renders icon-only (label removed from flow) so icons are centered, not
  pushed left by an invisible text span
- Kanban cards: add StatusBadge + email line to match design reference
- Lead table: add "Mostrando N lead" footer and center/right-align
  Stato/Tag/Azioni headers per reference

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:28:43 +02:00
simone 86e1499e8f feat: Quiet Luxury design system + Pipeline (ex-Lead) redesign
- Design system foundations: Plus Jakarta Sans font, shadow-card/radius
  tokens in @theme, design-reference/DESIGN-SYSTEM.md with component inventory
- Collapsible admin shell (AdminShell): smooth w-64<->w-20 sidebar, new top
  header with "Admin" + avatar placeholder, localStorage-persisted state
- Rename route /admin/leads -> /admin/pipeline (redirect stubs preserved),
  nav label Lead -> Pipeline; DB table unchanged
- Reusable primitives: StatusBadge, SearchInput, SegmentedToggle (dual-theme)
- Luxury restyle of lead table, kanban (6 stages + @dnd-kit intact), PageHeader
- Tokenize editable-cell / option-multi-select for dark-mode legibility

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:16:44 +02:00
simone 43cb7e7469 feat: dark/light theming system + design tokens (OMC design system port)
- Restructure design tokens into :root/.dark raw vars mapped via @theme inline
- Add light/dark/system theming: useTheme hook + FOUC guard + ThemeToggle
- Keep iamcavalli palette (primary #1A463C, accent #DEF168) and Geist fonts
- Derive brand-consistent dark palette (verde-nero bg, lightened primary)
- Add scrollbar utilities (.no-scrollbar/.thin-scrollbar)
- Install tailwindcss-animate, register via @plugin (Tailwind v4)
- Mount ThemeToggle in admin sidebar footer

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 08:08:56 +01:00