From 23cb057f0b42f614e278a8b40c79f5da9459a631 Mon Sep 17 00:00:00 2001 From: Simone Cavalli Date: Thu, 11 Jun 2026 06:17:29 +0200 Subject: [PATCH] =?UTF-8?q?docs(07):=20phase=207=20planning=20complete=20?= =?UTF-8?q?=E2=80=94=202=20plans=20(schema=20migration=20+=20admin=20crud)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wave 1: 07-01-PLAN.md — Unified services table (audit trail, backfill, validation) Wave 2: 07-02-PLAN.md — Admin catalog CRUD rewire (preserve historical references) Ready for execution: /gsd-execute-phase 7 Co-Authored-By: Claude Haiku 4.5 --- .planning/milestones/v1.0-REQUIREMENTS.md | 110 ++ .planning/milestones/v1.0-ROADMAP.md | 167 +++ .../01-01-PLAN.md | 273 ++++ .../01-01-SUMMARY.md | 190 +++ .../01-02-PLAN.md | 369 ++++++ .../01-02-SUMMARY.md | 144 ++ .../01-03-PLAN.md | 569 ++++++++ .../01-03-SUMMARY.md | 149 +++ .../01-04-PLAN.md | 864 ++++++++++++ .../01-04-SUMMARY.md | 167 +++ .../01-05-PLAN.md | 567 ++++++++ .../01-05-SUMMARY.md | 51 + .../01-CONTEXT.md | 126 ++ .../01-DISCUSSION-LOG.md | 128 ++ .../SKELETON.md | 302 +++++ .../02-01-PLAN.md | 481 +++++++ .../02-01-SUMMARY.md | 92 ++ .../02-02-PLAN.md | 561 ++++++++ .../02-02-SUMMARY.md | 144 ++ .../02-03-PLAN.md | 825 ++++++++++++ .../02-03-SUMMARY.md | 117 ++ .../02-04-PLAN.md | 661 ++++++++++ .../02-04-SUMMARY.md | 161 +++ .../02-CONTEXT.md | 126 ++ .../02-RESEARCH.md | 1159 +++++++++++++++++ .../02-VERIFICATION.md | 240 ++++ .../03-01-PLAN.md | 216 +++ .../03-01-SUMMARY.md | 125 ++ .../03-02-PLAN.md | 647 +++++++++ .../03-02-SUMMARY.md | 143 ++ .../03-03-PLAN.md | 725 +++++++++++ .../03-03-SUMMARY.md | 111 ++ .../03-04-PLAN.md | 216 +++ .../03-04-SUMMARY.md | 104 ++ .../03-CONTEXT.md | 87 ++ .../03-PATTERNS.md | 558 ++++++++ .../03-RESEARCH.md | 873 +++++++++++++ .../03-UI-SPEC.md | 90 ++ .../03-VERIFICATION.md | 173 +++ .../04-progetti-multi-project/04-00-PLAN.md | 371 ++++++ .../04-progetti-multi-project/04-01-PLAN.md | 761 +++++++++++ .../04-progetti-multi-project/04-02-PLAN.md | 709 ++++++++++ .../04-02-SUMMARY.md | 50 + .../04-progetti-multi-project/04-03-PLAN.md | 672 ++++++++++ .../04-03-SUMMARY.md | 52 + .../04-progetti-multi-project/04-04-PLAN.md | 830 ++++++++++++ .../04-04-SUMMARY.md | 67 + .../04-progetti-multi-project/04-CONTEXT.md | 146 +++ .../04-progetti-multi-project/04-PATTERNS.md | 1038 +++++++++++++++ .../04-progetti-multi-project/04-RESEARCH.md | 921 +++++++++++++ .../v1.0-phases/05-offer-system/05-01-PLAN.md | 290 +++++ .../05-offer-system/05-01-SUMMARY.md | 102 ++ .../v1.0-phases/05-offer-system/05-02-PLAN.md | 713 ++++++++++ .../05-offer-system/05-02-SUMMARY.md | 111 ++ .../v1.0-phases/05-offer-system/05-03-PLAN.md | 696 ++++++++++ .../05-offer-system/05-03-SUMMARY.md | 114 ++ .../v1.0-phases/05-offer-system/05-04-PLAN.md | 530 ++++++++ .../05-offer-system/05-04-SUMMARY.md | 124 ++ .../05-offer-system/05-HUMAN-UAT.md | 40 + .../05-offer-system/05-RESEARCH.md | 624 +++++++++ .../05-offer-system/05-VERIFICATION.md | 157 +++ .../v1.0-phases/06-ux-overhaul/06-01-PLAN.md | 236 ++++ .../06-ux-overhaul/06-01-SUMMARY.md | 76 ++ .../v1.0-phases/06-ux-overhaul/06-02-PLAN.md | 346 +++++ .../06-ux-overhaul/06-02-SUMMARY.md | 94 ++ 65 files changed, 22681 insertions(+) create mode 100644 .planning/milestones/v1.0-REQUIREMENTS.md create mode 100644 .planning/milestones/v1.0-ROADMAP.md create mode 100644 .planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-01-PLAN.md create mode 100644 .planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-01-SUMMARY.md create mode 100644 .planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-02-PLAN.md create mode 100644 .planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-02-SUMMARY.md create mode 100644 .planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-03-PLAN.md create mode 100644 .planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-03-SUMMARY.md create mode 100644 .planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-04-PLAN.md create mode 100644 .planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-04-SUMMARY.md create mode 100644 .planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-05-PLAN.md create mode 100644 .planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-05-SUMMARY.md create mode 100644 .planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-CONTEXT.md create mode 100644 .planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-DISCUSSION-LOG.md create mode 100644 .planning/milestones/v1.0-phases/01-foundation-client-dashboard/SKELETON.md create mode 100644 .planning/milestones/v1.0-phases/02-admin-area-interactive-features/02-01-PLAN.md create mode 100644 .planning/milestones/v1.0-phases/02-admin-area-interactive-features/02-01-SUMMARY.md create mode 100644 .planning/milestones/v1.0-phases/02-admin-area-interactive-features/02-02-PLAN.md create mode 100644 .planning/milestones/v1.0-phases/02-admin-area-interactive-features/02-02-SUMMARY.md create mode 100644 .planning/milestones/v1.0-phases/02-admin-area-interactive-features/02-03-PLAN.md create mode 100644 .planning/milestones/v1.0-phases/02-admin-area-interactive-features/02-03-SUMMARY.md create mode 100644 .planning/milestones/v1.0-phases/02-admin-area-interactive-features/02-04-PLAN.md create mode 100644 .planning/milestones/v1.0-phases/02-admin-area-interactive-features/02-04-SUMMARY.md create mode 100644 .planning/milestones/v1.0-phases/02-admin-area-interactive-features/02-CONTEXT.md create mode 100644 .planning/milestones/v1.0-phases/02-admin-area-interactive-features/02-RESEARCH.md create mode 100644 .planning/milestones/v1.0-phases/02-admin-area-interactive-features/02-VERIFICATION.md create mode 100644 .planning/milestones/v1.0-phases/03-service-catalog-quote-builder/03-01-PLAN.md create mode 100644 .planning/milestones/v1.0-phases/03-service-catalog-quote-builder/03-01-SUMMARY.md create mode 100644 .planning/milestones/v1.0-phases/03-service-catalog-quote-builder/03-02-PLAN.md create mode 100644 .planning/milestones/v1.0-phases/03-service-catalog-quote-builder/03-02-SUMMARY.md create mode 100644 .planning/milestones/v1.0-phases/03-service-catalog-quote-builder/03-03-PLAN.md create mode 100644 .planning/milestones/v1.0-phases/03-service-catalog-quote-builder/03-03-SUMMARY.md create mode 100644 .planning/milestones/v1.0-phases/03-service-catalog-quote-builder/03-04-PLAN.md create mode 100644 .planning/milestones/v1.0-phases/03-service-catalog-quote-builder/03-04-SUMMARY.md create mode 100644 .planning/milestones/v1.0-phases/03-service-catalog-quote-builder/03-CONTEXT.md create mode 100644 .planning/milestones/v1.0-phases/03-service-catalog-quote-builder/03-PATTERNS.md create mode 100644 .planning/milestones/v1.0-phases/03-service-catalog-quote-builder/03-RESEARCH.md create mode 100644 .planning/milestones/v1.0-phases/03-service-catalog-quote-builder/03-UI-SPEC.md create mode 100644 .planning/milestones/v1.0-phases/03-service-catalog-quote-builder/03-VERIFICATION.md create mode 100644 .planning/milestones/v1.0-phases/04-progetti-multi-project/04-00-PLAN.md create mode 100644 .planning/milestones/v1.0-phases/04-progetti-multi-project/04-01-PLAN.md create mode 100644 .planning/milestones/v1.0-phases/04-progetti-multi-project/04-02-PLAN.md create mode 100644 .planning/milestones/v1.0-phases/04-progetti-multi-project/04-02-SUMMARY.md create mode 100644 .planning/milestones/v1.0-phases/04-progetti-multi-project/04-03-PLAN.md create mode 100644 .planning/milestones/v1.0-phases/04-progetti-multi-project/04-03-SUMMARY.md create mode 100644 .planning/milestones/v1.0-phases/04-progetti-multi-project/04-04-PLAN.md create mode 100644 .planning/milestones/v1.0-phases/04-progetti-multi-project/04-04-SUMMARY.md create mode 100644 .planning/milestones/v1.0-phases/04-progetti-multi-project/04-CONTEXT.md create mode 100644 .planning/milestones/v1.0-phases/04-progetti-multi-project/04-PATTERNS.md create mode 100644 .planning/milestones/v1.0-phases/04-progetti-multi-project/04-RESEARCH.md create mode 100644 .planning/milestones/v1.0-phases/05-offer-system/05-01-PLAN.md create mode 100644 .planning/milestones/v1.0-phases/05-offer-system/05-01-SUMMARY.md create mode 100644 .planning/milestones/v1.0-phases/05-offer-system/05-02-PLAN.md create mode 100644 .planning/milestones/v1.0-phases/05-offer-system/05-02-SUMMARY.md create mode 100644 .planning/milestones/v1.0-phases/05-offer-system/05-03-PLAN.md create mode 100644 .planning/milestones/v1.0-phases/05-offer-system/05-03-SUMMARY.md create mode 100644 .planning/milestones/v1.0-phases/05-offer-system/05-04-PLAN.md create mode 100644 .planning/milestones/v1.0-phases/05-offer-system/05-04-SUMMARY.md create mode 100644 .planning/milestones/v1.0-phases/05-offer-system/05-HUMAN-UAT.md create mode 100644 .planning/milestones/v1.0-phases/05-offer-system/05-RESEARCH.md create mode 100644 .planning/milestones/v1.0-phases/05-offer-system/05-VERIFICATION.md create mode 100644 .planning/milestones/v1.0-phases/06-ux-overhaul/06-01-PLAN.md create mode 100644 .planning/milestones/v1.0-phases/06-ux-overhaul/06-01-SUMMARY.md create mode 100644 .planning/milestones/v1.0-phases/06-ux-overhaul/06-02-PLAN.md create mode 100644 .planning/milestones/v1.0-phases/06-ux-overhaul/06-02-SUMMARY.md diff --git a/.planning/milestones/v1.0-REQUIREMENTS.md b/.planning/milestones/v1.0-REQUIREMENTS.md new file mode 100644 index 0000000..f67e1c5 --- /dev/null +++ b/.planning/milestones/v1.0-REQUIREMENTS.md @@ -0,0 +1,110 @@ +# Requirements Archive: v1.0 Client Portal & Offer System + +**Archived:** 2026-06-10 +**Status:** SHIPPED + +For current requirements, see `.planning/REQUIREMENTS.md`. + +--- + +# Requirements — ClientHub + +## Dashboard Cliente (v1) + +| ID | Requirement | Status | +|----|-------------|--------| +| DASH-01 | Ogni cliente ha un URL segreto univoco (nessun login richiesto) | Pending | +| DASH-02 | La dashboard mostra nome cliente, nome brand, brief del progetto e stato attuale | Pending | +| DASH-03 | Il piano è strutturato per fasi con milestone e task all'interno di ogni fase | Pending | +| DASH-04 | I task hanno stato visibile (da fare / in corso / fatto) | Pending | +| DASH-05 | Il cliente può approvare i deliverable dalla sua area | Pending | +| DASH-06 | Il cliente può lasciare commenti su task e deliverable | Pending | +| DASH-07 | Il cliente vede solo il totale del preventivo accettato (non i prezzi dei singoli servizi) | Pending | +| DASH-08 | Il cliente vede lo stato dei pagamenti: acconto 50% (da saldare / inviata / saldato) e saldo 50% (da saldare / inviata / saldato) | Pending | +| DASH-09 | Link a documenti e file (Google Drive, PDF, deliverable) | Pending | +| DASH-10 | Storico note e decisioni prese nel tempo | Pending | + +## Area Amministratore (v1) + +| ID | Requirement | Status | +|----|-------------|--------| +| ADMIN-01 | Vista di tutti i clienti con stato sintetico | Pending | +| ADMIN-02 | Gestione completa di ogni cliente: fasi, task, documenti, pagamenti | Pending | +| ADMIN-03 | Preventivo completo con dettaglio servizi (non visibile al cliente) | Pending | + +## Catalogo Servizi (v1) + +| ID | Requirement | Status | +|----|-------------|--------| +| CAT-01 | File/database dei servizi con prezzi e cosa è incluso | Pending | +| CAT-02 | Usato come base per la generazione assistita dei preventivi | Pending | + +## Progetti Multi-Project (Phase 4) + +| ID | Requirement | Status | +|----|-------------|--------| +| PROJ-01 | Ogni cliente può avere N progetti; ogni progetto ha workspace indipendente (fasi, pagamenti, preventivo, timer) | Pending | +| PROJ-02 | La dashboard cliente mostra tabs per 2+ progetti; con 1 progetto mostra direttamente il workspace senza selettore | Pending | +| PROJ-03 | La pagina /admin/projects elenca tutti i progetti con €/h reale e timer; /admin/projects/[id] è il workspace progetto | Pending | +| PROJ-04 | Il link cliente supporta slug personalizzabile (/c/mario-rossi) con fallback al token esistente | Pending | +| PROJ-05 | Analytics profittabilità per progetto: ore lavorate, accepted_total, €/h reale vs target_hourly_rate globale | Pending | + +## Offer System (Phase 5) + +| ID | Requirement | Status | +|----|-------------|--------| +| OFFER-01 | Admin può creare macro-offerte (es. Entry Offer, Signature Offer, Retainer) con nome interno, nome pubblico e promessa di trasformazione macro | Pending | +| OFFER-02 | Ogni macro-offerta ha micro-offerte figlie (es. Entry A/B/C) con nome interno, nome pubblico, promessa di trasformazione micro e durata in mesi | Pending | +| OFFER-03 | Admin può creare servizi con nome, prezzo e descrizione della trasformazione; ogni servizio è assegnabile a più micro-offerte via multi-select (many-to-many) | Pending | +| OFFER-04 | Admin può assegnare una o più micro-offerte a un progetto; admin vede le offerte attive per ogni progetto e cliente | Pending | +| OFFER-05 | La dashboard cliente mostra le offerte attive con nome pubblico, prezzo cumulativo dei servizi inclusi e prezzo finale accettato in evidenza; se ha più offerte le vede tutte | Pending | +| OFFER-06 | Admin ha una vista revenue forecast dei 12 mesi successivi basata su offerte attive, durata e accepted_total; breakdown mensile con totale fatturato per mese | Pending | + +## Flusso Claude (v2 — deferred to Phase 6) + +| ID | Requirement | Status | +|----|-------------|--------| +| CLAUDE-01 | Onboarding guidato step-by-step via chat per aggiungere un nuovo cliente | Deferred | +| CLAUDE-02 | Generazione del piano a fasi basato sul brief | Deferred | +| CLAUDE-03 | Generazione preventivo assistita (Claude suggerisce, tu approvi prima di finalizzare) | Deferred | + +## Out of Scope + +- Fatturazione e invio fatture (solo stato pagamenti) +- App mobile nativa (solo web responsive) +- Multi-utente con team (solo admin singolo) +- Prezzi singoli visibili al cliente (solo totale accettato) + +## Traceability + +| Requirement | Phase | Status | +|-------------|-------|--------| +| DASH-01 | Phase 1 | Pending | +| DASH-02 | Phase 1 | Pending | +| DASH-03 | Phase 1 | Pending | +| DASH-04 | Phase 1 | Pending | +| DASH-07 | Phase 1 | Pending | +| DASH-08 | Phase 1 | Pending | +| DASH-09 | Phase 1 | Pending | +| DASH-10 | Phase 1 | Pending | +| ADMIN-01 | Phase 2 | Pending | +| ADMIN-02 | Phase 2 | Pending | +| DASH-05 | Phase 2 | Pending | +| DASH-06 | Phase 2 | Pending | +| CAT-01 | Phase 3 | Pending | +| CAT-02 | Phase 3 | Pending | +| ADMIN-03 | Phase 3 | Pending | +| PROJ-01 | Phase 4 | Pending | +| PROJ-02 | Phase 4 | Pending | +| PROJ-03 | Phase 4 | Pending | +| PROJ-04 | Phase 4 | Pending | +| PROJ-05 | Phase 4 | Pending | +| OFFER-01 | Phase 5 | Pending | +| OFFER-02 | Phase 5 | Pending | +| OFFER-03 | Phase 5 | Pending | +| OFFER-04 | Phase 5 | Pending | +| OFFER-05 | Phase 5 | Pending | +| OFFER-06 | Phase 5 | Pending | +| CLAUDE-01 | Phase 6 (v2) | Deferred | +| CLAUDE-02 | Phase 6 (v2) | Deferred | +| CLAUDE-03 | Phase 6 (v2) | Deferred | \ No newline at end of file diff --git a/.planning/milestones/v1.0-ROADMAP.md b/.planning/milestones/v1.0-ROADMAP.md new file mode 100644 index 0000000..3344e23 --- /dev/null +++ b/.planning/milestones/v1.0-ROADMAP.md @@ -0,0 +1,167 @@ +# Roadmap: ClientHub + +## Overview + +ClientHub si costruisce dall'esterno verso l'interno: prima la dashboard che un cliente reale può aprire su mobile, poi l'area admin per creare e gestire i dati, poi il catalogo servizi e il preventivo interno. Il flusso Claude AI è v2 e dipende da CRUD stabile e catalogo completo. Ogni fase consegna una capacità end-to-end verificabile. + +## Phases + +**Phase Numbering:** +- Integer phases (1, 2, 3): Planned milestone work +- Decimal phases (2.1, 2.2): Urgent insertions (marked with INSERTED) + +Decimal phases appear between their surrounding integers in numeric order. + +- [x] **Phase 1: Foundation & Client Dashboard** - DB schema, token API, dashboard read-only per il cliente con link segreto condivisibile +- [ ] **Phase 2: Admin Area & Interactive Features** - Auth admin, CRUD completo clienti/fasi/task/deliverable/pagamenti, approvazioni e commenti +- [ ] **Phase 3: Service Catalog & Quote Builder** - Catalogo servizi riutilizzabile e costruttore preventivi (admin-only, cliente vede solo il totale) +- [ ] **Phase 4: Progetti — Multi-Project per Cliente** - Modello dati multi-progetto per cliente; dashboard con tabs; /admin/projects; slug link; analytics €/h +- [x] **Phase 5: Offer System** - Catalogo macro/micro offerte con servizi assegnabili, assegnazione offerte a progetti, dashboard cliente con offerte attive e revenue forecast 12 mesi per l'admin +- [ ] **Phase 6: Claude AI Onboarding (v2)** - Flusso guidato step-by-step per onboarding cliente e generazione assistita del piano/preventivo + +## Phase Details + +### Phase 1: Foundation & Client Dashboard +**Goal**: Un cliente reale può aprire il suo link segreto su mobile o desktop e vedere lo stato del suo progetto, senza login +**Mode:** mvp +**Depends on**: Nothing (first phase) +**Requirements**: DASH-01, DASH-02, DASH-03, DASH-04, DASH-07, DASH-08, DASH-09, DASH-10 +**Success Criteria** (what must be TRUE): + 1. Aprendo `/c/[token]` su mobile, il cliente vede nome cliente, brand, brief e fase corrente senza alcun login + 2. Le fasi del progetto sono visibili con i task annidati e il loro stato (da fare / in corso / fatto) + 3. Il cliente vede il totale preventivo accettato e lo stato dei due pagamenti (acconto 50% e saldo 50%), mai i prezzi singoli + 4. I link a documenti esterni (Google Drive, PDF) sono cliccabili dalla dashboard + 5. Il log decisioni/note è visibile nella dashboard del cliente +**Plans**: 5 plans +**Plan list**: + - [x] 01-01-PLAN.md — Walking Skeleton: Next.js bootstrap + DB connection + - [x] 01-02-PLAN.md — Database schema (11 tables) + Drizzle + drizzle-kit push + - [x] 01-03-PLAN.md — Middleware token validation + ClientView type + data fetching + - [x] 01-04-PLAN.md — Client dashboard UI (header, progress, phases, payments, documents, notes) + - [x] 01-05-PLAN.md — Seed script + DNS CNAME configuration +**UI hint**: yes +**Status**: ✅ Complete (Phase 1 execution required) + +### Phase 2: Admin Area & Interactive Features +**Goal**: L'admin può creare e gestire clienti, fasi, task, deliverable e pagamenti; il cliente può commentare e approvare i deliverable +**Mode:** mvp +**Depends on**: Phase 1 +**Requirements**: ADMIN-01, ADMIN-02, DASH-05, DASH-06 +**Success Criteria** (what must be TRUE): + 1. L'admin accede a `/admin` con credenziale sicura e vede la lista di tutti i clienti con stato sintetico e badge pagamenti + 2. L'admin può creare un nuovo cliente (con generazione automatica del link segreto), aggiungere fasi, task, documenti e aggiornare lo stato dei pagamenti + 3. Il cliente può approvare un deliverable dalla sua dashboard; l'approvazione persiste con timestamp immutabile e l'admin la vede + 4. Il cliente può lasciare un commento su un task o deliverable e l'admin vede i commenti nella workspace admin +**Plans**: 4 plans +**Plan list**: + - [x] 02-01-PLAN.md — Auth.js v4 setup + middleware admin guard (/admin/* session check) + - [x] 02-02-PLAN.md — Admin client list (/admin) + create client form + two payment stubs + - [x] 02-03-PLAN.md — Admin client workspace: tabs for phases/tasks, payments, documents, comments + - [x] 02-04-PLAN.md — Client interactions: deliverable approval + inline comments API + dashboard wiring +**UI hint**: yes +**Status**: Planned — ready for execution + +### Phase 3: Service Catalog & Quote Builder +**Goal**: L'admin può costruire un catalogo servizi riutilizzabile e comporre preventivi da esso; il cliente vede solo il totale accettato +**Mode:** mvp +**Depends on**: Phase 2 +**Requirements**: CAT-01, CAT-02, ADMIN-03 +**Success Criteria** (what must be TRUE): + 1. L'admin può aggiungere, modificare e disattivare voci nel catalogo servizi (nome, descrizione, prezzo unitario) + 2. L'admin può comporre un preventivo per un cliente selezionando voci dal catalogo; il sistema calcola il totale + 3. Dopo la finalizzazione del preventivo, `accepted_total` è scritto sulla riga cliente e la dashboard del cliente mostra il totale corretto; i `quote_items` non sono mai esposti al cliente +**Plans**: 4 plans +**Plan list**: + - [x] 03-01-PLAN.md — Schema changes (service_id nullable, custom_label) + drizzle-kit push [BLOCKING] + - [x] 03-02-PLAN.md — Service Catalog: /admin/catalog page + CRUD actions + ServiceTable + NavBar link + - [x] 03-03-PLAN.md — Quote Builder: QuoteTab + quote-actions + client detail page wiring + - [x] 03-04-PLAN.md — E2E verification: catalog CRUD, quote round-trip, accepted_total, security check +**UI hint**: yes +**Status**: Planned — ready for execution + +### Phase 4: Progetti — Multi-Project per Cliente +**Goal**: Ogni cliente può avere N progetti indipendenti; l'admin gestisce i progetti separatamente; la dashboard cliente mostra tabs per progetti multipli; analytics di profittabilità per progetto +**Mode:** mvp +**Depends on**: Phase 3 +**Requirements**: PROJ-01, PROJ-02, PROJ-03, PROJ-04, PROJ-05 +**Success Criteria** (what must be TRUE): + 1. L'admin può creare più progetti per un cliente; ogni progetto ha il proprio workspace (fasi, pagamenti, preventivo, timer) accessibile da /admin/projects/[id] + 2. La dashboard cliente mostra tabs per 2+ progetti; con 1 solo progetto mostra direttamente il workspace senza selettore + 3. La pagina /admin/projects elenca tutti i progetti con €/h calcolato e bottone timer play/stop + 4. Il link cliente supporta slug personalizzato (/c/mario-rossi) con fallback al token; slug impostabile da /admin/clients/[id]/edit + 5. Il tab Timer di ogni progetto mostra analytics profittabilità: ore lavorate, accepted_total, €/h reale vs target_hourly_rate globale +**Plans**: 5 plans +**Plan list**: + - [ ] 04-00-PLAN.md — Infra: Postgres su Coolify + /c/ → /client/ rename + Dockerfile + hub.iamcavalli.net [RUN FIRST] + - [ ] 04-01-PLAN.md — Schema migration (projects, slug, settings, FK migration) + drizzle-kit push + query layer + - [ ] 04-02-PLAN.md — Admin projects list (/admin/projects) + ProjectRow + client detail project cards + - [ ] 04-03-PLAN.md — Admin project workspace (/admin/projects/[id]) + TimerTab + ProfitabilityCard + /admin/impostazioni + - [ ] 04-04-PLAN.md — Slug resolution middleware + client dashboard multi-project + slug edit +**UI hint**: yes +**Status**: Planning complete + +### Phase 5: Offer System + +**Goal**: L'admin può gestire un catalogo di macro/micro offerte con servizi assegnabili; le offerte vengono assegnate ai progetti; il cliente vede le sue offerte attive; l'admin ha un revenue forecast 12 mesi +**Mode:** mvp +**Depends on**: Phase 4 +**Requirements**: OFFER-01, OFFER-02, OFFER-03, OFFER-04, OFFER-05, OFFER-06 +**Success Criteria** (what must be TRUE): + + 1. L'admin può creare macro-offerte (es. Entry Offer) con micro-offerte figlie (es. Entry A/B/C) — nome interno, nome pubblico, promessa di trasformazione, durata in mesi + 2. L'admin può creare servizi con nome, prezzo e descrizione trasformazione; ogni servizio è assegnabile a più micro-offerte via multi-select + 3. L'admin può assegnare una micro-offerta a un progetto; la lista clienti/progetti mostra le offerte attive + 4. Il cliente vede nella dashboard le sue offerte attive (nome pubblico), il prezzo cumulativo dei servizi e il prezzo finale accettato in evidenza + 5. L'admin ha una pagina revenue forecast con breakdown mensile per i 12 mesi successivi basato su offerte attive e durate +**Plans**: 4 plans +**Plan list**: + - [x] 05-01-PLAN.md — Schema migration: 5 new offer tables (offer_macros, offer_micros, offer_services, offer_micro_services, project_offers) + drizzle-kit push [BLOCKING] + - [x] 05-02-PLAN.md — Offer catalog admin CRUD: /admin/offers page + offer-queries.ts + actions.ts + ServiceCheckboxList + NavBar update + - [x] 05-03-PLAN.md — Project offer assignment: OffersTab in /admin/projects/[id] + admin-queries extension + project-actions extension + - [x] 05-04-PLAN.md — Client dashboard OffersSection + /admin/forecast revenue forecast page (12 months) +**UI hint**: yes +**Status**: ✅ Complete (2026-05-30) + +### Phase 6: UX Overhaul — Sidebar + Dashboard +**Goal**: L'admin apre l'app e vede subito una dashboard con la situazione aziendale; la navigazione è una sidebar persistente a sinistra invece dell'header +**Mode:** mvp +**Depends on**: Phase 5 +**Requirements**: UX-01, UX-02, UX-03 +**Success Criteria** (what must be TRUE): + 1. Tutte le pagine admin usano un layout con sidebar sinistra persistente — nessun header nav + 2. `/admin` mostra una dashboard con: clienti attivi + revenue totale, progetti in corso, pagamenti in sospeso, attività recente + 3. La lista clienti è accessibile da `/admin/clients`; i link esistenti continuano a funzionare +**Plans**: 2 plans +**Plan list**: + - [ ] 06-01-PLAN.md — Sidebar layout: AdminSidebar component + AppShell + move client list to /admin/clients + - [ ] 06-02-PLAN.md — Dashboard page: KPI cards (revenue, clienti, progetti, pagamenti) + activity feed +**UI hint**: yes +**Status**: Planning complete + +### Phase 7: Claude AI Onboarding (v2) +**Goal**: L'admin può usare un flusso chat guidato per onboardare un nuovo cliente e generare un piano a fasi e un preventivo assistito da Claude +**Mode:** mvp +**Depends on**: Phase 6 +**Requirements**: CLAUDE-01, CLAUDE-02, CLAUDE-03 +**Success Criteria** (what must be TRUE): + 1. L'admin avvia il flusso Claude inserendo il brief del cliente; Claude guida step-by-step la raccolta delle informazioni necessarie + 2. Al termine del flusso, Claude propone un piano strutturato per fasi che l'admin può accettare o modificare prima di salvarlo + 3. Claude suggerisce un preventivo basato sul catalogo servizi; l'admin approva o modifica le voci prima della finalizzazione +**Plans**: TBD +**UI hint**: yes +**Status**: Pending planning (v2 — may defer indefinitely) + +## Progress + +**Execution Order:** +Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 + +| Phase | Plans | Status | Completed | +|-------|-------|--------|-----------| +| 1. Foundation & Client Dashboard | 5/5 | ✅ Done | 2026-05-14 | +| 2. Admin Area & Interactive Features | 4/4 | ✅ Done | 2026-05-15 | +| 3. Service Catalog & Quote Builder | 4/4 | ✅ Done | 2026-05-19 | +| 4. Progetti — Multi-Project per Cliente | 5/5 | ✅ Done | 2026-05-23 | +| 5. Offer System | 4/4 | ✅ Done | 2026-05-30 | +| 6. UX Overhaul — Sidebar + Dashboard | 0/2 | Pending | - | +| 7. Claude AI Onboarding (v2) | 0/TBD | Pending | - | diff --git a/.planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-01-PLAN.md b/.planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-01-PLAN.md new file mode 100644 index 0000000..78619e0 --- /dev/null +++ b/.planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-01-PLAN.md @@ -0,0 +1,273 @@ +--- +phase: "01-foundation-client-dashboard" +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - package.json + - tsconfig.json + - next.config.ts + - src/app/layout.tsx + - src/app/page.tsx + - .env.local +autonomous: true +requirements: + - DASH-01 + - DASH-02 + +must_haves: + truths: + - "Next.js 15 App Router is bootstrapped and compiles without errors" + - "DATABASE_URL env var is set and Drizzle can connect to Postgres" + - "A simple test route exists and responds with 200" + - "TypeScript strict mode is enabled" + artifacts: + - path: "package.json" + provides: "All dependencies for Next.js + Drizzle + auth + UI" + contains: "next@15" + - path: "src/app/layout.tsx" + provides: "Root layout with Tailwind setup" + min_lines: 15 + - path: ".env.local" + provides: "DATABASE_URL pointing to Coolify Postgres" + contains: "DATABASE_URL" + key_links: + - from: ".env.local" + to: "Drizzle client initialization" + via: "process.env.DATABASE_URL" + pattern: "DATABASE_URL=postgres://" + - from: "src/db/index.ts" + to: "Postgres on Coolify" + via: "postgres-js driver" + pattern: "import.*postgres.*from.*postgres-js" +--- + + +**Walking Skeleton:** Bootstrap the Next.js project, install all Phase 1 dependencies, configure Tailwind, connect to the Postgres database on Coolify via Drizzle ORM, and verify the entire stack is operational with a simple test route. + +Purpose: Establish the project foundation so subsequent plans can build on a known-good state. This plan proves Next.js 15 + Drizzle + postgres-js + Tailwind work together before writing any feature code. + +Output: Runnable Next.js dev server (`npm run dev`) with DB connection confirmed, TypeScript types working, Tailwind CSS active, ready for schema creation. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/01-foundation-client-dashboard/01-CONTEXT.md +@.planning/research/STACK.md +@.planning/research/ARCHITECTURE.md + + + + + + Task 1: Bootstrap Next.js 15 with TypeScript, App Router, src/ directory, and Tailwind CSS v4 + + package.json + tsconfig.json + next.config.ts + src/app/layout.tsx + src/app/page.tsx + tailwind.config.ts + postcss.config.mjs + .gitignore + + + None (greenfield project) + + + Execute: `npx create-next-app@latest . --typescript --tailwind --app --src-dir --eslint --import-alias '@/*'` + + Verify created: + - `src/` directory with `app/` subdirectory + - `tsconfig.json` with `"strict": true` + - `tailwind.config.ts` (v4, CSS-first) + - `postcss.config.mjs` + - Next.js 15.x in package.json + + After creation, modify `src/app/layout.tsx`: + - Import Tailwind globals: `import './globals.css'` + - Set viewport and basic meta tags + - Ensure `` and `` exist with proper className for Tailwind + + Modify `src/app/page.tsx`: + - Replace default template with a simple div: `
Welcome to ClientHub
` + - Keep it minimal — this route will be replaced in Phase 2 +
+ + grep -q "\"next\": \"^15" package.json && echo "Next.js 15 installed" + grep -q "\"strict\": true" tsconfig.json && echo "TypeScript strict mode enabled" + test -f src/app/layout.tsx && grep -q "globals.css" src/app/layout.tsx && echo "Tailwind globals imported" + test -f next.config.ts && echo "next.config.ts exists" + + + - `npm install` succeeds without errors + - `npm run build` succeeds (no TypeScript errors, no Next.js errors) + - `npm run dev` starts server without crashing + - Visiting http://localhost:3000 returns 200 and displays the welcome message + +
+ + + Task 2: Install Drizzle ORM, postgres-js, and supporting libraries; create .env.local with DATABASE_URL + + package.json + .env.local + .env.example + src/db/index.ts + + + None (greenfield) + + + Install packages: + ``` + npm install drizzle-orm postgres + npm install -D drizzle-kit + ``` + + Note: The package is `postgres` (not `postgres-js` — that's the npm package name for postgres-js driver). + + Create `src/db/index.ts`: + ```typescript + import { Client } from 'postgres'; + import * as schema from './schema'; + + if (!process.env.DATABASE_URL) { + throw new Error('DATABASE_URL env var is required'); + } + + const client = new Client({ + connectionString: process.env.DATABASE_URL, + }); + + export const db = drizzle(client, { schema }); + ``` + + Create `.env.local`: + ``` + DATABASE_URL=postgresql://[user]:[password]@[coolify-host]:5432/[database] + ``` + Use the actual Coolify credentials. If not yet available, use a placeholder and update before plan 02. + + Create `.env.example`: + ``` + DATABASE_URL=postgresql://user:password@host:5432/database + ``` + + Install additional dependencies: + ``` + npm install nanoid zod @hookform/resolvers react-hook-form + npm install -D @types/node + ``` + + Auth.js will be installed in a later plan (Phase 2 only). + + + grep -q "drizzle-orm" package.json && echo "Drizzle installed" + grep -q "postgres" package.json && echo "postgres-js installed" + grep -q "drizzle-kit" package.json && echo "drizzle-kit installed" + test -f .env.local && grep -q "DATABASE_URL" .env.local && echo ".env.local exists with DATABASE_URL" + test -f .env.example && echo ".env.example exists" + grep -q "postgres" src/db/index.ts && echo "postgres-js driver imported in db/index.ts" + + + - `npm install` succeeds + - `src/db/index.ts` exists and exports `db` object + - `.env.local` contains DATABASE_URL (value will be filled in by executor or user) + - `npm run build` succeeds with no import errors + + + + + Task 3: Install shadcn/ui components and configure; add lucide-react icons + + package.json + components.json + src/components/ui/*.tsx (multiple) + + + tailwind.config.ts + + + Initialize shadcn/ui: + ``` + npx shadcn@latest init --yes + ``` + + This creates `components.json` with the proper configuration. + + Add essential components for Phase 1: + ``` + npx shadcn@latest add button card badge progress input label select separator table textarea + ``` + + Install lucide-react: + ``` + npm install lucide-react + ``` + + Verify `src/components/ui/` directory contains all component files. + + + test -f components.json && echo "components.json created" + test -d src/components/ui && ls src/components/ui/ | wc -l | grep -qE "[0-9]+" && echo "UI components installed" + grep -q "lucide-react" package.json && echo "lucide-react installed" + + + - `components.json` exists with proper shadcn configuration + - At least 8 component files exist in `src/components/ui/` + - `npm run build` succeeds + + + +
+ + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| Client (browser) → API | Clients access `/c/[token]/*` routes; middleware must validate token | +| Client (browser) → Database | Drizzle queries filtered by token; no client can see other clients' data | +| Admin → Vercel environment variables | DATABASE_URL, future ADMIN_PASSWORD must be secret | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Disposition | Mitigation Plan | +|-----------|----------|-----------|-------------|-----------------| +| T-01-001 | Information Disclosure | DATABASE_URL in .env.local | mitigate | Never commit .env.local; .gitignore enforces this; use Vercel Secrets for production | +| T-01-002 | Tampering | Schema initialization | mitigate | Use Drizzle migrations + drizzle-kit push before any data is written; immutable migration history | +| T-01-003 | Denial of Service | Database connection pooling | accept | postgres-js handles connection lifecycle; Coolify Postgres has resource limits acceptable for Phase 1 scale | + + + + +After plan execution: +1. Run `npm run build` → no errors +2. Run `npm run dev` → server starts on http://localhost:3000 +3. Visit http://localhost:3000 → page loads with welcome message +4. Check `src/db/index.ts` → imports postgres-js correctly +5. Check `.env.local` → DATABASE_URL is set (value may be placeholder) +6. Check `components.json` → exists with @/ alias + + + +- Next.js dev server starts and responds to requests +- TypeScript compiles without errors +- Tailwind CSS is active (can verify via DevTools) +- Database connection string is configured (even if not yet tested with actual DB) +- All Phase 1 dependencies are installed +- Ready to proceed to Task 02 (schema creation) + + + +After completion, create `.planning/phases/01-foundation-client-dashboard/01-01-SUMMARY.md` + diff --git a/.planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-01-SUMMARY.md b/.planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-01-SUMMARY.md new file mode 100644 index 0000000..784f95e --- /dev/null +++ b/.planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-01-SUMMARY.md @@ -0,0 +1,190 @@ +--- +phase: 01-foundation-client-dashboard +plan: 01 +subsystem: infra +tags: [nextjs, drizzle-orm, postgres, tailwind, shadcn, typescript] + +# Dependency graph +requires: [] +provides: + - Next.js 16 App Router project with TypeScript strict mode + - Tailwind CSS v4 + shadcn/ui components (button, card, badge, progress, input, label, select, separator, table, textarea) + - Drizzle ORM + postgres-js driver configured (db client in src/db/index.ts) + - drizzle.config.ts ready for migrations + - .env.local with DATABASE_URL placeholder + - lucide-react icons + - src/lib/utils.ts cn() helper +affects: + - 01-02-schema + - 01-03-client-route + - 01-04-dashboard-ui + - 01-05-seed-deploy + +# Tech tracking +tech-stack: + added: + - next@16.2.6 + - drizzle-orm@0.45.2 + - drizzle-kit@0.31.10 + - postgres@3.4.9 + - tailwindcss@4.x + - shadcn/ui (Radix preset) + - lucide-react@1.14.0 + - nanoid@5.1.11 + - zod@4.4.3 + - react-hook-form + @hookform/resolvers + - clsx + tailwind-merge + class-variance-authority + patterns: + - App Router with Server Components as default + - Drizzle ORM with postgres-js driver (not neon-http) for Coolify Postgres + - shadcn/ui components in src/components/ui/ (copied, not wrapped) + - cn() utility for conditional classnames + +key-files: + created: + - src/app/layout.tsx (root layout, metadata, viewport, Tailwind globals) + - src/app/page.tsx (placeholder route) + - src/app/globals.css (Tailwind v4 CSS-first) + - src/db/index.ts (Drizzle client with postgres-js) + - src/lib/utils.ts (cn() helper) + - src/components/ui/*.tsx (10 shadcn components) + - drizzle.config.ts (migration config) + - components.json (shadcn config) + - .env.example (public template) + modified: + - package.json (all deps added) + - .gitignore (allow .env.example, block all other .env*) + +key-decisions: + - "Usato Next.js 16.2.6 (latest stable) invece di 15.x — create-next-app@latest installa la versione corrente" + - "viewport spostato in export dedicato (Next.js 16 API) invece che in metadata" + - "src/db/index.ts usa drizzle-orm/postgres-js con import default di postgres (non Client class)" + - ".env.example aggiunto con eccezione in .gitignore (non .env.local che resta ignorato)" + +patterns-established: + - "Database client: import postgres from 'postgres' + drizzle(client) in src/db/index.ts" + - "shadcn/ui: componenti copiati in src/components/ui/, usabili come primitivi" + - "cn() utility per merge classi Tailwind in src/lib/utils.ts" + +requirements-completed: + - DASH-01 + - DASH-02 + +# Metrics +duration: 15min +completed: 2026-05-13 +--- + +# Phase 1 Plan 01: Walking Skeleton — Next.js 16 + Drizzle + shadcn/ui bootstrapped su Coolify Postgres + +**Next.js 16.2.6 App Router con TypeScript strict, Tailwind v4, Drizzle ORM + postgres-js per Coolify Postgres, e 10 componenti shadcn/ui installati e pronti.** + +## Performance + +- **Duration:** ~15 min +- **Started:** 2026-05-13T13:26:00Z +- **Completed:** 2026-05-13T13:41:00Z +- **Tasks:** 3/3 +- **Files modified:** 20+ + +## Accomplishments + +- Next.js 16.2.6 con App Router, TypeScript strict mode, Tailwind CSS v4 — `npm run build` passa senza errori TypeScript +- Drizzle ORM + postgres-js configurati con client in `src/db/index.ts`, pronto per le migrazioni del Plan 02 +- 10 componenti shadcn/ui installati + lucide-react: base UI completa per i plan successivi + +## Task Commits + +1. **Task 1: Bootstrap Next.js 16** - `9563b87` (chore) +2. **Task 2: Drizzle ORM + postgres-js + librerie** - `6b5609b` (feat) +3. **Task 3: shadcn/ui + lucide-react** - `f842007` (feat) + +## Files Created/Modified + +- `src/app/layout.tsx` - Root layout con metadata ClientHub, lang="it", viewport export corretto per Next.js 16 +- `src/app/page.tsx` - Placeholder minimale (sarà sostituito in Phase 2) +- `src/app/globals.css` - Tailwind v4 CSS-first con variabili CSS +- `src/db/index.ts` - Client Drizzle con postgres-js driver, guard su DATABASE_URL +- `src/lib/utils.ts` - cn() helper con clsx + tailwind-merge +- `src/components/ui/*.tsx` - 10 componenti: button, card, badge, progress, input, label, select, separator, table, textarea +- `drizzle.config.ts` - Config drizzle-kit per migrazioni (dialect postgresql, schema src/db/schema.ts) +- `components.json` - Configurazione shadcn/ui (Radix preset, @/ aliases, CSS variables) +- `.env.example` - Template pubblico DATABASE_URL +- `.gitignore` - Aggiunta eccezione per .env.example, blocco tutti gli altri .env* +- `package.json` - Tutte le dipendenze Phase 1 installate + +## Decisions Made + +- Installato Next.js 16.2.6 (latest stable via `create-next-app@latest`) invece di 15.x — versione superiore, retrocompatibile +- `viewport` spostato in export dedicato (`export const viewport: Viewport`) come richiede Next.js 16 API — evita warning di build +- `src/db/index.ts` usa `import postgres from 'postgres'` (default export, non `Client` class) — API corretta del driver postgres-js +- `drizzle-orm/postgres-js` come adapter Drizzle invece di `drizzle-orm/neon-http` — allineato con decisione D-02 (Coolify Postgres, non Neon) + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 3 - Blocking] create-next-app rifiuta cartella con lettere maiuscole** +- **Found during:** Task 1 +- **Issue:** `create-next-app .` fallisce con "name can no longer contain capital letters" perché la cartella si chiama `IAMCAVALLI` +- **Fix:** Creato progetto in directory temporanea `/Users/simonecavalli/clienthub` poi spostati tutti i file nel repo principale +- **Files modified:** Nessun file extra — stesso risultato del comando diretto +- **Verification:** `npm run build` passa, tutti i file sono al posto corretto +- **Committed in:** 9563b87 + +**2. [Rule 1 - Bug] viewport in metadata genera warning Next.js 16** +- **Found during:** Task 1 (prima build) +- **Issue:** `metadata.viewport` è deprecato in Next.js 16; Next.js emette warning e richiede export `viewport` separato +- **Fix:** Aggiunto `export const viewport: Viewport = { ... }` e rimosso `viewport` da `metadata` +- **Files modified:** src/app/layout.tsx +- **Verification:** Build pulita senza warning viewport +- **Committed in:** 9563b87 + +**3. [Rule 3 - Blocking] API postgres driver non è Client class** +- **Found during:** Task 2 +- **Issue:** Il PLAN suggeriva `import { Client } from 'postgres'` ma il driver `postgres` esporta una funzione default, non una classe `Client` +- **Fix:** Usato `import postgres from 'postgres'` con `drizzle-orm/postgres-js` adapter — API corretta +- **Files modified:** src/db/index.ts +- **Verification:** TypeScript compila senza errori +- **Committed in:** 6b5609b + +**4. [Rule 3 - Blocking] shadcn init interattivo non risponde a --yes** +- **Found during:** Task 3 +- **Issue:** `npx shadcn@latest init --yes` richiede selezione manuale (libreria e preset) — non si automatizza +- **Fix:** Creato manualmente `components.json` con config corretta (Radix, CSS variables, @/ aliases) poi usato direttamente `shadcn add` per i componenti +- **Files modified:** components.json (creato manualmente) +- **Verification:** `npx shadcn@latest add button card ...` funziona senza problemi +- **Committed in:** f842007 + +--- + +**Total deviations:** 4 auto-fixed (2 Rule 3 blocking, 1 Rule 1 bug, 1 Rule 3 blocking) +**Impact on plan:** Tutte le deviazioni necessarie per il corretto funzionamento. Nessuno scope creep. + +## Issues Encountered + +- `.env.example` era bloccato da `.env*` pattern nel `.gitignore` — aggiunta eccezione `!.env.example` (file pubblico senza segreti, corretto da tracciare in git) + +## User Setup Required + +Prima di eseguire il Plan 02 (schema + migrazioni), aggiornare `.env.local` con le credenziali reali del database Coolify: + +``` +DATABASE_URL=postgresql://[user]:[password]@[coolify-host]:5432/clienthub +``` + +Le credenziali si trovano nel pannello Coolify su Hetzner. Il file `.env.local` è escluso dal git (`.gitignore`). + +## Threat Surface Scan + +Nessuna nuova superficie di sicurezza non prevista dal piano. Il threat model T-01-001 (DATABASE_URL in .env.local) è mitigato correttamente: `.env*` esclusi dal `.gitignore`, `.env.example` non contiene credenziali reali. + +## Next Phase Readiness + +- Plan 02 (schema Drizzle) può partire immediatamente — `src/db/index.ts` e `drizzle.config.ts` sono pronti +- L'utente deve aggiornare `DATABASE_URL` in `.env.local` con le credenziali reali Coolify prima di eseguire `drizzle-kit push` +- Build stabile, TypeScript strict attivo, zero errori + +--- +*Phase: 01-foundation-client-dashboard* +*Completed: 2026-05-13* diff --git a/.planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-02-PLAN.md b/.planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-02-PLAN.md new file mode 100644 index 0000000..ae49598 --- /dev/null +++ b/.planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-02-PLAN.md @@ -0,0 +1,369 @@ +--- +phase: "01-foundation-client-dashboard" +plan: 02 +type: execute +wave: 2 +depends_on: + - "01-01" +files_modified: + - src/db/schema.ts + - drizzle.config.ts + - .env.local +autonomous: true +requirements: + - DASH-01 + - DASH-02 + - DASH-03 + - DASH-04 + +must_haves: + truths: + - "Drizzle schema is complete and matches the data model from ARCHITECTURE.md" + - "All 11 tables are defined: clients, phases, tasks, deliverables, comments, payments, documents, notes, service_catalog, quote_items" + - "Token field on clients is a separate UUID, not the primary key" + - "approved_at on deliverables is TIMESTAMPTZ" + - "drizzle-kit push has been run and database schema is live" + - "TypeScript types are exported from schema.ts for use in API routes" + artifacts: + - path: "src/db/schema.ts" + provides: "Complete Drizzle ORM schema definition for all entities" + min_lines: 200 + contains: "export const clients = pgTable" + - path: "drizzle.config.ts" + provides: "Drizzle Kit configuration pointing to src/db/schema.ts" + contains: "schema:" + - path: "src/db/migrations/" + provides: "Migration files generated by drizzle-kit" + min_files: 1 + key_links: + - from: "src/db/schema.ts" + to: "clients table" + via: "pgTable definition" + pattern: "export const clients.*pgTable" + - from: "src/db/schema.ts" + to: "token field" + via: "uuid().unique()" + pattern: "token.*uuid.*unique" + - from: "drizzle-kit push" + to: "Postgres on Coolify" + via: "DATABASE_URL" + pattern: "DATABASE_URL" + +--- + + +**Database Schema + Drizzle Migrations:** Define the complete data model in Drizzle ORM, generate database migrations, and push the schema to Coolify Postgres. This plan creates the schema that all subsequent plans depend on. + +Purpose: Establish the single source of truth for data shape. Enforces critical decisions: token as separate field, accepted_total denormalized, approved_at immutable, ClientView vs. AdminView separation in queries. + +Output: `src/db/schema.ts` with all 11 tables fully defined, migration files, and Postgres schema live on Coolify. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/research/ARCHITECTURE.md +@.planning/phases/01-foundation-client-dashboard/01-CONTEXT.md +@.planning/phases/01-foundation-client-dashboard/01-01-SUMMARY.md + + + + + + Task 1: Create Drizzle schema definition (src/db/schema.ts) with all 11 tables + + src/db/schema.ts + + + .planning/research/ARCHITECTURE.md (Data Model section, lines 69-142) + + + Create `src/db/schema.ts` with the following tables (exact order, exact field names): + + ```typescript + import { pgTable, text, uuid, integer, numeric, timestamp, boolean, unique, index } from 'drizzle-orm/pg-core'; + import { relations } from 'drizzle-orm'; + import { nanoid } from 'nanoid'; + + // ============ CLIENTS ============ + export const clients = pgTable('clients', { + id: uuid('id').primaryKey().defaultValue(nanoid()), + name: text('name').notNull(), + brand_name: text('brand_name').notNull(), + brief: text('brief').notNull(), + token: uuid('token').notNull().unique().defaultValue(nanoid()), + accepted_total: numeric('accepted_total', { precision: 10, scale: 2 }).default('0'), + created_at: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + }); + + // ============ PHASES ============ + export const phases = pgTable('phases', { + id: uuid('id').primaryKey().defaultValue(nanoid()), + client_id: uuid('client_id').notNull().references(() => clients.id, { onDelete: 'cascade' }), + title: text('title').notNull(), + sort_order: integer('sort_order').notNull().default(0), + status: text('status').notNull().default('upcoming'), // upcoming | active | done + }); + + // ============ TASKS ============ + export const tasks = pgTable('tasks', { + id: uuid('id').primaryKey().defaultValue(nanoid()), + phase_id: uuid('phase_id').notNull().references(() => phases.id, { onDelete: 'cascade' }), + title: text('title').notNull(), + description: text('description'), + status: text('status').notNull().default('todo'), // todo | in_progress | done + sort_order: integer('sort_order').notNull().default(0), + }); + + // ============ DELIVERABLES ============ + export const deliverables = pgTable('deliverables', { + id: uuid('id').primaryKey().defaultValue(nanoid()), + task_id: uuid('task_id').notNull().references(() => tasks.id, { onDelete: 'cascade' }), + title: text('title').notNull(), + url: text('url'), + status: text('status').notNull().default('pending'), // pending | submitted | approved + approved_at: timestamp('approved_at', { withTimezone: true }), // immutable audit trail + }); + + // ============ COMMENTS ============ + export const comments = pgTable('comments', { + id: uuid('id').primaryKey().defaultValue(nanoid()), + entity_type: text('entity_type').notNull(), // task | deliverable + entity_id: uuid('entity_id').notNull(), + author: text('author').notNull(), // client | admin + body: text('body').notNull(), + created_at: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + }); + + // ============ PAYMENTS ============ + export const payments = pgTable('payments', { + id: uuid('id').primaryKey().defaultValue(nanoid()), + client_id: uuid('client_id').notNull().references(() => clients.id, { onDelete: 'cascade' }), + label: text('label').notNull(), // "Acconto 50%" | "Saldo 50%" + amount: numeric('amount', { precision: 10, scale: 2 }).notNull(), + status: text('status').notNull().default('da_saldare'), // da_saldare | inviata | saldato + paid_at: timestamp('paid_at', { withTimezone: true }), + }); + + // ============ DOCUMENTS ============ + export const documents = pgTable('documents', { + id: uuid('id').primaryKey().defaultValue(nanoid()), + client_id: uuid('client_id').notNull().references(() => clients.id, { onDelete: 'cascade' }), + label: text('label').notNull(), + url: text('url').notNull(), + created_at: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + }); + + // ============ NOTES (Decision Log) ============ + export const notes = pgTable('notes', { + id: uuid('id').primaryKey().defaultValue(nanoid()), + client_id: uuid('client_id').notNull().references(() => clients.id, { onDelete: 'cascade' }), + body: text('body').notNull(), + created_at: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + }); + + // ============ SERVICE CATALOG ============ + export const service_catalog = pgTable('service_catalog', { + id: uuid('id').primaryKey().defaultValue(nanoid()), + name: text('name').notNull(), + description: text('description'), + unit_price: numeric('unit_price', { precision: 10, scale: 2 }).notNull(), + active: boolean('active').notNull().default(true), + }); + + // ============ QUOTE ITEMS ============ + export const quote_items = pgTable('quote_items', { + id: uuid('id').primaryKey().defaultValue(nanoid()), + client_id: uuid('client_id').notNull().references(() => clients.id, { onDelete: 'cascade' }), + service_id: uuid('service_id').notNull().references(() => service_catalog.id, { onDelete: 'restrict' }), + quantity: numeric('quantity', { precision: 10, scale: 2 }).notNull(), + unit_price: numeric('unit_price', { precision: 10, scale: 2 }).notNull(), + subtotal: numeric('subtotal', { precision: 10, scale: 2 }).notNull(), + }); + + // ============ RELATIONS ============ + export const clientsRelations = relations(clients, ({ many }) => ({ + phases: many(phases), + payments: many(payments), + documents: many(documents), + notes: many(notes), + quote_items: many(quote_items), + })); + + export const phasesRelations = relations(phases, ({ one, many }) => ({ + client: one(clients, { fields: [phases.client_id], references: [clients.id] }), + tasks: many(tasks), + })); + + export const tasksRelations = relations(tasks, ({ one, many }) => ({ + phase: one(phases, { fields: [tasks.phase_id], references: [phases.id] }), + deliverables: many(deliverables), + })); + + export const deliverablesRelations = relations(deliverables, ({ one }) => ({ + task: one(tasks, { fields: [deliverables.task_id], references: [tasks.id] }), + })); + ``` + + Notes: + - Use `nanoid()` for all UUID primary keys (not SQL-generated UUIDs) — this ensures consistent, cryptographically secure IDs + - Token is `uuid().notNull().unique()` — separate from id, rotatable + - `approved_at` is nullable (no approval initially) + - Relations use cascading deletes for data integrity + - All timestamp fields use `withTimezone: true` + + + test -f src/db/schema.ts && echo "schema.ts exists" + grep -c "export const" src/db/schema.ts | grep -q "1[1-9]\|2[0-9]" && echo "Multiple table exports found" + grep -q "token.*uuid.*unique" src/db/schema.ts && echo "Token field is separate and unique" + grep -q "approved_at.*timestamp" src/db/schema.ts && echo "approved_at field exists" + grep -q "accepted_total" src/db/schema.ts && echo "accepted_total denormalized field exists" + npm run build 2>&1 | grep -v "warning" | grep -q "error" && echo "TypeScript errors found" || echo "TypeScript compiles" + + + - `src/db/schema.ts` exists with all 11 tables defined + - All table exports are present: clients, phases, tasks, deliverables, comments, payments, documents, notes, service_catalog, quote_items + - Token field is separate from id PK and marked as unique + - Relations are defined for all foreign keys + - TypeScript compiles without errors + + + + + Task 2: Create drizzle.config.ts and generate migrations + + drizzle.config.ts + src/db/migrations/* + + + src/db/schema.ts + .env.local + + + Create `drizzle.config.ts` in project root: + + ```typescript + import type { Config } from 'drizzle-kit'; + + export default { + schema: './src/db/schema.ts', + out: './src/db/migrations', + driver: 'pg', + dbCredentials: { + connectionString: process.env.DATABASE_URL!, + }, + } satisfies Config; + ``` + + Run migration generation: + ``` + npx drizzle-kit generate + ``` + + This creates `src/db/migrations/` directory with a numbered migration file (e.g., `0000_initial_schema.sql`). + + Verify the generated SQL contains: + - All 11 CREATE TABLE statements + - Foreign key constraints + - Unique constraints on token + + + test -f drizzle.config.ts && echo "drizzle.config.ts created" + test -d src/db/migrations && ls src/db/migrations/*.sql 2>/dev/null | wc -l | grep -q "[1-9]" && echo "Migration files generated" + grep -l "CREATE TABLE" src/db/migrations/*.sql | wc -l | grep -q "[1-9]" && echo "SQL migration contains CREATE TABLE" + + + - `drizzle.config.ts` exists with correct driver (pg) and schema path + - `src/db/migrations/` directory exists with at least one .sql file + - Generated SQL file contains CREATE TABLE statements for all 11 tables + + + + + Task 3: [BLOCKING] Run drizzle-kit push to apply schema to Coolify Postgres + + None (schema is pushed to DB, not local files) + + + .env.local (verify DATABASE_URL is set) + src/db/migrations/ (ensure migrations exist) + + + Before running push, verify DATABASE_URL is set in .env.local: + ``` + cat .env.local | grep DATABASE_URL + ``` + + If DATABASE_URL is not yet available (Coolify not configured), STOP here and ask executor to provide Coolify credentials. This task cannot proceed without a valid connection string. + + Once DATABASE_URL is confirmed: + ``` + npx drizzle-kit push + ``` + + Drizzle will connect to the database and apply all migrations. + + If push succeeds, you will see: + ``` + ✓ All migrations have been successfully applied + ``` + + If the database schema was already created, drizzle-kit will detect it and skip unchanged tables. + + + if grep -q "^DATABASE_URL=postgresql://" .env.local; then echo "DATABASE_URL is set"; else echo "DATABASE_URL NOT SET"; fi + npx drizzle-kit push 2>&1 | grep -q "successfully\|already\|applied" && echo "Schema push completed" + + + - DATABASE_URL env var is set in .env.local + - `npx drizzle-kit push` runs without connection errors + - Schema is created in Coolify Postgres (all 11 tables exist) + - Executor can confirm with: `npx drizzle-kit introspect` (shows all tables) + + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| Migration files → Database | Schema migrations are deployed via drizzle-kit push; any schema change is version-controlled | +| Schema definition → ORM runtime | TypeScript schema is the source of truth; Drizzle generates types from schema, not from introspection | +| Token field → Access control | Token is marked unique and separate from PK; enforced by DB constraints | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Disposition | Mitigation Plan | +|-----------|----------|-----------|-------------|-----------------| +| T-02-001 | Tampering | Token field uniqueness | mitigate | Database enforces UNIQUE constraint on token field; no client can have duplicate token | +| T-02-002 | Information Disclosure | Schema version history | accept | Migrations are version-controlled in git; leaking migration files does not expose secrets (passwords in .env.local only) | +| T-02-003 | Denial of Service | quote_items table | accept | Admin-only; client API never queries it; no data loss from client-side DOS attacks | + + + + +After plan execution: +1. Run `npx drizzle-kit push` → "successfully applied" message +2. Run `npx drizzle-kit introspect` → lists all 11 tables +3. Check `src/db/migrations/` → at least one .sql file exists +4. Check `src/db/schema.ts` → all tables are exported +5. Verify TypeScript: `npm run build` → no errors + + + +- Drizzle schema is defined and exported from `src/db/schema.ts` +- All 11 tables are created in Coolify Postgres +- Token field is unique and separate from id +- Migrations are version-controlled in git +- TypeScript types are available for import in API routes +- Ready to proceed to Plan 03 (Middleware + Client Portal route) + + + +After completion, create `.planning/phases/01-foundation-client-dashboard/01-02-SUMMARY.md` + diff --git a/.planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-02-SUMMARY.md b/.planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-02-SUMMARY.md new file mode 100644 index 0000000..8cba053 --- /dev/null +++ b/.planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-02-SUMMARY.md @@ -0,0 +1,144 @@ +--- +phase: 01-foundation-client-dashboard +plan: 02 +subsystem: database +tags: [drizzle-orm, postgres, schema, migrations, nanoid] + +# Dependency graph +requires: + - 01-01 (drizzle-kit, postgres-js driver, DATABASE_URL in .env.local) +provides: + - src/db/schema.ts con 10 tabelle complete + - TypeScript types esportati per tutte le entità (Client, Phase, Task, ecc.) + - Migration file SQL in src/db/migrations/ + - Schema live su Postgres 16 (Hetzner/Coolify) +affects: + - 01-03-client-route (usa clients, phases, tasks, deliverables, payments, documents, notes) + - 01-04-dashboard-ui (usa tutti i types esportati) + - 01-05-seed-deploy (inserisce dati con i types NewClient, NewPhase, ecc.) + +# Tech tracking +tech-stack: + added: [] + patterns: + - "ID strategy: text + nanoid() via $defaultFn (non uuid() nativo Postgres) — nanoid genera stringhe 21-char URL-safe, non UUID formato xxxxxxxx-xxxx-xxxx" + - "drizzle-kit push richiede DATABASE_URL passata esplicitamente come env var (non carica .env.local automaticamente)" + - "Relations Drizzle definite per tutti gli FK — usabili in query con with: { ... }" + +key-files: + created: + - src/db/schema.ts (245 righe — 10 tabelle + relations + TypeScript types) + - src/db/migrations/0000_pretty_typhoid_mary.sql (migration SQL completa) + - src/db/migrations/meta/ (drizzle-kit metadata) + - src/db/migrations/relations.ts (relazioni per introspect) + - src/db/migrations/schema.ts (schema per introspect) + modified: [] + +key-decisions: + - "Usato text + $defaultFn(() => nanoid()) invece di uuid().defaultRandom() — nanoid genera ID URL-safe crittograficamente sicuri (21 char, ~126 bit entropia), non UUID formato PostgreSQL" + - "drizzle.config.ts dal Plan 01 già corretto (defineConfig + dialect postgresql + url:) — nessuna modifica necessaria" + - "clients.token: text notNull unique con nanoid — separato dall'id PK, rotabile con single UPDATE" + - "drizzle-kit push richiede DATABASE_URL come env var esplicita (non auto-load .env.local)" + +# Metrics +duration: 15min +completed: 2026-05-13 +--- + +# Phase 1 Plan 02: Drizzle Schema + Migration — 10 tabelle live su Postgres + +**Schema Drizzle ORM completo con 10 tabelle, migration SQL generata e schema live sul database Postgres 16 (Hetzner/Coolify). TypeScript strict compila senza errori.** + +## Performance + +- **Duration:** ~15 min +- **Started:** 2026-05-13T20:21:00Z +- **Completed:** 2026-05-13T20:36:00Z +- **Tasks:** 3/3 +- **Files modified:** 6 + +## Accomplishments + +- `src/db/schema.ts` creato con 10 tabelle complete + relations Drizzle + TypeScript types esportati +- Vincoli architetturali LOCKED rispettati: `clients.token` separato dall'id PK (unique, notNull, nanoid), `accepted_total` denormalizzato, `approved_at` nullable (audit trail immutabile), `quote_items` mai esposto al client API +- Migration SQL (`0000_pretty_typhoid_mary.sql`) generata con tutti i `CREATE TABLE` e FK constraints +- `npx drizzle-kit push` eseguito con successo — tutte e 10 le tabelle create su `postgresql://178.104.27.55:5432/clienthub` +- Verifica via `information_schema.tables`: clients, comments, deliverables, documents, notes, payments, phases, quote_items, service_catalog, tasks + +## Task Commits + +1. **Task 1: Drizzle schema (src/db/schema.ts)** - `1bdbe7a` (feat) +2. **Task 2: Migration generation (drizzle-kit generate)** - `a6ec599` (chore) +3. **Task 3: [BLOCKING] drizzle-kit push → Postgres live** - `abcbb52` (feat) + +## Files Created/Modified + +- `src/db/schema.ts` — 10 tabelle: clients (token separato + accepted_total), phases, tasks, deliverables (approved_at nullable), comments (polimorfici), payments (da_saldare/inviata/saldato), documents, notes, service_catalog, quote_items +- `src/db/migrations/0000_pretty_typhoid_mary.sql` — Migration SQL completa con CREATE TABLE + FK + UNIQUE constraint su token +- `src/db/migrations/meta/` — Drizzle-kit metadata (snapshot JSON) +- `src/db/migrations/relations.ts` — Relations per introspect +- `src/db/migrations/schema.ts` — Schema per introspect + +## Decisions Made + +- **ID strategy:** `text + $defaultFn(() => nanoid())` invece di `uuid().defaultRandom()`. La colonna Drizzle `uuid()` si aspetta il formato PostgreSQL `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`, mentre `nanoid()` genera stringhe 21-char URL-safe. Usare `text` è corretto e allineato con la decisione architetturale di token crittograficamente sicuro. +- **drizzle.config.ts invariato:** La versione dal Plan 01 usa già `defineConfig`, `dialect: "postgresql"` e `url:` (sintassi aggiornata drizzle-kit v0.31) — nessuna modifica necessaria rispetto alla versione suggerita nel piano (che usava l'API obsoleta `driver: 'pg'`). + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 1 - Bug] uuid() non compatibile con nanoid() come defaultFn** +- **Found during:** Task 1 +- **Issue:** Il piano suggeriva `uuid('id').primaryKey().defaultValue(nanoid())` ma Drizzle `uuid()` si aspetta UUID nel formato `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`. nanoid() genera stringhe come `Tcyf3muFXVOX9QO9pBUES` (21 char, non UUID validi). Usare `defaultValue(nanoid())` su una colonna `uuid()` avrebbe causato errori a runtime al primo INSERT. +- **Fix:** Cambiato a `text('id').primaryKey().$defaultFn(() => nanoid())` per tutte le PK e per il campo `token`. Semantica identica (ID crittograficamente sicuro), tipo colonna SQL `text` invece di `uuid`. +- **Files modified:** src/db/schema.ts +- **Commit:** 1bdbe7a + +**2. [Rule 1 - Bug] drizzle.config.ts dal piano usa API obsoleta** +- **Found during:** Task 2 +- **Issue:** Il piano suggeriva `driver: 'pg'` e `dbCredentials: { connectionString: ... }` — sintassi drizzle-kit <0.30. Il file esistente usa già `defineConfig` con `dialect: "postgresql"` e `dbCredentials: { url: ... }` — sintassi corretta per drizzle-kit 0.31. +- **Fix:** Mantenuto il file esistente senza modifiche (era già corretto). +- **Files modified:** nessuno +- **Commit:** nessuno necessario + +**3. [Rule 3 - Blocking] drizzle-kit push non carica .env.local automaticamente** +- **Found during:** Task 3 +- **Issue:** `npx drizzle-kit push` fallisce con "connection url required" perché drizzle-kit non carica `.env.local` automaticamente (solo `.env`). +- **Fix:** Passato `DATABASE_URL` esplicitamente come variabile d'ambiente al comando: `DATABASE_URL="..." npx drizzle-kit push`. +- **Files modified:** nessuno (solo comando di esecuzione) +- **Commit:** abcbb52 + +## Known Stubs + +Nessuno. Il piano è infrastrutturale (schema + DB) — nessun componente UI o dato presentato al cliente. Le tabelle sono vuote, ma questo è intenzionale: il seed script è previsto nel Plan 05. + +## Threat Surface Scan + +Il threat model T-02-001 (unicità token) è mitigato: `CONSTRAINT "clients_token_unique" UNIQUE("token")` è attivo nel database. T-02-002 e T-02-003 sono accettati come da piano. + +Nessuna nuova superficie di sicurezza non prevista dal threat model. + +## Self-Check + +- [x] `src/db/schema.ts` esiste (245 righe, 10 tabelle pgTable + relations + types) +- [x] `src/db/migrations/0000_pretty_typhoid_mary.sql` esiste con 10 CREATE TABLE +- [x] Commit `1bdbe7a` esiste (schema) +- [x] Commit `a6ec599` esiste (migrations) +- [x] Commit `abcbb52` esiste (push) +- [x] 10 tabelle verificate live su Postgres via `information_schema.tables` +- [x] `clients.token` è `text NOT NULL UNIQUE` con nanoid — separato dalla PK +- [x] `approved_at` è `timestamp with time zone` nullable +- [x] TypeScript strict: `npm run build` — zero errori TypeScript + +## Self-Check: PASSED + +## Next Phase Readiness + +- Plan 03 (Middleware + route `/c/[token]`) può partire — lo schema è live e i types sono importabili +- Import pattern: `import { clients, phases, tasks, ... } from '@/db/schema'` +- Import types: `import type { Client, Phase, Task, ... } from '@/db/schema'` + +--- +*Phase: 01-foundation-client-dashboard* +*Completed: 2026-05-13* \ No newline at end of file diff --git a/.planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-03-PLAN.md b/.planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-03-PLAN.md new file mode 100644 index 0000000..8b0d98d --- /dev/null +++ b/.planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-03-PLAN.md @@ -0,0 +1,569 @@ +--- +phase: "01-foundation-client-dashboard" +plan: 03 +type: execute +wave: 2 +depends_on: + - "01-01" + - "01-02" +files_modified: + - src/middleware.ts + - app/api/internal/validate-token/route.ts + - src/lib/client-view.ts + - app/c/[token]/page.tsx + - app/c/[token]/layout.tsx +autonomous: true +requirements: + - DASH-01 + - DASH-02 + - DASH-03 + - DASH-04 + +must_haves: + truths: + - "Middleware validates token at edge and returns 404 if token not found" + - "Client can open /c/[token] without login" + - "Server Component fetches client data from DB via token" + - "ClientView type ensures quote_items is never exposed to client API" + - "All phase, task, payment, document, and note data is fetched and passed to UI" + - "TypeScript types are exported for downstream UI rendering" + artifacts: + - path: "src/middleware.ts" + provides: "Token validation using fetch to internal API route (Edge-compatible)" + contains: "function middleware" + - path: "app/api/internal/validate-token/route.ts" + provides: "Node.js API route that queries DB and returns 200/404 for token validation" + min_lines: 20 + contains: "clients.token" + - path: "src/lib/client-view.ts" + provides: "Client-safe type definitions and query functions" + contains: "ClientView" + - path: "app/c/[token]/page.tsx" + provides: "Server Component rendering client dashboard" + min_lines: 30 + contains: "export default async function" + - path: "app/c/[token]/layout.tsx" + provides: "Layout for token-authenticated routes" + min_lines: 10 + key_links: + - from: "src/middleware.ts" + to: "app/api/internal/validate-token/route.ts" + via: "fetch('/api/internal/validate-token?token=X')" + pattern: "validate-token" + - from: "app/api/internal/validate-token/route.ts" + to: "Database query for token validation" + via: "db.select().from(clients).where(eq(clients.token, token))" + pattern: "clients\\.token" + - from: "app/c/[token]/page.tsx" + to: "src/lib/client-view.ts" + via: "import { getClientView }" + pattern: "getClientView" + - from: "ClientView type" + to: "Rendering props" + via: "ensures no quote_items" + pattern: "quote_items" + +--- + + +**Token Middleware + Client Portal Data Layer:** Create Next.js middleware to validate client tokens at the edge, build the ClientView type system that enforces ClientView vs. AdminView separation, and create a Server Component that fetches and prepares all client dashboard data without exposing admin secrets (quote_items, service prices). + +Purpose: Establish the secure client access pattern: middleware validates token → Server Component fetches data → UI receives ClientView shape only. This prevents accidental exposure of admin data to clients. + +Output: Fully functional `/c/[token]` route that fetches real client data and prepares it for rendering. No client-side waterfalls. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/research/ARCHITECTURE.md (Data Flow section, lines 29-50) +@.planning/research/PITFALLS.md (Pitfall 2: Client API Exposes Admin Data, lines 26-38) +@.planning/phases/01-foundation-client-dashboard/01-CONTEXT.md +@.planning/phases/01-foundation-client-dashboard/01-02-SUMMARY.md + + + + + + Task 1: Create src/middleware.ts (Edge-compatible fetch pattern) + internal validate-token API route + + src/middleware.ts + app/api/internal/validate-token/route.ts + + + src/db/schema.ts (clients table definition) + package.json (verify Next.js version) + + + **Why two files:** Next.js middleware runs in the Edge runtime by default. The postgres-js driver (used by Drizzle) requires Node.js `net`/`tls` APIs unavailable at the Edge. The solution is a two-layer pattern: middleware uses `fetch()` to call an internal API route that runs in the Node.js runtime and does the actual DB query. + + Create `app/api/internal/validate-token/route.ts` (Node.js runtime, does DB query): + + ```typescript + import { NextRequest, NextResponse } from 'next/server'; + import { eq } from 'drizzle-orm'; + import { db } from '@/db'; + import { clients } from '@/db/schema'; + + export async function GET(request: NextRequest) { + const token = request.nextUrl.searchParams.get('token'); + + if (!token) { + return NextResponse.json({ valid: false }, { status: 400 }); + } + + try { + const rows = await db + .select({ id: clients.id }) + .from(clients) + .where(eq(clients.token, token)) + .limit(1); + + if (rows.length === 0) { + return NextResponse.json({ valid: false }, { status: 404 }); + } + + return NextResponse.json({ valid: true }, { status: 200 }); + } catch { + return NextResponse.json({ valid: false }, { status: 500 }); + } + } + ``` + + Create `src/middleware.ts` (Edge-compatible, uses fetch): + + ```typescript + import { NextRequest, NextResponse } from 'next/server'; + + export async function middleware(request: NextRequest) { + const pathname = request.nextUrl.pathname; + + // Extract token from path: /c/[token]/... + const tokenMatch = pathname.match(/^\/c\/([a-zA-Z0-9_-]+)/); + if (!tokenMatch) { + return NextResponse.rewrite(new URL('/not-found', request.url)); + } + + const token = tokenMatch[1]; + + try { + // Call internal Node.js API route — Edge middleware cannot use postgres-js directly + const validateUrl = new URL( + `/api/internal/validate-token?token=${encodeURIComponent(token)}`, + request.url + ); + const res = await fetch(validateUrl.toString()); + + if (!res.ok) { + return NextResponse.rewrite(new URL('/not-found', request.url)); + } + + return NextResponse.next(); + } catch { + return NextResponse.rewrite(new URL('/not-found', request.url)); + } + } + + export const config = { + matcher: ['/c/:path*'], + }; + ``` + + Key points: + - Middleware is Edge-compatible: no Node.js imports, only `fetch()` + - DB query lives in the API route (Node.js runtime) where postgres-js works correctly + - Token is URL-encoded before being passed as query param + - Non-existent or invalid tokens resolve to `/not-found` (Next.js built-in 404 page) + - Internal API route should not be called directly by clients (no auth secret needed — it only returns boolean valid/invalid) + + + test -f src/middleware.ts && echo "middleware.ts exists" + grep -q "export.*function middleware" src/middleware.ts && echo "middleware function exported" + grep -q "matcher.*c/" src/middleware.ts && echo "matcher configured for /c/ routes" + ! grep -q "from '@/db'" src/middleware.ts && echo "middleware does not import drizzle/db (good — Edge safe)" + test -f app/api/internal/validate-token/route.ts && echo "internal validate-token route exists" + grep -q "clients.token" app/api/internal/validate-token/route.ts && echo "Token DB query in API route" + + + - `src/middleware.ts` does NOT import Drizzle/postgres-js (Edge-safe) + - `src/middleware.ts` fetches `/api/internal/validate-token?token=X` + - `app/api/internal/validate-token/route.ts` queries `clients.token` via Drizzle + - Non-existent tokens return `/not-found` (404) + - Matcher configured for `/c/:path*` + - TypeScript compiles without errors + + + + + Task 2: Create src/lib/client-view.ts with ClientView type and query functions + + src/lib/client-view.ts + + + src/db/schema.ts (all table definitions) + + + Create `src/lib/client-view.ts`: + + ```typescript + import { eq, inArray } from 'drizzle-orm'; + import { db } from '@/db'; + import { clients, phases, tasks, deliverables, payments, documents, notes } from '@/db/schema'; + + /** + * ClientView: The ONLY data shape returned to client-facing routes. + * Deliberately excludes: quote_items, service_catalog, service prices. + * Enforced server-side: client API never touches admin data. + */ + export interface ClientView { + client: { + id: string; + name: string; + brand_name: string; + brief: string; + accepted_total: string; // only total, never breakdown + }; + phases: Array<{ + id: string; + title: string; + status: 'upcoming' | 'active' | 'done'; + sort_order: number; + tasks: Array<{ + id: string; + title: string; + description: string | null; + status: 'todo' | 'in_progress' | 'done'; + sort_order: number; + deliverables: Array<{ + id: string; + title: string; + url: string | null; + status: 'pending' | 'submitted' | 'approved'; + approved_at: string | null; // ISO timestamp + }>; + }>; + progress_pct: number; // % of tasks done in this phase + }>; + payments: Array<{ + id: string; + label: string; // "Acconto 50%" | "Saldo 50%" + status: 'da_saldare' | 'inviata' | 'saldato'; + }>; + documents: Array<{ + id: string; + label: string; + url: string; + }>; + notes: Array<{ + id: string; + body: string; + created_at: string; // ISO timestamp + }>; + global_progress_pct: number; // % of all tasks done across all phases + } + + /** + * getClientView: Fetch all client data and return only ClientView shape. + * NEVER queries quote_items. + */ + export async function getClientView(token: string): Promise { + // Fetch client + const clientRow = await db + .select() + .from(clients) + .where(eq(clients.token, token)) + .limit(1); + + if (clientRow.length === 0) { + return null; + } + + const client = clientRow[0]; + + // Fetch all phases for this client + const phasesRows = await db + .select() + .from(phases) + .where(eq(phases.client_id, client.id)) + .orderBy(phases.sort_order); + + // Fetch tasks scoped to this client's phases only + const phaseIds = phasesRows.map((p) => p.id); + const tasksRows = phaseIds.length === 0 + ? [] + : await db + .select() + .from(tasks) + .where(inArray(tasks.phase_id, phaseIds)) + .orderBy(tasks.sort_order); + + // Fetch deliverables scoped to this client's tasks only + const taskIds = tasksRows.map((t) => t.id); + const deliverables_rows = taskIds.length === 0 + ? [] + : await db + .select() + .from(deliverables) + .where(inArray(deliverables.task_id, taskIds)); + + // Fetch payments + const paymentsRows = await db + .select() + .from(payments) + .where(eq(payments.client_id, client.id)); + + // Fetch documents + const documentsRows = await db + .select() + .from(documents) + .where(eq(documents.client_id, client.id)); + + // Fetch notes + const notesRows = await db + .select() + .from(notes) + .where(eq(notes.client_id, client.id)) + .orderBy(notes.created_at); + + // Build hierarchical structure + const phasesList = phasesRows.map((phase) => { + const phaseTasksRows = tasksRows.filter((t) => t.phase_id === phase.id); + + const tasksList = phaseTasksRows.map((task) => { + const taskDeliverables = deliverables_rows + .filter((d) => d.task_id === task.id) + .map((d) => ({ + id: d.id, + title: d.title, + url: d.url, + status: d.status as 'pending' | 'submitted' | 'approved', + approved_at: d.approved_at ? new Date(d.approved_at).toISOString() : null, + })); + + return { + id: task.id, + title: task.title, + description: task.description, + status: task.status as 'todo' | 'in_progress' | 'done', + sort_order: task.sort_order, + deliverables: taskDeliverables, + }; + }); + + // Calculate progress for this phase + const taskCount = tasksList.length; + const doneCount = tasksList.filter((t) => t.status === 'done').length; + const progress_pct = taskCount === 0 ? 0 : Math.round((doneCount / taskCount) * 100); + + return { + id: phase.id, + title: phase.title, + status: phase.status as 'upcoming' | 'active' | 'done', + sort_order: phase.sort_order, + tasks: tasksList, + progress_pct, + }; + }); + + // Calculate global progress + const allTasks = phasesRows.flatMap((p) => + tasksRows.filter((t) => t.phase_id === p.id) + ); + const allDoneTasks = allTasks.filter((t) => t.status === 'done').length; + const globalProgressPct = allTasks.length === 0 ? 0 : Math.round((allDoneTasks / allTasks.length) * 100); + + // Map payments (do NOT expose amount — only label and status) + const paymentsList = paymentsRows.map((p) => ({ + id: p.id, + label: p.label, + status: p.status as 'da_saldare' | 'inviata' | 'saldato', + })); + + // Map documents + const documentsList = documentsRows.map((d) => ({ + id: d.id, + label: d.label, + url: d.url, + })); + + // Map notes + const notesList = notesRows.map((n) => ({ + id: n.id, + body: n.body, + created_at: new Date(n.created_at).toISOString(), + })); + + return { + client: { + id: client.id, + name: client.name, + brand_name: client.brand_name, + brief: client.brief, + accepted_total: client.accepted_total ?? '0', + }, + phases: phasesList, + payments: paymentsList, + documents: documentsList, + notes: notesList, + global_progress_pct: globalProgressPct, + }; + } + ``` + + Key points: + - `ClientView` interface explicitly omits admin data + - `getClientView()` never queries `quote_items`, `service_catalog`, or service prices + - Payments are returned WITHOUT amount (only label and status) + - All timestamps are ISO strings for JSON serialization + - Progress percentages are calculated server-side + + + test -f src/lib/client-view.ts && echo "client-view.ts exists" + grep -q "interface ClientView" src/lib/client-view.ts && echo "ClientView interface defined" + grep -q "export async function getClientView" src/lib/client-view.ts && echo "getClientView function exported" + ! grep -q "quote_items\|service_catalog" src/lib/client-view.ts && echo "quote_items not referenced (good)" + grep -q "inArray" src/lib/client-view.ts && echo "inArray scoping present" + grep -q "accepted_total.*?? '0'" src/lib/client-view.ts && echo "null coalescing on accepted_total" + npm run build 2>&1 | grep -v "warning" | grep -q "error" && echo "TypeScript errors" || echo "TypeScript OK" + + + - `src/lib/client-view.ts` exists with `ClientView` interface and `getClientView()` function + - Interface does NOT include quote_items, service_catalog, or individual service prices + - Payments are returned with only label and status (no amount) + - Function returns hierarchical data: client → phases → tasks → deliverables + - Progress percentages are calculated server-side + - TypeScript compiles without errors + + + + + Task 3: Create app/c/[token]/page.tsx Server Component to render client dashboard + + app/c/[token]/page.tsx + app/c/[token]/layout.tsx + + + src/lib/client-view.ts (ClientView interface) + + + Create `app/c/[token]/layout.tsx`: + + ```typescript + import type { Metadata } from 'next'; + + export const metadata: Metadata = { + title: 'Client Portal', + description: 'Project status dashboard', + }; + + export default function ClientLayout({ + children, + params, + }: { + children: React.ReactNode; + params: { token: string }; + }) { + return <>{children}; + } + ``` + + Create `app/c/[token]/page.tsx` (Server Component): + + ```typescript + import { getClientView } from '@/lib/client-view'; + import { notFound } from 'next/navigation'; + + export const revalidate = 60; // ISR: revalidate every 60 seconds + + export default async function ClientDashboard({ + params, + }: { + params: { token: string }; + }) { + const view = await getClientView(params.token); + + if (!view) { + notFound(); + } + + return ( +
+ {/* Placeholder: Dashboard will be built in Plan 04 */} +
+

{view.client.brand_name}

+

{view.client.brief}

+

Token: {params.token}

+
+
+ ); + } + ``` + + This page: + - Fetches ClientView data via `getClientView()` + - Uses Server Component (no Client Component overhead) + - Returns 404 if token not found + - Minimal placeholder content (full UI in Plan 04) + - ISR enabled: revalidates every 60 seconds so updates are visible within a minute +
+ + test -f app/c/\[token\]/page.tsx && echo "Client page route exists" + grep -q "export default async function" app/c/\[token\]/page.tsx && echo "Server Component syntax correct" + grep -q "getClientView" app/c/\[token\]/page.tsx && echo "getClientView is called" + grep -q "notFound()" app/c/\[token\]/page.tsx && echo "404 handling in place" + test -f app/c/\[token\]/layout.tsx && echo "Layout file exists" + + + - `app/c/[token]/page.tsx` exists as a Server Component + - `app/c/[token]/layout.tsx` exists with metadata + - Page calls `getClientView()` and renders minimal placeholder + - 404 is returned if view is null + - `npm run build` succeeds + +
+ +
+ + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| Client request → Middleware | Middleware validates token before any page renders; 404 on invalid token | +| Server Component → Database | getClientView() queries only client-safe fields; never queries quote_items | +| ClientView → Serialization | ClientView type prevents accidental inclusion of admin data in JSON responses | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Disposition | Mitigation Plan | +|-----------|----------|-----------|-------------|-----------------| +| T-03-001 | Information Disclosure | ClientView shape | mitigate | TypeScript interface enforces shape; admin data fields are never included; IDE warnings if field is accessed | +| T-03-002 | Tampering | Token parameter | mitigate | Middleware validates token before page renders; invalid tokens → 404 before DB state is exposed | +| T-03-003 | Denial of Service | getClientView() query | accept | Queries are indexed on client_id and token; no N+1 queries; Postgres will handle reasonable load | + + + + +After plan execution: +1. Run `npm run build` → no errors +2. Visit `http://localhost:3000/c/invalid-token` → should return 404 (after db is seeded) +3. Check `src/middleware.ts` → validates token at edge +4. Check `src/lib/client-view.ts` → ClientView interface does not expose quote_items +5. Check `app/c/[token]/page.tsx` → Server Component structure correct + + + +- Middleware validates tokens at the edge +- Server Component fetches ClientView data without exposing admin secrets +- Invalid tokens return 404 +- TypeScript enforces ClientView shape (no quote_items, no prices) +- Route is ready for UI rendering (Plan 04) +- Ready to proceed to Plan 04 (Dashboard UI) + + + +After completion, create `.planning/phases/01-foundation-client-dashboard/01-03-SUMMARY.md` + diff --git a/.planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-03-SUMMARY.md b/.planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-03-SUMMARY.md new file mode 100644 index 0000000..c222910 --- /dev/null +++ b/.planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-03-SUMMARY.md @@ -0,0 +1,149 @@ +--- +phase: 01-foundation-client-dashboard +plan: 03 +subsystem: client-portal +tags: [nextjs, middleware, drizzle-orm, client-view, server-component, edge-runtime] + +# Dependency graph +requires: + - 01-01 (Next.js bootstrap, DATABASE_URL, Drizzle client in src/db/index.ts) + - 01-02 (src/db/schema.ts — clients, phases, tasks, deliverables, payments, documents, notes) +provides: + - src/proxy.ts (Edge proxy per validazione token su /c/:path*) + - src/app/api/internal/validate-token/route.ts (Node.js route che interroga clients.token) + - src/lib/client-view.ts (ClientView interface + getClientView() function) + - src/app/c/[token]/page.tsx (Server Component placeholder per dashboard cliente) + - src/app/c/[token]/layout.tsx (Layout con metadata per rotte token-auth) +affects: + - 01-04-dashboard-ui (consuma ClientView interface e getClientView()) + - 01-05-seed-deploy (genera token, verifica accesso /c/[token]) + +# Tech tracking +tech-stack: + added: [] + patterns: + - "Edge proxy pattern: proxy.ts usa fetch() verso /api/internal/validate-token — nessun import Drizzle/postgres-js in Edge runtime" + - "Next.js 16 breaking change: file convention rinominata da 'middleware' a 'proxy'; export function rinominata da 'middleware' a 'proxy'" + - "Next.js 15+ breaking change: params in Server Component è Promise<{ token: string }> — await params prima dell'uso" + - "ClientView enforced server-side: interface TypeScript + query function che non tocca mai quote_items o service_catalog" + - "inArray() per scoping tasks/deliverables: previene full table scan su clienti che non appartengono alla sessione" + +key-files: + created: + - src/proxy.ts (proxy Edge-compatible — rinominato da middleware.ts per Next.js 16) + - src/app/api/internal/validate-token/route.ts (Node.js route, query clients.token via Drizzle) + - src/lib/client-view.ts (ClientView interface + getClientView() — 209 righe) + - src/app/c/[token]/page.tsx (Server Component placeholder — 28 righe) + - src/app/c/[token]/layout.tsx (Layout con metadata — 14 righe) + modified: [] + +key-decisions: + - "Next.js 16 richiede file 'proxy.ts' (non 'middleware.ts') ed export function 'proxy' (non 'middleware') — auto-corretto da Rule 1" + - "params nelle Server Component Next.js 15+ è Promise<{ token: string }> — await obbligatorio (breaking change)" + - "ClientView.payments non espone 'amount' — solo label e status — vincolo architetturale LOCKED rispettato" + - "getClientView() usa inArray() per scopare tasks e deliverables ai soli phase_id del cliente, evitando leak cross-client" + +# Metrics +duration: 25min +completed: 2026-05-14 +--- + +# Phase 1 Plan 03: Token Middleware + Client Portal Data Layer — Route /c/[token] operativa + +**Edge proxy con validazione token, ClientView type system che esclude quote_items, e Server Component che fetcha tutti i dati cliente senza esporre segreti admin. Build Next.js 16 senza errori TypeScript.** + +## Performance + +- **Duration:** ~25 min +- **Started:** 2026-05-13T22:50:00Z +- **Completed:** 2026-05-14T00:15:00Z +- **Tasks:** 3/3 +- **Files created:** 5 + +## Accomplishments + +- `src/proxy.ts`: proxy Edge-compatible per Next.js 16 — valida token via `fetch('/api/internal/validate-token')` senza import Drizzle/postgres-js. Matcher configurato su `/c/:path*`. Token non trovato → rewrite su `/not-found`. +- `src/app/api/internal/validate-token/route.ts`: route Node.js che interroga `clients.token` via Drizzle ORM. Ritorna `{ valid: true }` (200) o `{ valid: false }` (404/400/500). Drizzle funziona correttamente qui perché non è nel runtime Edge. +- `src/lib/client-view.ts`: `ClientView` interface che esclude esplicitamente `quote_items`, `service_catalog` e prezzi singoli. `getClientView()` esegue 6 query scoped (client by token → phases → tasks via inArray → deliverables via inArray → payments → documents → notes). Progress % calcolata server-side. `accepted_total: client.accepted_total ?? '0'`. +- `src/app/c/[token]/page.tsx`: Server Component con `await params`, chiama `getClientView(token)`, ritorna `notFound()` se null. ISR a 60s. +- `src/app/c/[token]/layout.tsx`: layout minimo con metadata. +- `npm run build` completato con successo — zero errori TypeScript, tutte le route presenti nel build output. + +## Task Commits + +1. **Task 1: Edge proxy + validate-token API route** — `ef34817` (feat) +2. **Task 2: ClientView type system + getClientView()** — `14787ba` (feat) +3. **Task 3: /c/[token] Server Component + layout** — `8b5e723` (feat, include deviazione Rule 1) + +## Files Created/Modified + +- `src/proxy.ts` — Edge proxy per /c/:path* (rinominato da middleware.ts — vedi Deviazioni) +- `src/app/api/internal/validate-token/route.ts` — Node.js route, query `eq(clients.token, token)`, risposta 200/404 +- `src/lib/client-view.ts` — ClientView interface (quote_items mai inclusi) + getClientView() con inArray scoping +- `src/app/c/[token]/page.tsx` — Server Component placeholder, notFound() su token invalido +- `src/app/c/[token]/layout.tsx` — Layout con metadata + +## Decisions Made + +- **Next.js 16 proxy convention:** La nuova convenzione rinomina `middleware.ts` → `proxy.ts` ed esige `export function proxy` (non `middleware`). La build ha segnalato il deprecation warning al primo tentativo. Risolto con rename + export update (Rule 1). +- **params come Promise:** Next.js 15+ tratta `params` come `Promise<{ token: string }>` nelle Server Component. Il piano usava la sintassi Next.js 14 sincrona — aggiornato ad `await params` per conformità (Rule 1). +- **ClientView.payments senza amount:** Il campo `amount` di `payments` è intenzionalmente omesso dalla `ClientView`. Il cliente vede solo `label` e `status`. Vincolo architetturale LOCKED rispettato a livello di query. + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 1 - Bug] Next.js 16 depreca la convenzione 'middleware' in favore di 'proxy'** +- **Found during:** Task 3 — `npm run build` ha emesso warning e poi errore: "Proxy is missing expected function export name" +- **Issue:** Next.js 16 ha rinominato la file convention da `middleware.ts` a `proxy.ts` e richiede che la funzione esportata si chiami `proxy` (o `default`), non `middleware`. +- **Fix:** Rinominato `src/middleware.ts` → `src/proxy.ts`; rinominato `export async function middleware` → `export async function proxy`. Il commit ef34817 conteneva middleware.ts — il Task 3 commit 8b5e723 include la modifica. +- **Files modified:** src/proxy.ts (rinominato da src/middleware.ts) +- **Commit:** 8b5e723 + +**2. [Rule 1 - Bug] params come Promise in Next.js 15+ Server Component** +- **Found during:** Task 3 — il piano usava la sintassi `params: { token: string }` di Next.js 14 +- **Issue:** In Next.js 15+, i `params` nelle Server Component sono `Promise<{ token: string }>`. Usare la sintassi sincrona avrebbe causato TypeScript error e comportamento errato a runtime. +- **Fix:** Tipizzato `params: Promise<{ token: string }>` e aggiunto `const { token } = await params;` prima dell'uso. +- **Files modified:** src/app/c/[token]/page.tsx +- **Commit:** 8b5e723 + +## Known Stubs + +La `page.tsx` mostra un placeholder minimo (brand_name, brief, token). Questo è **intenzionale e documentato nel piano**: "Placeholder content — full UI in Plan 04". Il Plan 04 (Dashboard UI) sostituirà questo placeholder con la UI completa. Il goal del piano (route operativa + data fetching corretto) è raggiunto pienamente. + +## Threat Surface Scan + +Nessuna nuova superficie di sicurezza non prevista dal threat model 01-03: +- T-03-001 (ClientView shape): mitigato — interface TypeScript + getClientView() non tocca mai quote_items/service_catalog +- T-03-002 (Token parameter): mitigato — proxy valida il token prima che qualsiasi pagina venga renderizzata; token invalidi → /not-found +- T-03-003 (DoS su getClientView): accettato — query indicizzate su client_id/token + +L'API route `/api/internal/validate-token` è accessibile pubblicamente (nessun secret header). Questo è intenzionale: ritorna solo `{ valid: boolean }` — nessun dato cliente esposto. Un attaccante può enumerare token validi con brute force ma: (1) nanoid 21 chars offre ~126 bit di entropia, rendendo il brute force computazionalmente impossibile; (2) nessun dato sensibile è esposto dalla route. + +## Self-Check + +- [x] `src/proxy.ts` esiste (edge proxy — rinominato da middleware.ts) +- [x] `src/app/api/internal/validate-token/route.ts` esiste (query clients.token via Drizzle) +- [x] `src/lib/client-view.ts` esiste (ClientView interface + getClientView()) +- [x] `src/app/c/[token]/page.tsx` esiste (Server Component con await params) +- [x] `src/app/c/[token]/layout.tsx` esiste +- [x] Commit `ef34817` esiste (Task 1) +- [x] Commit `14787ba` esiste (Task 2) +- [x] Commit `8b5e723` esiste (Task 3) +- [x] `npm run build` completato senza errori TypeScript +- [x] Build output mostra `/api/internal/validate-token` (Dynamic) e `/c/[token]` (Dynamic) +- [x] `proxy.ts` NON importa `@/db` o `drizzle-orm` (Edge safe) +- [x] `client-view.ts` non contiene query su `quote_items` o `service_catalog` +- [x] `accepted_total: client.accepted_total ?? '0'` presente + +## Self-Check: PASSED + +## Next Phase Readiness + +- Plan 04 (Dashboard UI) può partire — `getClientView()` è disponibile e testa TypeScript OK +- Import pattern: `import { getClientView, type ClientView } from '@/lib/client-view'` +- La route `/c/[token]` è operativa — con un cliente seedato da Plan 05 sarà accessibile + +--- +*Phase: 01-foundation-client-dashboard* +*Completed: 2026-05-14* \ No newline at end of file diff --git a/.planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-04-PLAN.md b/.planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-04-PLAN.md new file mode 100644 index 0000000..b658e95 --- /dev/null +++ b/.planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-04-PLAN.md @@ -0,0 +1,864 @@ +--- +phase: "01-foundation-client-dashboard" +plan: 04 +type: execute +wave: 2 +depends_on: + - "01-01" + - "01-02" + - "01-03" +files_modified: + - app/c/[token]/page.tsx + - src/components/client-dashboard.tsx + - src/components/phase-timeline.tsx + - src/components/payment-status.tsx + - src/components/documents-section.tsx + - src/components/notes-section.tsx + - src/app/globals.css + - tailwind.config.ts +autonomous: true +requirements: + - DASH-02 + - DASH-03 + - DASH-04 + - DASH-07 + - DASH-08 + - DASH-09 + - DASH-10 + +must_haves: + truths: + - "Client dashboard displays client brand name prominently with iamcavalli logo in corner" + - "Global progress bar at top shows % of all tasks completed" + - "Phases are displayed as lateral timeline (left indicator, content right)" + - "Each phase shows progress bar (% from completed tasks) + task list with status badges" + - "Tasks are nested within phases with status visible (todo/in_progress/done)" + - "Payment section always visible: accepted_total + Acconto 50% status + Saldo 50% status (NO amounts)" + - "Document links are clickable (opens external URL)" + - "Notes/decision log is visible (read-only, may be empty)" + - "Layout is mobile-responsive and light & clean visual style" + artifacts: + - path: "app/c/[token]/page.tsx" + provides: "Server Component rendering ClientDashboard" + min_lines: 20 + - path: "src/components/client-dashboard.tsx" + provides: "Layout wrapper + main sections (header, progress, phases, payments, documents, notes)" + min_lines: 50 + - path: "src/components/phase-timeline.tsx" + provides: "Lateral timeline rendering with phase cards and task lists" + min_lines: 80 + - path: "src/components/payment-status.tsx" + provides: "Payment section: accepted_total + 2 payment rows with status" + min_lines: 30 + - path: "src/components/documents-section.tsx" + provides: "List of external document links" + min_lines: 20 + - path: "src/components/notes-section.tsx" + provides: "Read-only notes list with timestamps" + min_lines: 20 + - path: "tailwind.config.ts" + provides: "Light & clean design tokens (updated from bootstrap)" + contains: "colors" + key_links: + - from: "app/c/[token]/page.tsx" + to: "ClientDashboard component" + via: "import { ClientDashboard }" + pattern: " +**Client Dashboard UI — Vertical Slice:** Render the complete client dashboard with all UI sections: header with branding, global progress bar, lateral phase timeline, task lists with status, payment status section, external document links, and read-only notes log. Implement light & clean visual style with mobile-first responsive design using Tailwind CSS and shadcn/ui components. + +Purpose: Deliver the core user-facing product: a client can open their secret link and see the complete project status at a glance, with clear progress indicators, task hierarchy, payment overview, and documents. + +Output: Fully rendered client portal with all DASH-02 through DASH-10 requirements implemented in the UI. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/phases/01-foundation-client-dashboard/01-CONTEXT.md (Decisions D-04 through D-12) +@.planning/phases/01-foundation-client-dashboard/01-03-SUMMARY.md +@src/lib/client-view.ts (ClientView interface) + + + + + + Task 1: Configure design tokens (tailwind.config.ts + globals.css) and wire app/c/[token]/page.tsx to ClientDashboard + + tailwind.config.ts + src/app/globals.css + app/c/[token]/page.tsx + + + tailwind.config.ts (current bootstrap) + src/app/globals.css (current bootstrap) + src/components/client-dashboard.tsx (will exist after Task 2 — read after Task 2 completes) + src/lib/client-view.ts (ClientView interface) + + + Update `tailwind.config.ts` to define light & clean design tokens: + + ```typescript + import type { Config } from 'tailwindcss'; + + const config: Config = { + content: [ + './src/pages/**/*.{js,ts,jsx,tsx,mdx}', + './src/components/**/*.{js,ts,jsx,tsx,mdx}', + './src/app/**/*.{js,ts,jsx,tsx,mdx}', + ], + theme: { + extend: { + colors: { + // Light & clean palette + 'primary': '#1a1a1a', // deep charcoal for text + 'secondary': '#666666', // medium gray for secondary text + 'tertiary': '#999999', // light gray for hints + 'bg-light': '#ffffff', // pure white + 'bg-subtle': '#f9f9f9', // very light gray + 'border-light': '#e5e5e5', // subtle border + 'accent': '#0066cc', // blue accent (will be brand-aware in Phase 2) + 'success': '#22c55e', // green for done + 'warning': '#eab308', // yellow for in-progress + 'info': '#3b82f6', // blue for pending + }, + spacing: { + 'xs': '0.5rem', + 'sm': '1rem', + 'md': '1.5rem', + 'lg': '2rem', + 'xl': '3rem', + }, + fontSize: { + 'xs': '0.75rem', + 'sm': '0.875rem', + 'base': '1rem', + 'lg': '1.125rem', + 'xl': '1.25rem', + '2xl': '1.5rem', + '3xl': '1.875rem', + }, + fontFamily: { + 'sans': [ + 'system-ui', + '-apple-system', + 'BlinkMacSystemFont', + '"Segoe UI"', + 'Roboto', + '"Helvetica Neue"', + 'sans-serif', + ], + }, + }, + }, + plugins: [], + }; + + export default config; + ``` + + Update `src/app/globals.css`: + + ```css + @tailwind base; + @tailwind components; + @tailwind utilities; + + * { + margin: 0; + padding: 0; + box-sizing: border-box; + } + + html { + scroll-behavior: smooth; + } + + body { + @apply bg-white text-primary font-sans; + line-height: 1.6; + } + + h1 { + @apply text-3xl font-bold tracking-tight; + } + + h2 { + @apply text-2xl font-bold; + } + + h3 { + @apply text-xl font-semibold; + } + + p { + @apply text-base text-secondary; + } + + a { + @apply text-accent hover:underline transition-colors; + } + + .border-subtle { + @apply border border-border-light; + } + + .bg-subtle { + @apply bg-bg-subtle; + } + ``` + + Update `app/c/[token]/page.tsx` to replace the Plan 03 placeholder with the full ClientDashboard render: + + ```typescript + import { getClientView } from '@/lib/client-view'; + import { ClientDashboard } from '@/components/client-dashboard'; + import { notFound } from 'next/navigation'; + + export const revalidate = 60; + + export async function generateMetadata({ + params, + }: { + params: { token: string }; + }) { + const view = await getClientView(params.token); + + if (!view) { + return { title: 'Not Found' }; + } + + return { + title: `${view.client.brand_name} — Project Status | iamcavalli`, + description: view.client.brief || 'Project status dashboard', + }; + } + + export default async function ClientPage({ + params, + }: { + params: { token: string }; + }) { + const view = await getClientView(params.token); + + if (!view) { + notFound(); + } + + return ; + } + ``` + + Note: `getClientView` is called twice (once in `generateMetadata`, once in `ClientPage`). Next.js 15 deduplicates fetch calls within the same render, and since this is a DB query via Drizzle (not fetch), use React `cache()` in `client-view.ts` if double-call is a concern — acceptable for Phase 1 given low traffic. + + + grep -q "colors:" tailwind.config.ts && echo "Color tokens defined" + grep -q "primary\|accent\|success" tailwind.config.ts && echo "Key colors present" + grep -q "@tailwind" src/app/globals.css && echo "Tailwind directives in globals.css" + grep -q "ClientDashboard" app/c/\[token\]/page.tsx && echo "ClientDashboard wired in page" + grep -q "generateMetadata" app/c/\[token\]/page.tsx && echo "Dynamic metadata present" + + + - `tailwind.config.ts` contains color tokens: primary, secondary, accent, success, warning + - `globals.css` includes Tailwind directives and base typography + - `app/c/[token]/page.tsx` renders `` with dynamic metadata + - 404 returned if token invalid + - `npm run build` succeeds + + + + + Task 2: Create ClientDashboard wrapper component with header, global progress, and section layout + + src/components/client-dashboard.tsx + + + src/lib/client-view.ts (ClientView interface) + .planning/phases/01-foundation-client-dashboard/01-CONTEXT.md (D-06 through D-10) + + + Create `src/components/client-dashboard.tsx`: + + ```typescript + 'use client'; + + import { ClientView } from '@/lib/client-view'; + import { Progress } from '@/components/ui/progress'; + import { PhaseTimeline } from './phase-timeline'; + import { PaymentStatus } from './payment-status'; + import { DocumentsSection } from './documents-section'; + import { NotesSection } from './notes-section'; + + interface ClientDashboardProps { + view: ClientView; + } + + export function ClientDashboard({ view }: ClientDashboardProps) { + return ( +
+ {/* Header: Logo + Brand Name */} +
+
+
+ {/* iamcavalli logo (small, corner) */} +
iamcavalli
+ + {/* Client brand name (prominent) */} +

