diff --git a/.planning/STATE.md b/.planning/STATE.md index 1f353ab..2a47786 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -1,143 +1,68 @@ --- gsd_state_version: 1.0 -milestone: v2.3 -milestone_name: Email & Accesso +milestone: v2.4 +milestone_name: Post-vendita status: executing -stopped_at: "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" -last_updated: "2026-08-08T12:20:00.000Z" -last_activity: 2026-08-08 -- anteprima admin in sola lettura del portale cliente + toggle password sul login +stopped_at: "Phase 13 e 26 in produzione. Nessun lavoro in sospeso: il prossimo va scelto dal backlog in REQUIREMENTS.md." +last_updated: "2026-08-08T20:30:00.000Z" +last_activity: 2026-08-08 -- riordino della documentazione di progetto progress: - total_phases: 3 - completed_phases: 3 - total_plans: 3 - completed_plans: 3 + total_phases: 2 + completed_phases: 2 + total_plans: 2 + completed_plans: 2 percent: 100 --- # Project State +> Digest per i comandi `/gsd-*`. La narrativa completa — cosa è stato fatto, cosa +> manca, le lezioni operative — sta in **`STATUS.md`** alla radice del repo. +> Questo file resta sotto le 100 righe di proposito. + ## Project Reference -See: .planning/PROJECT.md (updated 2026-06-21) +See: .planning/PROJECT.md (updated 2026-08-08) **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 focus:** Milestone **v2.4 "Post-vendita"** — tutto ciò che era pianificato è in produzione. Nessuna fase aperta. ## 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 +Phase: 2 of 2 (Phase 13 Ciclo di vita servizi ricorrenti · Phase 26 Anteprima admin) +Plan: 2 of 2 in current milestone +Status: Phase complete — nessuna fase aperta, prossimo lavoro da scegliere dal backlog +Last activity: 2026-08-08 — riordino della documentazione (`.planning/` e doc di root) -### 2026-08-08 — Anteprima admin del portale + occhiolino sul login +Progress: [██████████] 100% -**Non è in produzione.** Codice scritto e verificato in locale (`npm run build` verde, nessun nuovo problema di lint), nessun commit e nessun push fatti. +Entrambe le fasi sono **in produzione e verificate**: -**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/?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_` 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/` → "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 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. +- Phase 13 → `5177a37`, migr. 0016, prod 2026-08-01 · [13-SUMMARY.md](phases/13-ciclo-vita-servizi-ricorrenti/13-SUMMARY.md) +- Phase 26 → `09a5b1f` + `187550f`, prod 2026-08-08 · [26-SUMMARY.md](phases/26-anteprima-admin-e-login/26-SUMMARY.md) ## Performance Metrics -**Velocity:** - -- Total plans completed: 7 (v2.1) + 9 (v2.2) = 16 totali -- Average duration: — -- Total execution time: — - -**By Phase:** +**Velocity:** 21 plans completati in totale — 7 (v2.1) + 9 (v2.2) + 3 (v2.3) + 2 (v2.4). +**Recent Trend:** — · v2.3 e v2.4 sono state eseguite fuori dal ciclo GSD, quindi non cronometrate. | Phase | Plans | Total | Avg/Plan | |-------|-------|-------|----------| -| Phase 11 P01 | 25min | 2 tasks | 6 files | -| 11 | 4 | - | - | -| 14 | 3 | - | - | - -**Recent Trend:** - -- Last 5 plans: — -- Trend: — +| 13 | 1 | — | — | +| 26 | 1 | — | — | *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: +Log completo in PROJECT.md (Key Decisions). Rilevanti per il lavoro corrente: -- **[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) +- **[Phase 26, 2026-08-08] Deviazione consapevole dal vincolo LOCKED #4** — una route `/client/*` ora legge anche la sessione Auth.js per l'anteprima admin. Non indebolisce il gate per i clienti; annotata in `CLAUDE.md`. +- **[Phase 26] L'anteprima è in sola lettura a livello UI, non API** — le route `/api/client/*` autenticano sul token nel body. L'obiettivo è impedire l'incidente, non difendersi da sé stessi. +- **[Phase 13] Storico di vendita ≠ forecast** — `getOffersSoldBreakdown` non filtra per stato: escludere le offerte cessate riscriverebbe il fatturato passato. +- **[v2.3, 2026-07-28] Sessione OTP a 90 giorni invece di 30** — rientro più fluido, compensato dalla revoca in blocco lato admin (OTP-08). ### Pending Todos @@ -147,31 +72,27 @@ 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`. +- **Whitelist portale vuota per 3 clienti su 4** (non bloccante: l'utente li re-invita) — da popolare da `/admin/clients/` → "Accessi al portale". +- **`.env.local` punta al DB di PRODUZIONE** e non è allineato a Coolify per `ADMIN_PASSWORD` / `NEXTAUTH_SECRET` (ruotati il 2026-07-28). Nessun DB di sviluppo separato: ogni prova locale scrive su dati reali. +- **Ogni fase con schema** DEVE avere la migration applicata a prod PRIMA del push del codice dipendente. `drizzle-kit generate` è rotto → SQL a mano. Procedura in `CLAUDE.md`. +- **Debito design (DEBT-01)** — ~40 file, ~450 occorrenze di palette raw/hex. Misurato il 2026-08-08. Dettaglio in `STATUS.md`. ## 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 | +| v2.4 | RET-06 — Canoni mensili tracciabili (serve tabella nuova) | Backlog | 2026-08-01 | +| v2.4 | SEND-01/02 — Invio link preventivo via email (mailer già pronto) | Backlog | 2026-07-28 | +| v2.4+ | PROP-03 — Stripe Payment Link sul deck | Backlog | v2.3 kickoff | +| v2.4+ | PROP-04 — Auto-provisioning cliente/progetto/fasi al "Vinto" | Backlog | v2.3 kickoff | +| Design | DEBT-01 — Migrazione a token semantici | Backlog | 2026-07-28 | +| Tech debt | DEBT-02 — Tabelle legacy catalogo + dead code | Backlog | v2.1 | +| v2+ | OFFER-14 — Sezioni analitiche stile Notion | Backlog | v2.1 kickoff | +| v2+ | ARCH-01 — Split modulo in deploy separato | Backlog (solo se cresce) | v2.1 kickoff | ## 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 +Last session: 2026-08-08 20:30 +Stopped at: Riordino della documentazione — milestone chiuse archiviate, v2.4 documentata, STATUS.md unico documento narrativo. +Next: Popolare la whitelist dei 3 clienti reali, oppure attaccare DEBT-01 (debito design). +Resume file: None diff --git a/STATUS.md b/STATUS.md index 8402eb6..8fb7cde 100644 --- a/STATUS.md +++ b/STATUS.md @@ -1,44 +1,132 @@ # ClientHub (IAMCAVALLI) — Status -_Ultimo aggiornamento: 2026-08-01_ +_Ultimo aggiornamento: 2026-08-08_ + +Questo è **l'unico documento narrativo** del progetto: a che punto siamo, cosa manca, +cosa abbiamo imparato. `.planning/STATE.md` è il digest che leggono i comandi +`/gsd-*` — non raddoppia questo file, ci rimanda. ## Stato attuale -In prod su Coolify (Gitea → deploy automatico su push a `main`). Build verde, `npm audit` pulito. +In produzione su `hub.iamcavalli.net` (Coolify/Hetzner, deploy automatico su push a +`main` via Gitea). Build verde, `npm audit` pulito. -Milestone **v2.3 "Email & Accesso" chiusa e in produzione** dal 2026-07-29: il portale cliente non è più apribile col solo link. In corso **v2.4 Phase 13** — ciclo di vita dei servizi ricorrenti. +Milestone **v2.4 "Post-vendita"** — Phase 13 e Phase 26 consegnate e in produzione. +Nessun lavoro in sospeso non committato. ---- +| Milestone | Fasi | Stato | +|---|---|---| +| v2.4 Post-vendita | 13, 26 | 🔨 in corso — consegnato tutto ciò che era pianificato | +| v2.3 Email & Accesso | 23–25 | ✅ shipped 2026-07-29, verificata E2E | +| v2.2 Sales Loop | 18–22 | ✅ shipped 2026-06-20 | +| v2.1 Offer Studio + CRM | 11, 12, 14 | ✅ chiusa per reset 2026-06-19 | +| v1.0 + v2.0 | 1–10 | ✅ shipped giugno 2026 | -## Fatto (recente, cumulativo) +## Fatto -- **[v2.4, in corso] Ciclo di vita dei servizi ricorrenti**: `project_offers.status` (attivo/sospeso/cessato) + `end_date` (migr. 0016, in prod). Prima un retainer non poteva finire e il forecast lo sommava a ogni mese in eterno. Comandi Sospendi/Riattiva/Cessa nella tab Offerte del progetto; il cliente vede stato, "attivo dal" e canone mensile. -- **[v2.3] Gate OTP sul portale cliente**: whitelist email per cliente (`client_emails`), codice a 6 cifre via Resend, sessione firmata 90 giorni, revoca in blocco dall'admin (`clients.sessions_valid_from`). Migr. 0015. Verificato E2E in produzione. -- **[2026-07] Audit di sicurezza chiuso**: 4 vulnerabilità risolte e deployate, slug clienti ruotati a 12 char CSPRNG, `INTERNAL_SECRET` e `ADMIN_PASSWORD` configurati su Coolify. Report in `.planning/SECURITY-*.md`. -- **Tassonomie centralizzate** in Impostazioni (modello Notion, pool persistenti `src/lib/taxonomy.ts`). -- **Lead → Cliente**: `clients.email/phone` + `leads.archived` (migr. 0011). `convertLeadToClient` riusa `createClientCore`, porta i transcript, archivia il lead mantenendo "won". -- **Offerta → Fasi/Task**: `importOfferIntoProject` crea fasi raggruppando i servizi del tier per `services.fase`. -- **Offerte (modello + UI)**: `offer_macros.offer_type` ('una_tantum'|'retainer') + toggle "Modalità" nell'editor (migr. 0012). Tab Offerte a 2 step (Offerta → Tier con prezzo). +### v2.4 — Post-vendita + +- **[Phase 13, prod 2026-08-01] Ciclo di vita dei servizi ricorrenti.** + `project_offers.status` (attivo/sospeso/cessato) + `end_date` (migr. 0016). Prima + un retainer non poteva finire e il forecast lo sommava a ogni mese in eterno. + Comandi Sospendi/Riattiva/Cessa nella tab Offerte; il cliente vede stato, "attivo + dal" e canone mensile. Dettaglio: [`.planning/phases/13-…/13-SUMMARY.md`](.planning/phases/13-ciclo-vita-servizi-ricorrenti/13-SUMMARY.md) +- **[Phase 26, prod 2026-08-08] Anteprima admin del portale + toggle password.** + `?preview=1` più una sessione Auth.js valida apre il portale di un cliente in + **sola lettura**, senza passare dal gate OTP. Deviazione consapevole dal vincolo + LOCKED #4, annotata in `CLAUDE.md`. Dettaglio: [`.planning/phases/26-…/26-SUMMARY.md`](.planning/phases/26-anteprima-admin-e-login/26-SUMMARY.md) + +### v2.3 — Email & Accesso + +- **Gate OTP sul portale cliente**: whitelist email per cliente (`client_emails`), + codice a 6 cifre via Resend, sessione firmata 90 giorni, revoca in blocco + dall'admin (`clients.sessions_valid_from`). Migr. 0015. Verificato E2E in + produzione. Archivio: [`v2.3-ROADMAP.md`](.planning/milestones/v2.3-ROADMAP.md) + +### Prima di v2.3 + +- **[2026-07] Audit di sicurezza chiuso**: 4 vulnerabilità risolte e deployate, slug + clienti ruotati a 12 char CSPRNG, `INTERNAL_SECRET` e `ADMIN_PASSWORD` configurati + su Coolify. Report in [`.planning/security/`](.planning/security/). +- **Design system "Quiet Luxury"**: dashboard, liste (Clienti/Offerte/Catalogo/ + Preventivi/Progetti), Conversazioni, Impostazioni, Pipeline+Kanban, dettaglio Lead + e portale cliente base sono a token semantici e dual-theme. +- **Tassonomie centralizzate** in Impostazioni (modello Notion, pool persistenti + `src/lib/taxonomy.ts`). +- **Lead → Cliente**: `clients.email/phone` + `leads.archived` (migr. 0011). + `convertLeadToClient` riusa `createClientCore`, porta i transcript, archivia il + lead mantenendo "won". +- **Offerta → Fasi/Task**: `importOfferIntoProject` crea fasi raggruppando i servizi + del tier per `services.fase`. +- **Offerte (modello + UI)**: `offer_macros.offer_type` ('una_tantum'|'retainer') + + toggle "Modalità" nell'editor (migr. 0012). Tab Offerte a 2 step. ## Da fare -- [ ] **Whitelist portale**: dei 4 clienti solo alcuni hanno email autorizzate. Chi ha la whitelist vuota non entra nel proprio portale — si popola da `/admin/clients/` → "Accessi al portale". -- [ ] **Fasi/Task dall'offerta** funzionano solo se i servizi hanno il campo **Fase** valorizzato nel Catalogo (altrimenti finiscono in "Generale"). +- [ ] **Whitelist portale**: dei 4 clienti solo uno ha email autorizzate. Chi ha la + whitelist vuota **non entra nel proprio portale** — si popola da + `/admin/clients/` → "Accessi al portale", poi va reinviato il link. +- [ ] **Fasi/Task dall'offerta** funzionano solo se i servizi hanno il campo **Fase** + valorizzato nel Catalogo (altrimenti finiscono in "Generale"). +- [ ] **Debito design (DEBT-01)**: **~40 file, ~450 occorrenze** di palette Tailwind + raw e hex literal invece dei token semantici. I cluster: `/admin/projects/[id]` + e i suoi tab (~182), `/admin/offers/[id]/edit` (~79), `/admin/clients/[id]` + (~59), tutto `/quote/[token]` (~48, ed è rivolto al cliente), `ChatPanel` (37), + più `ui/dialog.tsx` che propaga il look vecchio a ogni modale. *Esclusi perché + legittimi:* `AdminSidebar` (eccezione brand documentata), `src/lib/mailer.ts` + (HTML email), i colori di stato di `StatusBadge` (sanzionati dal design system, + hanno già le varianti `dark:`). - [ ] Micro legacy "Mantenimento" senza tier: valutare se rimuoverlo/normalizzarlo. -- [ ] **Debito design**: 11 pagine ancora a palette raw invece che a token semantici. Le più pesanti: `/admin/projects/[id]` (~140 occorrenze fra i suoi tab), `/admin/offers/[id]/edit`, e tutto `/quote/[token]` (~40, ed è rivolto al cliente). Anche `ui/dialog.tsx`, che propaga il look vecchio a ogni modale. -- [ ] **Backlog v2.5**: canoni mensili tracciabili (agosto pagato / settembre no) — serve una tabella nuova, `payments` è protetta e la sua riscalatura è pensata per i piani una tantum. Più PROP-03 (Stripe sul deck), PROP-04 (auto-provisioning al "Vinto"), SEND-01/02 (invio preventivo via email — il mailer è già pronto). +- [ ] **Backlog**: canoni mensili tracciabili (RET-06 — serve una tabella nuova), + PROP-03 (Stripe sul deck), PROP-04 (auto-provisioning al "Vinto"), SEND-01/02 + (invio preventivo via email — il mailer è già pronto). Elenco completo in + [`.planning/REQUIREMENTS.md`](.planning/REQUIREMENTS.md). + +## Lezioni operative + +Cose imparate a caro prezzo. Non sono documentazione di feature: sono trappole in +cui si ricasca. + +- **Il gate OTP non va nel layout.** Prima implementazione: gate in + `client/[token]/layout.tsx` che rendeva `` al posto di `{children}`. + **Non protegge nulla.** Nell'App Router il segmento `page` è 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 sta in cima alla `page`, prima di ogni query, via `getClientGate()`. + **Ogni nuova route sotto `/client/[token]/` deve fare lo stesso.** +- **Ricreare il dominio su Resend rigenera la chiave DKIM.** Se 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`. +- **`.env.local` non è allineato a produzione.** `ADMIN_PASSWORD` e + `NEXTAUTH_SECRET` sono stati ruotati il 2026-07-28 **solo su Coolify**. Per + costruire firme o hash validi in prod vanno letti da Coolify, non da `.env.local`. +- **L'API Coolify rifiuta `is_build_time`** con 422 sul POST a + `/api/v1/applications//envs`: mandare solo `key`, `value`, `is_preview`. +- **Playwright non funziona contro `npm run dev`**: la CSP blocca `eval` e i client + component non si idratano. Serve il build di produzione. ## Note tecniche -- **Il DB di `.env.local` È la produzione** (178.104.27.55). Non esiste un database di sviluppo separato: qualunque cosa si esegua in locale scrive su dati reali. Verificare i conteggi delle tabelle protette prima e dopo ogni prova. -- **Migrazioni**: SQL scritto a mano in `src/db/migrations/` (`drizzle-kit generate` è rotto — vanno tenuti in sync `schema.ts` e l'SQL). Si applicano **da locale via SSH + docker exec**, senza tunnel: - `cat src/db/migrations/NNNN.sql | ssh root@178.104.27.55 "docker exec -i xwkk0040w0kk0gsgcgog8owk psql -U clienthub -d clienthub -v ON_ERROR_STOP=1 --single-transaction"` - Il tunnel `ssh -f -N -L 54321:localhost:54321` serve solo per puntare il tooling locale (es. `npx tsx`) al DB di prod, riscrivendo l'host di `DATABASE_URL` a `127.0.0.1:54321`. -- **Ordine di deploy con schema**: applicare la migrazione a prod **prima** del push (il deploy fa girare subito il codice nuovo). -- **`NEXTAUTH_SECRET` locale ≠ produzione**: per costruire firme o hash validi in prod va letto da Coolify, non da `.env.local`. -- **Coolify API**: credenziali in `~/.coolify.env` (formato `export VAR=…`, va sorgentato). App uuid `xsksow44g4kcoo8wocsgkscc`. Il POST su `/api/v1/applications//envs` rifiuta il campo `is_build_time` con 422. -- **`overrides` in package.json** forzano `postcss >= 8.5.18` e `sharp >= 0.35.0`: le versioni che Next si porta dietro hanno CVE high e non c'è fix upstream. Se un aggiornamento di Next rompe qualcosa, è il primo posto dove guardare. +- **Il DB di `.env.local` È la produzione** (178.104.27.55). Non esiste un database + di sviluppo separato: qualunque cosa si esegua in locale scrive su dati reali. + Verificare i conteggi delle tabelle protette prima e dopo ogni prova. +- **Migrazioni**: SQL scritto a mano in `src/db/migrations/` (`drizzle-kit generate` + è rotto — vanno tenuti in sync `schema.ts` e l'SQL). Si applicano **da locale via + SSH + docker exec**, senza tunnel — procedura completa in `CLAUDE.md`. Il tunnel + `ssh -f -N -L 54321:localhost:54321` serve solo per puntare il tooling locale + (es. `npx tsx`) al DB di prod. +- **Ordine di deploy con schema**: applicare la migrazione a prod **prima** del push + (il deploy fa girare subito il codice nuovo). +- **Coolify API**: credenziali in `~/.coolify.env` (formato `export VAR=…`, va + sorgentato). App uuid `xsksow44g4kcoo8wocsgkscc`. +- **`overrides` in package.json** forzano `postcss >= 8.5.18` e `sharp >= 0.35.0`: + le versioni che Next si porta dietro hanno CVE high e non c'è fix upstream. Se un + aggiornamento di Next rompe qualcosa, è il primo posto dove guardare. - `offer_micros` non ha `created_at` (no "tier più vecchio" affidabile). +- **Debito tecnico non bloccante**: tabelle legacy `service_catalog` / + `offer_services` / `offer_micro_services` come deadweight; `createService` / + `serviceSchema` dead code in `src/app/admin/catalog/actions.ts`. ## File chiave @@ -54,3 +142,16 @@ Milestone **v2.3 "Email & Accesso" chiusa e in produzione** dal 2026-07-29: il p | src/components/admin/tabs/OffersTab.tsx | Tab Offerte + comandi ciclo di vita | | src/lib/admin-queries.ts / client-view.ts | I due layer separati: admin vs proiezioni client-safe | | src/db/migrations/ | 0011 (email/phone), 0012 (offer_type), 0015 (OTP), 0016 (ciclo di vita) | + +## Dove sta il resto + +| Cosa | Dove | +|---|---| +| Regole per Claude, procedure deploy/DB, vincoli LOCKED | `CLAUDE.md` | +| Digest di stato per i comandi `/gsd-*` | `.planning/STATE.md` | +| Requisiti e backlog della milestone corrente | `.planning/REQUIREMENTS.md` | +| Roadmap e storico milestone | `.planning/ROADMAP.md`, `.planning/MILESTONES.md` | +| Archivi delle milestone chiuse | `.planning/milestones/` | +| Fasi della milestone in corso | `.planning/phases/` | +| Report dell'audit di sicurezza (chiuso) | `.planning/security/` | +| Design system e mock per pagina | `design-reference/` |