Commit Graph

316 Commits

Author SHA1 Message Date
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
simone add2176a6b feat: tier names + per-tier prices on offer cards, unified admin UI
- offer editor: per-tier name input (mirrors public_name/internal_name)
- offer list cards: show 3-tier services total + manual public price
- shared PageHeader component, full-width layout across all admin pages
- UI-RULES.md design conventions doc

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 16:58:18 +02:00
simone 9abe1fe4bb fix: offer name in client detail, retainer forecast, LTV with offers, offers-sold chart
- Client detail offers: show offer macro name ("Mantenimento") instead of tier letter
- Forecast: retainers project the monthly fee across every month from start_date
  (no longer capped by duration_months); una_tantum unchanged
- Clients list LTV: per project max(accepted_total, sum of assigned offers) so a
  retainer with unset quote still counts
- Dashboard: new "Offerte vendute" chart (count by offer + tier)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 14:14:13 +02:00
simone ae355c33a6 feat: offer badges, unified dashboard, income forecast, Pipedrive-style follow-up
- Client detail: category + tier badges on active offers
- Dashboard: remove top KPI cards + recent activity; fold current KPIs into
  bottom MetricCard style; add 12-month income forecast chart
- Forecast query: branch by offer_type (retainer = monthly fee, una_tantum =
  spread over duration), filter archived projects
- Payments: set the month a payment was collected (paid_at) from PaymentsTab
- Redesign FollowUpWidget in clean Pipedrive style (brand green, no orange)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 22:28:12 +02:00
simone fc766ca1ee feat: add iamcavalli logo as favicon
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 21:51:05 +02:00
simone 0b8a7b3809 feat: add Opus/Sonnet model selector to "Genera preventivo" page
Adds a "Modello AI" toggle (Opus default, Sonnet option) styled like the
existing Soggetto toggle. The server action validates the keyword and maps
it to the real model id; the agent receives the resolved id and uses it in
the Anthropic messages.create call. The proposals DB record stores the
actual model used.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 15:34:04 +02:00
simone e10d1f70cb fix: cumulative price from offer_tier_services, offers accordion, retainer phase placeholder
- client-view.ts: read cumulative_price + services list from offer_tier_services→services
  (Phase 12 catalog); legacy fallback to offer_micro_services→offer_services when no
  new-style rows exist. Extend activeOffers type with services[] in both ProjectView and ClientView.
- OffersSection.tsx: extract OfferCard client component with useState accordion;
  hide "Valore incluso" row when price is 0; add "Cosa è compreso" chevron toggle
  listing included services (name + description when present).
- client-dashboard.tsx: hide progress bar and PhaseViewToggle when hasRetainer;
  render polished empty-state placeholder with RefreshCw icon instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 14:53:25 +02:00
simone 64030afef3 feat: sidebar CTA removal, delete phase/task, public dashboard UX, chat panel
1. AdminSidebar: removed yellow "Genera preventivo" CTA (duplicated Preventivi tab nav item)
2. Delete phase/task: new deletePhase/deleteTask server actions with FK cascade and phase-status recompute; DeletePhaseTaskButton client component with window.confirm; trash icons in PhasesTab per phase and per task
3. Public dashboard: extend activeOffers query with offer_macros join (offer_name, offer_type); reorder sidebar 1°Offerte 2°Pagamenti 3°Documenti; OffersSection shows macro public_name as heading (never tier letter/internal_name); PaymentStatus gains totalLabel/overrideAmount/hideRows props — retainer=monthly label + no Acconto/Saldo rows
4. Offer editor: Descrizione textarea (nota interna) and Modalità toggle moved directly under title; Tags section now contains only Categoria/Ticket/Tipo/Obiettivo
5. Basecamp chat: API route supports entity_type="phase" with authorization scoping; comments scope in client-view broadened to include phaseIds and client_id (general); CommentsTab updated with phase/general entities; ChatProvider React context; ChatPanel fixed slide-in panel with FAB (bottom-right) and composer tag selector (Generale + phases); PhaseCard gets chat-bubble icon pre-tagging that phase; inline ChatSection block removed from dashboard main column

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 14:11:55 +02:00
simone 4b28f254ba docs: correct DB-access procedure (direct SSH+docker exec, remote is gitea)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 10:39:45 +02:00
simone 186bb9ea19 feat: payments plans, phase auto-cascade, transcripts, manual timer
- Pagamenti: totale ereditato dalla somma accepted_total delle offerte
  attive (con override) + selettore schema rate 1/2/3 step; nuova colonna
  payments.percent per rescalare gli importi preservando label/stato
