Files
clienthub/.planning/STATE.md
T
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

18 KiB

gsd_state_version, milestone, milestone_name, status, stopped_at, last_updated, last_activity, progress
gsd_state_version milestone milestone_name status stopped_at last_updated last_activity progress
1.0 v2.3 Email & Accesso executing anteprima admin del portale + occhiolino login: scritti, buildati e verificati in locale, NON ancora committati né deployati. Resta da popolare la whitelist dei 3 clienti reali 2026-08-08T12:20:00.000Z 2026-08-08 -- anteprima admin in sola lettura del portale cliente + toggle password sul login
total_phases completed_phases total_plans completed_plans percent
3 3 3 3 100

Project State

Project Reference

See: .planning/PROJECT.md (updated 2026-06-21)

Core value: Il cliente apre il link e vede esattamente a che punto è il suo progetto, cosa deve ancora succedere e cosa ha già approvato — senza dover scrivere email per chiedere aggiornamenti.

Current focus: Milestone v2.3 "Email & Accesso"consegnata. Il portale cliente è protetto da gate email OTP. L'invio del preventivo via email (SEND-01/02) è stato rinviato a v2.4 — si manda a mano.

Current Position

Phase: v2.3 (23/24/25) e v2.4 Phase 13 in produzione. Sopra, lavoro non ancora committato: anteprima admin del portale + occhiolino sul login. Plan: ~/.claude/plans/per-fare-l-accesso-all-admin-elegant-teapot.md Status: scritto, buildato, verificato in locale contro il DB di produzione. Non committato, non deployato. Last activity: 2026-08-08 — anteprima admin in sola lettura del portale cliente

2026-08-08 — Anteprima admin del portale + occhiolino sul login

Non è in produzione. Codice scritto e verificato in locale (npm run build verde, nessun nuovo problema di lint), nessun commit e nessun push fatti.

Cosa risolve. Quando un cliente diceva "non trovo una cosa" non c'era modo di guardare il portale con i suoi occhi: il gate OTP lascia entrare solo lui. Ora l'elenco clienti (ClientRow) ha un'icona che apre /client/<slug>?preview=1 in una scheda nuova.

Come funziona. getClientGate() accetta { previewRequested } e salta il gate solo se il query param c'è e getServerSession(authOptions) è valida. Ritorna preview: true senza sintetizzare una ClientSession: un admin in anteprima non è un cliente autenticato, e confondere i due stati renderebbe i casi indistinguibili a valle. La sola lettura passa da PreviewProvider (usePreview()), consumato da ApproveButton e dal composer di ChatPanel — context e non prop drilling, perché ApproveButton sta quattro livelli sotto la dashboard.

