Files
clienthub/.planning/phases/01-foundation-client-dashboard/01-04-PLAN.md
T
Simone Cavalli 81c667838f docs(01-foundation-client-dashboard): complete phase 1 planning with 5-plan structure
Create comprehensive phase plans for Foundation & Client Dashboard:
- 01-01-PLAN.md: Walking Skeleton (Next.js 15 bootstrap + DB connection)
- 01-02-PLAN.md: Database schema (11 tables, Drizzle ORM, drizzle-kit push)
- 01-03-PLAN.md: Middleware token validation + ClientView type + data fetching
- 01-04-PLAN.md: Client dashboard UI (header, timeline, progress, payments, docs, notes)
- 01-05-PLAN.md: Seed script + DNS CNAME configuration

Also create SKELETON.md documenting locked architectural decisions for all future phases:
- Next.js 15 + Drizzle + postgres-js driver (Coolify Postgres)
- Token as separate rotatable field (not PK)
- ClientView enforcement (no quote_items exposed to client API)
- Approved_at immutable audit trail
- Two independent auth systems (client token + admin session)
- Vercel deployment with custom domain

Update ROADMAP.md to mark Phase 1 as planned (5 plans created) and ready for execution.

All plans follow MVP vertical-slice structure with 2-3 tasks per plan.
Walking Skeleton proves the entire stack works end-to-end.
Requirements mapping: DASH-01 through DASH-04, DASH-07 through DASH-10 covered.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 11:27:19 +02:00

32 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
01-foundation-client-dashboard 04 execute 2
01-01
01-02
01-03
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
true
DASH-02
DASH-03
DASH-04
DASH-07
DASH-08
DASH-09
DASH-10
truths artifacts key_links
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
path provides min_lines
app/c/[token]/page.tsx Server Component rendering ClientDashboard 20
path provides min_lines
src/components/client-dashboard.tsx Layout wrapper + main sections (header, progress, phases, payments, documents, notes) 50
path provides min_lines
src/components/phase-timeline.tsx Lateral timeline rendering with phase cards and task lists 80
path provides min_lines
src/components/payment-status.tsx Payment section: accepted_total + 2 payment rows with status 30
path provides min_lines
src/components/documents-section.tsx List of external document links 20
path provides min_lines
src/components/notes-section.tsx Read-only notes list with timestamps 20
path provides contains
tailwind.config.ts Light & clean design tokens (updated from bootstrap) colors
from to via pattern
app/c/[token]/page.tsx ClientDashboard component import { ClientDashboard } <ClientDashboard
from to via pattern
ClientDashboard PhaseTimeline + PaymentStatus + DocumentsSection + NotesSection nested component props view.phases
from to via pattern
PhaseTimeline task status badges status className mapping status.*todo.*in_progress.*done
**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.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_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) 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 (
    <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
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 (
    <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
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 (
    <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>
  );
}
```

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:
- 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 <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
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

<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>

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)

<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>
After completion, create `.planning/phases/01-foundation-client-dashboard/01-04-SUMMARY.md`