Files
Simone Cavalli 2123dc9d00 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>
2026-05-13 15:20:50 +02:00

865 lines
32 KiB
Markdown

---
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: "<ClientDashboard"
- from: "ClientDashboard"
to: "PhaseTimeline + PaymentStatus + DocumentsSection + NotesSection"
via: "nested component props"
pattern: "view\\.phases"
- from: "PhaseTimeline"
to: "task status badges"
via: "status className mapping"
pattern: "status.*todo.*in_progress.*done"
---
<objective>
**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.
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.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)
</context>
<tasks>
<task type="auto">
<name>Task 1: Configure design tokens (tailwind.config.ts + globals.css) and wire app/c/[token]/page.tsx to ClientDashboard</name>
<files>
tailwind.config.ts
src/app/globals.css
app/c/[token]/page.tsx
</files>
<read_first>
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)
</read_first>
<action>
Update `tailwind.config.ts` to define light & clean design tokens:
```typescript
import type { Config } from 'tailwindcss';
const config: Config = {
content: [
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
colors: {
// Light & clean palette
'primary': '#1a1a1a', // deep charcoal for text
'secondary': '#666666', // medium gray for secondary text
'tertiary': '#999999', // light gray for hints
'bg-light': '#ffffff', // pure white
'bg-subtle': '#f9f9f9', // very light gray
'border-light': '#e5e5e5', // subtle border
'accent': '#0066cc', // blue accent (will be brand-aware in Phase 2)
'success': '#22c55e', // green for done
'warning': '#eab308', // yellow for in-progress
'info': '#3b82f6', // blue for pending
},
spacing: {
'xs': '0.5rem',
'sm': '1rem',
'md': '1.5rem',
'lg': '2rem',
'xl': '3rem',
},
fontSize: {
'xs': '0.75rem',
'sm': '0.875rem',
'base': '1rem',
'lg': '1.125rem',
'xl': '1.25rem',
'2xl': '1.5rem',
'3xl': '1.875rem',
},
fontFamily: {
'sans': [
'system-ui',
'-apple-system',
'BlinkMacSystemFont',
'"Segoe UI"',
'Roboto',
'"Helvetica Neue"',
'sans-serif',
],
},
},
},
plugins: [],
};
export default config;
```
Update `src/app/globals.css`:
```css
@tailwind base;
@tailwind components;
@tailwind utilities;
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
@apply bg-white text-primary font-sans;
line-height: 1.6;
}
h1 {
@apply text-3xl font-bold tracking-tight;
}
h2 {
@apply text-2xl font-bold;
}
h3 {
@apply text-xl font-semibold;
}
p {
@apply text-base text-secondary;
}
a {
@apply text-accent hover:underline transition-colors;
}
.border-subtle {
@apply border border-border-light;
}
.bg-subtle {
@apply bg-bg-subtle;
}
```
Update `app/c/[token]/page.tsx` to replace the Plan 03 placeholder with the full ClientDashboard render:
```typescript
import { getClientView } from '@/lib/client-view';
import { ClientDashboard } from '@/components/client-dashboard';
import { notFound } from 'next/navigation';
export const revalidate = 60;
export async function generateMetadata({
params,
}: {
params: { token: string };
}) {
const view = await getClientView(params.token);
if (!view) {
return { title: 'Not Found' };
}
return {
title: `${view.client.brand_name} — Project Status | iamcavalli`,
description: view.client.brief || 'Project status dashboard',
};
}
export default async function ClientPage({
params,
}: {
params: { token: string };
}) {
const view = await getClientView(params.token);
if (!view) {
notFound();
}
return <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>
<verify>
<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 "@tailwind" src/app/globals.css && echo "Tailwind directives in globals.css"</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>
<acceptance_criteria>
- `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 `<ClientDashboard view={view} />` with dynamic metadata
- 404 returned if token invalid
- `npm run build` succeeds
</acceptance_criteria>
</task>
<task type="auto">
<name>Task 2: Create ClientDashboard wrapper component with header, global progress, and section layout</name>
<files>
src/components/client-dashboard.tsx
</files>
<read_first>
src/lib/client-view.ts (ClientView interface)
.planning/phases/01-foundation-client-dashboard/01-CONTEXT.md (D-06 through D-10)
</read_first>
<action>
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 (
<div className="min-h-screen bg-white">
{/* Header: Logo + Brand Name */}
<header className="bg-white border-b border-subtle sticky top-0 z-10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="flex items-center justify-between">
{/* iamcavalli logo (small, corner) */}
<div className="text-xs font-semibold text-tertiary">iamcavalli</div>
{/* Client brand name (prominent) */}
<h1 className="text-2xl sm:text-3xl font-bold text-primary flex-1 text-center mx-4">
{view.client.brand_name}
</h1>
{/* Spacer for balance */}
<div className="w-20" />
</div>
</div>
</header>
{/* Global Progress Bar */}
<section className="bg-bg-subtle border-b border-subtle">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="space-y-2">
<p className="text-sm font-semibold text-primary">Project Progress</p>
<Progress
value={view.global_progress_pct}
className="h-2"
/>
<p className="text-xs text-tertiary">
{view.global_progress_pct}% Complete
</p>
</div>
</div>
</section>
{/* Main Content */}
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Brief */}
{view.client.brief && (
<section className="mb-12">
<p className="text-lg text-secondary italic">
"{view.client.brief}"
</p>
</section>
)}
{/* Phase Timeline */}
<section className="mb-12">
<h2 className="text-2xl font-bold mb-8">Project Phases</h2>
<PhaseTimeline phases={view.phases} />
</section>
{/* Payment Status */}
<section className="mb-12">
<h2 className="text-2xl font-bold mb-6">Payment Status</h2>
<PaymentStatus
accepted_total={view.client.accepted_total}
payments={view.payments}
/>
</section>
{/* Documents */}
{view.documents.length > 0 && (
<section className="mb-12">
<h2 className="text-2xl font-bold mb-6">Documents & Files</h2>
<DocumentsSection documents={view.documents} />
</section>
)}
{/* Notes / Decision Log */}
{view.notes.length > 0 && (
<section className="mb-12">
<h2 className="text-2xl font-bold mb-6">Notes & Decisions</h2>
<NotesSection notes={view.notes} />
</section>
)}
</main>
{/* Footer */}
<footer className="bg-bg-subtle border-t border-subtle mt-16">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<p className="text-xs text-tertiary text-center">
This is a private project dashboard. Do not share your unique link.
</p>
</div>
</footer>
</div>
);
}
```
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
</action>
<verify>
<automated>test -f src/components/client-dashboard.tsx && echo "ClientDashboard component exists"</automated>
<automated>grep -q "export function ClientDashboard" src/components/client-dashboard.tsx && echo "Component exported"</automated>
<automated>grep -q "iamcavalli" src/components/client-dashboard.tsx && echo "Logo text present"</automated>
<automated>grep -q "brand_name" src/components/client-dashboard.tsx && echo "Brand name rendered"</automated>
<automated>grep -q "global_progress_pct" src/components/client-dashboard.tsx && echo "Progress bar displays"</automated>
</verify>
<acceptance_criteria>
- 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
</acceptance_criteria>
</task>
<task type="auto">
<name>Task 3: Create PhaseTimeline component for lateral timeline layout with task lists</name>
<files>
src/components/phase-timeline.tsx
</files>
<read_first>
src/lib/client-view.ts (phase and task structure)
.planning/phases/01-foundation-client-dashboard/01-CONTEXT.md (D-07, D-08)
</read_first>
<action>
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 (
<div className="space-y-8">
{phases.map((phase, index) => (
<div key={phase.id} className="flex gap-6">
{/* Left: Timeline Indicator */}
<div className="flex flex-col items-center gap-2">
{/* Circle indicator */}
<div className="relative z-10 w-10 h-10 bg-white border-2 border-accent rounded-full flex items-center justify-center shadow-sm">
{phase.status === 'done' ? (
<CheckCircle2 className="w-6 h-6 text-success" />
) : phase.status === 'active' ? (
<Circle className="w-6 h-6 text-accent" />
) : (
<Clock className="w-6 h-6 text-tertiary" />
)}
</div>
{/* Vertical line (not on last) */}
{index < phases.length - 1 && (
<div className="flex-1 w-0.5 bg-border-light" style={{ minHeight: '120px' }} />
)}
</div>
{/* Right: Phase Content */}
<div className="flex-1 pb-8">
{/* Phase Card */}
<Card className="p-6 border-subtle hover:shadow-md transition-shadow">
{/* Phase Header */}
<div className="flex items-start justify-between mb-4">
<h3 className="text-xl font-bold text-primary">
{phase.title}
</h3>
<Badge
className={`capitalize ${
phase.status === 'done' ? 'bg-success text-white' :
phase.status === 'active' ? 'bg-accent text-white' :
'bg-tertiary text-white'
}`}
>
{phase.status === 'upcoming' ? 'Upcoming' :
phase.status === 'active' ? 'In Progress' : 'Done'}
</Badge>
</div>
{/* Phase Progress Bar */}
<div className="mb-6 space-y-2">
<div className="flex justify-between items-center">
<p className="text-xs font-semibold text-secondary">
Phase Progress
</p>
<p className="text-xs text-tertiary">
{phase.progress_pct}%
</p>
</div>
<Progress value={phase.progress_pct} className="h-2" />
</div>
{/* Task List */}
<div className="space-y-3">
<p className="text-sm font-semibold text-secondary">
Tasks ({phase.tasks.filter(t => t.status === 'done').length} of {phase.tasks.length})
</p>
{phase.tasks.length === 0 ? (
<p className="text-sm text-tertiary italic">No tasks yet</p>
) : (
<ul className="space-y-2">
{phase.tasks.map((task) => (
<li
key={task.id}
className="flex items-start gap-3 p-2 rounded hover:bg-bg-subtle transition-colors"
>
{/* Task Status Icon */}
{task.status === 'done' ? (
<CheckCircle2 className="w-5 h-5 text-success mt-0.5 flex-shrink-0" />
) : task.status === 'in_progress' ? (
<Circle className="w-5 h-5 text-warning mt-0.5 flex-shrink-0" />
) : (
<Circle className="w-5 h-5 text-info mt-0.5 flex-shrink-0" />
)}
{/* Task Content */}
<div className="flex-1">
<p className={`text-sm ${task.status === 'done' ? 'line-through text-tertiary' : 'text-primary'}`}>
{task.title}
</p>
{task.description && (
<p className="text-xs text-tertiary mt-1">
{task.description}
</p>
)}
{/* Deliverables */}
{task.deliverables.length > 0 && (
<div className="mt-2 space-y-1">
{task.deliverables.map((d) => (
<div
key={d.id}
className="text-xs p-1 bg-bg-subtle rounded flex items-center justify-between gap-2"
>
<span className="text-secondary truncate">
{d.title}
</span>
{d.status === 'approved' && (
<Badge className="bg-success text-white text-xs">
Approved
</Badge>
)}
</div>
))}
</div>
)}
</div>
</li>
))}
</ul>
)}
</div>
</Card>
</div>
</div>
))}
</div>
);
}
```
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
</action>
<verify>
<automated>test -f src/components/phase-timeline.tsx && echo "PhaseTimeline component exists"</automated>
<automated>grep -q "export function PhaseTimeline" src/components/phase-timeline.tsx && echo "Component exported"</automated>
<automated>grep -q "CheckCircle2\|Circle" src/components/phase-timeline.tsx && echo "Icons imported"</automated>
<automated>grep -q "progress_pct" src/components/phase-timeline.tsx && echo "Progress bar displays"</automated>
</verify>
<acceptance_criteria>
- 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
</acceptance_criteria>
</task>
<task type="auto">
<name>Task 4: Create PaymentStatus component (accepted_total + payment rows with status badges)</name>
<files>
src/components/payment-status.tsx
</files>
<read_first>
src/lib/client-view.ts (payments shape, PaymentStatus type)
.planning/phases/01-foundation-client-dashboard/01-CONTEXT.md (D-10, D-11)
</read_first>
<action>
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 (
<Card className="p-6 border-subtle">
{/* Total */}
<div className="mb-6 pb-6 border-b border-subtle">
<p className="text-sm text-secondary font-semibold mb-2">
Totale Preventivo Accettato
</p>
<p className="text-3xl font-bold text-primary">
€{parseFloat(accepted_total || '0').toLocaleString('it-IT', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}
</p>
</div>
{/* Payment Rows */}
<div className="space-y-4">
{payments.map((payment) => {
const config = statusConfig[payment.status as keyof typeof statusConfig];
const Icon = config?.icon || Clock;
return (
<div
key={payment.id}
className="flex items-center justify-between p-4 bg-bg-subtle rounded-lg border border-subtle"
>
<div className="flex items-center gap-3">
<Icon className="w-5 h-5 text-secondary flex-shrink-0" />
<p className="text-sm font-semibold text-primary">
{payment.label}
</p>
</div>
<Badge
className={`capitalize ${config?.color} text-${config?.text}`}
>
{config?.label || payment.status}
</Badge>
</div>
);
})}
</div>
{/* Note */}
<p className="text-xs text-tertiary italic mt-6 pt-6 border-t border-subtle">
I pagamenti sono suddivisi in due rate da 50% ciascuna.
Contattaci per domande sui dettagli.
</p>
</Card>
);
}
```
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`:
```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 (
<div className="space-y-3">
{documents.map((doc) => (
<Card
key={doc.id}
className="p-4 border-subtle hover:shadow-md transition-shadow"
>
<a
href={doc.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-between gap-3 text-accent hover:text-accent hover:underline group"
>
<span className="font-semibold text-primary group-hover:text-accent">
{doc.label}
</span>
<ExternalLink className="w-4 h-4 flex-shrink-0" />
</a>
</Card>
))}
</div>
);
}
```
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 (
<p className="text-secondary italic text-sm">
No notes yet. Decisions will appear here as they are made.
</p>
);
}
return (
<div className="space-y-4">
{notes.map((note) => (
<Card key={note.id} className="p-4 border-subtle">
<p className="text-sm text-primary leading-relaxed">
{note.body}
</p>
<p className="text-xs text-tertiary mt-3">
{new Date(note.created_at).toLocaleDateString('it-IT', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
})}
</p>
</Card>
))}
</div>
);
}
```
Key points:
- DocumentsSection: clickable external links with ExternalLink icon, `rel="noopener noreferrer"` for security
- NotesSection: read-only, client never writes (admin writes in Phase 2 admin area)
- NotesSection: empty state shown as italic hint when no notes exist
- Timestamps formatted in Italian locale
</action>
<verify>
<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>grep -q "export function DocumentsSection" src/components/documents-section.tsx && echo "DocumentsSection exported"</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>
<acceptance_criteria>
- 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
</acceptance_criteria>
</task>
</tasks>
<threat_model>
## 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 |
</threat_model>
<verification>
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)
</verification>
<success_criteria>
- 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)
</success_criteria>
<output>
After completion, create `.planning/phases/01-foundation-client-dashboard/01-04-SUMMARY.md`
</output>