- Fasi & Task: recomputePhaseStatus auto-cascade (task -> fase) su
  updateTaskStatus/addTask, fix UI stale, etichette Da iniziare/In corso/Completata
- Pagina pubblica: colori fasi (grigio/blu/#1A463C) + fasi collassabili
- Transcript del cliente visibili in tab Documenti admin e dashboard pubblica
- Timer: inserimento manuale (+30/+60, minuti liberi, data) + elimina entry
- CLAUDE.md: procedura Deploy & DB Access; push su main automatico

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 10:38:56 +02:00
simone 988f6f425a docs: add STATUS.md session handoff 2026-06-22 14:34:17 +02:00
simone f98828f75e feat: offer_type (una tantum/retainer), dedup tiers, redesigned Offerte tab
- offer_macros.offer_type ('una_tantum'|'retainer') + editor "Modalità" toggle
  (migration 0012, applied to prod)
- migration 0012 also adds UNIQUE(macro_id, tier_letter) — prevents duplicate
  tiers; ran after a one-time dedup of Web Domination's duplicate A/B/C tiers
- OffersTab redesigned: two-step assign (Offer → Tier cards with price), type
  badge "Una tantum/Ricorrente" instead of misleading "X mesi", no redundant
  "· A" when public_name == tier letter, cleaner active-offers cards
- getProjectFullDetail: availableMicros grouped by macro + defensive dedup;
  projectOffers/availableMicros carry offer_type/category/price
- proposal deck PricingSection shows offer type (fallback to duration for
  pre-existing proposals)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 14:32:59 +02:00
simone 1824cb643f feat: wire offer→project phases/tasks, redo Offerte tab, cleanup project tabs
- importOfferIntoProject(): materializes project phases/tasks from the assigned
  tier's services grouped by services.fase (merge-by-title, dedup tasks)
- assignOfferToProject: optional import_phases flag triggers the import
- getProjectFullDetail: projectOffers/availableMicros now include macro name +
  tier_letter (dropdown shows "Offerta — Tier X"); availableMicros filters
  non-archived macros
- OffersTab redone: shows macro + tier badge, "Importa fasi e task" checkbox
- removed project "Preventivo" tab + deleted QuoteTab.tsx (legacy quote_items)
- sidebar: Lead before Clienti
- no DB migration (reuses existing tables)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:08:28 +02:00
simone f5f90cd643 feat: lead → client conversion + client contact fields (email/phone)
Blocco A+B (milestone v2.3). Migration 0011 is additive (ADD COLUMN only):
clients.email, clients.phone, leads.archived.

- createClientCore() extracted from createClient and reused by conversion
- clients.email/phone added to create + edit forms and shown on client header
  (never exposed on the public /client/[token] page)
- convertLeadToClient(): reuses core, carries over lead transcripts
  (client_transcripts.client_id), links project.created_from_lead_id,
  archives the lead while keeping its "won" status; idempotent
- "Converti in cliente" button on won leads; "Convertito → Apri cliente"
  once converted (clientId resolved via created_from_lead_id)
- archived leads hidden from list/kanban; detail page fetches by id incl. archived

Migration must be applied to the prod DB BEFORE this is deployed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 08:18:41 +02:00
simone e1b3e8c3d5 style: refine settings taxonomy UI (field cells, icon headers, 2-col grid)
- PoolManager: defined field cells with value count, refined chips,
  inline ghost + button, focus ring, pending/empty states
- TaxonomyManager: icon-badged section headers, ordered 2-col grid,
  sync explainer note
- Settings page: wider container (max-w-4xl) for the grid to breathe

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 14:46:13 +02:00
simone e80c95f838 feat: centralized Notion-style taxonomy management in settings
Single persistent option pool per taxonomy (7 fields: offer
categoria/ticket/tipo/obiettivo + catalog fase/offerta/pacchetto),
stored as JSON in the settings table (no DB migration).

- src/lib/taxonomy.ts: pools with lazy seed from in-use values,
  add/remove(cascade)/rename helpers
- Inline creation anywhere registers into the pool (save offer,
  addServiceOption, updateServiceField fase, quickAdd, create macro)
- Deselecting on a row never touches the pool; global delete only
  from settings (cascade-strips tags / nulls columns)
- getOfferFieldOptions + getCatalogFieldOptions read from pools
- Settings: TaxonomyManager (offer + catalog groups), confirm on delete

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 14:24:47 +02:00