0d7186ba03
Il pattern che vale la pena rileggere: la chiave del canale è derivata, non è una colonna, e sta in un modulo condiviso perché le due sponde devono concordare al carattere. Più il perché del letto/non-letto asimmetrico (tabella per il cliente, timestamp per l'admin). Chiude anche il caveat in STATUS.md sulla risposta admin che finiva sempre sul thread generale. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
384 lines
24 KiB
Markdown
384 lines
24 KiB
Markdown
# 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.~~ **Chiuso dalla chat a canali**
|
||
(sotto): la risposta va sull'entità del canale attivo.
|
||
|
||
### Chat a canali (2026-08-21, in produzione)
|
||
|
||
Il portale aveva **una** conversazione con un selettore di fase in un dropdown. Due
|
||
problemi veri: il cliente non aveva modo di vedere *dove* c'era del non letto, e una
|
||
risposta dell'admin poteva atterrare su un'entità diversa da quella della domanda.
|
||
|
||
Ora i messaggi si organizzano in **canali** — "Generale" più uno per fase. Il canale non
|
||
è una colonna: `comments` resta polimorfica (`entity_type` + `entity_id`) e la chiave si
|
||
*deriva*. La derivazione sta in un posto solo, `src/lib/chat-channels.ts`, funzioni pure
|
||
importate da entrambe le sponde — **perché se cliente e admin la calcolassero ognuno per
|
||
conto suo, una risposta finirebbe in un tab diverso da quello della domanda, che è
|
||
esattamente il bug che questo giro chiude.** Task e deliverable non sono più scrivibili,
|
||
ma lo storico non resta orfano: rientra nel canale della fase proprietaria, conservando
|
||
il nome dell'entità come badge sul messaggio.
|
||
|
||
**Il letto/non-letto è asimmetrico fra le due sponde, di proposito.** Il cliente ha una
|
||
tabella nuova, `client_channel_reads`, una riga per `(client_id, channel_key)`: un
|
||
singolo `client_last_read_at` segnerebbe letti *tutti* i canali all'apertura della chat,
|
||
e il pallino sul singolo tab — l'unica cosa che dice al cliente dove guardare — perderebbe
|
||
senso. L'admin invece resta su `clients.admin_last_read_at` (già esistente dalla `0014`):
|
||
lì la lettura è per conversazione e il pallino per canale si deriva da quel timestamp,
|
||
quindi nessuna tabella nuova. Il thread admin porta `adminLastReadAt` come **snapshot**
|
||
congelato al caricamento: senza, l'auto-mark-read all'apertura cancellerebbe i pallini
|
||
sotto gli occhi di chi sta leggendo.
|
||
|
||
Migration `0021`, additiva, applicata a prod **prima** del push e tabella riletta a
|
||
conferma. Dentro anche un indice su `comments (entity_id, created_at)`: la tabella non ne
|
||
ha mai avuto uno, **nemmeno su `entity_id`**, dal giorno 0 — e il feed si legge esattamente
|
||
così. Deploy atterrato, immagine taggata `ac74a81` = HEAD.
|
||
|
||
⚠️ **Buildata, non provata a mano** — stesso motivo di sopra (`.env.local` scaduto).
|
||
**Da fare:** aprire il portale di un cliente, scrivere su una fase, rispondere da
|
||
`/admin/conversazioni` e verificare che la risposta torni **in quel tab**.
|
||
|
||
### 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/<id>` → "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 `<OtpGate/>` 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/<uuid>/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/` |
|