diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md new file mode 100644 index 0000000..689dbb1 --- /dev/null +++ b/.planning/ROADMAP.md @@ -0,0 +1,93 @@ +# 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: 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**: TBD +**UI hint**: yes +**Status**: Pending planning + +### 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**: TBD +**UI hint**: yes +**Status**: Pending planning + +### Phase 4: 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 3 +**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 + +| Phase | Plans | Status | Completed | +|-------|-------|--------|-----------| +| 1. Foundation & Client Dashboard | 5/5 | ✅ Planned | Ready for execution | +| 2. Admin Area & Interactive Features | 0/TBD | Planning next | - | +| 3. Service Catalog & Quote Builder | 0/TBD | Pending | - | +| 4. Claude AI Onboarding (v2) | 0/TBD | Pending | - | diff --git a/.planning/phases/01-foundation-client-dashboard/01-01-PLAN.md b/.planning/phases/01-foundation-client-dashboard/01-01-PLAN.md new file mode 100644 index 0000000..78619e0 --- /dev/null +++ b/.planning/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/phases/01-foundation-client-dashboard/01-02-PLAN.md b/.planning/phases/01-foundation-client-dashboard/01-02-PLAN.md new file mode 100644 index 0000000..968b476 --- /dev/null +++ b/.planning/phases/01-foundation-client-dashboard/01-02-PLAN.md @@ -0,0 +1,369 @@ +--- +phase: "01-foundation-client-dashboard" +plan: 02 +type: execute +wave: 1 +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/phases/01-foundation-client-dashboard/01-03-PLAN.md b/.planning/phases/01-foundation-client-dashboard/01-03-PLAN.md new file mode 100644 index 0000000..5244b74 --- /dev/null +++ b/.planning/phases/01-foundation-client-dashboard/01-03-PLAN.md @@ -0,0 +1,521 @@ +--- +phase: "01-foundation-client-dashboard" +plan: 03 +type: execute +wave: 2 +depends_on: + - "01-01" + - "01-02" +files_modified: + - src/middleware.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 at Next.js edge middleware" + contains: "function middleware" + - 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: "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 to validate client tokens at the edge + + src/middleware.ts + + + src/db/schema.ts (clients table definition) + package.json (verify Next.js version) + + + Create `src/middleware.ts` at project root (NOT in src/app): + + ```typescript + import { NextRequest, NextResponse } from 'next/server'; + import { eq } from 'drizzle-orm'; + import { db } from '@/db'; + import { clients } from '@/db/schema'; + + export async function middleware(request: NextRequest) { + const pathname = request.nextUrl.pathname; + + // Only validate client portal routes /c/[token]/* + if (!pathname.startsWith('/c/')) { + return NextResponse.next(); + } + + // Extract token from path: /c/[token]/... + const tokenMatch = pathname.match(/^\/c\/([a-zA-Z0-9_-]+)/); + if (!tokenMatch) { + return NextResponse.rewrite(new URL('/404', request.url), { status: 404 }); + } + + const token = tokenMatch[1]; + + try { + // Check if token exists in database + const client = await db + .select({ id: clients.id }) + .from(clients) + .where(eq(clients.token, token)) + .limit(1); + + if (client.length === 0) { + return NextResponse.rewrite(new URL('/404', request.url), { status: 404 }); + } + + // Token is valid, proceed + return NextResponse.next(); + } catch (error) { + console.error('Middleware error validating token:', error); + return NextResponse.rewrite(new URL('/500', request.url), { status: 500 }); + } + } + + export const config = { + matcher: ['/c/:path*'], + }; + ``` + + Key points: + - Middleware runs at the edge before any page renders + - Token is extracted from URL: /c/[token] + - Database query is a simple SELECT to check token existence + - Returns 404 if token not found (no enumeration hints) + - All errors return 500 (generic error handling) + + + 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 "clients.token" src/middleware.ts && echo "Token validation query present" + + + - `src/middleware.ts` exists and exports middleware function + - Matcher is configured for `/c/:path*` + - Token validation query checks `clients.token` + - Non-existent tokens return 404 + - 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 } 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 all tasks + const tasksRows = await db + .select() + .from(tasks) + .orderBy(tasks.sort_order); + + // Fetch all deliverables + const deliverables_rows = await db + .select() + .from(deliverables); + + // 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, + }, + 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)" + 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/phases/01-foundation-client-dashboard/01-04-PLAN.md b/.planning/phases/01-foundation-client-dashboard/01-04-PLAN.md new file mode 100644 index 0000000..8a335d6 --- /dev/null +++ b/.planning/phases/01-foundation-client-dashboard/01-04-PLAN.md @@ -0,0 +1,861 @@ +--- +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: Update tailwind.config.ts with light & clean design tokens and extend globals.css + + tailwind.config.ts + src/app/globals.css + + + tailwind.config.ts (current bootstrap) + src/app/globals.css (current bootstrap) + + + 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; + } + + /* Typography */ + 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; + } + + /* Subtle border utilities */ + .border-subtle { + @apply border border-border-light; + } + + .bg-subtle { + @apply bg-bg-subtle; + } + ``` + + + 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" + npm run build 2>&1 | grep -v "warning" | grep -q "error" && echo "TypeScript errors" || echo "Build OK" + + + - `tailwind.config.ts` contains color tokens: primary, secondary, accent, success, warning + - `globals.css` includes Tailwind directives and base typography + - `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, DocumentsSection, and NotesSection components + + src/components/payment-status.tsx + src/components/documents-section.tsx + src/components/notes-section.tsx + + + src/lib/client-view.ts (payments, documents, notes shapes) + .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. +

