--- phase: "01-foundation-client-dashboard" plan: 04 type: execute wave: 2 depends_on: - "01-01" - "01-02" - "01-03" files_modified: - app/c/[token]/page.tsx - src/components/client-dashboard.tsx - src/components/phase-timeline.tsx - src/components/payment-status.tsx - src/components/documents-section.tsx - src/components/notes-section.tsx - src/app/globals.css - tailwind.config.ts autonomous: true requirements: - DASH-02 - DASH-03 - DASH-04 - DASH-07 - DASH-08 - DASH-09 - DASH-10 must_haves: truths: - "Client dashboard displays client brand name prominently with iamcavalli logo in corner" - "Global progress bar at top shows % of all tasks completed" - "Phases are displayed as lateral timeline (left indicator, content right)" - "Each phase shows progress bar (% from completed tasks) + task list with status badges" - "Tasks are nested within phases with status visible (todo/in_progress/done)" - "Payment section always visible: accepted_total + Acconto 50% status + Saldo 50% status (NO amounts)" - "Document links are clickable (opens external URL)" - "Notes/decision log is visible (read-only, may be empty)" - "Layout is mobile-responsive and light & clean visual style" artifacts: - path: "app/c/[token]/page.tsx" provides: "Server Component rendering ClientDashboard" min_lines: 20 - path: "src/components/client-dashboard.tsx" provides: "Layout wrapper + main sections (header, progress, phases, payments, documents, notes)" min_lines: 50 - path: "src/components/phase-timeline.tsx" provides: "Lateral timeline rendering with phase cards and task lists" min_lines: 80 - path: "src/components/payment-status.tsx" provides: "Payment section: accepted_total + 2 payment rows with status" min_lines: 30 - path: "src/components/documents-section.tsx" provides: "List of external document links" min_lines: 20 - path: "src/components/notes-section.tsx" provides: "Read-only notes list with timestamps" min_lines: 20 - path: "tailwind.config.ts" provides: "Light & clean design tokens (updated from bootstrap)" contains: "colors" key_links: - from: "app/c/[token]/page.tsx" to: "ClientDashboard component" via: "import { ClientDashboard }" pattern: " **Client Dashboard UI — Vertical Slice:** Render the complete client dashboard with all UI sections: header with branding, global progress bar, lateral phase timeline, task lists with status, payment status section, external document links, and read-only notes log. Implement light & clean visual style with mobile-first responsive design using Tailwind CSS and shadcn/ui components. Purpose: Deliver the core user-facing product: a client can open their secret link and see the complete project status at a glance, with clear progress indicators, task hierarchy, payment overview, and documents. Output: Fully rendered client portal with all DASH-02 through DASH-10 requirements implemented in the UI. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/phases/01-foundation-client-dashboard/01-CONTEXT.md (Decisions D-04 through D-12) @.planning/phases/01-foundation-client-dashboard/01-03-SUMMARY.md @src/lib/client-view.ts (ClientView interface) Task 1: 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: ```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 ; } ``` 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" 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 Task 2: Create ClientDashboard wrapper component with header, global progress, and section layout src/components/client-dashboard.tsx src/lib/client-view.ts (ClientView interface) .planning/phases/01-foundation-client-dashboard/01-CONTEXT.md (D-06 through D-10) Create `src/components/client-dashboard.tsx`: ```typescript 'use client'; import { ClientView } from '@/lib/client-view'; import { Progress } from '@/components/ui/progress'; import { PhaseTimeline } from './phase-timeline'; import { PaymentStatus } from './payment-status'; import { DocumentsSection } from './documents-section'; import { NotesSection } from './notes-section'; interface ClientDashboardProps { view: ClientView; } export function ClientDashboard({ view }: ClientDashboardProps) { return (
{/* Header: Logo + Brand Name */}
{/* iamcavalli logo (small, corner) */}
iamcavalli
{/* Client brand name (prominent) */}

{view.client.brand_name}

{/* Spacer for balance */}
{/* Global Progress Bar */}

Project Progress

{view.global_progress_pct}% Complete

{/* Main Content */}
{/* Brief */} {view.client.brief && (

"{view.client.brief}"

)} {/* Phase Timeline */}

Project Phases

{/* Payment Status */}

Payment Status

{/* Documents */} {view.documents.length > 0 && (

Documents & Files

)} {/* Notes / Decision Log */} {view.notes.length > 0 && (

Notes & Decisions

)}
{/* Footer */}

This is a private project dashboard. Do not share your unique link.

); } ``` Key points: - Header: small "iamcavalli" logo (top-left), client brand_name centered (prominent) - Global progress bar shows % of all tasks done - Section headers are h2 (consistent sizing) - Responsive layout: max-width container with mobile padding - Brief is quoted and italicized - Documents and Notes sections show only if data exists
test -f src/components/client-dashboard.tsx && echo "ClientDashboard component exists" grep -q "export function ClientDashboard" src/components/client-dashboard.tsx && echo "Component exported" grep -q "iamcavalli" src/components/client-dashboard.tsx && echo "Logo text present" grep -q "brand_name" src/components/client-dashboard.tsx && echo "Brand name rendered" grep -q "global_progress_pct" src/components/client-dashboard.tsx && echo "Progress bar displays" - Component is exported and accepts ClientView props - Header displays iamcavalli logo (small) + brand_name (prominent) - Global progress bar shows project completion % - Main sections: brief, phases, payments, documents (conditional), notes (conditional) - Responsive layout with max-width container
Task 3: Create PhaseTimeline component for lateral timeline layout with task lists src/components/phase-timeline.tsx src/lib/client-view.ts (phase and task structure) .planning/phases/01-foundation-client-dashboard/01-CONTEXT.md (D-07, D-08) Create `src/components/phase-timeline.tsx`: ```typescript 'use client'; import { ClientView } from '@/lib/client-view'; import { Progress } from '@/components/ui/progress'; import { Card } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { CheckCircle2, Circle, Clock } from 'lucide-react'; interface PhaseTimelineProps { phases: ClientView['phases']; } export function PhaseTimeline({ phases }: PhaseTimelineProps) { return (
{phases.map((phase, index) => (
{/* Left: Timeline Indicator */}
{/* Circle indicator */}
{phase.status === 'done' ? ( ) : phase.status === 'active' ? ( ) : ( )}
{/* Vertical line (not on last) */} {index < phases.length - 1 && (
)}
{/* Right: Phase Content */}
{/* Phase Card */} {/* Phase Header */}

{phase.title}

{phase.status === 'upcoming' ? 'Upcoming' : phase.status === 'active' ? 'In Progress' : 'Done'}
{/* Phase Progress Bar */}

Phase Progress

{phase.progress_pct}%

{/* Task List */}

Tasks ({phase.tasks.filter(t => t.status === 'done').length} of {phase.tasks.length})

{phase.tasks.length === 0 ? (

No tasks yet

) : (
    {phase.tasks.map((task) => (
  • {/* Task Status Icon */} {task.status === 'done' ? ( ) : task.status === 'in_progress' ? ( ) : ( )} {/* Task Content */}

    {task.title}

    {task.description && (

    {task.description}

    )} {/* Deliverables */} {task.deliverables.length > 0 && (
    {task.deliverables.map((d) => (
    {d.title} {d.status === 'approved' && ( Approved )}
    ))}
    )}
  • ))}
)}
))}
); } ``` Key points: - Left indicator: circle with icon (checkmark for done, dot for upcoming/active) - Vertical line connects phases (not on last phase) - Right content: phase card with title, status badge, progress bar, task list - Task status shown with icons and colors (success/warning/info) - Deliverables nested under tasks with "Approved" badge if applicable - Empty state if phase has no tasks test -f src/components/phase-timeline.tsx && echo "PhaseTimeline component exists" grep -q "export function PhaseTimeline" src/components/phase-timeline.tsx && echo "Component exported" grep -q "CheckCircle2\|Circle" src/components/phase-timeline.tsx && echo "Icons imported" grep -q "progress_pct" src/components/phase-timeline.tsx && echo "Progress bar displays" - Component renders lateral timeline layout - Each phase shows: title, status badge, progress bar, task count - Tasks show status with icons (checkmark/circle) - Deliverables are nested and show "Approved" badge if applicable - Empty state for phases with no tasks Task 4: Create PaymentStatus component (accepted_total + payment rows with status badges) src/components/payment-status.tsx src/lib/client-view.ts (payments shape, PaymentStatus type) .planning/phases/01-foundation-client-dashboard/01-CONTEXT.md (D-10, D-11) Create `src/components/payment-status.tsx`: ```typescript 'use client'; import { Card } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { ClientView } from '@/lib/client-view'; import { CheckCircle2, Clock, AlertCircle } from 'lucide-react'; interface PaymentStatusProps { accepted_total: string; payments: ClientView['payments']; } export function PaymentStatus({ accepted_total, payments }: PaymentStatusProps) { const statusConfig = { da_saldare: { color: 'bg-info', icon: Clock, label: 'Da Saldare', text: 'white' }, inviata: { color: 'bg-warning', icon: AlertCircle, label: 'Inviata', text: 'white' }, saldato: { color: 'bg-success', icon: CheckCircle2, label: 'Saldato', text: 'white' }, }; return ( {/* Total */}

Totale Preventivo Accettato

€{parseFloat(accepted_total || '0').toLocaleString('it-IT', { minimumFractionDigits: 2, maximumFractionDigits: 2, })}

{/* Payment Rows */}
{payments.map((payment) => { const config = statusConfig[payment.status as keyof typeof statusConfig]; const Icon = config?.icon || Clock; return (

{payment.label}

{config?.label || payment.status}
); })}
{/* Note */}

I pagamenti sono suddivisi in due rate da 50% ciascuna. Contattaci per domande sui dettagli.

); } ``` 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 'use client'; import { ClientView } from '@/lib/client-view'; import { Card } from '@/components/ui/card'; import { ExternalLink } from 'lucide-react'; interface DocumentsSectionProps { documents: ClientView['documents']; } export function DocumentsSection({ documents }: DocumentsSectionProps) { return (
{documents.map((doc) => ( {doc.label} ))}
); } ``` Create `src/components/notes-section.tsx`: ```typescript 'use client'; import { ClientView } from '@/lib/client-view'; import { Card } from '@/components/ui/card'; interface NotesSectionProps { notes: ClientView['notes']; } export function NotesSection({ notes }: NotesSectionProps) { if (notes.length === 0) { return (

No notes yet. Decisions will appear here as they are made.

); } return (
{notes.map((note) => (

{note.body}

{new Date(note.created_at).toLocaleDateString('it-IT', { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', })}

))}
); } ``` Key points: - 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/documents-section.tsx && echo "DocumentsSection component exists" test -f src/components/notes-section.tsx && echo "NotesSection component exists" 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" - 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
## Trust Boundaries | Boundary | Description | |----------|-------------| | Client browser → CSS/HTML | UI rendering is client-safe; no admin secrets in HTML source | | Link click → External URL | External document links open in new tab with `rel="noopener noreferrer"` | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-04-001 | Information Disclosure | Payment amounts | mitigate | Payments row shows status only; amounts never rendered on client dashboard | | T-04-002 | Tampering | External links | accept | Links are user-provided URLs; client-side link validation (hostname check) could be added in Phase 2 | | T-04-003 | Denial of Service | Image rendering | accept | Dashboard contains only text and icons; no resource-heavy assets | After plan execution: 1. Run `npm run build` → no errors 2. Verify all component files exist: client-dashboard, phase-timeline, payment-status, documents-section, notes-section 3. Check page rendering logic in `app/c/[token]/page.tsx` 4. Verify mobile responsiveness: layout scales correctly on narrow screens 5. Check that payment amounts are NOT displayed (only status) - All UI components are created and exported - Client dashboard renders complete project status - Global progress bar and per-phase progress bars display correctly - Payment section shows only status (no amounts) - Document links are clickable - Notes section shows read-only list (or empty state) - Layout is responsive and uses light & clean design - Mobile-first design works on small screens - Ready to proceed to Plan 05 (Seed script + DNS) After completion, create `.planning/phases/01-foundation-client-dashboard/01-04-SUMMARY.md`