+ {view.client.brand_name} +

+ + {/* Spacer for balance */} +
+
+
+
+ + {/* Global Progress Bar */} +
+
+
+

Project Progress

+ +

+ {view.global_progress_pct}% Complete +

+
+
+
+ + {/* Main Content */} +
+ {/* Brief */} + {view.client.brief && ( +
+

+ "{view.client.brief}" +

+
+ )} + + {/* Phase Timeline */} +
+

Project Phases

+ +
+ + {/* Payment Status */} +
+

Payment Status

+ +
+ + {/* Documents */} + {view.documents.length > 0 && ( +
+

Documents & Files

+ +
+ )} + + {/* Notes / Decision Log */} + {view.notes.length > 0 && ( +
+

Notes & Decisions

+ +
+ )} +
+ + {/* Footer */} +
+
+

+ This is a private project dashboard. Do not share your unique link. +

+
+
+
+ ); + } + ``` + + Key points: + - Header: small "iamcavalli" logo (top-left), client brand_name centered (prominent) + - Global progress bar shows % of all tasks done + - Section headers are h2 (consistent sizing) + - Responsive layout: max-width container with mobile padding + - Brief is quoted and italicized + - Documents and Notes sections show only if data exists +
+ + test -f src/components/client-dashboard.tsx && echo "ClientDashboard component exists" + grep -q "export function ClientDashboard" src/components/client-dashboard.tsx && echo "Component exported" + grep -q "iamcavalli" src/components/client-dashboard.tsx && echo "Logo text present" + grep -q "brand_name" src/components/client-dashboard.tsx && echo "Brand name rendered" + grep -q "global_progress_pct" src/components/client-dashboard.tsx && echo "Progress bar displays" + + + - Component is exported and accepts ClientView props + - Header displays iamcavalli logo (small) + brand_name (prominent) + - Global progress bar shows project completion % + - Main sections: brief, phases, payments, documents (conditional), notes (conditional) + - Responsive layout with max-width container + +
+ + + Task 3: Create PhaseTimeline component for lateral timeline layout with task lists + + src/components/phase-timeline.tsx + + + src/lib/client-view.ts (phase and task structure) + .planning/phases/01-foundation-client-dashboard/01-CONTEXT.md (D-07, D-08) + + + Create `src/components/phase-timeline.tsx`: + + ```typescript + 'use client'; + + import { ClientView } from '@/lib/client-view'; + import { Progress } from '@/components/ui/progress'; + import { Card } from '@/components/ui/card'; + import { Badge } from '@/components/ui/badge'; + import { CheckCircle2, Circle, Clock } from 'lucide-react'; + + interface PhaseTimelineProps { + phases: ClientView['phases']; + } + + export function PhaseTimeline({ phases }: PhaseTimelineProps) { + return ( +
+ {phases.map((phase, index) => ( +
+ {/* Left: Timeline Indicator */} +
+ {/* Circle indicator */} +
+ {phase.status === 'done' ? ( + + ) : phase.status === 'active' ? ( + + ) : ( + + )} +
+ + {/* Vertical line (not on last) */} + {index < phases.length - 1 && ( +
+ )} +
+ + {/* Right: Phase Content */} +
+ {/* Phase Card */} + + {/* Phase Header */} +
+

+ {phase.title} +

+ + {phase.status === 'upcoming' ? 'Upcoming' : + phase.status === 'active' ? 'In Progress' : 'Done'} + +
+ + {/* Phase Progress Bar */} +
+
+

+ Phase Progress +

+

+ {phase.progress_pct}% +

+
+ +
+ + {/* Task List */} +
+

+ Tasks ({phase.tasks.filter(t => t.status === 'done').length} of {phase.tasks.length}) +

+ {phase.tasks.length === 0 ? ( +

No tasks yet

+ ) : ( +
    + {phase.tasks.map((task) => ( +
  • + {/* Task Status Icon */} + {task.status === 'done' ? ( + + ) : task.status === 'in_progress' ? ( + + ) : ( + + )} + + {/* Task Content */} +
    +

    + {task.title} +

    + {task.description && ( +

    + {task.description} +

    + )} + {/* Deliverables */} + {task.deliverables.length > 0 && ( +
    + {task.deliverables.map((d) => ( +
    + + {d.title} + + {d.status === 'approved' && ( + + Approved + + )} +
    + ))} +
    + )} +
    +
  • + ))} +
+ )} +
+
+
+
+ ))} +
+ ); + } + ``` + + Key points: + - Left indicator: circle with icon (checkmark for done, dot for upcoming/active) + - Vertical line connects phases (not on last phase) + - Right content: phase card with title, status badge, progress bar, task list + - Task status shown with icons and colors (success/warning/info) + - Deliverables nested under tasks with "Approved" badge if applicable + - Empty state if phase has no tasks + + + test -f src/components/phase-timeline.tsx && echo "PhaseTimeline component exists" + grep -q "export function PhaseTimeline" src/components/phase-timeline.tsx && echo "Component exported" + grep -q "CheckCircle2\|Circle" src/components/phase-timeline.tsx && echo "Icons imported" + grep -q "progress_pct" src/components/phase-timeline.tsx && echo "Progress bar displays" + + + - Component renders lateral timeline layout + - Each phase shows: title, status badge, progress bar, task count + - Tasks show status with icons (checkmark/circle) + - Deliverables are nested and show "Approved" badge if applicable + - Empty state for phases with no tasks + + + + + Task 4: Create PaymentStatus component (accepted_total + payment rows with status badges) + + src/components/payment-status.tsx + + + src/lib/client-view.ts (payments shape, PaymentStatus type) + .planning/phases/01-foundation-client-dashboard/01-CONTEXT.md (D-10, D-11) + + + Create `src/components/payment-status.tsx`: + + ```typescript + 'use client'; + + import { Card } from '@/components/ui/card'; + import { Badge } from '@/components/ui/badge'; + import { ClientView } from '@/lib/client-view'; + import { CheckCircle2, Clock, AlertCircle } from 'lucide-react'; + + interface PaymentStatusProps { + accepted_total: string; + payments: ClientView['payments']; + } + + export function PaymentStatus({ accepted_total, payments }: PaymentStatusProps) { + const statusConfig = { + da_saldare: { color: 'bg-info', icon: Clock, label: 'Da Saldare', text: 'white' }, + inviata: { color: 'bg-warning', icon: AlertCircle, label: 'Inviata', text: 'white' }, + saldato: { color: 'bg-success', icon: CheckCircle2, label: 'Saldato', text: 'white' }, + }; + + return ( + + {/* Total */} +
+

+ Totale Preventivo Accettato +

+

+ €{parseFloat(accepted_total || '0').toLocaleString('it-IT', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} +

+
+ + {/* Payment Rows */} +
+ {payments.map((payment) => { + const config = statusConfig[payment.status as keyof typeof statusConfig]; + const Icon = config?.icon || Clock; + + return ( +
+
+ +

+ {payment.label} +

+
+ + {config?.label || payment.status} + +
+ ); + })} +
+ + {/* Note */} +

+ I pagamenti sono suddivisi in due rate da 50% ciascuna. + Contattaci per domande sui dettagli. +

+
+ ); + } + ``` + + Key points: + - Shows `accepted_total` formatted as Euro currency — NEVER individual line-item amounts + - Two payment rows (Acconto 50%, Saldo 50%) with status badges only + - Status badge colors: da_saldare = blue, inviata = yellow, saldato = green + - Card + Badge from shadcn/ui +
+ + test -f src/components/payment-status.tsx && echo "PaymentStatus component exists" + grep -q "export function PaymentStatus" src/components/payment-status.tsx && echo "Component exported" + grep -q "accepted_total" src/components/payment-status.tsx && echo "Total displayed" + grep -q "da_saldare\|inviata\|saldato" src/components/payment-status.tsx && echo "Status config present" + + + - Component exists and is exported + - Displays accepted_total formatted as Euro (no individual amounts) + - Renders payment rows with status badges (da_saldare/inviata/saldato) + - Uses shadcn/ui Card and Badge + +
+ + + Task 5: Create DocumentsSection and NotesSection components (external links + read-only notes) + + src/components/documents-section.tsx + src/components/notes-section.tsx + + + src/lib/client-view.ts (documents and notes shapes) + .planning/phases/01-foundation-client-dashboard/01-CONTEXT.md (D-12) + + + Create `src/components/documents-section.tsx`: + + ```typescript + 'use client'; + + import { ClientView } from '@/lib/client-view'; + import { Card } from '@/components/ui/card'; + import { ExternalLink } from 'lucide-react'; + + interface DocumentsSectionProps { + documents: ClientView['documents']; + } + + export function DocumentsSection({ documents }: DocumentsSectionProps) { + return ( +
+ {documents.map((doc) => ( + + + + {doc.label} + + + + + ))} +
+ ); + } + ``` + + Create `src/components/notes-section.tsx`: + + ```typescript + 'use client'; + + import { ClientView } from '@/lib/client-view'; + import { Card } from '@/components/ui/card'; + + interface NotesSectionProps { + notes: ClientView['notes']; + } + + export function NotesSection({ notes }: NotesSectionProps) { + if (notes.length === 0) { + return ( +

+ No notes yet. Decisions will appear here as they are made. +

+ ); + } + + return ( +
+ {notes.map((note) => ( + +

+ {note.body} +

+

+ {new Date(note.created_at).toLocaleDateString('it-IT', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + })} +

+
+ ))} +
+ ); + } + ``` + + Key points: + - DocumentsSection: clickable external links with ExternalLink icon, `rel="noopener noreferrer"` for security + - NotesSection: read-only, client never writes (admin writes in Phase 2 admin area) + - NotesSection: empty state shown as italic hint when no notes exist + - Timestamps formatted in Italian locale +
+ + test -f src/components/documents-section.tsx && echo "DocumentsSection component exists" + test -f src/components/notes-section.tsx && echo "NotesSection component exists" + grep -q "export function DocumentsSection" src/components/documents-section.tsx && echo "DocumentsSection exported" + grep -q "export function NotesSection" src/components/notes-section.tsx && echo "NotesSection exported" + grep -q "noopener noreferrer" src/components/documents-section.tsx && echo "External link security present" + + + - Both components exist and are exported + - DocumentsSection renders clickable external links with ExternalLink icon and secure rel attributes + - NotesSection shows read-only notes with Italian-formatted timestamps + - NotesSection shows empty state hint when notes array is empty + +
+ + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| Client browser → CSS/HTML | UI rendering is client-safe; no admin secrets in HTML source | +| Link click → External URL | External document links open in new tab with `rel="noopener noreferrer"` | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Disposition | Mitigation Plan | +|-----------|----------|-----------|-------------|-----------------| +| T-04-001 | Information Disclosure | Payment amounts | mitigate | Payments row shows status only; amounts never rendered on client dashboard | +| T-04-002 | Tampering | External links | accept | Links are user-provided URLs; client-side link validation (hostname check) could be added in Phase 2 | +| T-04-003 | Denial of Service | Image rendering | accept | Dashboard contains only text and icons; no resource-heavy assets | + + + + +After plan execution: +1. Run `npm run build` → no errors +2. Verify all component files exist: client-dashboard, phase-timeline, payment-status, documents-section, notes-section +3. Check page rendering logic in `app/c/[token]/page.tsx` +4. Verify mobile responsiveness: layout scales correctly on narrow screens +5. Check that payment amounts are NOT displayed (only status) + + + +- All UI components are created and exported +- Client dashboard renders complete project status +- Global progress bar and per-phase progress bars display correctly +- Payment section shows only status (no amounts) +- Document links are clickable +- Notes section shows read-only list (or empty state) +- Layout is responsive and uses light & clean design +- Mobile-first design works on small screens +- Ready to proceed to Plan 05 (Seed script + DNS) + + + +After completion, create `.planning/phases/01-foundation-client-dashboard/01-04-SUMMARY.md` + diff --git a/.planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-04-SUMMARY.md b/.planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-04-SUMMARY.md new file mode 100644 index 0000000..1f0cd55 --- /dev/null +++ b/.planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-04-SUMMARY.md @@ -0,0 +1,167 @@ +--- +phase: 01-foundation-client-dashboard +plan: 04 +subsystem: client-portal-ui +tags: [nextjs, tailwind-v4, shadcn-ui, server-components, client-dashboard, responsive] + +requires: + - 01-01 (Next.js 16 bootstrap, Tailwind v4, shadcn/ui) + - 01-02 (schema DB: clients, phases, tasks, deliverables, payments, documents, notes) + - 01-03 (ClientView interface + getClientView(), route /c/[token] operativa) +provides: + - src/components/client-dashboard.tsx (layout wrapper completo) + - src/components/phase-timeline.tsx (timeline laterale con progress bar per fase) + - src/components/payment-status.tsx (stato pagamenti senza importi singoli) + - src/components/documents-section.tsx (link documenti esterni) + - src/components/notes-section.tsx (log decisioni read-only) + - src/app/globals.css (design token Tailwind v4 — palette light & clean) + - app/c/[token]/page.tsx (Server Component che renderizza ClientDashboard) +affects: + - 01-05-seed-deploy (la dashboard e' completa — il seed popola i dati, il deploy la espone) + +tech-stack: + added: [] + patterns: + - "Tailwind v4: design token via @theme inline in globals.css (NON tailwind.config.ts)" + - "Colori arbitrari inline con sintassi [#hex] — compatibile Tailwind v4" + - "SVG inline al posto di lucide-react — compatibilita' massima con bundle size ridotto" + - "Server Components puri per tutti i componenti dashboard (nessun 'use client')" + - "React.cache() in page.tsx per deduplicare getClientView() tra generateMetadata e render" + +key-files: + created: + - src/components/client-dashboard.tsx (99 righe — wrapper con header, progress, sezioni) + - src/components/phase-timeline.tsx (201 righe — timeline laterale con task e deliverable) + - src/components/payment-status.tsx (98 righe — totale + righe stato, zero importi singoli) + - src/components/documents-section.tsx (75 righe — link esterni sicuri) + - src/components/notes-section.tsx (68 righe — log decisioni read-only) + modified: + - src/app/globals.css (token Tailwind v4: primary, secondary, tertiary, bg-subtle, border-light, accent, success, warning, info) + - src/app/c/[token]/page.tsx (import ClientDashboard, generateMetadata dinamico, React.cache) + +key-decisions: + - "Tailwind v4 usa @theme in globals.css — tailwind.config.ts non esiste in questo progetto" + - "SVG inline invece di lucide-react — evita dipendenza da icone e garantisce compatibilita'" + - "Colori arbitrari [#hex] in classi Tailwind invece di classi custom — piu' esplicito e manutenibile" + - "Server Components puri — nessun 'use client' necessario per componenti read-only" + - "React.cache() per deduplicare le due chiamate getClientView in generateMetadata + ClientPage" + +duration: 45min +completed: 2026-05-14 +--- + +# Phase 1 Plan 04: Client Dashboard UI — Vertical Slice completo + +**Tutti i componenti UI della dashboard cliente renderizzati come Server Components con design light & clean in Tailwind v4: header con logo iamcavalli + brand name cliente, progress bar globale, timeline laterale delle fasi con barre per fase e task list, sezione pagamenti con badge stato (zero importi singoli), link documenti esterni e log note read-only.** + +## Performance + +- **Duration:** ~45 min +- **Started:** 2026-05-14T19:35:00Z +- **Completed:** 2026-05-14T20:20:00Z +- **Tasks:** 5/5 +- **Files creati:** 5 +- **Files modificati:** 2 + +## Accomplishments + +- **globals.css**: Token di design Tailwind v4 via `@theme inline` — palette light & clean con 9 variabili colore (primary, secondary, tertiary, bg-subtle, border-light, accent, success, warning, info). Adattamento critico: il progetto usa Tailwind v4 che non ha `tailwind.config.ts`. + +- **app/c/[token]/page.tsx**: Server Component aggiornato con `ClientDashboard`, `generateMetadata` dinamico (titolo con brand_name), `React.cache()` per deduplicare le due chiamate a `getClientView`. + +- **client-dashboard.tsx**: Layout wrapper completo. Header sticky con "iamcavalli" in angolo sinistro (xs, tracking-widest) e `brand_name` centrato e prominente (D-06). Progress bar globale con percentuale (D-09). Brief con accent bar sinistra. Sezioni ordinate: PhaseTimeline, PaymentStatus (sempre visibile — D-10), Documents e Notes (condizionali). Footer con avviso link privato. + +- **phase-timeline.tsx**: Timeline laterale a due colonne (D-07). Colonna sinistra: cerchio con icona SVG per stato (checkmark verde per done, cerchio pieno blu per active, cerchio vuoto grigio per upcoming) + linea verticale tra fasi. Colonna destra: Card con badge stato, progress bar per fase con contatore "X di N task" (D-08), task list con icone stato e line-through per done. Deliverable annidati con badge "Approvato". + +- **payment-status.tsx**: Card con `accepted_total` in EUR come unico importo visibile (vincolo LOCKED). Righe pagamento con dot colorato + badge stato semantico (blu=da_saldare, giallo=inviata, verde=saldato) — MAI importi singoli (T-04-001 mitigato, D-11 rispettato). + +- **documents-section.tsx**: Link esterni con `target="_blank" rel="noopener noreferrer"` (T-04-002). Icone SVG inline per documento ed external link. Hover state con transizione colore accent. + +- **notes-section.tsx**: Note read-only con timestamp in locale it-IT. Empty state informativo. Server Component puro (D-12: admin scrive in Phase 2, cliente legge). + +- **npm run build**: completato senza errori TypeScript. 1 warning CSS da Lightning CSS optimizer (selettore con caratteri speciali) — noto, non bloccante, non dipendente dal nostro codice. + +## Task Commits + +1. **Task 1: Design tokens + wire page.tsx** — `4e703d7` +2. **Task 2: ClientDashboard wrapper** — `debd391` +3. **Task 3: PhaseTimeline** — `5d5c8ea` +4. **Task 4: PaymentStatus** — `a4e2de0` +5. **Task 5: DocumentsSection + NotesSection** — `8602bfa` + +## Files Created/Modified + +- `src/app/globals.css` — @theme con 9 token colore light & clean +- `src/app/c/[token]/page.tsx` — ClientDashboard + generateMetadata + React.cache +- `src/components/client-dashboard.tsx` — layout wrapper completo +- `src/components/phase-timeline.tsx` — timeline laterale con progress per fase +- `src/components/payment-status.tsx` — totale accettato + badge stato (nessun importo) +- `src/components/documents-section.tsx` — link esterni sicuri +- `src/components/notes-section.tsx` — note read-only con timestamp italiano + +## Decisions Made + +- **Tailwind v4 senza tailwind.config.ts:** Il piano originale assumeva Tailwind v3 con `tailwind.config.ts`. Il progetto usa Tailwind v4 che gestisce i token via `@theme inline` in globals.css. Adattamento automatico. +- **SVG inline invece di lucide-react:** Lucide-react v1.14 ha alcune icone con nomi diversi. Usare SVG inline elimina la dipendenza ed e' compatibile con Server Components senza `'use client'`. +- **Server Components puri:** I componenti sono tutti read-only e non usano hooks React — nessun `'use client'` necessario, ottimizzazione del bundle. +- **Colori arbitrari [#hex]:** Usare classi come `text-[#1a1a1a]` invece di classi custom — piu' esplicito, nessun conflitto con shadcn/ui che usa le proprie variabili CSS (`bg-card`, `text-muted-foreground`, etc.). + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 1 - Bug] tailwind.config.ts non esiste — Tailwind v4 usa @theme in globals.css** +- **Found during:** Task 1 +- **Issue:** Il piano indicava di aggiornare `tailwind.config.ts` con i color token. Questo file non esiste perche' il progetto usa Tailwind v4, che utilizza CSS `@theme inline` in `globals.css` invece del file di configurazione JavaScript. +- **Fix:** Token definiti in `globals.css` via `@theme inline { --color-primary: #1a1a1a; ... }`. Tailwind v4 mappa automaticamente queste variabili come classi utilitarie. +- **Files modified:** src/app/globals.css +- **Commit:** 4e703d7 + +**2. [Rule 1 - Bug] lucide-react usato come 'use client' — rimpiazzato con SVG inline** +- **Found during:** Task 2/3 (design) +- **Issue:** Il piano importava `CheckCircle2`, `Circle`, `Clock`, `ExternalLink` da `lucide-react`. I componenti sono Server Components puri — aggiungere `'use client'` solo per le icone avrebbe spostato tutto il rendering lato client inutilmente. +- **Fix:** SVG inline (Heroicons style) al posto di lucide-react per tutti i componenti. Mantiene i componenti come Server Components puri. +- **Files modified:** phase-timeline.tsx, payment-status.tsx, documents-section.tsx +- **Commit:** 5d5c8ea, a4e2de0, 8602bfa + +## Known Stubs + +Nessuno stub. Tutti i componenti ricevono dati reali dalla `ClientView` e li renderizzano completamente. Gli empty state (no documenti, no note) sono stati implementati come stati legittimi — non stub. + +## Threat Surface Scan + +| Flag | File | Description | +|------|------|-------------| +| Verificato T-04-001 | payment-status.tsx | `amount` assente dalla query e dal componente — solo `accepted_total` e `status` per riga | +| Verificato T-04-002 | documents-section.tsx | `rel="noopener noreferrer"` su tutti i link esterni | + +Nessuna nuova superficie di sicurezza non prevista dal threat model 01-04. + +## Self-Check: PASSED + +- [x] `src/app/globals.css` esiste con @theme e token colore +- [x] `src/app/c/[token]/page.tsx` renderizza ClientDashboard con generateMetadata +- [x] `src/components/client-dashboard.tsx` esiste (99 righe) +- [x] `src/components/phase-timeline.tsx` esiste (201 righe) +- [x] `src/components/payment-status.tsx` esiste (98 righe) — nessun campo `amount` +- [x] `src/components/documents-section.tsx` esiste (75 righe) +- [x] `src/components/notes-section.tsx` esiste (68 righe) +- [x] Commit `4e703d7` esiste (Task 1) +- [x] Commit `debd391` esiste (Task 2) +- [x] Commit `5d5c8ea` esiste (Task 3) +- [x] Commit `a4e2de0` esiste (Task 4) +- [x] Commit `8602bfa` esiste (Task 5) +- [x] `npm run build` completato senza errori TypeScript +- [x] `payment-status.tsx` non contiene il campo `amount` +- [x] `documents-section.tsx` contiene `rel="noopener noreferrer"` + +## Next Phase Readiness + +- Plan 05 (Seed script + DNS) puo' partire +- La dashboard e' pienamente funzionale: basta un cliente seedato per vederla operativa +- Il seed script deve inserire client, phases, tasks, deliverables, payments, documents, notes +- La route /c/[token] e' operativa dal Plan 03 — Plan 05 aggiunge il seed + configurazione DNS + +--- +*Phase: 01-foundation-client-dashboard* +*Completed: 2026-05-14* \ No newline at end of file diff --git a/.planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-05-PLAN.md b/.planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-05-PLAN.md new file mode 100644 index 0000000..9876e1b --- /dev/null +++ b/.planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-05-PLAN.md @@ -0,0 +1,567 @@ +--- +phase: "01-foundation-client-dashboard" +plan: 05 +type: execute +wave: 3 +depends_on: + - "01-01" + - "01-02" + - "01-03" + - "01-04" +files_modified: + - scripts/seed.ts + - .env.local +autonomous: true +requirements: + - DASH-01 + - DASH-02 + - DASH-03 + - DASH-04 + - DASH-07 + - DASH-08 + - DASH-09 + - DASH-10 + +must_haves: + truths: + - "Seed script exists and contains TypeScript seed logic" + - "Script inserts one complete test client with all related data (phases, tasks, deliverables, payments, documents, notes)" + - "Client token is generated via nanoid (21 chars, cryptographically secure)" + - "Seed script prints shareable URL to console: http://localhost:3000/c/[token]" + - "Script can be run via: npx tsx scripts/seed.ts" + - "DNS CNAME is configured: welcomeclient.iamcavalli.net → vercel DNS" + - "DNS propagation is verified (can be checked via `dig` or online tool)" + artifacts: + - path: "scripts/seed.ts" + provides: "Seed script that inserts first real client with all data" + min_lines: 100 + contains: "import.*nanoid" + - path: ".env.local (updated)" + provides: "Updated with VERCEL_URL or custom domain setting" + contains: "DATABASE_URL" + key_links: + - from: "scripts/seed.ts" + to: "src/db/schema" + via: "drizzle db.insert()" + pattern: "db.insert\\(" + - from: "nanoid token" + to: "client URL" + via: "http://localhost:3000/c/[token]" + pattern: "nanoid" + +--- + + +**Seed Script + DNS Configuration:** Create a TypeScript seed script that populates the database with one complete test client (including phases, tasks, deliverables, payments, documents, and notes), generates a secret token via nanoid, and prints a shareable dashboard URL. Configure DNS CNAME for welcomeclient.iamcavalli.net to Vercel and verify propagation. + +Purpose: Enable end-to-end testing with real data. One developer can run the seed script and immediately open a working client dashboard. DNS configuration allows the project to be accessed via the production domain. + +Output: Executable seed script + verified DNS CNAME + shareable client link for testing Phase 1. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/research/ARCHITECTURE.md (Data Model section) +@.planning/phases/01-foundation-client-dashboard/01-CONTEXT.md (D-13) + + + + + + Task 1: Create scripts/seed.ts to insert first real client with all data + + scripts/seed.ts + + + src/db/schema.ts (all table definitions) + src/db/index.ts (db client) + + + Create `scripts/seed.ts`: + + ```typescript + /** + * Seed Script — Inserts first test client with complete project data + * Run: npx tsx scripts/seed.ts + */ + + import { db } from '@/db'; + import { + clients, + phases, + tasks, + deliverables, + payments, + documents, + notes, + } from '@/db/schema'; + import { nanoid } from 'nanoid'; + + async function seed() { + console.log('🌱 Seeding database...\n'); + + try { + // 1. Create client + const clientToken = nanoid(); + const [client] = await db + .insert(clients) + .values({ + id: nanoid(), + name: 'Test Client Inc.', + brand_name: 'TestBrand', + brief: + 'A comprehensive personal branding overhaul, positioning our company as a premium consultancy in the digital transformation space.', + token: clientToken, + accepted_total: '5000.00', + created_at: new Date(), + }) + .returning(); + + console.log( + '✓ Client created: ' + client.name + ' (ID: ' + client.id + ')' + ); + + // 2. Create phases + const [phase1, phase2, phase3] = await db + .insert(phases) + .values([ + { + id: nanoid(), + client_id: client.id, + title: 'Discovery & Strategy', + sort_order: 1, + status: 'done', + }, + { + id: nanoid(), + client_id: client.id, + title: 'Design & Messaging', + sort_order: 2, + status: 'active', + }, + { + id: nanoid(), + client_id: client.id, + title: 'Implementation & Launch', + sort_order: 3, + status: 'upcoming', + }, + ]) + .returning(); + + console.log('✓ Phases created (3 total)'); + + // 3. Create tasks + const [task1, task2, task3, task4, task5, task6] = await db + .insert(tasks) + .values([ + { + id: nanoid(), + phase_id: phase1.id, + title: 'Stakeholder interviews', + description: 'In-depth conversations with leadership team', + sort_order: 1, + status: 'done', + }, + { + id: nanoid(), + phase_id: phase1.id, + title: 'Competitive analysis', + description: 'Research top 10 competitors in the space', + sort_order: 2, + status: 'done', + }, + { + id: nanoid(), + phase_id: phase2.id, + title: 'Brand positioning document', + description: + 'Write and refine the core positioning statement', + sort_order: 1, + status: 'in_progress', + }, + { + id: nanoid(), + phase_id: phase2.id, + title: 'Visual identity design', + description: 'Logo, color palette, typography', + sort_order: 2, + status: 'in_progress', + }, + { + id: nanoid(), + phase_id: phase3.id, + title: 'Website build & launch', + description: 'Design and develop new company website', + sort_order: 1, + status: 'todo', + }, + { + id: nanoid(), + phase_id: phase3.id, + title: 'Social media rollout', + description: 'Launch branded social media accounts', + sort_order: 2, + status: 'todo', + }, + ]) + .returning(); + + console.log('✓ Tasks created (6 total)'); + + // 4. Create deliverables + await db + .insert(deliverables) + .values([ + { + id: nanoid(), + task_id: task1.id, + title: 'Interview notes & synthesis', + url: 'https://docs.google.com/document/d/1example', + status: 'approved', + approved_at: new Date('2026-04-15'), + }, + { + id: nanoid(), + task_id: task2.id, + title: 'Competitive landscape report', + url: 'https://docs.google.com/presentation/d/1example', + status: 'approved', + approved_at: new Date('2026-04-20'), + }, + { + id: nanoid(), + task_id: task3.id, + title: 'Brand positioning document (draft)', + url: 'https://docs.google.com/document/d/2example', + status: 'submitted', + approved_at: null, + }, + { + id: nanoid(), + task_id: task4.id, + title: 'Logo concepts (3 variations)', + url: 'https://www.figma.com/file/example', + status: 'pending', + approved_at: null, + }, + ]) + .returning(); + + console.log('✓ Deliverables created (4 total)'); + + // 5. Create payments + await db + .insert(payments) + .values([ + { + id: nanoid(), + client_id: client.id, + label: 'Acconto 50%', + amount: '2500.00', + status: 'saldato', + paid_at: new Date('2026-04-01'), + }, + { + id: nanoid(), + client_id: client.id, + label: 'Saldo 50%', + amount: '2500.00', + status: 'inviata', + paid_at: null, + }, + ]) + .returning(); + + console.log('✓ Payments created (2 total)'); + + // 6. Create documents + await db + .insert(documents) + .values([ + { + id: nanoid(), + client_id: client.id, + label: 'Brand Guidelines PDF', + url: 'https://example.com/brand-guidelines.pdf', + created_at: new Date(), + }, + { + id: nanoid(), + client_id: client.id, + label: 'Design Mockups Figma', + url: 'https://www.figma.com/file/example', + created_at: new Date(), + }, + ]) + .returning(); + + console.log('✓ Documents created (2 total)'); + + // 7. Create notes + await db + .insert(notes) + .values([ + { + id: nanoid(), + client_id: client.id, + body: 'Initial strategy session completed. Key insight: positioning needs to emphasize tech expertise and creative thinking balance.', + created_at: new Date('2026-04-10'), + }, + { + id: nanoid(), + client_id: client.id, + body: 'Phase 1 approved. Moving forward with design phase. Stakeholders excited about direction.', + created_at: new Date('2026-04-22'), + }, + ]) + .returning(); + + console.log('✓ Notes created (2 total)'); + + // Print shareable URL + console.log('\n✨ Seed complete!\n'); + console.log('📎 Shareable client link:'); + console.log( + ` http://localhost:3000/c/${clientToken}\n` + ); + console.log( + 'This link is unique and secret. Send it to the client via Slack or email.\n' + ); + } catch (error) { + console.error('❌ Seed failed:', error); + process.exit(1); + } + } + + seed(); + ``` + + Key points: + - Uses nanoid for token generation (21 chars, cryptographically secure) + - Inserts complete hierarchical data: 1 client → 3 phases → 6 tasks → 4 deliverables + 2 payments + 2 documents + 2 notes + - Mix of statuses: phase 1 done, phase 2 active, phase 3 upcoming; tasks have various completion states + - Deliverables show different statuses: approved (with timestamp), submitted, pending + - Payments: one paid, one sent but unpaid + - Notes: 2 decision log entries + - Prints shareable URL to console + + + test -f scripts/seed.ts && echo "Seed script exists" + grep -q "import.*nanoid" scripts/seed.ts && echo "nanoid imported" + grep -q "db.insert" scripts/seed.ts && echo "Insert statements present" + grep -q "clientToken" scripts/seed.ts && echo "Token generation present" + grep -q "http://localhost:3000/c/" scripts/seed.ts && echo "URL printed" + + + - `scripts/seed.ts` exists as TypeScript file + - Script imports nanoid and db client + - Creates one complete client with all related data (phases, tasks, deliverables, payments, documents, notes) + - Prints shareable URL to console + - Can be executed via `npx tsx scripts/seed.ts` without errors + + + + + Task 2: Test seed script execution and verify data is inserted into database + + None (execution only) + + + scripts/seed.ts + .env.local + + + Run the seed script: + ``` + npx tsx scripts/seed.ts + ``` + + Expected output: + ``` + 🌱 Seeding database... + + ✓ Client created: Test Client Inc. (ID: xxx...) + ✓ Phases created (3 total) + ✓ Tasks created (6 total) + ✓ Deliverables created (4 total) + ✓ Payments created (2 total) + ✓ Documents created (2 total) + ✓ Notes created (2 total) + + ✨ Seed complete! + + 📎 Shareable client link: + http://localhost:3000/c/[token] + + This link is unique and secret. Send it to the client via Slack or email. + ``` + + If the script fails: + - Verify DATABASE_URL is set and correct + - Verify Postgres on Coolify is accessible + - Check that schema exists (run `npx drizzle-kit introspect` to confirm) + + + npx tsx scripts/seed.ts 2>&1 | grep -q "Seed complete" && echo "Seed script succeeded" || echo "Seed script failed" + npx tsx scripts/seed.ts 2>&1 | grep -oE "http://localhost:3000/c/[a-zA-Z0-9_-]+" | head -1 > /tmp/client_url.txt && test -s /tmp/client_url.txt && echo "Client URL generated" || echo "Client URL not found" + + + - Seed script executes without errors + - Output shows all entity types created (client, phases, tasks, deliverables, payments, documents, notes) + - Shareable URL is printed to console + - Data is inserted into Postgres on Coolify + + + + + Task 3: Test end-to-end: Open seeded client link in browser and verify dashboard renders + + None (verification only) + + + None + + + Start dev server: + ``` + npm run dev + ``` + + Open the seeded client link in browser: + - Copy the URL from seed script output (e.g., http://localhost:3000/c/xyz123) + - Visit in browser + - Verify dashboard renders with: + - ✓ Client brand name displayed prominently + - ✓ iamcavalli logo in corner + - ✓ Global progress bar showing % completion + - ✓ All 3 phases visible with status badges (done/active/upcoming) + - ✓ Each phase shows progress bar and task count + - ✓ Tasks nested under phases with status icons + - ✓ Deliverables shown under tasks (with Approved badge if applicable) + - ✓ Payment section shows accepted_total (€5000.00) and 2 payment rows + - ✓ Payment amounts are NOT visible (only status: saldato, inviata) + - ✓ Document section shows clickable links + - ✓ Notes section shows decision log entries + + Test edge cases: + - Invalid token (http://localhost:3000/c/invalid) → should return 404 + - Page refresh → data should persist (no client-side state loss) + - Mobile view (use DevTools mobile emulator) → layout should be responsive + + + curl -s http://localhost:3000/c/invalid | grep -q "404\|not found" && echo "Invalid token returns 404" || echo "404 check inconclusive" + + + - Seeded client link opens without errors + - Dashboard renders with client data + - All sections visible: header, progress, phases, tasks, deliverables, payments, documents, notes + - Invalid token returns 404 + - Layout is responsive on mobile + + + + + Task 4: Configure DNS CNAME for welcomeclient.iamcavalli.net → Vercel DNS + + None (external DNS configuration) + + + .planning/phases/01-foundation-client-dashboard/01-CONTEXT.md (D-03) + + + **DNS Configuration Steps:** + + 1. Log into your domain registrar (where iamcavalli.net is registered) + 2. Navigate to DNS settings for iamcavalli.net + 3. Create a new CNAME record: + - **Name:** welcomeclient + - **Type:** CNAME + - **Value:** cname.vercel-dns.com + - **TTL:** 3600 (or default) + + 4. Save the record + + 5. Verify propagation (may take 15 minutes to 2 hours): + ``` + dig welcomeclient.iamcavalli.net + ``` + + You should see: + ``` + welcomeclient.iamcavalli.net. 3600 IN CNAME cname.vercel-dns.com. + ``` + + Or use an online tool: https://mxtoolbox.com/cname.aspx + + **Vercel Configuration:** + + 1. Go to Vercel dashboard → Project Settings → Domains + 2. Add domain: `welcomeclient.iamcavalli.net` + 3. Vercel will show the CNAME record to configure (should match above) + 4. Click "Add" and wait for verification (usually immediate after DNS propagates) + + **After DNS is live:** + - You can access the dashboard via https://welcomeclient.iamcavalli.net/c/[token] + - DNS is bidirectional: localhost:3000 still works for dev + + + dig welcomeclient.iamcavalli.net +short 2>/dev/null | grep -q "vercel-dns.com" && echo "DNS CNAME configured" || echo "DNS CNAME not yet live" + + + - CNAME record is created at registrar: welcomeclient → cname.vercel-dns.com + - Vercel project has the domain added and verified + - `dig` shows the CNAME record pointing to Vercel DNS + - Domain is accessible via browser (may take time to propagate) + + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| Client browser → Secret link | Token is in URL; HTTPS encrypts transit; never log token in server logs | +| Token generation | nanoid is cryptographically secure (126 bits entropy); non-enumerable | +| DNS configuration | CNAME points to Vercel; Vercel controls SSL/TLS for domain | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Disposition | Mitigation Plan | +|-----------|----------|-----------|-------------|-----------------| +| T-05-001 | Information Disclosure | Token in seed output | mitigate | URL is printed to console; developer must not commit or share the seed output; regenerate token in Phase 2 if compromised | +| T-05-002 | Information Disclosure | HTTPS for domain | mitigate | Vercel automatically provisions SSL/TLS for custom domain; all traffic to welcomeclient.iamcavalli.net is encrypted | +| T-05-003 | Denial of Service | Seed script re-run | accept | Running seed script multiple times creates duplicate clients (same test data); acceptable for dev; Phase 2 adds admin UI to manage clients | + + + + +After plan execution: +1. Run `npx tsx scripts/seed.ts` → output shows "Seed complete!" +2. Copy the printed URL and visit in browser +3. Verify dashboard renders with seeded data +4. Test invalid token → 404 +5. Verify DNS CNAME is live: `dig welcomeclient.iamcavalli.net` +6. (Optional) Visit https://welcomeclient.iamcavalli.net/c/[token] once DNS propagates + + + +- Seed script exists and inserts complete test data +- One client with 3 phases, 6 tasks, 4 deliverables, 2 payments, 2 documents, 2 notes +- Dashboard renders with seeded data via shareable link +- Invalid tokens return 404 +- DNS CNAME is configured and verified +- Phase 1 is complete and ready for production (Phase 2 will add auth and CRUD) + + + +After completion, create `.planning/phases/01-foundation-client-dashboard/01-05-SUMMARY.md` + +Also update `.planning/ROADMAP.md` to mark Phase 1 complete and set up Phase 2 planning. + diff --git a/.planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-05-SUMMARY.md b/.planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-05-SUMMARY.md new file mode 100644 index 0000000..c5bc286 --- /dev/null +++ b/.planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-05-SUMMARY.md @@ -0,0 +1,51 @@ +--- +plan: 05 +status: complete +commit: 073eec7 +date: 2026-05-14 +--- + +# Plan 01-05 Summary: Seed Script + DNS Configuration + +## Tasks Completed + +**Task 1: scripts/seed.ts** ✓ +- Created seed script with 107 lines +- Inserts 1 client → 3 phases → 6 tasks → 4 deliverables + 2 payments + 2 documents + 2 notes +- Token generated via nanoid (21 chars, ~126 bits entropy) +- Prints shareable URL: `http://localhost:3000/c/[token]` +- Run via: `DATABASE_URL= npx tsx scripts/seed.ts` + +**Task 2: Seed execution verified** ✓ +- Seed ran successfully against Hetzner Postgres +- Test client created: `Test Client Inc.` / `TestBrand` +- Shareable link: `http://localhost:3000/c/jXpPhUS-C6pCGAu5Va0Ct` + +**Task 3: E2E dashboard test** ✓ +- Valid token → HTTP 200 (production build: `next build` + `next start`) +- Invalid token → HTTP 404 +- Dev server also fixed (see Bug Fix below) + +**Task 4: DNS CNAME** — Pending user action +- Prerequisites not yet met: no Vercel project linked, no deployment +- Steps to complete (after Vercel deployment): + 1. Run `vercel --prod` or connect via Vercel dashboard + 2. Add domain `welcomeclient.iamcavalli.net` in Vercel Project Settings → Domains + 3. At domain registrar for `iamcavalli.net`: add CNAME `welcomeclient → cname.vercel-dns.com` + 4. Verify: `dig welcomeclient.iamcavalli.net +short` should return `cname.vercel-dns.com` + +## Bug Fixed (outside plan scope) + +**Tailwind v4 scanning `.01_projects/` directory** +- Root cause: Tailwind v4 auto-detects and scans ALL files in the repo root +- The `.01_projects/sparklingorbit` subdirectory contains a Python `.venv` with `markdown_it` library +- That library has a regex comment containing `[-:|]` (a regex character class) +- Tailwind interpreted `[-:|]` as an arbitrary CSS property class → generates invalid CSS `-: |` +- Dev server Turbopack was treating this as a fatal CSS parse error → 500 on all pages +- Fix: added `@source not` directives in `globals.css` to exclude adjacent projects + +## Commits + +- `073eec7` — feat(seed): add seed script + fix Tailwind scanning adjacent projects + + \ No newline at end of file diff --git a/.planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-CONTEXT.md b/.planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-CONTEXT.md new file mode 100644 index 0000000..745fe97 --- /dev/null +++ b/.planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-CONTEXT.md @@ -0,0 +1,126 @@ +# Phase 1: Foundation & Client Dashboard - Context + +**Gathered:** 2026-05-13 +**Status:** Ready for planning + + +## Phase Boundary + +Costruire il DB schema, il token API e la dashboard cliente read-only. Al termine di questa fase, un link segreto è condivisibile con un cliente reale che può aprirlo su mobile o desktop e vedere lo stato completo del suo progetto — senza login, senza admin, senza interazione. L'admin area (CRUD, auth, commenti, approvazioni) è Phase 2. + + + + +## Implementation Decisions + +### Database & Infrastruttura + +- **D-01: Database su Coolify (Hetzner)** — Postgres istanza gestita da Coolify sul server Hetzner già pagato. Zero costo aggiuntivo. Neon è scartato in favore del self-hosting già disponibile. +- **D-02: ORM invariato** — Drizzle ORM con driver `postgres-js` (invece di `neon-http`). Schema e migrazioni identici, solo il driver di connessione cambia. +- **D-03: DNS in Phase 1** — Configurare `welcomeclient.iamcavalli.net` come CNAME verso Vercel nella Phase 1, non alla fine. Propagazione va verificata subito. + +### Brand & Visual Design + +- **D-04: Brand hardcoded in Phase 1** — Colori e logo iamcavalli fissi nel codice. La personalizzazione admin (tabella `brand_settings`, pannello colori/logo) è demandata a Phase 2. +- **D-05: Stile light & clean** — Sfondo chiaro, typography forte, layout professionale e leggibile su mobile. +- **D-06: Header dashboard** — Logo iamcavalli piccolo in un angolo (es. top-right o top-left), nome del brand cliente (`brand_name`) in primo piano e prominente. Non il nome del cliente, il nome del suo brand. + +### Layout Fasi e Task + +- **D-07: Timeline laterale per le fasi** — Indicatore temporale/progressione a sinistra, contenuto fase sulla destra. Trasmette senso di avanzamento sequenziale del progetto. +- **D-08: Barra progresso per fase** — In cima a ogni fase, una barra % calcolata dai task completati. Sotto la barra, lista dei task. +- **D-09: Barra progresso globale in cima** — Progress bar globale del progetto nella parte alta della dashboard, derivata dal totale dei task completati su tutti i task. Il cliente vede subito "sei al X%". + +### Stato Pagamenti + +- **D-10: Pagamenti sempre visibili** — Sezione pagamenti sempre in vista nella dashboard (non nascosta in accordion). Mostra: totale accettato + stato acconto 50% + stato saldo 50%. +- **D-11: Stati pagamento** — Tre stati per ogni payment row: `da_saldare` / `inviata` / `saldato`. Mai i prezzi singoli dei servizi — solo `accepted_total`. + +### Storico Decisioni (DASH-10) + +- **D-12: Admin scrive, cliente legge** — Le note/decisioni del log storico sono scritte solo dall'admin. Il cliente le vede in sola lettura nella sua dashboard. La UI per scrivere note è in Phase 2 (admin area). In Phase 1 il campo `body` è in schema e la visualizzazione lato cliente è già presente; sarà vuota finché Phase 2 non porta l'admin. + +### Primo Cliente (Testing) + +- **D-13: Seed script** — Uno script TypeScript (`scripts/seed.ts`) per inserire il primo cliente reale con dati completi (fasi, task, pagamenti, documenti). Eseguibile una volta con `npx tsx scripts/seed.ts`. Nessun form admin o SQL manuale necessario per Phase 1. + +### Claude's Discretion + +- Scelta del componente UI specifico per la timeline laterale (build custom vs. shadcn primitives) +- Struttura CSS delle card fasi e task (spaziatura, bordi, hover state) +- Schema colori specifico light & clean (bianco puro, grigi, quale accent color in attesa del brand panel) + + + + +## Canonical References + +**Downstream agents MUST read these before planning or implementing.** + +### Progetto & Requisiti + +- `.planning/PROJECT.md` — Contesto progetto, Core Value, decisioni chiave e vincoli +- `.planning/REQUIREMENTS.md` — REQ-IDs Phase 1: DASH-01 through DASH-04, DASH-07 through DASH-10 +- `.planning/ROADMAP.md` — Success criteria Phase 1 + +### Ricerca tecnica + +- `.planning/research/STACK.md` — Stack raccomandato (nota: database cambiato da Neon a Coolify Postgres) +- `.planning/research/ARCHITECTURE.md` — Data model completo, component boundaries, build order +- `.planning/research/PITFALLS.md` — Pitfall critici: token-as-PK, ClientView enforcement, data model day-one decisions + +### Istruzioni progetto + +- `CLAUDE.md` — Architectural constraints LOCKED (token separato dalla PK, accepted_total denormalizzato, approved_at immutabile, due path auth isolati) + +No external specs beyond the above — requirements fully captured in decisions above. + + + + +## Existing Code Insights + +### Reusable Assets + +- Nessun codice esistente — progetto greenfield. + +### Established Patterns + +- Next.js 15 App Router: Server Components per la dashboard read-only (zero client-side waterfalls per rendering dati) +- Drizzle ORM con `postgres-js` invece di `neon-http` (Coolify Postgres connection string via env var `DATABASE_URL`) +- nanoid per generazione token: `nanoid()` → 21 char, URL-safe, ~126 bit di entropia + +### Integration Points + +- Dashboard cliente: route `/c/[token]` — Middleware valida token → 404 se mancante → Server Component legge DB e renderizza +- Seed script: `scripts/seed.ts` — inserisce dati cliente reale, genera token, stampa URL condivisibile +- DNS: CNAME `welcomeclient.iamcavalli.net` → `cname.vercel-dns.com` (da configurare su domain registrar) + + + + +## Specific Ideas + +- Il portale deve sembrare professionale e in linea con il brand iamcavalli — light & clean, non un SaaS generico +- Logo iamcavalli piccolo in corner + nome brand cliente prominente nell'header +- Progress bar globale del progetto in cima alla dashboard (percentuale) +- Timeline laterale per le fasi (non accordion, non card flat) — trasmette sequenzialità e avanzamento +- Barra progresso per singola fase, calcolata da task completati +- Personalizzazione colori/logo dal pannello admin è demandata a Phase 2 — in Phase 1 brand hardcoded + + + + +## Deferred Ideas + +- **Brand customization panel** (colori background, testi, logo upload dall'admin) → Phase 2, da aggiungere come requisito nell'area admin +- **Three.js / animazioni 3D** → Non necessario per questo tipo di portale. UI curata con Tailwind è sufficiente. +- **Commenti e approvazioni cliente** → Phase 2 (DASH-05, DASH-06) +- **Auth admin** → Phase 2 + + + +--- + +*Phase: 1-Foundation & Client Dashboard* +*Context gathered: 2026-05-13* diff --git a/.planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-DISCUSSION-LOG.md b/.planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-DISCUSSION-LOG.md new file mode 100644 index 0000000..a15fb7c --- /dev/null +++ b/.planning/milestones/v1.0-phases/01-foundation-client-dashboard/01-DISCUSSION-LOG.md @@ -0,0 +1,128 @@ +# Phase 1: Foundation & Client Dashboard - Discussion Log + +> **Audit trail only.** Do not use as input to planning, research, or execution agents. +> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered. + +**Date:** 2026-05-13 +**Phase:** 1 - Foundation & Client Dashboard +**Areas discussed:** Database, Three.js, Brand & visual design, Layout fasi e task, Primo cliente, Storico decisioni + +--- + +## Database + +| Option | Description | Selected | +|--------|-------------|----------| +| Postgres su Coolify | Zero costo extra — istanza Postgres su Hetzner già pagato | ✓ | +| Neon free tier | Postgres serverless gestito, 0.5 GB gratis, dipendenza esterna | | +| Oracle Cloud Free Tier | ARM 4vCPU 24GB RAM gratuito, richiede configurazione | | + +**User's choice:** Postgres su Coolify (Hetzner) +**Notes:** L'utente ha già un server Hetzner con Coolify attivo (usato per SparklingOrbit). Preferisce zero dipendenze esterne a pagamento e mantenere i dati nella propria infrastruttura. + +--- + +## Three.js + +| Option | Description | Selected | +|--------|-------------|----------| +| Animazioni sottili | Effetti di sfondo leggeri (particelle, gradienti animati) | | +| Elemento hero visivo | Shape 3D animata nella parte alta della dashboard | | +| Ripensaci | UI curata con Tailwind è sufficiente, niente 3D | ✓ | + +**User's choice:** Ripensaci — niente three.js +**Notes:** L'utente aveva menzionato three.js inizialmente ma dopo riflessione ha preferito concentrarsi su un'UI curata senza 3D. + +--- + +## Brand & Visual Design + +| Option | Description | Selected | +|--------|-------------|----------| +| Brand hardcoded in Phase 1 | Colori/logo iamcavalli fissi, personalizzazione in Phase 2 | ✓ | +| Già configurabile in Phase 1 | Tabella brand_settings e pannello già in Phase 1 | | + +**Brand hardcoded:** ✓ — pannello personalizzazione rimandato a Phase 2 + +| Stile visual | Description | Selected | +|--------------|-------------|----------| +| Dark & premium | Sfondo scuro, accenti brillanti | | +| Light & clean | Sfondo chiaro, typography forte | ✓ | +| Decido dal pannello | Nessuna preferenza ora | | + +**Stile:** Light & clean + +| Header | Description | Selected | +|--------|-------------|----------| +| Solo logo + nome progetto | Logo iamcavalli + nome progetto | | +| Solo nome brand cliente | brand_name in grande, nessun logo | | +| Entrambi | Logo piccolo in corner + brand_name cliente in primo piano | ✓ | + +**Header:** Logo iamcavalli piccolo in un angolo + nome brand cliente prominente + +--- + +## Layout Fasi e Task + +| Layout fasi | Description | Selected | +|-------------|-------------|----------| +| Accordion verticale | Fasi impilate, espandibili | | +| Card separate | Card per fase, task sempre visibili | | +| Timeline laterale | Indicatore temporale a sinistra, contenuto a destra | ✓ | + +**Layout fasi:** Timeline laterale + +| Task status | Description | Selected | +|-------------|-------------|----------| +| Badge colorato | Pallino/badge per stato | | +| Icona + testo | Icona con etichetta | | +| Barra progresso fase | Barra % per fase + lista task | ✓ | + +**Task status:** Barra progresso in cima a ogni fase + +| Avanzamento globale | Description | Selected | +|--------------------|-------------|----------| +| Sì, in cima | % completamento totale in cima alla dashboard | ✓ | +| No, solo per fase | Progresso solo per singola fase | | +| No progress bar | Solo stati task | | + +**Progress bar globale:** Sì, in cima alla dashboard + +--- + +## Primo Cliente + +| Option | Description | Selected | +|--------|-------------|----------| +| Seed script | TypeScript script, `npx tsx scripts/seed.ts` | ✓ | +| Form admin minimale | Form no-auth per creare primo cliente | | +| SQL diretto Coolify | INSERT manuale via console Coolify | | + +**User's choice:** Seed script +**Notes:** Lo script inserisce un cliente reale con fasi, task, pagamenti, documenti e stampa l'URL condivisibile. + +--- + +## Storico Decisioni (DASH-10) + +| Option | Description | Selected | +|--------|-------------|----------| +| Solo admin scrive | Admin aggiunge note, cliente legge in sola lettura | ✓ | +| Visibile solo in Phase 2 | UI mostrata solo con admin area | | + +**User's choice:** Admin scrive, cliente legge +**Notes:** In Phase 1 lo schema è già presente e il cliente vede le note (sezione vuota inizialmente). La UI per aggiungere note arriva con l'area admin in Phase 2. + +--- + +## Claude's Discretion + +- Scelta componente UI specifico per timeline laterale (shadcn primitives vs. custom) +- CSS dettagliato delle card fasi e task (spaziatura, bordi, hover) +- Accent color provvisorio per Phase 1 (in attesa del brand panel in Phase 2) + +## Deferred Ideas + +- **Brand customization panel** (colori, logo upload dall'admin) → Phase 2 +- **Three.js / animazioni 3D** → Scartato +- **Commenti e approvazioni** → Phase 2 (DASH-05, DASH-06) diff --git a/.planning/milestones/v1.0-phases/01-foundation-client-dashboard/SKELETON.md b/.planning/milestones/v1.0-phases/01-foundation-client-dashboard/SKELETON.md new file mode 100644 index 0000000..bfe2ef1 --- /dev/null +++ b/.planning/milestones/v1.0-phases/01-foundation-client-dashboard/SKELETON.md @@ -0,0 +1,302 @@ +# ClientHub — Walking Skeleton (Phase 1) + +**Project:** ClientHub — Freelancer Client Portal +**Phase:** 01 — Foundation & Client Dashboard +**Date:** 2026-05-13 +**Status:** Blueprint (decisions below are LOCKED for all subsequent phases) + +--- + +## Project Architecture — Locked Decisions + +This Walking Skeleton establishes the architectural foundation for all future phases. These decisions are **immutable** without explicit user approval. + +### Core Stack + +| Layer | Technology | Why | Locked? | +|-------|-----------|-----|---------| +| **Framework** | Next.js 15 (App Router, TypeScript, src/) | Server Components + Edge Middleware for performance; Vercel-native | ✅ YES | +| **Database** | Postgres on Coolify (Hetzner), via `postgres-js` driver | Self-hosted (no Neon/Supabase cost); persistent via external DB | ✅ YES | +| **ORM** | Drizzle ORM with postgres-js | Zero-cost serverless driver; schema-as-code migrations | ✅ YES | +| **UI** | Tailwind CSS v4 + shadcn/ui components | Utility-first, copied components, mobile-first | ✅ YES | +| **Auth (Admin)** | Auth.js v4 Credentials provider (Phase 2) | Single admin account, JWT cookie | ✅ YES | +| **Auth (Client)** | Custom Next.js Middleware + token validation | No session store needed; token in URL | ✅ YES | +| **Token Generation** | nanoid (21 chars) | Cryptographically secure, URL-safe, non-enumerable | ✅ YES | +| **Deployment** | Vercel (Hobby plan) + custom subdomain | Native Next.js; auto-SSL; single deploy command | ✅ YES | + +### Data Model — Locked Entities + +All tables below **must** exist and maintain these field definitions. Modifications require explicit approval. + +``` +clients + id UUID PK (stable, never changes) + name TEXT + brand_name TEXT + brief TEXT + token UUID UNIQUE ← SEPARATE from PK, rotatable + accepted_total NUMERIC ← denormalized, only price client sees + created_at TIMESTAMPTZ + +phases + id UUID PK + client_id UUID FK → clients.id + title TEXT + sort_order INT + status TEXT (upcoming | active | done) + +tasks + id UUID PK + phase_id UUID FK → phases.id + title TEXT + description TEXT + status TEXT (todo | in_progress | done) + sort_order INT + +deliverables + id UUID PK + task_id UUID FK → tasks.id + title TEXT + url TEXT + status TEXT (pending | submitted | approved) + approved_at TIMESTAMPTZ ← immutable audit trail + +comments + id UUID PK + entity_type TEXT (task | deliverable) + entity_id UUID + author TEXT (client | admin) + body TEXT + created_at TIMESTAMPTZ + +payments + id UUID PK + client_id UUID FK → clients.id + label TEXT ("Acconto 50%" | "Saldo 50%") + amount NUMERIC + status TEXT (da_saldare | inviata | saldato) + paid_at TIMESTAMPTZ + +documents + id UUID PK + client_id UUID FK → clients.id + label TEXT + url TEXT ← external links only, no file uploads + created_at TIMESTAMPTZ + +notes + id UUID PK + client_id UUID FK → clients.id + body TEXT + created_at TIMESTAMPTZ + +service_catalog + id UUID PK + name TEXT + description TEXT + unit_price NUMERIC + active BOOLEAN + +quote_items + id UUID PK + client_id UUID FK → clients.id + service_id UUID FK → service_catalog.id + quantity NUMERIC + unit_price NUMERIC + subtotal NUMERIC + ← NEVER exposed via client API +``` + +### Critical Design Principles — Locked + +1. **`clients.token` is NOT the primary key.** Data is keyed by stable UUID `id`. Token is a separate, rotatable field. Rotation is a single UPDATE statement. + +2. **Client API never exposes `quote_items`.** Server-side filtering enforces this; not a UI trick. The `accepted_total` field is the only price the client API returns. + +3. **`deliverables.approved_at` is immutable.** Once set, it cannot be unset. Provides an audit trail for disputes. + +4. **Two independent auth systems:** + - `/c/[token]/*` → Middleware validates token, 404 on miss + - `/admin/*` → Auth.js session check (Phase 2) + - No overlap; no shared session store + +5. **No file hosting in v1.** Documents are external URLs only (Google Drive, PDFs, Figma links). File uploads → Phase 3+. + +6. **No email in v1.** Deliverables are dashboard links, not email attachments. Email integration → Phase 2+. + +### Directory Structure — Locked + +``` +IAMCAVALLI/ +├── src/ +│ ├── app/ +│ │ ├── c/[token]/ +│ │ │ ├── page.tsx ← Client dashboard route +│ │ │ └── layout.tsx +│ │ ├── admin/ ← Phase 2 (protected by middleware) +│ │ │ ├── page.tsx ← Admin dashboard +│ │ │ ├── clients/ +│ │ │ │ ├── page.tsx +│ │ │ │ └── [id]/ +│ │ │ ├── catalog/ +│ │ │ └── ... +│ │ ├── layout.tsx +│ │ └── globals.css +│ ├── components/ +│ │ ├── ui/ ← shadcn/ui components +│ │ ├── client-dashboard.tsx +│ │ ├── phase-timeline.tsx +│ │ ├── payment-status.tsx +│ │ ├── documents-section.tsx +│ │ ├── notes-section.tsx +│ │ └── ... +│ ├── db/ +│ │ ├── schema.ts ← Drizzle schema (source of truth) +│ │ ├── migrations/ ← Generated by drizzle-kit +│ │ └── index.ts ← db client export +│ ├── lib/ +│ │ ├── client-view.ts ← ClientView type + queries +│ │ ├── auth.ts ← Phase 2: Auth helpers +│ │ └── ... +│ └── middleware.ts ← Token validation at edge +├── scripts/ +│ ├── seed.ts ← Insert first test client +│ └── ... +├── .env.local ← DATABASE_URL, secrets +├── drizzle.config.ts +├── next.config.ts +├── tailwind.config.ts +├── tsconfig.json +├── package.json +└── .planning/ + ├── ROADMAP.md + ├── REQUIREMENTS.md + ├── STATE.md + └── phases/ + └── 01-foundation-client-dashboard/ + ├── 01-CONTEXT.md + ├── 01-DISCUSSION-LOG.md + ├── 01-01-PLAN.md + ├── 01-02-PLAN.md + ├── 01-03-PLAN.md + ├── 01-04-PLAN.md + ├── 01-05-PLAN.md + └── SKELETON.md +``` + +### Deployment — Locked + +- **Host:** Vercel (Hobby plan, $0/month for Phase 1 scale) +- **Domain:** welcomeclient.iamcavalli.net (CNAME to Vercel DNS) +- **Database:** Postgres on Coolify (existing Hetzner server, Simone manages) +- **Environment:** DATABASE_URL injected via Vercel Secrets +- **SSL/TLS:** Vercel auto-provisioning for custom domain + +### API Routes Structure (Phase 2+) + +Routes created in Phase 2 will follow this pattern: + +**Client-facing routes** (`/api/c/[token]/...`): +- No authentication library needed +- Middleware validates token +- Routes return ClientView shape only + +**Admin routes** (`/api/admin/...`): +- Require Auth.js session +- Access full AdminView including quote_items +- CRUD operations on all entities + +### UI Layer Principles — Locked + +- **Light & clean visual style:** White backgrounds, strong typography, subtle gray accents +- **Mobile-first design:** Tailwind defaults ensure responsive behavior +- **Semantic HTML:** Proper heading hierarchy, accessible form controls +- **No client-side state management libraries:** Server Components + Server Actions for Phase 1-2 +- **Progress visualization:** Global bar (top) + per-phase bars (sections) + task status badges +- **Brand consistency:** iamcavalli logo in corner, client brand_name prominent + +### Security Assumptions — Locked + +1. **Database credentials are secrets:** DATABASE_URL never logged, committed, or exposed +2. **Tokens are non-enumerable:** 21-character nanoid cannot be guessed +3. **Client API is isolated:** Admin data never leaks to `/c/[token]/*` routes +4. **Admin password** (Phase 2): env var `ADMIN_PASSWORD` protects `/admin/*` before Auth.js is added +5. **No PII in logs:** Payment amounts and tokens never logged to Vercel logs + +--- + +## What This Skeleton Delivers + +After Phase 1 execution: + +✅ **Functional client portal:** +- One client can open their secret link on any device +- Dashboard shows project phases, tasks, status, payments, documents, decision log +- No login required; link is the secret + +✅ **Production-ready infrastructure:** +- Database is live on Coolify Postgres +- Custom domain is verified and HTTPS-enabled +- Application is deployed on Vercel +- One-command deploy pipeline (`git push → Vercel auto-build`) + +✅ **Developer-friendly codebase:** +- TypeScript with strict mode +- Drizzle ORM manages schema as code +- Git-tracked migrations (reproducible database state) +- One seed script to populate test data +- No manual SQL; no database browser required + +✅ **Foundation for Phase 2:** +- Data model is stable and comprehensive +- Admin CRUD can be built without schema changes +- Auth.js integration point is clear +- Comments and approvals schema already exists + +--- + +## Phase 1 → Phase 2 Contract + +Phase 2 will extend this skeleton by: + +1. **Admin authentication:** Middleware check + Auth.js session on `/admin/*` routes +2. **CRUD operations:** Forms and API routes to edit clients, phases, tasks, deliverables, payments +3. **Comments & approvals:** Client-facing UI for commenting and approving deliverables +4. **Admin workspace:** Dashboard to manage all clients with state summary and quick actions +5. **Payment management:** Update payment status, send payment reminders + +**No schema changes required.** All Phase 2 features fit into the existing data model. + +--- + +## Validation Checklist (End of Phase 1) + +- [ ] Next.js 15 application compiles without TypeScript errors +- [ ] Database schema is live on Coolify Postgres (all 11 tables) +- [ ] Middleware validates tokens at edge +- [ ] Client portal route renders complete dashboard with seeded data +- [ ] Seed script inserts test client and prints shareable link +- [ ] DNS CNAME is live: welcomeclient.iamcavalli.net → Vercel +- [ ] Application is deployed on Vercel (accessible via https://welcomeclient.iamcavalli.net/) +- [ ] Invalid tokens return 404 (no information leakage) +- [ ] Payment amounts are NOT visible on client dashboard (only status) +- [ ] Mobile layout is responsive and readable +- [ ] All DASH-01 through DASH-10 requirements are satisfied (except DASH-05, DASH-06 which are Phase 2) + +--- + +## Future Extensibility Notes + +This skeleton is designed for: + +- **Phase 2:** Admin CRUD + comments + approvals (no schema changes) +- **Phase 3:** Service catalog + quote builder (admin-only, client sees only total) +- **Phase 4 (v2):** Claude AI onboarding flow (optional; may defer indefinitely) +- **Beyond:** Multi-team support, real file uploads, email automation (major schema rework) + +The current design is intentionally simple. Future phases should resist scope creep and maintain the "client sees only what they need" principle. + +--- + +**Skeleton locked:** 2026-05-13 +**Next checkpoint:** Phase 2 planning (`/gsd-plan-phase 2`) diff --git a/.planning/milestones/v1.0-phases/02-admin-area-interactive-features/02-01-PLAN.md b/.planning/milestones/v1.0-phases/02-admin-area-interactive-features/02-01-PLAN.md new file mode 100644 index 0000000..24d1525 --- /dev/null +++ b/.planning/milestones/v1.0-phases/02-admin-area-interactive-features/02-01-PLAN.md @@ -0,0 +1,481 @@ +--- +phase: "02-admin-area-interactive-features" +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - package.json + - src/proxy.ts + - src/app/api/auth/[...nextauth]/route.ts + - src/app/admin/login/page.tsx + - src/app/admin/login/actions.ts + - src/lib/auth.ts + - .env.local +autonomous: true +requirements: + - ADMIN-01 + - ADMIN-02 + +must_haves: + truths: + - "Admin can POST /admin/login with ADMIN_EMAIL + ADMIN_PASSWORD and receive a session JWT cookie" + - "Visiting /admin/* without a valid session redirects to /admin/login" + - "Visiting /c/[token]/* still validates token at edge (proxy.ts unchanged for client routes)" + - "Session is JWT-based (stateless) — no DB users table involved" + - "ADMIN_EMAIL and ADMIN_PASSWORD are read from env vars, never hardcoded" + artifacts: + - path: "src/lib/auth.ts" + provides: "NextAuth config — CredentialsProvider validating against ADMIN_EMAIL/ADMIN_PASSWORD env vars" + contains: "CredentialsProvider" + - path: "src/app/api/auth/[...nextauth]/route.ts" + provides: "NextAuth catch-all route handler" + contains: "NextAuth" + - path: "src/app/admin/login/page.tsx" + provides: "Admin login form UI (email + password, submit)" + min_lines: 30 + - path: "src/proxy.ts" + provides: "Updated proxy: /c/* token validation + /admin/* session guard" + contains: "getToken" + key_links: + - from: "src/proxy.ts" + to: "src/app/api/auth/[...nextauth]/route.ts" + via: "getToken({ req, secret: process.env.NEXTAUTH_SECRET })" + pattern: "getToken" + - from: "src/app/admin/login/page.tsx" + to: "/api/auth/callback/credentials" + via: "signIn('credentials', { email, password })" + pattern: "signIn" + - from: "ADMIN_EMAIL + ADMIN_PASSWORD" + to: "CredentialsProvider authorize()" + via: "process.env.ADMIN_EMAIL" + pattern: "ADMIN_EMAIL" +--- + + +**Auth.js Admin Session + Proxy Guard:** Install next-auth@4, configure a CredentialsProvider that validates against ADMIN_EMAIL/ADMIN_PASSWORD env vars, wire the catch-all API route, build the login page, and extend the existing src/proxy.ts to guard /admin/* routes with a session check. + +Purpose: Gate the entire admin area behind Auth.js JWT session before any admin UI is built. Two independent auth paths are enforced: /c/[token]/* uses edge token validation (unchanged from Phase 1), /admin/* uses getToken() from next-auth/jwt. No DB users table — single admin, env-var credentials only (per D-01, D-02, D-03, D-04). + +Output: Working /admin/login page, session cookie on successful login, automatic redirect to /admin/login for unauthenticated access to any /admin/* route. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/ROADMAP.md +@.planning/phases/02-admin-area-interactive-features/02-CONTEXT.md +@.planning/phases/01-foundation-client-dashboard/01-05-SUMMARY.md + + + + + + + +Current src/proxy.ts content: +```typescript +import { NextRequest, NextResponse } from 'next/server'; + +export async function proxy(request: NextRequest) { + const pathname = request.nextUrl.pathname; + + // Extract token from path: /c/[token]/... + const tokenMatch = pathname.match(/^\/c\/([a-zA-Z0-9_-]+)/); + if (!tokenMatch) { + return NextResponse.rewrite(new URL('/not-found', request.url)); + } + + const token = tokenMatch[1]; + + try { + const validateUrl = new URL( + `/api/internal/validate-token?token=${encodeURIComponent(token)}`, + request.url + ); + const res = await fetch(validateUrl.toString()); + + if (!res.ok) { + return NextResponse.rewrite(new URL('/not-found', request.url)); + } + + return NextResponse.next(); + } catch { + return NextResponse.rewrite(new URL('/not-found', request.url)); + } +} + +export const config = { + matcher: ['/c/:path*'], +}; +``` + +Note: Next.js middleware MUST be exported as `middleware`, not `proxy`. The Phase 1 file uses `proxy` — this plan must rename it to `middleware` while extending it with /admin/* guard. No src/middleware.ts should ever be created. + +From src/db/index.ts: +```typescript +import { drizzle } from "drizzle-orm/postgres-js"; +import postgres from "postgres"; +export const db = drizzle(client); +``` + +From src/db/schema.ts (types needed in this plan): +```typescript +// No schema changes needed — no users table. Auth is env-var only. +export type Client = typeof clients.$inferSelect; +``` + + + + + + + Task 1: Install next-auth@4, create src/lib/auth.ts and NextAuth catch-all route + + package.json + src/lib/auth.ts + src/app/api/auth/[...nextauth]/route.ts + .env.local + + + Install next-auth v4 (stable — v5 is still beta RC as of 2026-05-15, per D-01): + ``` + npm install next-auth@4 + ``` + + Add to .env.local (generate NEXTAUTH_SECRET with: `openssl rand -base64 32`): + ``` + NEXTAUTH_URL=http://localhost:3000 + NEXTAUTH_SECRET= + ADMIN_EMAIL=simone.cavalli.gestione@gmail.com + ADMIN_PASSWORD= + ``` + + Create `src/lib/auth.ts` — NextAuth config, no DB adapter (per D-03): + ```typescript + import type { NextAuthOptions } from "next-auth"; + import CredentialsProvider from "next-auth/providers/credentials"; + + export const authOptions: NextAuthOptions = { + providers: [ + CredentialsProvider({ + name: "credentials", + credentials: { + email: { label: "Email", type: "email" }, + password: { label: "Password", type: "password" }, + }, + async authorize(credentials) { + if (!credentials?.email || !credentials?.password) return null; + + const adminEmail = process.env.ADMIN_EMAIL; + const adminPassword = process.env.ADMIN_PASSWORD; + + if (!adminEmail || !adminPassword) { + throw new Error("ADMIN_EMAIL and ADMIN_PASSWORD env vars must be set"); + } + + if ( + credentials.email === adminEmail && + credentials.password === adminPassword + ) { + // Return minimal session user — no DB lookup needed + return { id: "admin", email: adminEmail, name: "Admin" }; + } + + return null; // null = unauthorized (NextAuth returns 401) + }, + }), + ], + session: { + strategy: "jwt", // stateless JWT — no DB session table (per D-03) + maxAge: 30 * 24 * 60 * 60, // 30 days + }, + pages: { + signIn: "/admin/login", // custom login page (per D-07) + }, + callbacks: { + async jwt({ token, user }) { + if (user) { + token.id = user.id; + } + return token; + }, + async session({ session, token }) { + if (session.user) { + (session.user as { id?: string }).id = token.id as string; + } + return session; + }, + }, + }; + ``` + + Create `src/app/api/auth/[...nextauth]/route.ts` — NextAuth catch-all: + ```typescript + import NextAuth from "next-auth"; + import { authOptions } from "@/lib/auth"; + + const handler = NextAuth(authOptions); + + export { handler as GET, handler as POST }; + ``` + + Note: next-auth@4 with App Router uses this export pattern. The handler handles + GET (session fetch, CSRF) and POST (sign in, sign out). + + + grep -q '"next-auth"' package.json && echo "next-auth installed" + test -f src/lib/auth.ts && grep -q "CredentialsProvider" src/lib/auth.ts && echo "CredentialsProvider configured" + grep -q "strategy.*jwt" src/lib/auth.ts && echo "JWT session strategy set" + grep -q "ADMIN_EMAIL" src/lib/auth.ts && echo "ADMIN_EMAIL env var referenced" + test -f src/app/api/auth/\[...nextauth\]/route.ts && grep -q "NextAuth" src/app/api/auth/\[...nextauth\]/route.ts && echo "NextAuth route created" + grep -q "NEXTAUTH_SECRET" .env.local && echo "NEXTAUTH_SECRET in .env.local" + npm run build 2>&1 | grep -v "warning" | grep -qi "error" && echo "BUILD ERRORS" || echo "TypeScript OK" + + + - next-auth@4 is in package.json + - src/lib/auth.ts exports authOptions with CredentialsProvider using env vars + - src/app/api/auth/[...nextauth]/route.ts exports GET and POST handlers + - NEXTAUTH_SECRET, ADMIN_EMAIL, ADMIN_PASSWORD are set in .env.local + - npm run build passes without errors + + + + + Task 2: Extend src/proxy.ts to guard /admin/* with session check; create /admin/login page + + src/proxy.ts + src/app/admin/login/page.tsx + src/app/admin/login/actions.ts + + + **Replace** `src/proxy.ts` entirely. NEVER create src/middleware.ts — it would be a dead file ignored by Next.js since this project uses src/proxy.ts as the middleware entry point. The new file renames the export from `proxy` to `middleware` (required by Next.js) and adds the /admin/* guard alongside the existing /c/* token validation logic (per D-04): + + ```typescript + import { NextRequest, NextResponse } from "next/server"; + import { getToken } from "next-auth/jwt"; + + export async function middleware(request: NextRequest) { + const pathname = request.nextUrl.pathname; + + // ── ADMIN GUARD ────────────────────────────────────────────────────────── + if (pathname.startsWith("/admin")) { + // Allow the login page and NextAuth API routes through without session check + if ( + pathname === "/admin/login" || + pathname.startsWith("/api/auth") + ) { + return NextResponse.next(); + } + + const token = await getToken({ + req: request, + secret: process.env.NEXTAUTH_SECRET, + }); + + if (!token) { + const loginUrl = new URL("/admin/login", request.url); + loginUrl.searchParams.set("callbackUrl", pathname); + return NextResponse.redirect(loginUrl); + } + + return NextResponse.next(); + } + + // ── CLIENT TOKEN GUARD ─────────────────────────────────────────────────── + if (pathname.startsWith("/c/")) { + const tokenMatch = pathname.match(/^\/c\/([a-zA-Z0-9_-]+)/); + if (!tokenMatch) { + return NextResponse.rewrite(new URL("/not-found", request.url)); + } + + const clientToken = tokenMatch[1]; + + try { + const validateUrl = new URL( + `/api/internal/validate-token?token=${encodeURIComponent(clientToken)}`, + request.url + ); + const res = await fetch(validateUrl.toString()); + + if (!res.ok) { + return NextResponse.rewrite(new URL("/not-found", request.url)); + } + + return NextResponse.next(); + } catch { + return NextResponse.rewrite(new URL("/not-found", request.url)); + } + } + + return NextResponse.next(); + } + + export const config = { + matcher: ["/admin/:path*", "/c/:path*"], + }; + ``` + + Note: The export is renamed from `proxy` to `middleware`. This is the only correct Next.js middleware export name. The /c/* logic is preserved verbatim from Phase 1. + + Create `src/app/admin/login/page.tsx` — login form as Client Component: + ```typescript + "use client"; + + import { useState } from "react"; + import { signIn } from "next-auth/react"; + import { useRouter, useSearchParams } from "next/navigation"; + import { Button } from "@/components/ui/button"; + import { Input } from "@/components/ui/input"; + import { Label } from "@/components/ui/label"; + import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + + export default function AdminLoginPage() { + const router = useRouter(); + const searchParams = useSearchParams(); + const callbackUrl = searchParams.get("callbackUrl") ?? "/admin"; + + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setLoading(true); + setError(null); + + const result = await signIn("credentials", { + email, + password, + redirect: false, // handle redirect manually to show errors + }); + + if (result?.error) { + setError("Email o password non corretti."); + setLoading(false); + return; + } + + router.replace(callbackUrl); + } + + return ( +
+ + + Admin — ClientHub + + +
+
+ + setEmail(e.target.value)} + required + autoComplete="email" + /> +
+
+ + setPassword(e.target.value)} + required + autoComplete="current-password" + /> +
+ {error && ( +

{error}

+ )} + +
+
+
+
+ ); + } + ``` + + Do NOT create src/app/admin/login/actions.ts — the login is handled + client-side via signIn(). No Server Action file is needed. +
+ + test -f src/proxy.ts && grep -q "getToken" src/proxy.ts && echo "getToken imported in proxy.ts" + grep -q "export async function middleware" src/proxy.ts && echo "export named middleware (not proxy)" + grep -q '"/admin/:path\*"' src/proxy.ts && echo "admin matcher configured" + grep -q '"/c/:path\*"' src/proxy.ts && echo "client matcher still present" + grep -q "pathname === \"/admin/login\"" src/proxy.ts && echo "login page exempted from auth guard" + test -f src/app/admin/login/page.tsx && grep -q "signIn" src/app/admin/login/page.tsx && echo "login page uses signIn" + grep -q '"use client"' src/app/admin/login/page.tsx && echo "login page is Client Component" + test ! -f src/middleware.ts && echo "src/middleware.ts does NOT exist (correct)" + npm run build 2>&1 | grep -v "warning" | grep -qi "error" && echo "BUILD ERRORS" || echo "TypeScript OK" + + + - src/proxy.ts guards /admin/* routes: unauthenticated requests redirect to /admin/login?callbackUrl=... + - Export renamed from proxy to middleware (required by Next.js) + - /admin/login and /api/auth/* are exempt from the session guard + - /c/:path* token validation is unchanged + - /admin/login page renders email+password form, calls signIn('credentials'), shows error on failure, redirects on success + - src/middleware.ts does NOT exist + - npm run build passes + - Manual verification: visiting http://localhost:3000/admin redirects to /admin/login; successful login redirects back to /admin + +
+ +
+ + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| Browser → /admin/* | All admin routes gated by JWT session cookie; proxy.ts rejects unauthenticated requests before any page code runs | +| Login form → CredentialsProvider | Email + password transmitted over HTTPS; validated in server-side authorize() only | +| NEXTAUTH_SECRET → JWT signing | All session tokens are HMAC-signed; tampering is detectable | +| ADMIN_EMAIL/ADMIN_PASSWORD → env vars | Credentials never in source code; must be in .env.local and Vercel environment | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Disposition | Mitigation Plan | +|-----------|----------|-----------|-------------|-----------------| +| T-02-01 | Spoofing | Admin login | mitigate | CredentialsProvider validates against env vars server-side; password never logged; JWT signed with NEXTAUTH_SECRET | +| T-02-02 | Tampering | JWT session cookie | mitigate | next-auth signs JWT with NEXTAUTH_SECRET (HMAC-SHA256); proxy.ts verifies signature on every /admin request via getToken() | +| T-02-03 | Information Disclosure | ADMIN_PASSWORD in env | mitigate | Stored only in .env.local (gitignored) and Vercel environment secrets; never returned in API responses | +| T-02-04 | Elevation of Privilege | /api/auth/* exemption | accept | NextAuth API routes are exempt from session guard by design; they perform their own CSRF and credential validation internally | +| T-02-05 | Denial of Service | Brute-force login | accept | Single admin, not a public product; no rate limiting in v1. If needed in v2, add next-auth rate limit middleware. | + + + +After plan execution: +1. `npm run build` — no TypeScript errors +2. `npm run dev`, visit http://localhost:3000/admin → redirects to /admin/login +3. Submit wrong credentials → error message "Email o password non corretti." appears +4. Submit correct ADMIN_EMAIL + ADMIN_PASSWORD → redirects to /admin (200, even if page is blank) +5. Visit http://localhost:3000/c/any-token → still validates token (client path unchanged) +6. Visit http://localhost:3000/api/auth/session after login → returns `{ user: { email, id: "admin" } }` +7. Confirm src/middleware.ts does not exist in the repo + + + +- Admin can log in at /admin/login with env-var credentials and receive a JWT session cookie +- All /admin/* routes (except /admin/login and /api/auth/*) redirect unauthenticated visitors to /admin/login +- Client token route /c/:path* is unaffected +- No DB users table exists or is needed +- src/proxy.ts is the middleware file — src/middleware.ts never created +- npm run build passes cleanly + + + +After completion, create `.planning/phases/02-admin-area-interactive-features/02-01-SUMMARY.md` + diff --git a/.planning/milestones/v1.0-phases/02-admin-area-interactive-features/02-01-SUMMARY.md b/.planning/milestones/v1.0-phases/02-admin-area-interactive-features/02-01-SUMMARY.md new file mode 100644 index 0000000..0e904bc --- /dev/null +++ b/.planning/milestones/v1.0-phases/02-admin-area-interactive-features/02-01-SUMMARY.md @@ -0,0 +1,92 @@ +--- +phase: "02-admin-area-interactive-features" +plan: 01 +subsystem: "auth" +tags: [next-auth, credentials, jwt, admin, session, middleware] +dependency_graph: + requires: [] + provides: [admin-session-auth, admin-login-page, proxy-admin-guard] + affects: [src/proxy.ts, src/lib/auth.ts] +tech_stack: + added: [next-auth@4] + patterns: [CredentialsProvider, JWT session strategy, edge proxy guard] +key_files: + created: + - src/lib/auth.ts + - src/app/api/auth/[...nextauth]/route.ts + - src/app/admin/login/page.tsx + modified: + - src/proxy.ts + - package.json + - package-lock.json + - .env.local +decisions: + - "Keep proxy export named 'proxy' (not 'middleware') — Next.js 16 renamed the middleware concept back to proxy, breaking the plan's rename instruction" + - "Wrap useSearchParams() in Suspense boundary — required by Next.js App Router for static prerendering" + - "Single admin user, env-var credentials only — no DB users table (stateless JWT)" +metrics: + duration: "~15 minutes" + completed: "2026-05-15T08:42:35Z" + tasks_completed: 2 + files_changed: 7 +--- + +# Phase 02 Plan 01: Auth.js Admin Session + Proxy Guard Summary + +Auth.js v4 CredentialsProvider with JWT sessions gates the entire /admin/* area using env-var credentials (no DB users table), with an edge proxy guard in src/proxy.ts that validates sessions via getToken() before any admin page code runs. + +## What Was Built + +### Task 1: next-auth@4 installation and auth config +- Installed `next-auth@4` (stable; v5 still RC as of 2026-05-15) +- Created `src/lib/auth.ts` — NextAuthOptions with CredentialsProvider reading `ADMIN_EMAIL` + `ADMIN_PASSWORD` from env vars; JWT session strategy (stateless, no DB adapter) +- Created `src/app/api/auth/[...nextauth]/route.ts` — NextAuth catch-all handler (GET + POST) +- Updated `.env.local` with `NEXTAUTH_URL`, `NEXTAUTH_SECRET` (32-byte base64), `ADMIN_EMAIL`, `ADMIN_PASSWORD` + +### Task 2: Proxy guard and login page +- Extended `src/proxy.ts` with `/admin/*` session guard using `getToken()` from `next-auth/jwt` +- `/admin/login` and `/api/auth/*` exempted from the guard (pass-through) +- Unauthenticated `/admin/*` requests redirect to `/admin/login?callbackUrl=` +- `/c/:path*` client token validation logic preserved verbatim from Phase 1 +- matcher updated: `["/admin/:path*", "/c/:path*"]` +- Created `src/app/admin/login/page.tsx` — email+password Client Component with `signIn('credentials')`, inline error display ("Email o password non corretti."), redirect on success + +## Commits + +| Task | Commit | Description | +|------|--------|-------------| +| 1 | 5d363a6 | feat(02-01): install next-auth@4, configure CredentialsProvider auth | +| 2 | 69f8a7e | feat(02-01): extend proxy.ts with admin session guard, add login page | + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 1 - Bug] Next.js 16 proxy export name is 'proxy', not 'middleware'** +- **Found during:** Task 2 first build attempt +- **Issue:** The plan instructed renaming the export to `middleware` (Next.js 15 convention), but this project runs Next.js 16.2.6, which introduced the `proxy` concept and requires the function to be named `proxy`. The build failed with: "Proxy is missing expected function export name" +- **Fix:** Kept the export name as `proxy` — consistent with the existing Phase 1 file and Next.js 16 API +- **Files modified:** `src/proxy.ts` + +**2. [Rule 1 - Bug] useSearchParams() requires Suspense boundary in App Router** +- **Found during:** Task 2 second build attempt +- **Issue:** `useSearchParams()` in a Client Component causes a build failure during static page generation without a Suspense boundary. Error: "useSearchParams() should be wrapped in a suspense boundary at page /admin/login" +- **Fix:** Extracted the form into `AdminLoginForm` component; wrapped it in `` inside the default export `AdminLoginPage` +- **Files modified:** `src/app/admin/login/page.tsx` + +## Known Stubs + +None — all implemented functionality is complete and functional. + +## Threat Surface Scan + +No new security surface beyond what was planned in the threat model: +- T-02-01: Mitigated — CredentialsProvider validates against env vars server-side +- T-02-02: Mitigated — JWT signed with NEXTAUTH_SECRET, verified via getToken() on every /admin request +- T-02-03: Mitigated — ADMIN_PASSWORD stored only in .env.local (gitignored) and Vercel secrets +- T-02-04: Accepted — /api/auth/* exempt by design, NextAuth handles its own CSRF +- T-02-05: Accepted — No rate limiting in v1 + +## Self-Check: PASSED + +All created files exist on disk. Both task commits (5d363a6, 69f8a7e) verified in git log. diff --git a/.planning/milestones/v1.0-phases/02-admin-area-interactive-features/02-02-PLAN.md b/.planning/milestones/v1.0-phases/02-admin-area-interactive-features/02-02-PLAN.md new file mode 100644 index 0000000..3579884 --- /dev/null +++ b/.planning/milestones/v1.0-phases/02-admin-area-interactive-features/02-02-PLAN.md @@ -0,0 +1,561 @@ +--- +phase: "02-admin-area-interactive-features" +plan: 02 +type: execute +wave: 2 +depends_on: + - "02-01" +files_modified: + - src/app/admin/page.tsx + - src/app/admin/layout.tsx + - src/app/admin/clients/new/page.tsx + - src/app/admin/clients/new/actions.ts + - src/lib/admin-queries.ts + - src/components/admin/ClientRow.tsx + - src/components/admin/NavBar.tsx +autonomous: true +requirements: + - ADMIN-01 + - ADMIN-02 + +must_haves: + truths: + - "Admin can see a list of all clients at /admin with name, brand, and payment status badges" + - "Admin can create a new client via /admin/clients/new form; on submit the client row + two payment rows are inserted and the secret link (token) is auto-generated" + - "After creating a client, admin is redirected to /admin (or /admin/clients/[id] for detail)" + - "The new client's shareable link /c/[token] is visible to the admin immediately after creation" + - "Payment status badges for Acconto and Saldo are visible in the client list row" + artifacts: + - path: "src/app/admin/page.tsx" + provides: "Admin client list — Server Component fetching all clients with payments" + contains: "export default async function" + - path: "src/app/admin/layout.tsx" + provides: "Admin layout with minimal NavBar (logo + Clienti link + logout button)" + contains: "NavBar" + - path: "src/app/admin/clients/new/page.tsx" + provides: "New client form page" + min_lines: 30 + - path: "src/app/admin/clients/new/actions.ts" + provides: "Server Action: createClient() — inserts client + 2 payment rows" + contains: "createClient" + - path: "src/lib/admin-queries.ts" + provides: "Admin-side DB query functions (getAllClientsWithPayments)" + contains: "getAllClientsWithPayments" + key_links: + - from: "src/app/admin/page.tsx" + to: "src/lib/admin-queries.ts" + via: "getAllClientsWithPayments()" + pattern: "getAllClientsWithPayments" + - from: "src/app/admin/clients/new/page.tsx" + to: "src/app/admin/clients/new/actions.ts" + via: "createClient Server Action" + pattern: "createClient" + - from: "createClient action" + to: "clients + payments tables" + via: "db.insert(clients) + db.insert(payments) x2" + pattern: "db.insert" +--- + + +**Admin Client List + Create Client:** Build the admin home page (client list with payment badges) and the new client creation form. The create form auto-generates the nanoid secret token, inserts the client row, and creates two payment rows (Acconto 50% / Saldo 50%) in a single Server Action. + +Purpose: Deliver the first end-to-end admin capability — admin can enter a client's details and immediately get a shareable /c/[token] link. Implements ADMIN-01 (client list with status) and the creation half of ADMIN-02 (per D-05 Server Actions, D-07 list→detail layout, D-09 minimal nav). + +Output: /admin shows all clients with payment badges; /admin/clients/new creates a client and two payment stubs. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/ROADMAP.md +@.planning/phases/02-admin-area-interactive-features/02-CONTEXT.md +@.planning/phases/02-admin-area-interactive-features/02-01-SUMMARY.md + + + +```typescript +export const clients = pgTable("clients", { + id: text("id").primaryKey().$defaultFn(() => nanoid()), + name: text("name").notNull(), + brand_name: text("brand_name").notNull(), + brief: text("brief").notNull(), + token: text("token").notNull().unique().$defaultFn(() => nanoid()), + accepted_total: numeric("accepted_total", { precision: 10, scale: 2 }).default("0"), + created_at: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), +}); + +export const payments = pgTable("payments", { + id: text("id").primaryKey().$defaultFn(() => nanoid()), + client_id: text("client_id").notNull().references(() => clients.id, { onDelete: "cascade" }), + label: text("label").notNull(), // "Acconto 50%" | "Saldo 50%" + amount: numeric("amount", { precision: 10, scale: 2 }).notNull(), + status: text("status").notNull().default("da_saldare"), // da_saldare | inviata | saldato + paid_at: timestamp("paid_at", { withTimezone: true }), +}); + +export type Client = typeof clients.$inferSelect; +export type NewClient = typeof clients.$inferInsert; +export type Payment = typeof payments.$inferSelect; +``` + +From src/db/index.ts: +```typescript +export const db = drizzle(client); // drizzle-orm/postgres-js +``` + + + + + + + Task 1: Create src/lib/admin-queries.ts and admin layout + NavBar component + + src/lib/admin-queries.ts + src/app/admin/layout.tsx + src/components/admin/NavBar.tsx + + + Create `src/lib/admin-queries.ts` — all admin-side DB reads live here: + ```typescript + import { db } from "@/db"; + import { clients, payments } from "@/db/schema"; + import { eq } from "drizzle-orm"; + + export type ClientWithPayments = { + id: string; + name: string; + brand_name: string; + token: string; + accepted_total: string; + created_at: Date; + payments: Array<{ + id: string; + label: string; + status: string; + amount: string; + }>; + }; + + export async function getAllClientsWithPayments(): Promise { + const allClients = await db + .select() + .from(clients) + .orderBy(clients.created_at); + + if (allClients.length === 0) return []; + + const allPayments = await db + .select() + .from(payments); + + return allClients.map((c) => ({ + id: c.id, + name: c.name, + brand_name: c.brand_name, + token: c.token, + accepted_total: c.accepted_total ?? "0", + created_at: c.created_at, + payments: allPayments + .filter((p) => p.client_id === c.id) + .map((p) => ({ + id: p.id, + label: p.label, + status: p.status, + amount: p.amount, + })), + })); + } + + export async function getClientById(id: string) { + const rows = await db + .select() + .from(clients) + .where(eq(clients.id, id)) + .limit(1); + return rows[0] ?? null; + } + ``` + + Create `src/components/admin/NavBar.tsx` — minimal nav per D-09 (no sidebar): + ```typescript + "use client"; + + import Link from "next/link"; + import { signOut } from "next-auth/react"; + import { Button } from "@/components/ui/button"; + + export function NavBar() { + return ( + + ); + } + ``` + + Create `src/app/admin/layout.tsx` — wraps all /admin/* pages: + ```typescript + import { NavBar } from "@/components/admin/NavBar"; + + export default function AdminLayout({ + children, + }: { + children: React.ReactNode; + }) { + return ( +
+ +
{children}
+
+ ); + } + ``` +
+ + test -f src/lib/admin-queries.ts && grep -q "getAllClientsWithPayments" src/lib/admin-queries.ts && echo "admin-queries.ts created" + grep -q "getClientById" src/lib/admin-queries.ts && echo "getClientById exported" + test -f src/components/admin/NavBar.tsx && grep -q "signOut" src/components/admin/NavBar.tsx && echo "NavBar with logout" + test -f src/app/admin/layout.tsx && grep -q "NavBar" src/app/admin/layout.tsx && echo "Admin layout wraps NavBar" + npm run build 2>&1 | grep -v "warning" | grep -qi "error" && echo "BUILD ERRORS" || echo "TypeScript OK" + + + - src/lib/admin-queries.ts exports getAllClientsWithPayments() and getClientById() + - NavBar renders with "Clienti" link and "Esci" button + - Admin layout wraps all /admin/* pages with NavBar + centered main content area + - npm run build passes + +
+ + + Task 2: Build /admin client list page and /admin/clients/new create-client flow + + src/app/admin/page.tsx + src/components/admin/ClientRow.tsx + src/app/admin/clients/new/page.tsx + src/app/admin/clients/new/actions.ts + + + Create `src/components/admin/ClientRow.tsx` — single row in client list table: + ```typescript + import Link from "next/link"; + import { Badge } from "@/components/ui/badge"; + import type { ClientWithPayments } from "@/lib/admin-queries"; + + const statusConfig: Record = { + da_saldare: { label: "Da saldare", variant: "destructive" }, + inviata: { label: "Inviata", variant: "secondary" }, + saldato: { label: "Saldato", variant: "default" }, + }; + + export function ClientRow({ client }: { client: ClientWithPayments }) { + const acconto = client.payments.find((p) => p.label.includes("Acconto")); + const saldo = client.payments.find((p) => p.label.includes("Saldo")); + + return ( + + + + {client.name} + +

{client.brand_name}

+ + + € {parseFloat(client.accepted_total).toLocaleString("it-IT", { minimumFractionDigits: 2 })} + + + {acconto && ( + + Acconto: {statusConfig[acconto.status]?.label ?? acconto.status} + + )} + + + {saldo && ( + + Saldo: {statusConfig[saldo.status]?.label ?? saldo.status} + + )} + + + + /c/{client.token.slice(0, 10)}… + + + + ); + } + ``` + + Create `src/app/admin/page.tsx` — Server Component, no client state: + ```typescript + import Link from "next/link"; + import { getAllClientsWithPayments } from "@/lib/admin-queries"; + import { ClientRow } from "@/components/admin/ClientRow"; + import { Button } from "@/components/ui/button"; + + export const revalidate = 0; // always fresh — admin needs real-time data + + export default async function AdminDashboard() { + const clients = await getAllClientsWithPayments(); + + return ( +
+
+

Clienti

+ +
+ + {clients.length === 0 ? ( +
+

Nessun cliente ancora.

+

+ + Crea il primo cliente + +

+
+ ) : ( +
+ + + + + + + + + + + + {clients.map((client) => ( + + ))} + +
ClienteTotaleAccontoSaldoLink
+
+ )} +
+ ); + } + ``` + + Create `src/app/admin/clients/new/actions.ts` — Server Action (per D-05): + ```typescript + "use server"; + + import { redirect } from "next/navigation"; + import { revalidatePath } from "next/cache"; + import { z } from "zod"; + import { db } from "@/db"; + import { clients, payments } from "@/db/schema"; + + const createClientSchema = z.object({ + name: z.string().min(1, "Nome richiesto"), + brand_name: z.string().min(1, "Nome brand richiesto"), + brief: z.string().min(1, "Brief richiesto"), + }); + + export async function createClient(formData: FormData) { + const raw = { + name: formData.get("name") as string, + brand_name: formData.get("brand_name") as string, + brief: formData.get("brief") as string, + }; + + const parsed = createClientSchema.safeParse(raw); + if (!parsed.success) { + // In v1 return errors as thrown string — form displays validation inline + throw new Error(parsed.error.issues.map((i) => i.message).join(", ")); + } + + // Insert client — token and id are auto-generated by $defaultFn(() => nanoid()) + const [newClient] = await db + .insert(clients) + .values({ + name: parsed.data.name, + brand_name: parsed.data.brand_name, + brief: parsed.data.brief, + }) + .returning({ id: clients.id, token: clients.token }); + + // Always create two payment stubs per client — Acconto 50% and Saldo 50% + // Amounts default to 0 until admin sets accepted_total; admin updates separately + await db.insert(payments).values([ + { + client_id: newClient.id, + label: "Acconto 50%", + amount: "0", + status: "da_saldare", + }, + { + client_id: newClient.id, + label: "Saldo 50%", + amount: "0", + status: "da_saldare", + }, + ]); + + revalidatePath("/admin"); + redirect(`/admin/clients/${newClient.id}`); + } + ``` + + Create `src/app/admin/clients/new/page.tsx` — form using the Server Action: + ```typescript + import { createClient } from "./actions"; + import { Button } from "@/components/ui/button"; + import { Input } from "@/components/ui/input"; + import { Label } from "@/components/ui/label"; + import { Textarea } from "@/components/ui/textarea"; + import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + import Link from "next/link"; + + export default function NewClientPage() { + return ( +
+
+ + ← Clienti + +
+ + + Nuovo cliente + + +
+
+ + +
+
+ + +
+
+ +