From 2123dc9d006170c38dfefed1fcd2a7ae81a78890 Mon Sep 17 00:00:00 2001 From: Simone Cavalli Date: Wed, 13 May 2026 15:20:50 +0200 Subject: [PATCH] =?UTF-8?q?fix(01-foundation):=20resolve=20plan=20checker?= =?UTF-8?q?=20blockers=20=E2=80=94=203=20fixes=20across=2001-02,=2001-03,?= =?UTF-8?q?=2001-04?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 01-02: wave corrected from 1 to 2 (has depends_on: ["01-01"]) - 01-03: middleware rewritten to Edge-compatible fetch pattern; internal API route app/api/internal/validate-token/route.ts handles DB query in Node.js runtime; tasks/deliverables queries scoped with inArray(); accepted_total null-coalesced - 01-04: Task 1 and Task 6 merged → 5 tasks total (was 6, exceeded threshold) - STATE.md: updated to reflect Phase 1 planning verified, ready for execution Co-Authored-By: Claude Sonnet 4.6 --- .planning/STATE.md | 68 +++++++ .../01-02-PLAN.md | 2 +- .../01-03-PLAN.md | 158 +++++++++------ .../01-04-PLAN.md | 187 +++++++++--------- 4 files changed, 267 insertions(+), 148 deletions(-) create mode 100644 .planning/STATE.md diff --git a/.planning/STATE.md b/.planning/STATE.md new file mode 100644 index 0000000..8057d33 --- /dev/null +++ b/.planning/STATE.md @@ -0,0 +1,68 @@ +# Project State + +## Project Reference + +See: .planning/PROJECT.md (updated 2026-05-09) + +**Core value:** Il cliente apre il link e vede esattamente a che punto è il suo progetto, cosa deve ancora succedere e cosa ha già approvato — senza dover scrivere email per chiedere aggiornamenti. +**Current focus:** Phase 1 — Foundation & Client Dashboard + +## Current Position + +Phase: 1 of 4 (Foundation & Client Dashboard) +Plan: 0 of 5 in current phase +Status: Ready to execute — all 5 plans verified, zero blockers +Last activity: 2026-05-13 — Phase 1 planning complete, ready for `/gsd-execute-phase 1` + +Progress: [░░░░░░░░░░] 0% + +## Performance Metrics + +**Velocity:** +- Total plans completed: 0 +- Average duration: - +- Total execution time: 0 hours + +**By Phase:** + +| Phase | Plans | Total | Avg/Plan | +|-------|-------|-------|----------| +| - | - | - | - | + +**Recent Trend:** +- Last 5 plans: - +- Trend: - + +*Updated after each plan completion* + +## Accumulated Context + +### Decisions + +Decisions are logged in PROJECT.md Key Decisions table. +Recent decisions affecting current work: + +- Phase 1: `clients.token` deve essere un campo separato (non la PK) — rotazionabile via single UPDATE +- Phase 1: `clients.accepted_total` denormalizzato — la client API non tocca mai `quote_items` +- Phase 1: `deliverables.approved_at` come audit trail immutabile dal giorno uno +- Phase 1: DNS su `welcomeclient.iamcavalli.net` da configurare nella Fase 1 + +### Pending Todos + +None yet. + +### Blockers/Concerns + +None yet. + +## Deferred Items + +| Category | Item | Status | Deferred At | +|----------|------|--------|-------------| +| v2 | Claude AI onboarding (CLAUDE-01, CLAUDE-02, CLAUDE-03) | Phase 4 | Roadmap init | + +## Session Continuity + +Last session: 2026-05-13 +Stopped at: Phase 1 planning complete — 5 plans verified (3 iterations, all blockers resolved) +Resume with: `/gsd-execute-phase 1` starting from 01-01-PLAN.md \ No newline at end of file diff --git a/.planning/phases/01-foundation-client-dashboard/01-02-PLAN.md b/.planning/phases/01-foundation-client-dashboard/01-02-PLAN.md index 968b476..ae49598 100644 --- a/.planning/phases/01-foundation-client-dashboard/01-02-PLAN.md +++ b/.planning/phases/01-foundation-client-dashboard/01-02-PLAN.md @@ -2,7 +2,7 @@ phase: "01-foundation-client-dashboard" plan: 02 type: execute -wave: 1 +wave: 2 depends_on: - "01-01" files_modified: diff --git a/.planning/phases/01-foundation-client-dashboard/01-03-PLAN.md b/.planning/phases/01-foundation-client-dashboard/01-03-PLAN.md index 5244b74..8b0d98d 100644 --- a/.planning/phases/01-foundation-client-dashboard/01-03-PLAN.md +++ b/.planning/phases/01-foundation-client-dashboard/01-03-PLAN.md @@ -8,6 +8,7 @@ depends_on: - "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 @@ -28,8 +29,12 @@ must_haves: - "TypeScript types are exported for downstream UI rendering" artifacts: - path: "src/middleware.ts" - provides: "Token validation at Next.js edge middleware" + 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" @@ -42,6 +47,10 @@ must_haves: 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" @@ -79,82 +88,111 @@ Output: Fully functional `/c/[token]` route that fetches real client data and pr - Task 1: Create src/middleware.ts to validate client tokens at the edge + 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) - Create `src/middleware.ts` at project root (NOT in src/app): - + **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 middleware(request: NextRequest) { - const pathname = request.nextUrl.pathname; - - // Only validate client portal routes /c/[token]/* - if (!pathname.startsWith('/c/')) { - return NextResponse.next(); + + export async function GET(request: NextRequest) { + const token = request.nextUrl.searchParams.get('token'); + + if (!token) { + return NextResponse.json({ valid: false }, { status: 400 }); } - - // 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 + const rows = 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 }); + + if (rows.length === 0) { + return NextResponse.json({ valid: false }, { 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 }); + + 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 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) + - 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 "clients.token" src/middleware.ts && echo "Token validation query present" + ! 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` exists and exports middleware function - - Matcher is configured for `/c/:path*` - - Token validation query checks `clients.token` - - Non-existent tokens return 404 + - `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 @@ -171,7 +209,7 @@ Output: Fully functional `/c/[token]` route that fetches real client data and pr Create `src/lib/client-view.ts`: ```typescript - import { eq } from 'drizzle-orm'; + import { eq, inArray } from 'drizzle-orm'; import { db } from '@/db'; import { clients, phases, tasks, deliverables, payments, documents, notes } from '@/db/schema'; @@ -252,16 +290,24 @@ Output: Fully functional `/c/[token]` route that fetches real client data and pr .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 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 @@ -356,7 +402,7 @@ Output: Fully functional `/c/[token]` route that fetches real client data and pr name: client.name, brand_name: client.brand_name, brief: client.brief, - accepted_total: client.accepted_total, + accepted_total: client.accepted_total ?? '0', }, phases: phasesList, payments: paymentsList, @@ -379,6 +425,8 @@ Output: Fully functional `/c/[token]` route that fetches real client data and pr 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" diff --git a/.planning/phases/01-foundation-client-dashboard/01-04-PLAN.md b/.planning/phases/01-foundation-client-dashboard/01-04-PLAN.md index 8a335d6..b658e95 100644 --- a/.planning/phases/01-foundation-client-dashboard/01-04-PLAN.md +++ b/.planning/phases/01-foundation-client-dashboard/01-04-PLAN.md @@ -97,14 +97,17 @@ Output: Fully rendered client portal with all DASH-02 through DASH-10 requiremen - Task 1: Update tailwind.config.ts with light & clean design tokens and extend globals.css + 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: @@ -190,7 +193,6 @@ Output: Fully rendered client portal with all DASH-02 through DASH-10 requiremen line-height: 1.6; } - /* Typography */ h1 { @apply text-3xl font-bold tracking-tight; } @@ -211,7 +213,6 @@ Output: Fully rendered client portal with all DASH-02 through DASH-10 requiremen @apply text-accent hover:underline transition-colors; } - /* Subtle border utilities */ .border-subtle { @apply border border-border-light; } @@ -220,16 +221,62 @@ Output: Fully rendered client portal with all DASH-02 through DASH-10 requiremen @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" - npm run build 2>&1 | grep -v "warning" | grep -q "error" && echo "TypeScript errors" || echo "Build OK" + 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 @@ -552,14 +599,12 @@ Output: Fully rendered client portal with all DASH-02 through DASH-10 requiremen - Task 4: Create PaymentStatus, DocumentsSection, and NotesSection components + Task 4: Create PaymentStatus component (accepted_total + payment rows with status badges) src/components/payment-status.tsx - src/components/documents-section.tsx - src/components/notes-section.tsx - src/lib/client-view.ts (payments, documents, notes shapes) + src/lib/client-view.ts (payments shape, PaymentStatus type) .planning/phases/01-foundation-client-dashboard/01-CONTEXT.md (D-10, D-11) @@ -637,6 +682,37 @@ Output: Fully rendered client portal with all DASH-02 through DASH-10 requiremen } ``` + 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 @@ -721,99 +797,26 @@ Output: Fully rendered client portal with all DASH-02 through DASH-10 requiremen ``` 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 + - 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/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" + 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" - - 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 + - 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 - - 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 - -