--- gsd_state_version: 1.0 milestone: v2.3 milestone_name: Email & Accesso status: executing stopped_at: "v2.3 deployata in produzione; resta da popolare la whitelist dei 3 clienti reali" last_updated: "2026-07-29T21:20:00.000Z" last_activity: 2026-07-29 -- dominio Resend verificato, gate OTP deployato in produzione progress: total_phases: 3 completed_phases: 3 total_plans: 3 completed_plans: 3 percent: 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: 23/24/25 completate e deployate Plan: `~/.claude/plans/si-ma-abbiamo-un-jaunty-micali.md` Status: in produzione. Resta da popolare la whitelist dei 3 clienti reali. Last activity: 2026-07-29 — dominio Resend verificato, deploy su `hub.iamcavalli.net` ### ✅ 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. ### Da fare dopo il deploy Popolare la whitelist dei 3 clienti reali da `/admin/clients/` → "Accessi al portale". Non è bloccante (l'utente li re-invita): la migration ha seedato solo `mario@test.it` (cliente di test "Rossi Inc"), mentre Protocollo Estetico, Caruso Speaker e Teckell partono con whitelist vuota e finché è vuota 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 prod** — `client_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_`, 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 `` 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 pura** — `client_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 Resend~~ — **risolto 2026-07-29**: dominio ricreato e `verified`, invio dal dominio reale confermato. - ~~`RESEND_API_KEY` + `RESEND_FROM` su Coolify~~ — **fatto 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/` → "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//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/` → "Accessi al portale", e reinviare loro il link. Resume file: .planning/STATE.md