"Quanto e' costata questa fase" non era una domanda che si potesse fare:
time_entries aveva la sola project_id.
Migration 0018 (gia' applicata a prod): phase_id e task_id su time_entries,
piu' due indici, piu' projects.due_date che serve al blocco successivo. Solo
ADD COLUMN e CREATE INDEX.
Due scelte che vale la pena spiegare:
- ON DELETE SET NULL, non CASCADE. Cancellare un task NON deve cancellare le
ore lavorate su di esso: sono storico fatturabile. L'entry ricade a livello
progetto e il totale del progetto non cambia mai. Con CASCADE, ripulire una
fase avrebbe silenziosamente abbassato il fatturato tracciato. Verificato
sul DB di produzione dentro una transazione con ROLLBACK: cancellato il
task, l'entry sopravvive con task_id NULL, phase_id intatto e i secondi
invariati.
- Il timer su un task scrive ENTRAMBE le colonne. Cosi' il totale di una fase
e' un group-by diretto su phase_id, senza risalire dai task, e comprende
anche il tempo imputato alla fase ma a nessun task in particolare.
Resta un solo timer attivo in tutto l'hub. Da qui una conseguenza in UI: se
sta girando su un task, il timer del tab "Timer" NON si mostra acceso —
mostrarlo acceso farebbe credere che siano due cronometri diversi. Il tab lo
dice a parole e avvisa che avviarlo fermerebbe l'altro.
Le 8 entry esistenti restano valide con entrambe le colonne a NULL, cioe'
"tempo di progetto": e' esattamente cio' che sono.
PhasesTab passa ai token semantici mentre lo si tocca. Non e' zelo: ci si
infila dentro una TimerCell che i token li usa gia', e in dark mode un badge a
token dentro una card bg-white si vede. Un pezzo di DEBT-01 in meno.
Cade startTimerForClient, senza chiamanti da quando la lista progetti non ha
piu' il timer.
Build pulito. L'avvio/arresto dal browser non e' ancora stato provato: si
verifica in produzione, che e' l'unico posto dove esiste il DB.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sette tabelle additive per la milestone v2.5 "Audit": audits, audit_findings,
audit_optimizations, checklist_items, audit_checklist_results, audit_runs e
audit_visits. Nessun DROP, nessun TRUNCATE, nessuna colonna rimossa.
Tre scelte che vale la pena spiegare:
- Colonne scalari su audits, non un jsonb unico. A differenza di proposals non
c'e' snapshot da congelare: il copy fisso sta in moduli TS versionati e
l'editor mappa 1:1 sui campi. Tutti i campi di contenuto sono NULLABLE — e'
cio' che rende possibile "si salva sempre, anche a meta'".
- checklist_items.profili e' jsonb: 71 voci su 264 valgono per entrambi i
profili, una colonna singola costringerebbe a duplicarle.
- audit_checklist_results.esito ammette 'non_verificabile'. E' l'esito piu'
frequente misurato sullo spike (107 su 204) e serve a sapere quanto il motore
NON riesce a vedere: buttarlo via renderebbe impossibile misurare se le
rilevazioni Lighthouse stanno recuperando terreno.
Migration gia' applicata in produzione il 2026-08-18, dati esistenti intatti.
CLAUDE.md annota la deroga al vincolo LOCKED #5, limitata agli asset di audit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Un retainer, una volta assegnato, non si poteva fermare: project_offers
aveva solo start_date e il forecast sommava il canone a ogni mese
dell'orizzonte da li in poi, per sempre. Un cliente che disdiceva
continuava a gonfiare il forecast a 12 mesi e a vedersi l'abbonamento
attivo nel portale.
- migration 0016 (gia applicata a prod): project_offers.status
(attivo|sospeso|cessato, CHECK) + end_date. Additiva pura, default
'attivo' cosi le righe esistenti conservano il comportamento di prima
- forecast: i retainer si fermano a end_date, sospesi e cessati escono.
getOffersSoldBreakdown NON filtra per stato: e uno storico di vendita,
escludere le cessate riscriverebbe il passato
- offersAcceptedTotal esclude le cessate (default del piano pagamenti)
- admin: comandi Sospendi/Riattiva/Cessa + data fine nella tab Offerte,
solo per i ricorrenti. setProjectOfferLifecycle valida con Zod e filtra
anche per project_id, cosi un id arbitrario non tocca altri progetti
- portale: "Attivo dal", "fino al", badge In pausa, "Canone mensile"
invece di "Prezzo finale"; le cessate non arrivano al client
- fix: un retainer sospeso continuava a intestare i pagamenti "Totale
Pagamento Mensile" e a sovrascrivere l'importo
Igiene nello stesso giro:
- STATUS.md riscritto: era fermo al 22 giugno e diceva che node/docker non
sono disponibili sul server e che le migrazioni si applicano da locale
con uno script postgres.js — il contrario della procedura reale
- rimossi ChatSection/CommentList/CommentForm, senza importatori (308 righe)
- overrides postcss>=8.5.18 e sharp>=0.35.0: 3 CVE high transitive di Next
senza fix upstream. npm audit ora pulito, build verde
Verifica: forecast controllato sui dati veri in 5 scenari (baseline
invariata, end_date, sospeso, cessato, ripristino); portale verificato nei
4 stati; tab admin verificata con Playwright sul build di produzione
(i comandi non compaiono sulle una tantum). Dati di test ripuliti,
tabelle protette invariate 4/5/11/10.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Il portale /client/<slug> era protetto dal solo token in URL: chiunque
ricevesse o intercettasse il link entrava, per sempre, senza identificarsi.
Ora l'admin registra le email autorizzate per cliente e il cliente si
identifica con un codice usa-e-getta prima di vedere qualsiasi dato.
- Resend 6.18.1 + src/lib/mailer.ts (Result tipizzato, mai catch silenzioso)
- migration 0015 (gia applicata a prod): client_emails, otp_codes,
clients.sessions_valid_from. Additiva pura, conteggi verificati pre/post
- admin: sezione "Accessi al portale" in /admin/clients/[id] con whitelist
e revoca sessioni in blocco
- gate: codice 6 cifre CSPRNG, hash SHA-256 (mai il codice in chiaro),
TTL 15 min, max 5 tentativi, rate limit su entrambi gli endpoint,
risposta identica per email in whitelist e non (no enumeration)
- sessione: cookie HMAC per-cliente, 90 giorni, httpOnly/secure/lax
Il gate sta in cima alla page, NON nel layout: nell'App Router il segmento
page viene renderizzato in parallelo al layout, quindi gattare nel layout
nascondeva la dashboard a schermo ma lasciava fasi, task e pagamenti nel
payload RSC dell'HTML (46907 byte -> 17594 dopo il fix). Verificato.
Verifica: build OK, 9/9 test E2E in locale contro il DB di produzione.
NON DEPLOYARE prima di: RESEND_API_KEY+RESEND_FROM su Coolify e whitelist
popolata per i 3 clienti reali (oggi vuota) - altrimenti il gate li chiude
fuori dal loro portale. Checklist in .planning/STATE.md.
SEND-01/02 (invio preventivo via email) rinviati a v2.4 su richiesta.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nuova pagina admin /admin/conversazioni: vista WhatsApp-style (lista
conversazioni a sinistra, thread + risposta a destra) che aggrega i
messaggi di tutti i clienti dalla tabella comments, cross-cliente.
- Migration additiva 0014: clients.admin_last_read_at per tracciare
letto/non-letto (pallini + badge in sidebar). Applicata a prod.
- conversations-queries.ts: aggregazione entity_id → cliente
(general/phase/task/deliverable), getConversations /
getConversationThread / getUnreadConversationsCount.
- Risposte admin salvate come commento "general" (visibili anche nella
chat cliente e nel CommentsTab della scheda).
- Voce di menu + badge non-letti (AdminSidebar/AdminShell/layout).
- Token semantici per dual light/dark; refresh manuale (no polling).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Pagamenti: totale ereditato dalla somma accepted_total delle offerte
attive (con override) + selettore schema rate 1/2/3 step; nuova colonna
payments.percent per rescalare gli importi preservando label/stato
- Fasi & Task: recomputePhaseStatus auto-cascade (task -> fase) su
updateTaskStatus/addTask, fix UI stale, etichette Da iniziare/In corso/Completata
- Pagina pubblica: colori fasi (grigio/blu/#1A463C) + fasi collassabili
- Transcript del cliente visibili in tab Documenti admin e dashboard pubblica
- Timer: inserimento manuale (+30/+60, minuti liberi, data) + elimina entry
- CLAUDE.md: procedura Deploy & DB Access; push su main automatico
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Blocco A+B (milestone v2.3). Migration 0011 is additive (ADD COLUMN only):
clients.email, clients.phone, leads.archived.
- createClientCore() extracted from createClient and reused by conversion
- clients.email/phone added to create + edit forms and shown on client header
(never exposed on the public /client/[token] page)
- convertLeadToClient(): reuses core, carries over lead transcripts
(client_transcripts.client_id), links project.created_from_lead_id,
archives the lead while keeping its "won" status; idempotent
- "Converti in cliente" button on won leads; "Convertito → Apri cliente"
once converted (clientId resolved via created_from_lead_id)
- archived leads hidden from list/kanban; detail page fetches by id incl. archived
Migration must be applied to the prod DB BEFORE this is deployed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Turn the catalog into Notion/Airtable select properties:
- Tag + Pacchetto: multi-select with a SHARED pool — created values persist and
are selectable from a dropdown across all services (no more re-typing)
- Categoria + Fase: single-select chips with the same dropdown + create-on-the-fly
- Rename an option once -> propagates to every row (renameServiceOption)
- Deterministic colors per value (shared option-colors util)
- Quick-add row now sets name+description+categoria+fase+prezzo then Enter (active)
- Search broadened to name/categoria/fase/tag/pacchetto
Data model (additive): tag/pacchetto in polymorphic tags table (entity_type
services / services.pacchetto); categoria/fase as single-select columns on
services (new: services.fase). Pools derived from distinct values.
New: OptionSelect, OptionMultiSelect, option-colors. Removed tag-multi-select.
Migration 0007_add_services_fase.sql must be applied before runtime.
tsc + eslint + next build clean. CSV bulk import (OFFER-12) stays Phase 12.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add polymorphic tags table (entity_type/entity_id/name) with unique
index on (entity_type, entity_id, name) and lookup index on
(entity_type, entity_id) — pattern follows comments table (D-05/D-06/D-07)
- Add Tag/NewTag types and tagsRelations (no direct FK, query-time join)
- Hand-write src/db/migrations/0006_add_tags_table.sql following the
established project convention (drizzle-kit generate is unusable here —
meta/_journal.json snapshots out of sync since 0001, pre-existing since
Phase 8) and add journal entry for 0006_add_tags_table
- Add scripts/push-11-tags-migration.ts (idempotent CREATE TABLE/INDEX IF
NOT EXISTS) with production deploy note for manual SSH+docker exec apply
Stage C of staged Phase 10 redo. Restores dangling phase10-wip work:
- schema.ts: leads expansion + activities/reminders tables + relations
- lead-service.ts / lead-validators.ts service layer
- /admin/leads list + detail + actions, LeadTable/LeadDetail/LeadForm
- LogActivityModal, SendQuoteModal, FollowUpWidget on dashboard
- migration 0005 (applied to prod DB in Stage B, along with
previously-missing 0001/0003/0004 — root cause of all post-deploy
500s: prod DB had no migrations applied since 0000)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Add primaryKey to drizzle-orm/pg-core import
- Define offer_macros, offer_micros, offer_services, offer_micro_services, project_offers
- Add Drizzle relations for all five new tables (+ projectOffers in projectsRelations)
- Export TypeScript infer types for all new tables
- Remove .notNull() from service_id to allow free-form items without catalog ref
- Add custom_label: text("custom_label") for free-form item label storage
- TypeScript compiles with zero errors (QuoteItem.service_id now string | null)
Schema:
- clients.archived boolean (default false)
- time_entries table (client_id, started_at, ended_at, duration_seconds)
Client management:
- /admin/clients/[id]/edit — form pre-compilato con nome, brand, brief
- ClientActions: Modifica / Archivia / Elimina con doppia conferma
- setClientArchived: toggle archiviazione senza perdere dati
- deleteClient: elimina con cascade, redirect a /admin
- Admin list: toggle "Mostra archiviati" via ?archived=1, righe archiviate opache
Time tracker:
- startTimer: crea sessione, ferma automaticamente quella precedente
- stopTimer: chiude sessione, calcola duration_seconds
- TimerCell: ▶/⏹ per ogni cliente, contatore live in secondi, totale cumulativo
- Una sola sessione attiva alla volta su tutta la lista
Analytics:
- Sezione "Fatturato" (invariata) + sezione "Tempo tracciato" separata
- Ore totali per anno + barre orizzontali per cliente
- getTotalTrackedHours, getTimeByClient queries
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- clients: token as separate text field (notNull, unique, nanoid) — never PK
- accepted_total denormalized on clients — client API never touches quote_items
- deliverables.approved_at immutable timestamp (TIMESTAMPTZ) — audit trail
- payments: label (Acconto 50% / Saldo 50%), status (da_saldare/inviata/saldato)
- comments: polymorphic entity_type+entity_id pattern
- service_catalog + quote_items: admin-only, never exposed to client API
- Full relations defined for all FK chains
- TypeScript types exported: Client, Phase, Task, Deliverable, etc.
- ID strategy: text + nanoid() via $defaultFn (cryptographically secure, URL-safe)