fix(01-foundation): resolve plan checker blockers — 3 fixes across 01-02, 01-03, 01-04
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
phase: "01-foundation-client-dashboard"
|
phase: "01-foundation-client-dashboard"
|
||||||
plan: 02
|
plan: 02
|
||||||
type: execute
|
type: execute
|
||||||
wave: 1
|
wave: 2
|
||||||
depends_on:
|
depends_on:
|
||||||
- "01-01"
|
- "01-01"
|
||||||
files_modified:
|
files_modified:
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ depends_on:
|
|||||||
- "01-02"
|
- "01-02"
|
||||||
files_modified:
|
files_modified:
|
||||||
- src/middleware.ts
|
- src/middleware.ts
|
||||||
|
- app/api/internal/validate-token/route.ts
|
||||||
- src/lib/client-view.ts
|
- src/lib/client-view.ts
|
||||||
- app/c/[token]/page.tsx
|
- app/c/[token]/page.tsx
|
||||||
- app/c/[token]/layout.tsx
|
- app/c/[token]/layout.tsx
|
||||||
@@ -28,8 +29,12 @@ must_haves:
|
|||||||
- "TypeScript types are exported for downstream UI rendering"
|
- "TypeScript types are exported for downstream UI rendering"
|
||||||
artifacts:
|
artifacts:
|
||||||
- path: "src/middleware.ts"
|
- 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"
|
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"
|
- path: "src/lib/client-view.ts"
|
||||||
provides: "Client-safe type definitions and query functions"
|
provides: "Client-safe type definitions and query functions"
|
||||||
contains: "ClientView"
|
contains: "ClientView"
|
||||||
@@ -42,6 +47,10 @@ must_haves:
|
|||||||
min_lines: 10
|
min_lines: 10
|
||||||
key_links:
|
key_links:
|
||||||
- from: "src/middleware.ts"
|
- 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"
|
to: "Database query for token validation"
|
||||||
via: "db.select().from(clients).where(eq(clients.token, token))"
|
via: "db.select().from(clients).where(eq(clients.token, token))"
|
||||||
pattern: "clients\\.token"
|
pattern: "clients\\.token"
|
||||||
@@ -79,82 +88,111 @@ Output: Fully functional `/c/[token]` route that fetches real client data and pr
|
|||||||
<tasks>
|
<tasks>
|
||||||
|
|
||||||
<task type="auto">
|
<task type="auto">
|
||||||
<name>Task 1: Create src/middleware.ts to validate client tokens at the edge</name>
|
<name>Task 1: Create src/middleware.ts (Edge-compatible fetch pattern) + internal validate-token API route</name>
|
||||||
<files>
|
<files>
|
||||||
src/middleware.ts
|
src/middleware.ts
|
||||||
|
app/api/internal/validate-token/route.ts
|
||||||
</files>
|
</files>
|
||||||
<read_first>
|
<read_first>
|
||||||
src/db/schema.ts (clients table definition)
|
src/db/schema.ts (clients table definition)
|
||||||
package.json (verify Next.js version)
|
package.json (verify Next.js version)
|
||||||
</read_first>
|
</read_first>
|
||||||
<action>
|
<action>
|
||||||
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
|
```typescript
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
import { db } from '@/db';
|
import { db } from '@/db';
|
||||||
import { clients } from '@/db/schema';
|
import { clients } from '@/db/schema';
|
||||||
|
|
||||||
export async function middleware(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
const pathname = request.nextUrl.pathname;
|
const token = request.nextUrl.searchParams.get('token');
|
||||||
|
|
||||||
// Only validate client portal routes /c/[token]/*
|
if (!token) {
|
||||||
if (!pathname.startsWith('/c/')) {
|
return NextResponse.json({ valid: false }, { status: 400 });
|
||||||
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 {
|
try {
|
||||||
// Check if token exists in database
|
const rows = await db
|
||||||
const client = await db
|
|
||||||
.select({ id: clients.id })
|
.select({ id: clients.id })
|
||||||
.from(clients)
|
.from(clients)
|
||||||
.where(eq(clients.token, token))
|
.where(eq(clients.token, token))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (client.length === 0) {
|
if (rows.length === 0) {
|
||||||
return NextResponse.rewrite(new URL('/404', request.url), { status: 404 });
|
return NextResponse.json({ valid: false }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Token is valid, proceed
|
return NextResponse.json({ valid: true }, { status: 200 });
|
||||||
return NextResponse.next();
|
} catch {
|
||||||
} catch (error) {
|
return NextResponse.json({ valid: false }, { status: 500 });
|
||||||
console.error('Middleware error validating token:', error);
|
|
||||||
return NextResponse.rewrite(new URL('/500', request.url), { 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 = {
|
export const config = {
|
||||||
matcher: ['/c/:path*'],
|
matcher: ['/c/:path*'],
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
Key points:
|
Key points:
|
||||||
- Middleware runs at the edge before any page renders
|
- Middleware is Edge-compatible: no Node.js imports, only `fetch()`
|
||||||
- Token is extracted from URL: /c/[token]
|
- DB query lives in the API route (Node.js runtime) where postgres-js works correctly
|
||||||
- Database query is a simple SELECT to check token existence
|
- Token is URL-encoded before being passed as query param
|
||||||
- Returns 404 if token not found (no enumeration hints)
|
- Non-existent or invalid tokens resolve to `/not-found` (Next.js built-in 404 page)
|
||||||
- All errors return 500 (generic error handling)
|
- Internal API route should not be called directly by clients (no auth secret needed — it only returns boolean valid/invalid)
|
||||||
</action>
|
</action>
|
||||||
<verify>
|
<verify>
|
||||||
<automated>test -f src/middleware.ts && echo "middleware.ts exists"</automated>
|
<automated>test -f src/middleware.ts && echo "middleware.ts exists"</automated>
|
||||||
<automated>grep -q "export.*function middleware" src/middleware.ts && echo "middleware function exported"</automated>
|
<automated>grep -q "export.*function middleware" src/middleware.ts && echo "middleware function exported"</automated>
|
||||||
<automated>grep -q "matcher.*c/" src/middleware.ts && echo "matcher configured for /c/ routes"</automated>
|
<automated>grep -q "matcher.*c/" src/middleware.ts && echo "matcher configured for /c/ routes"</automated>
|
||||||
<automated>grep -q "clients.token" src/middleware.ts && echo "Token validation query present"</automated>
|
<automated>! grep -q "from '@/db'" src/middleware.ts && echo "middleware does not import drizzle/db (good — Edge safe)"</automated>
|
||||||
|
<automated>test -f app/api/internal/validate-token/route.ts && echo "internal validate-token route exists"</automated>
|
||||||
|
<automated>grep -q "clients.token" app/api/internal/validate-token/route.ts && echo "Token DB query in API route"</automated>
|
||||||
</verify>
|
</verify>
|
||||||
<acceptance_criteria>
|
<acceptance_criteria>
|
||||||
- `src/middleware.ts` exists and exports middleware function
|
- `src/middleware.ts` does NOT import Drizzle/postgres-js (Edge-safe)
|
||||||
- Matcher is configured for `/c/:path*`
|
- `src/middleware.ts` fetches `/api/internal/validate-token?token=X`
|
||||||
- Token validation query checks `clients.token`
|
- `app/api/internal/validate-token/route.ts` queries `clients.token` via Drizzle
|
||||||
- Non-existent tokens return 404
|
- Non-existent tokens return `/not-found` (404)
|
||||||
|
- Matcher configured for `/c/:path*`
|
||||||
- TypeScript compiles without errors
|
- TypeScript compiles without errors
|
||||||
</acceptance_criteria>
|
</acceptance_criteria>
|
||||||
</task>
|
</task>
|
||||||
@@ -171,7 +209,7 @@ Output: Fully functional `/c/[token]` route that fetches real client data and pr
|
|||||||
Create `src/lib/client-view.ts`:
|
Create `src/lib/client-view.ts`:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq, inArray } from 'drizzle-orm';
|
||||||
import { db } from '@/db';
|
import { db } from '@/db';
|
||||||
import { clients, phases, tasks, deliverables, payments, documents, notes } from '@/db/schema';
|
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))
|
.where(eq(phases.client_id, client.id))
|
||||||
.orderBy(phases.sort_order);
|
.orderBy(phases.sort_order);
|
||||||
|
|
||||||
// Fetch all tasks
|
// Fetch tasks scoped to this client's phases only
|
||||||
const tasksRows = await db
|
const phaseIds = phasesRows.map((p) => p.id);
|
||||||
.select()
|
const tasksRows = phaseIds.length === 0
|
||||||
.from(tasks)
|
? []
|
||||||
.orderBy(tasks.sort_order);
|
: await db
|
||||||
|
.select()
|
||||||
// Fetch all deliverables
|
.from(tasks)
|
||||||
const deliverables_rows = await db
|
.where(inArray(tasks.phase_id, phaseIds))
|
||||||
.select()
|
.orderBy(tasks.sort_order);
|
||||||
.from(deliverables);
|
|
||||||
|
// 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
|
// Fetch payments
|
||||||
const paymentsRows = await db
|
const paymentsRows = await db
|
||||||
@@ -356,7 +402,7 @@ Output: Fully functional `/c/[token]` route that fetches real client data and pr
|
|||||||
name: client.name,
|
name: client.name,
|
||||||
brand_name: client.brand_name,
|
brand_name: client.brand_name,
|
||||||
brief: client.brief,
|
brief: client.brief,
|
||||||
accepted_total: client.accepted_total,
|
accepted_total: client.accepted_total ?? '0',
|
||||||
},
|
},
|
||||||
phases: phasesList,
|
phases: phasesList,
|
||||||
payments: paymentsList,
|
payments: paymentsList,
|
||||||
@@ -379,6 +425,8 @@ Output: Fully functional `/c/[token]` route that fetches real client data and pr
|
|||||||
<automated>grep -q "interface ClientView" src/lib/client-view.ts && echo "ClientView interface defined"</automated>
|
<automated>grep -q "interface ClientView" src/lib/client-view.ts && echo "ClientView interface defined"</automated>
|
||||||
<automated>grep -q "export async function getClientView" src/lib/client-view.ts && echo "getClientView function exported"</automated>
|
<automated>grep -q "export async function getClientView" src/lib/client-view.ts && echo "getClientView function exported"</automated>
|
||||||
<automated>! grep -q "quote_items\|service_catalog" src/lib/client-view.ts && echo "quote_items not referenced (good)"</automated>
|
<automated>! grep -q "quote_items\|service_catalog" src/lib/client-view.ts && echo "quote_items not referenced (good)"</automated>
|
||||||
|
<automated>grep -q "inArray" src/lib/client-view.ts && echo "inArray scoping present"</automated>
|
||||||
|
<automated>grep -q "accepted_total.*?? '0'" src/lib/client-view.ts && echo "null coalescing on accepted_total"</automated>
|
||||||
<automated>npm run build 2>&1 | grep -v "warning" | grep -q "error" && echo "TypeScript errors" || echo "TypeScript OK"</automated>
|
<automated>npm run build 2>&1 | grep -v "warning" | grep -q "error" && echo "TypeScript errors" || echo "TypeScript OK"</automated>
|
||||||
</verify>
|
</verify>
|
||||||
<acceptance_criteria>
|
<acceptance_criteria>
|
||||||
|
|||||||
@@ -97,14 +97,17 @@ Output: Fully rendered client portal with all DASH-02 through DASH-10 requiremen
|
|||||||
<tasks>
|
<tasks>
|
||||||
|
|
||||||
<task type="auto">
|
<task type="auto">
|
||||||
<name>Task 1: Update tailwind.config.ts with light & clean design tokens and extend globals.css</name>
|
<name>Task 1: Configure design tokens (tailwind.config.ts + globals.css) and wire app/c/[token]/page.tsx to ClientDashboard</name>
|
||||||
<files>
|
<files>
|
||||||
tailwind.config.ts
|
tailwind.config.ts
|
||||||
src/app/globals.css
|
src/app/globals.css
|
||||||
|
app/c/[token]/page.tsx
|
||||||
</files>
|
</files>
|
||||||
<read_first>
|
<read_first>
|
||||||
tailwind.config.ts (current bootstrap)
|
tailwind.config.ts (current bootstrap)
|
||||||
src/app/globals.css (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)
|
||||||
</read_first>
|
</read_first>
|
||||||
<action>
|
<action>
|
||||||
Update `tailwind.config.ts` to define light & clean design tokens:
|
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;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Typography */
|
|
||||||
h1 {
|
h1 {
|
||||||
@apply text-3xl font-bold tracking-tight;
|
@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;
|
@apply text-accent hover:underline transition-colors;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Subtle border utilities */
|
|
||||||
.border-subtle {
|
.border-subtle {
|
||||||
@apply border border-border-light;
|
@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;
|
@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 <ClientDashboard view={view} />;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
</action>
|
</action>
|
||||||
<verify>
|
<verify>
|
||||||
<automated>grep -q "colors:" tailwind.config.ts && echo "Color tokens defined"</automated>
|
<automated>grep -q "colors:" tailwind.config.ts && echo "Color tokens defined"</automated>
|
||||||
<automated>grep -q "primary\|accent\|success" tailwind.config.ts && echo "Key colors present"</automated>
|
<automated>grep -q "primary\|accent\|success" tailwind.config.ts && echo "Key colors present"</automated>
|
||||||
<automated>grep -q "@tailwind" src/app/globals.css && echo "Tailwind directives in globals.css"</automated>
|
<automated>grep -q "@tailwind" src/app/globals.css && echo "Tailwind directives in globals.css"</automated>
|
||||||
<automated>npm run build 2>&1 | grep -v "warning" | grep -q "error" && echo "TypeScript errors" || echo "Build OK"</automated>
|
<automated>grep -q "ClientDashboard" app/c/\[token\]/page.tsx && echo "ClientDashboard wired in page"</automated>
|
||||||
|
<automated>grep -q "generateMetadata" app/c/\[token\]/page.tsx && echo "Dynamic metadata present"</automated>
|
||||||
</verify>
|
</verify>
|
||||||
<acceptance_criteria>
|
<acceptance_criteria>
|
||||||
- `tailwind.config.ts` contains color tokens: primary, secondary, accent, success, warning
|
- `tailwind.config.ts` contains color tokens: primary, secondary, accent, success, warning
|
||||||
- `globals.css` includes Tailwind directives and base typography
|
- `globals.css` includes Tailwind directives and base typography
|
||||||
|
- `app/c/[token]/page.tsx` renders `<ClientDashboard view={view} />` with dynamic metadata
|
||||||
|
- 404 returned if token invalid
|
||||||
- `npm run build` succeeds
|
- `npm run build` succeeds
|
||||||
</acceptance_criteria>
|
</acceptance_criteria>
|
||||||
</task>
|
</task>
|
||||||
@@ -552,14 +599,12 @@ Output: Fully rendered client portal with all DASH-02 through DASH-10 requiremen
|
|||||||
</task>
|
</task>
|
||||||
|
|
||||||
<task type="auto">
|
<task type="auto">
|
||||||
<name>Task 4: Create PaymentStatus, DocumentsSection, and NotesSection components</name>
|
<name>Task 4: Create PaymentStatus component (accepted_total + payment rows with status badges)</name>
|
||||||
<files>
|
<files>
|
||||||
src/components/payment-status.tsx
|
src/components/payment-status.tsx
|
||||||
src/components/documents-section.tsx
|
|
||||||
src/components/notes-section.tsx
|
|
||||||
</files>
|
</files>
|
||||||
<read_first>
|
<read_first>
|
||||||
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)
|
.planning/phases/01-foundation-client-dashboard/01-CONTEXT.md (D-10, D-11)
|
||||||
</read_first>
|
</read_first>
|
||||||
<action>
|
<action>
|
||||||
@@ -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
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>test -f src/components/payment-status.tsx && echo "PaymentStatus component exists"</automated>
|
||||||
|
<automated>grep -q "export function PaymentStatus" src/components/payment-status.tsx && echo "Component exported"</automated>
|
||||||
|
<automated>grep -q "accepted_total" src/components/payment-status.tsx && echo "Total displayed"</automated>
|
||||||
|
<automated>grep -q "da_saldare\|inviata\|saldato" src/components/payment-status.tsx && echo "Status config present"</automated>
|
||||||
|
</verify>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- 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
|
||||||
|
</acceptance_criteria>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 5: Create DocumentsSection and NotesSection components (external links + read-only notes)</name>
|
||||||
|
<files>
|
||||||
|
src/components/documents-section.tsx
|
||||||
|
src/components/notes-section.tsx
|
||||||
|
</files>
|
||||||
|
<read_first>
|
||||||
|
src/lib/client-view.ts (documents and notes shapes)
|
||||||
|
.planning/phases/01-foundation-client-dashboard/01-CONTEXT.md (D-12)
|
||||||
|
</read_first>
|
||||||
|
<action>
|
||||||
Create `src/components/documents-section.tsx`:
|
Create `src/components/documents-section.tsx`:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
@@ -721,99 +797,26 @@ Output: Fully rendered client portal with all DASH-02 through DASH-10 requiremen
|
|||||||
```
|
```
|
||||||
|
|
||||||
Key points:
|
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, `rel="noopener noreferrer"` for security
|
||||||
- DocumentsSection: clickable external links with ExternalLink icon
|
- NotesSection: read-only, client never writes (admin writes in Phase 2 admin area)
|
||||||
- NotesSection: read-only notes with formatted timestamps
|
- NotesSection: empty state shown as italic hint when no notes exist
|
||||||
- All use Card + Badge components from shadcn/ui
|
- Timestamps formatted in Italian locale
|
||||||
</action>
|
</action>
|
||||||
<verify>
|
<verify>
|
||||||
<automated>test -f src/components/payment-status.tsx && echo "PaymentStatus component exists"</automated>
|
|
||||||
<automated>test -f src/components/documents-section.tsx && echo "DocumentsSection component exists"</automated>
|
<automated>test -f src/components/documents-section.tsx && echo "DocumentsSection component exists"</automated>
|
||||||
<automated>test -f src/components/notes-section.tsx && echo "NotesSection component exists"</automated>
|
<automated>test -f src/components/notes-section.tsx && echo "NotesSection component exists"</automated>
|
||||||
<automated>grep -q "accepted_total" src/components/payment-status.tsx && echo "Total displayed"</automated>
|
<automated>grep -q "export function DocumentsSection" src/components/documents-section.tsx && echo "DocumentsSection exported"</automated>
|
||||||
<automated>grep -q "ExternalLink" src/components/documents-section.tsx && echo "Link icon present"</automated>
|
<automated>grep -q "export function NotesSection" src/components/notes-section.tsx && echo "NotesSection exported"</automated>
|
||||||
|
<automated>grep -q "noopener noreferrer" src/components/documents-section.tsx && echo "External link security present"</automated>
|
||||||
</verify>
|
</verify>
|
||||||
<acceptance_criteria>
|
<acceptance_criteria>
|
||||||
- All three components exist and are exported
|
- Both components exist and are exported
|
||||||
- PaymentStatus displays accepted_total + 2 payment rows with status (no amounts)
|
- DocumentsSection renders clickable external links with ExternalLink icon and secure rel attributes
|
||||||
- DocumentsSection shows clickable external links
|
- NotesSection shows read-only notes with Italian-formatted timestamps
|
||||||
- NotesSection shows read-only notes with timestamps (or empty state)
|
- NotesSection shows empty state hint when notes array is empty
|
||||||
- All components use shadcn/ui Card and Badge
|
|
||||||
</acceptance_criteria>
|
</acceptance_criteria>
|
||||||
</task>
|
</task>
|
||||||
|
|
||||||
<task type="auto">
|
|
||||||
<name>Task 5: Update app/c/[token]/page.tsx to render ClientDashboard with real data</name>
|
|
||||||
<files>
|
|
||||||
app/c/[token]/page.tsx
|
|
||||||
</files>
|
|
||||||
<read_first>
|
|
||||||
src/components/client-dashboard.tsx
|
|
||||||
src/lib/client-view.ts
|
|
||||||
</read_first>
|
|
||||||
<action>
|
|
||||||
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 <ClientDashboard view={view} />;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This page:
|
|
||||||
- Fetches ClientView data
|
|
||||||
- Returns 404 if not found
|
|
||||||
- Generates dynamic metadata with client brand name
|
|
||||||
- Renders ClientDashboard with real data
|
|
||||||
</action>
|
|
||||||
<verify>
|
|
||||||
<automated>test -f app/c/\[token\]/page.tsx && echo "Page file exists"</automated>
|
|
||||||
<automated>grep -q "ClientDashboard" app/c/\[token\]/page.tsx && echo "ClientDashboard rendered"</automated>
|
|
||||||
<automated>grep -q "getClientView" app/c/\[token\]/page.tsx && echo "Data fetched"</automated>
|
|
||||||
<automated>npm run build 2>&1 | grep -v "warning" | grep -q "error" && echo "Build errors" || echo "Build OK"</automated>
|
|
||||||
</verify>
|
|
||||||
<acceptance_criteria>
|
|
||||||
- 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
|
|
||||||
</acceptance_criteria>
|
|
||||||
</task>
|
|
||||||
|
|
||||||
</tasks>
|
</tasks>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user