+
+ ); + } + ``` + + 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: + - PaymentStatus: shows accepted_total + 2 payment rows (Acconto 50%, Saldo 50%) with status badges (no amounts) + - DocumentsSection: clickable external links with ExternalLink icon + - NotesSection: read-only notes with formatted timestamps + - All use Card + Badge components from shadcn/ui +
+ + test -f src/components/payment-status.tsx && echo "PaymentStatus component exists" + test -f src/components/documents-section.tsx && echo "DocumentsSection component exists" + test -f src/components/notes-section.tsx && echo "NotesSection component exists" + grep -q "accepted_total" src/components/payment-status.tsx && echo "Total displayed" + grep -q "ExternalLink" src/components/documents-section.tsx && echo "Link icon present" + + + - All three components exist and are exported + - PaymentStatus displays accepted_total + 2 payment rows with status (no amounts) + - DocumentsSection shows clickable external links + - NotesSection shows read-only notes with timestamps (or empty state) + - All components use shadcn/ui Card and Badge + +
+ + + Task 5: Update app/c/[token]/page.tsx to render ClientDashboard with real data + + app/c/[token]/page.tsx + + + src/components/client-dashboard.tsx + src/lib/client-view.ts + + + Update `app/c/[token]/page.tsx`: + + ```typescript + import { getClientView } from '@/lib/client-view'; + import { ClientDashboard } from '@/components/client-dashboard'; + import { notFound } from 'next/navigation'; + + export const revalidate = 60; // ISR: revalidate every 60 seconds + + 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 | ClientHub`, + 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 ; + } + ``` + + This page: + - Fetches ClientView data + - Returns 404 if not found + - Generates dynamic metadata with client brand name + - Renders ClientDashboard with real data + + + test -f app/c/\[token\]/page.tsx && echo "Page file exists" + grep -q "ClientDashboard" app/c/\[token\]/page.tsx && echo "ClientDashboard rendered" + grep -q "getClientView" app/c/\[token\]/page.tsx && echo "Data fetched" + npm run build 2>&1 | grep -v "warning" | grep -q "error" && echo "Build errors" || echo "Build OK" + + + - Page renders ClientDashboard component with ClientView data + - 404 is returned if token is invalid + - Page metadata is dynamic (includes client brand name) + - `npm run build` succeeds + + + + + + +## 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/phases/01-foundation-client-dashboard/01-05-PLAN.md b/.planning/phases/01-foundation-client-dashboard/01-05-PLAN.md new file mode 100644 index 0000000..9876e1b --- /dev/null +++ b/.planning/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/phases/01-foundation-client-dashboard/SKELETON.md b/.planning/phases/01-foundation-client-dashboard/SKELETON.md new file mode 100644 index 0000000..bfe2ef1 --- /dev/null +++ b/.planning/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`)