# ClientHub (IAMCAVALLI) — Status _Ultimo aggiornamento: 2026-08-21_ 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 produzione su `hub.iamcavalli.net` (Coolify/Hetzner, deploy automatico su push a `main` via Gitea). Build verde, `npm audit` pulito. Milestone **v2.5 "Audit"** in **pausa** a Phase 27 a metà: in produzione c'è solo lo schema dell'audit, nessuna pagina. La pausa è una scelta del 2026-08-19 — prima le **modifiche all'hub** chieste il 2026-08-18, poi il motore. | Milestone | Fasi | Stato | |---|---|---| | Modifiche hub | — | 🔨 in corso — dashboard e progetti in produzione, pipeline a metà | | v2.5 Audit | 27–30 | ⏸️ in pausa — Phase 27 ~50% | | v2.4 Post-vendita | 13, 26 | ✅ chiusa 2026-08-08, entrambe in produzione | | 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 | ## In corso ### Modifiche hub (richieste 2026-08-18) Tre aree: dashboard, progetti, pipeline. Piano in `~/.claude/plans/sei-arrivato-qua-search-recursive-kettle.md`, **fuori dal repo**. **In produzione dal 2026-08-19** (commit `4b135ce` → `19ed377`): - **Progetti.** Via il tab Commenti (duplicava `/admin/conversazioni`, che è un sovrainsieme stretto) e il timer dalla lista. Riepilogo soldi + avanzamento in testa al progetto. **Timer per fase e task** — migration `0018`, `ON DELETE SET NULL` perché cancellare un task non deve cancellare le ore lavorate. - **Dashboard.** Inbox promossa in cima a piena larghezza, con da-quanto-aspetta e su cosa è stato scritto. **Analytics per linea di prodotto** (Entry/Signature/Retainer, letti dalla tassonomia). **Timeline delle consegne** con semaforo ritardo/anticipo, scadenza dedotta da offerta + durata con override manuale `projects.due_date`. - **Pipeline.** `POST /api/webhooks/lead`, un endpoint solo per form del sito e bridge; regge payload piatto, `fields` annidati di Elementor e urlencoded, e non duplica chi compila due volte. Provato contro il DB di produzione via tunnel, poi ripulito. **In produzione dal 2026-08-20** (`3fcb10d`, `5547e55`) — due modifiche uscite dall'uso reale del pannello, nessuna migration (verificato: `tasks.status` è `text` senza `CHECK`): - **Rinomina di un valore di tassonomia** da `/admin/impostazioni`. Prima si poteva solo aggiungere o eliminare: per cambiare nome a una fase bisognava cancellarla — il che la strappava via da ogni servizio — e ricrearla a mano. `renamePoolValue` c'era già e propagava ovunque tranne in un punto: `importOfferIntoProject` copia `services.fase` dentro `phases.title` e poi ritrova la fase **confrontando i titoli**, perché `phases.offer_phase_id` esiste in schema ma non viene mai popolata. Un rename fermo al catalogo lasciava le fasi dei progetti col vecchio nome e al re-import ne nasceva una duplicata. Ora propaga anche lì, con lo stesso match `trim`+`lowercase` del merge, ed è l'unico rename che chiede conferma — dicendo quante fasi e quanti progetti tocca. - **Stato task "In revisione"**, fra "In corso" e "Fatto", visibile anche al cliente. Il lavoro vero non era il nuovo stato ma i tre letterali ricopiati a mano in otto file: ora tutto deriva da `TASK_STATUSES` in `src/lib/task-status.ts`. **In produzione dal 2026-08-20**, secondo giro (`b49d4bf`, `1fa8e1a`, migration `0019`): - **Tab pagamenti.** Mettere una rata su "saldato" la faceva saltare in fondo: `payments` non aveva **nessuna** colonna d'ordine e nessuna delle 13 query che la leggono aveva un `ORDER BY`, quindi Postgres restituiva l'ordine fisico e una `UPDATE` in MVCC riscrive la tupla in coda. In produzione **3 progetti su 5 erano già scombinati**. Per questo il backfill della `0019` **non** ordina per `ctid` (avrebbe fotografato lo scombinamento) ma per `percent DESC` con tie su `label`. Ora le rate si rinominano, gli importi si sovrascrivono a mano (`amount_locked` li esclude dal ricalcolo) e lo scarto fra somma rate e totale viene **dichiarato**, non aggiustato di nascosto. Schema a 3 rate: 50/25/25. - **Riordino dei task** dentro la fase, trascinando. `PhasesTab` è un Server Component con closure `"use server"` inline, quindi non è stato convertito: le righe restano server-renderizzate e `SortableTaskList` ci monta attorno solo la maniglia. Due bug chiusi per strada, entrambi vivi in produzione: `rescalePayments` azzerava le righe con `percent` NULL appena una riga del progetto ne aveva uno (ed è proprio quello che produce `splitPayment`), e il selettore di schema cancellava `status`/`paid_at` senza chiedere, con due rate già saldate a rischio. ⚠️ **Provato a runtime, non ancora cliccato.** Build di produzione contro il DB vero via tunnel SSH, in sola lettura: pagina progetto 200, 27 maniglie di trascinamento (esattamente i task delle fasi con più di un task), rate nell'ordine giusto. Restano da esercitare a mano le tre scritture nuove — `reorderTasks`, `updatePaymentField`, `clearPaymentOverride` — e il drag vero e proprio. ⚠️ **Il giro precedente resta deployato ma non provato a mano.** Build e lint verdi, immagine `9a57e45` viva, `/admin/login` risponde 200. Restano da esercitare in UI le due interazioni: la matita di rinomina con il dialog di conferma, e il drag di un task in "In revisione" con il controllo che la fase risulti *attiva*. Non sono state automatizzate di proposito: **l'unico DB raggiungibile in locale è la produzione**, e una prova di rinomina riscriverebbe titoli di fasi veri. **In produzione dal 2026-08-21** (migration `0020`) — portale cliente: - **Barra di avanzamento compatta e a tutta larghezza.** Rubava ~147px di altezza sopra la piega per dire una cosa sola. Ora "Step N" e lo stato stanno sulla stessa riga, il titolo della fase sotto — due righe invece di tre — e il padding scende da `py-8` a `py-3`: ~88px. Il mock sta in `design-reference/Client-Portal-Progress-Bar`, scritto in `slate-*` grezzi come tutti i mock: tradotto a token, non copiato. - **Card offerta senza accordion.** Via "Cosa è compreso": la lista dei servizi non serve al cliente e non viene più nemmeno **proiettata** — `client-view.ts` ora legge dai servizi i soli prezzi. `OffersSection` ha perso il suo unico `useState` e con esso il `"use client"`. - **"Valore incluso" → "Valore dell'offerta", con override.** Era sempre la somma dei prezzi di catalogo dei servizi del tier: un artefatto che si muove quando si muove il listino. In produzione produceva **€20.250 su offerte vendute a 7.000 e 5.500** — al cliente sembrava uno sconto del 70%. Ora `project_offers.offer_value_override` vince sulla somma; NULL torna al calcolo, `0` nasconde la riga. Si gestisce dal tab Offerte del progetto, che mostra accanto la cifra calcolata per far vedere cosa si sta sostituendo. La somma vive in `src/lib/offer-value.ts`, letta **sia** dal portale sia dall'admin: duplicarla avrebbe fatto divergere le due viste. - **"Prezzo finale" → "Investimento finale"** (solo una tantum; il ricorrente resta "Canone mensile"). ✅ **Verificato quel che si poteva verificare.** Build verde, lint senza nuovi warning, migration `0020` applicata a prod **prima** del push e colonna riletta dal DB a conferma; deploy atterrato (l'immagine in esecuzione è taggata `44be190`, cioè HEAD). **La scrittura dell'override è provata sul campo**: il 2026-08-21 in prod `Caruso Speaker / B` ha `offer_value_override = 7500.00` a fronte di un calcolo di 20.250 — la `0020` aggiunge la colonna e basta, non fa backfill, quindi quel numero è stato messo a mano dal tab Offerte. ⚠️ **Non reso a runtime, e va detto.** Le pagine non sono state aperte contro i dati veri: `.env.local` è scaduto **su tre fronti** — `ADMIN_PASSWORD`, `NEXTAUTH_SECRET` **e la password del DB** — e l'host che dichiara (`178.104.27.55:5432`) è chiuso dal firewall. Estrarre la password viva dal container è stato **bloccato dal classifier dei permessi**, e non è stato aggirato. Quindi: *scritto e buildato*, non *provato*. **Da fare a mano:** aprire il portale di un cliente e guardare barra e card offerta. **Resta un override da mettere**: `Rossi Inc / B` è venduta a 7.000 e il portale le mostra ancora i 20.250 del calcolo. (`Teckell / A`: calcolo e venduto coincidono a 200, non serve toccarla.) Perché la verifica locale torni possibile serve riallineare `.env.local` alle variabili di Coolify (vedi § Note tecniche). Cosa manca, e perché: - **[BLOCCANTE] TidyCal.** Non ha webhook — è scritto nella loro FAQ, la strada suggerita è Zapier/Make. Ha una REST API con Personal Access Token su tutti i piani, ma **path, filtri per data e paginazione stanno dietro il login** e non sono indicizzati. Serve che l'utente apra `tidycal.com/integrations` → API Keys e passi token o documentazione: dedurre la forma dell'API violerebbe la regola che il progetto si è già dato dopo il caso PageSpeed. - **Alleggerire l'hub.** Senza perimetro. Da affrontare guardando cosa è davvero poco usato, ora che dashboard e progetti sono in produzione. - **Whop → audit.** Dipende dal motore: oggi sarebbe un innesco che non innesca nulla. ⚠️ Il tab Commenti permetteva di rispondere sulla **singola entità**; `replyToConversation` salva sempre sul thread generale. Non è una regressione introdotta ora — era già una scelta di prodotto — ma da oggi è l'unica via. ### v2.5 — Audit (Phases 27–30), in pausa Il servizio di analisi sito (tre livelli: **Radiografia / Prima-Dopo / Rotta**) diventa un documento privato su `/audit/[slug]`, generato da un motore multi-agente e rifinito a mano prima della consegna. I tre livelli sono **configurazioni di un unico documento**: i blocchi non pertinenti non esistono nel DOM. Fatto finora (Phase 27, ~50%): - **[prod 2026-08-18] Schema.** Migration `0017_audits.sql`, 7 tabelle additive, più `checklist_items` seminata con 264 voci. Nessuna UI le legge ancora. - **[prod 2026-08-19, inerte] Le fonti del motore.** `src/lib/audit/sources/` — PageSpeed, CrUX, Wayback, RDAP, robots/sitemap/JSON-LD, header. Provate sul campo su giojello.com: giro completo in 73 s, tutte e cinque hanno risposto. Deployate, ma **nessuna route le chiama**. Manca: agent + sintetizzatore + pipeline, storage immagini, editor admin, pagina pubblica. Dettaglio in [`.planning/ROADMAP.md`](.planning/ROADMAP.md) e [`.planning/REQUIREMENTS.md`](.planning/REQUIREMENTS.md). ⚠️ **I due piani della milestone stanno in `~/.claude/plans/`, fuori dal repo** (`…woolly-puddle.md` per il documento, `…radiant-valley.md` per il motore). Senza quei file la milestone non è ricostruibile. ## Fatto ### 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 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. - [ ] **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, e la deriva è peggiorata.** Il 2026-07-28 erano stati ruotati **solo su Coolify** `ADMIN_PASSWORD` e `NEXTAUTH_SECRET`; il 2026-08-21 si è scoperto stale anche **la password del DB** (Postgres risponde `password authentication failed`) e chiuso dal firewall l'host che il file dichiara, `178.104.27.55:5432` — il DB vero è su `127.0.0.1:54321` dietro tunnel, quindi vanno riscritti host **e** porta. Recuperare la password viva dal container è **bloccato dal classifier**: non si aggira, si chiede all'utente di riallineare il file. Conseguenza operativa: finché resta così, **una modifica al portale si verifica solo guardandola in produzione** — e va detto, invece di far passare "buildato" per "funziona". Le migration non ne soffrono (`docker exec` non usa quelle credenziali). - **L'API Coolify rifiuta `is_build_time`** con 422 sul POST a `/api/v1/applications//envs`: mandare solo `key`, `value`, `is_preview`. - **Una lista di valori validi dimentica in silenzio, una negazione no.** `recomputePhaseStatus` decideva "fase iniziata" con `status === "in_progress" || status === "done"`. Aggiungendo "In revisione", una fase con tutti i task in revisione non rientrava né in `allDone` né in `anyActive` e **retrocedeva a "upcoming"**: si leggeva "non iniziata" quando era quasi finita. Stesso schema in `ClientKanban`, dove i task erano ripartiti da un oggetto a tre chiavi fisse invece che dalle colonne: un task fuori da quelle spariva da ogni colonna e da ogni contatore, e il cliente ne vedeva meno di quanti ce n'erano, **senza errore**. La causa a monte di entrambi era lo stesso `as` al confine del portale, che TypeScript non verifica. Quando un insieme di stati può crescere: derivare le colonne dalla costante, scrivere il predicato come negazione, e normalizzare al confine invece di castare. - **Playwright non funziona contro `npm run dev`**: la CSP blocca `eval` e i client component non si idratano. Serve il build di produzione. - **Le API di Google cambiano forma sotto i piedi, e in silenzio.** Scrivendo `src/lib/audit/sources/` due estrazioni dedotte dalla documentazione hanno restituito valori vuoti *senza errore*: `largest-contentful-paint-element` non esiste più (ora è `lcp-breakdown-insight`, con `subpart`/`duration` e senza percentuali) e `configSettings.screenEmulation` non esiste affatto nelle risposte pubbliche. Trovate solo perché le fonti sono state fatte girare su un sito vero prima di costruirci sopra. **Ogni estrazione da un'API di terzi va vista funzionare, non dedotta dai docs.** - **Laboratorio e campo misurano cose diverse, e la differenza *è* il risultato.** Su giojello.com Lighthouse dà `server-response-time` **7 ms** e CrUX dà TTFB p75 **3.553 ms con l'1% di visite nel verde**: il server risponde in fretta al datacenter Google e lento a tutti gli altri. Due numeri con lo stesso nome verrebbero fusi in uno — da qui `risposta_server_ms` invece di `ttfb_ms`. - **I punteggi PageSpeed ballano fra un giro e l'altro**: performance mobile 52 e poi 64 sullo stesso sito a 30 minuti di distanza. Nei documenti consegnati un punteggio va sempre con la sua data, mai presentato come una costante del sito. ## Note tecniche - **Non esiste un database di sviluppo.** Quello puntato da `.env.local` **è** la produzione: qualunque cosa si esegua in locale scrive su dati reali — verificare i conteggi delle tabelle protette prima e dopo ogni prova. ⚠️ Dal 2026-08-21 quelle credenziali **non autenticano più** e l'host che il file dichiara è chiuso: per raggiungere il DB servono il tunnel su `127.0.0.1:54321` **e** una password viva presa da Coolify. Fino ad allora `docker exec` via SSH è l'unica via. - **Controllare che un deploy sia atterrato**: `docker ps` sul server mostra l'immagine, e **il tag È lo SHA del commit** — si confronta con `git rev-parse --short HEAD`. Coolify ci mette ~2 minuti e cambia il suffisso del container a ogni giro: un controllo immediato fa credere a torto che il webhook sia rotto. - **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 | File | Scopo | |---|---| | src/proxy.ts | Middleware (Next 16 lo chiama `proxy`, non `middleware`) | | src/lib/client-gate.ts | Gate OTP — va chiamato in cima a ogni page sotto `/client/[token]/` | | src/lib/otp.ts, client-session.ts | Codici OTP e cookie di sessione 90gg | | src/lib/mailer.ts | Unico punto di invio email (Resend) | | src/lib/forecast-queries.ts | Forecast 12 mesi — rispetta stato e `end_date` dei retainer | | src/lib/taxonomy.ts | Pool tassonomie (Impostazioni) | | src/app/admin/projects/project-actions.ts | `importOfferIntoProject`, `setProjectOfferLifecycle`, piani pagamento | | 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/lib/audit/sources/ | Le 5 fonti del motore audit (nessun LLM) — `fetch` espone anche gli helper di rete condivisi | | src/db/migrations/ | 0011 (email/phone), 0012 (offer_type), 0015 (OTP), 0016 (ciclo di vita), 0017 (audit) | ## 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/` |