Perché sola lettura e non anteprima interattiva. Il portale scrive davvero: /api/client/approve e /api/client/comment autenticano sul token nel body, non sulla sessione. Un click distratto approverebbe un deliverable, e approved_at è immutabile una volta impostato (vincolo LOCKED #3). La protezione è a livello di UI, non di API: un admin può ancora chiamare le route a mano. È voluto — l'obiettivo è impedire l'incidente, non difendersi da sé stessi.

Deviazione consapevole dal vincolo LOCKED #4 (/client/* → token, /admin/* → Auth.js): ora una route client legge anche la sessione Auth.js. Non indebolisce nulla — per i clienti il gate OTP è identico — ma CLAUDE.md non è stato ancora aggiornato: serve l'ok dell'utente per toccare la sezione LOCKED.

Verificato in locale (build di produzione su :3100, DB di prod via tunnel SSH, sole letture): ?preview=1 senza sessione admin → gate OTP; con cookie di sessione contraffatto → gate OTP; ?preview=0, ?preview=abc, ?preview= → gate OTP; senza query param e con sessione admin → gate OTP (nessuna regressione); con sessione admin valida + ?preview=1 → portale con banner e composer disattivato, sia sul cliente a progetto singolo sia su quello a due progetti (tab).

Non verificato dal vivo: il ramo ApproveButton — in produzione la tabella deliverables è vuota (0 righe), quindi quel pulsante oggi non si renderizza mai. Wiring controllato solo a livello di codice. Il click dell'occhiolino sul login è verificato solo nel markup renderizzato (type="button", tabindex="-1", aria-label): manca la prova a schermo, Playwright non ha browser in cache.

Perché la password di .env.local non funzionava. Non era un bug: ADMIN_PASSWORD è stata ruotata il 2026-07-28 solo su Coolify (vedi SECURITY-REMEDIATION-PLAN.md), e .env.local è rimasto alla precedente. Vale anche per NEXTAUTH_SECRET. .env.local non è allineato a produzione — non trattarlo come fonte di verità per le credenziali.

Nota su .env.local: contiene due righe DATABASE_URL; vince l'ultima (porta 54321) e punta al DB di produzione. Il DB non è raggiungibile dall'esterno: per puntarci in locale serve ssh -f -N -L 54321:localhost:54321 root@178.104.27.55 e sostituire l'host con localhost nella stringa di connessione.

Prerequisiti email risolti (2026-07-29)

  • Dominio iamcavalli.net verificato su Resend. L'utente ha ricreato la registrazione del dominio (nuovo id f81202f1-3bba-47c5-8c0f-84101440b960, la precedente a2a80798-… non esiste più) e messo i DNS. Tutti e tre i record verified: DKIM TXT su resend._domainkey, SPF TXT + MX su send. Invio da no-reply@iamcavalli.net verso un indirizzo esterno confermato riuscito.
    • Nota: ricreare il dominio su Resend rigenera la chiave DKIM. Se in futuro il dominio torna failed, non fidarsi di valori DKIM annotati in passato — rileggerli da GET /domains e confrontarli con dig +short TXT resend._domainkey.iamcavalli.net @8.8.8.8.
  • RESEND_API_KEY + RESEND_FROM configurate su Coolify (production e preview) via API — verificate presenti.
  • Mailer verificato: sendEmail() col template OTP reale ha restituito {ok:true, id:…}.
  • Verificato che quando Resend rifiuta, la route risponde comunque col messaggio neutro e logga l'errore lato server — il no-enumeration tiene anche a provider guasto.

Verificato in produzione (2026-07-29, commit 27da969)

Su hub.iamcavalli.net: gate mostrato senza cookie e zero dati di progetto nell'HTML (12.487 byte); email fuori whitelist e in whitelist danno risposta identica e solo la seconda genera un OTP; codice sbagliato rifiutato, corretto accettato; cookie ch_sess_<id> con Secure + HttpOnly + SameSite=lax + Max-Age=7776000 (90 giorni); rientro col cookie mostra la dashboard; sessione di un cliente sull'URL di un altro mostra il gate. Nessun errore d'invio nei log del container. Dati di test rimossi, tabelle protette invariate (4/5/11/10).

Da fare

Popolare la whitelist dei 3 clienti reali da /admin/clients/<id> → "Accessi al portale", e reinviare loro il link. La migration aveva seedato solo mario@test.it (cliente di test "Rossi Inc"); Protocollo Estetico, Caruso Speaker e Teckell hanno whitelist vuota e finché lo è il loro portale non è accessibile.

Cosa è stato consegnato in v2.3 (codice locale, buildato e testato)

  • Resend: resend@6.18.1, src/lib/mailer.ts (Result tipizzato, mai catch silenzioso) + template OTP in italiano.
  • Schema: migration 0015_otp_access.sql già applicata a prodclient_emails (whitelist, unique case-insensitive), otp_codes (hash del codice, mai il codice), clients.sessions_valid_from (revoca). Additiva pura: conteggi pre/post identici su clients 4 / projects 5 / payments 11 / phases 10.
  • Admin: sezione "Accessi al portale" in /admin/clients/[id] — aggiungi/rimuovi email + "Revoca sessioni attive". Server actions in clients/[id]/actions.ts. Scritta a token semantici benché la pagina attorno sia ancora a palette vecchia.
  • Gate: src/lib/otp.ts (codice 6 cifre CSPRNG, hash SHA-256 con NEXTAUTH_SECRET+clientId, TTL 15 min, max 5 tentativi), src/lib/client-session.ts (cookie HMAC per-cliente ch_sess_<id>, 90 giorni, httpOnly/secure/lax, path=/client), src/lib/client-gate.ts, route /api/client/otp/request|verify, componente OtpGate.

⚠️ Lezione: il gate NON va nel layout

Prima implementazione: gate in client/[token]/layout.tsx che rendeva <OtpGate/> al posto di {children}. Non funziona come protezione. Nell'App Router il segmento page viene renderizzato in parallelo al layout: la dashboard spariva a schermo ma fasi, task e pagamenti restavano leggibili nel payload RSC dell'HTML (46.907 byte → 17.594 dopo il fix). Il gate è ora in cima alla page, prima di ogni query, via getClientGate(). Ogni nuova route sotto /client/[token]/ deve fare lo stesso — il layout porta un commento che lo ricorda.

Lavoro recente precedente (in prod)

  • [2026-07-27/28] Audit di sicurezza: 4 vulnerabilità chiuse e deployate (secondo gate admin, hardening slug, XSS, CSP/HSTS); slug clienti deboli ruotati a 12 char CSPRNG; INTERNAL_SECRET e ADMIN_PASSWORD configurati su Coolify. Finding #1 (password Postgres committata) declassato CRITICO→BASSO: verificata inattiva, già ruotata. Report in .planning/SECURITY-*.md.
  • [2026-07-28] Riorganizzazione cartella: fasi di planning consolidate, script one-off archiviati in cestino/, CLAUDE.md arricchito.
  • Design system "Quiet Luxury": dashboard, liste (Clienti/Offerte/Catalogo/Preventivi/Progetti), Conversazioni, Impostazioni, Pipeline+Kanban, dettaglio Lead e portale cliente base sono a token e dual-theme. Ancora a design vecchio (funzionanti, solo estetica): /admin/offers/[id]/edit, /admin/projects/[id] (il cluster peggiore, ~140 occorrenze fra i suoi tab), /admin/projects/new, /admin/clients/[id], /admin/clients/[id]/edit, /admin/login, badge in /admin/preventivi/[id], chat portale cliente — più, non censiti prima: tutto /quote/[token] (~40 occorrenze, pagina rivolta al cliente) e ui/dialog.tsx, che propaga la palette vecchia a ogni modale.
  • Tassonomie: gestione centralizzata categorie/tag in Impostazioni (src/lib/taxonomy.ts).
  • Lead → Cliente (A+B): clients.email/phone + leads.archived (migration 0011); convertLeadToClient.

Fasi completate (v2.2, storico)

Phase 18 (cleanup), Phase 19 (Kanban CRM), Phase 20 (transcript KB), Phase 21 (AI agent), Phase 22 (deck pubblico) — consegnate e in prod 2026-06-20.

Performance Metrics

Velocity:

  • Total plans completed: 7 (v2.1) + 9 (v2.2) = 16 totali
  • Average duration: —
  • Total execution time: —

By Phase:

Phase Plans Total Avg/Plan
Phase 11 P01 25min 2 tasks 6 files
11 4 - -
14 3 - -

Recent Trend:

  • Last 5 plans: —
  • Trend: —

Updated after each plan completion | Phase 11 P02 | 12min | 2 tasks | 2 files | | Phase 11 P03 | 9min | 2 tasks | 2 files | | Phase 11 P04 | 12min | 2 tasks | 4 files |

Accumulated Context

Decisions

Decisions are logged in PROJECT.md Key Decisions table. Recent decisions affecting current work:

  • [v2.3 2026-06-21] Resend come provider email unico — PUB-03 (invio preventivo) e AUTH-OTP-01 (OTP gate) condividono la stessa integrazione Resend. Phase 23 configura SDK + env vars, Phase 25 li riusa.
  • [v2.3 2026-06-21] OTP gate è strato aggiuntivo, non rimpiazzo del token middleware — proxy.ts e /api/internal/validate-token rimangono invariati. Il gate OTP interviene dopo la validazione del token, nel rendering della route /client/[token]/*.
  • [v2.3 2026-06-21] Migration Phase 24 è additiva puraclient_emails e otp_codes sono nuove tabelle. Nessun drop/truncate. SQL a mano (drizzle-kit generate rotto). Applicare via SSH prima del codice dipendente.
  • [RESET 2026-06-19] Milestone v2.2 "Sales Loop" sostituisce le fasi residue v2.1. Decisioni bloccate: (1) URL preventivo = /preventivo/[slug] pubblico; (2) tagliare Forecast + quote builder manuale + Phase 15, fondere /admin/analytics nella dashboard; (3) portale post-vendita resta core, non si tocca (Phase 13 congelata); (4) agente AI = "io scelgo l'offerta, l'AI personalizza" leggendo i transcript, provider Claude. Piano: .claude/plans/glittery-sprouting-pudding.md
  • [SUPERSEDED dal reset] v2.1 roadmap: Offer Studio (Phases 11-15) sequenced before Proposal AI (Phases 16-17) — clean/fast data UX before the AI builder
  • Phase 11 bundles catalog database-view UX (OFFER-07..10) with legacy consolidation (OFFER-13) since the new UX should be built on a single unified services table, not on top of legacy service_catalog/offer_services
  • Phase 13 (Workspace — Servizi Attivi) is independent of Phases 11/12 — can execute in parallel order if useful, but numbered after for narrative flow
  • Phase 15 (Dashboard Revenue Stats / DASH-11) is isolated and BLOCKED on user-provided mockup; no other phase depends on it — can be deferred/skipped without blocking Phase 16/17
  • Phase 16/17 split: schema/automation (payment link field + auto-provisioning) first, then AI builder + public page redesign + email — keeps the AI-dependent work last
  • [Phase 11]: Phase 11: hand-write Drizzle migration SQL (0006_add_tags_table.sql) following the project's established convention since drizzle-kit generate is non-functional (meta snapshots out of sync since migration 0001, pre-existing since Phase 8) — Avoids architectural snapshot-reconciliation work (Rule 4, out of scope) while matching exact precedent from migrations 0003-0005
  • [Phase 11]: Phase 11 Plan 02: onConflictDoNothing() without explicit target compiles cleanly for tags table (single unique index tags_entity_name_unique) — used as written in plan, no fallback needed — Avoids unnecessary deviation; Drizzle's no-target ON CONFLICT DO NOTHING is correct given the single unique constraint from Plan 01
  • [Phase 11]: Phase 11 Plan 03: removed the plan's prescribed value-sync useEffect (and a follow-up render-time ref-read attempt) from EditableCell — both violate this project's react-hooks lint rules (set-state-in-effect, refs-during-render / React Compiler). tempValue is now only (re)initialized in startEdit()/cancel(), and the toggle display branch reads value directly instead of tempValue — Rule 1 lint fix, no behavioral change to the 8 spec'd test behaviors
  • [Phase 11]: Phase 11 Plan 04: left createService/serviceSchema in src/app/admin/catalog/actions.ts as unused dead code after deleting ServiceForm.tsx (its only consumer) — actions.ts was outside this plan's files_modified scope and updateService still depends on serviceSchema; logged to deferred-items.md for future cleanup
  • [Phase 18-02]: fmtEur unified to number version (analytics/page.tsx variant); KPI card callers using DB string values wrapped with parseFloat() — cleaner than maintaining two named variants
  • [Phase 18-02]: /admin/analytics route deleted; YearSelector now routes to /admin?year=Y — single admin entry point for statistics (CLEAN-03)

Pending Todos

[From .planning/todos/pending/ — ideas captured during sessions]

None yet.

Blockers/Concerns

  • Record DKIM / dominio Resendrisolto 2026-07-29: dominio ricreato e verified, invio dal dominio reale confermato.
  • RESEND_API_KEY + RESEND_FROM su Coolifyfatto 2026-07-29, production e preview.
  • Whitelist vuota per 3 clienti su 4 (non bloccante: l'utente li re-invita) — da popolare da /admin/clients/<id> → "Accessi al portale".
  • Coolify API: credenziali in ~/.coolify.env (export COOLIFY_URL/COOLIFY_TOKEN, va sorgentato con set -a; . ~/.coolify.env). App ClientHub uuid xsksow44g4kcoo8wocsgkscc. Il POST su /api/v1/applications/<uuid>/envs non accetta il campo is_build_time (422): mandare solo key, value, is_preview. I token Hetzner/Cloudflare nel file sono vuoti → il DNS non è modificabile via API.
  • Migrations (sempre valido): ogni fase con schema DEVE avere la migration applicata a prod PRIMA di pushare il codice dipendente. drizzle-kit generate rotto → SQL a mano. La 0015 è già applicata. Due strade: cat migration.sql | ssh root@178.104.27.55 "docker exec -i xwkk0040w0kk0gsgcgog8owk psql -U clienthub -d clienthub -v ON_ERROR_STOP=1 --single-transaction" (autoritativa, nessun tunnel), oppure tunnel ssh -f -N -L 54321:localhost:54321 root@178.104.27.55 con DATABASE_URL riscritto a 127.0.0.1:54321 se serve puntarci il tooling locale.
  • .env.local punta al DB di PRODUZIONE (178.104.27.55:54321, richiede il tunnel). Non esiste un DB di sviluppo separato: qualsiasi test in locale scrive su dati reali. Verificare sempre i conteggi delle tabelle protette prima e dopo.
  • Debito tecnico (non bloccante): tabelle legacy service_catalog/offer_services/offer_micro_services restano come deadweight; createService/serviceSchema dead code in catalog/actions.ts.

Deferred Items

Items acknowledged and carried forward from previous milestone close:

Category Item Status Deferred At
v2.4 PROP-03 — Stripe Payment Link su deck pubblico Backlog v2.3 kickoff
v2.4 PROP-04 — Auto-provisioning cliente/progetto/fasi al "Vinto" Backlog v2.3 kickoff
v2+ Phase 13 — Servizi attivi/ricorrenti post-vendita Congelata v2.1 kickoff
v2 OFFER-14 — Sezioni analitiche stile Notion Backlog v2.1 kickoff
v2 ARCH-01 — Split modulo "compartimento stagno" in deploy separato Backlog (only if module grows) v2.1 kickoff
v2.4 SEND-01/02 — Invio link preventivo via email dall'admin Backlog (mailer già pronto) 2026-07-28
Design 11 pagine ancora a palette vecchia — vedi elenco in "Lavoro recente" Backlog 2026-07-28

Session Continuity

Last session: 2026-07-29T21:20:00.000Z Stopped at: v2.3 deployata in produzione. Dominio Resend verificato, Coolify configurato, gate OTP live su hub.iamcavalli.net. Next: popolare la whitelist dei 3 clienti reali da /admin/clients/<id> → "Accessi al portale", e reinviare loro il link. Resume file: .planning/STATE.md