diff --git a/.planning/phases/01-foundation-dashboard/01-01-PLAN.md b/.planning/phases/01-foundation-dashboard/01-01-PLAN.md
new file mode 100644
index 000000000..d0da35dbe
--- /dev/null
+++ b/.planning/phases/01-foundation-dashboard/01-01-PLAN.md
@@ -0,0 +1,286 @@
+---
+phase: 01-foundation-dashboard
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - src/app/stores/sensorStore.ts
+ - src/app/api/client.ts
+ - src/app/App.tsx
+ - vitest.config.ts
+ - src/test/setup.ts
+ - package.json
+autonomous: true
+requirements:
+ - DASH-01
+ - DASH-02
+ - DASH-03
+ - DASH-04
+ - DASH-05
+ - DASH-06
+ - DASH-07
+ - UI-04
+must_haves:
+ truths:
+ - Zustand store exists with sensor data types
+ - API client with mock fallback works
+ - Port-based mode detection works (8080=kiosk, 9090=remote)
+ - Test infrastructure is ready
+ artifacts:
+ - path: src/app/stores/sensorStore.ts
+ provides: Zustand store for sensor data
+ exports: useSensorStore, SensorState
+ - path: src/app/api/client.ts
+ provides: API client with mock fallback
+ exports: fetchSensorData, mockSensorData
+ - path: src/app/App.tsx
+ provides: Mode detection and app root
+ min_lines: 30
+ - path: vitest.config.ts
+ provides: Test configuration
+ key_links:
+ - from: sensorStore
+ to: client.ts
+ via: fetchSensorData updates store
+ - from: App.tsx
+ to: sensorStore
+ via: Initializes data polling
+---
+
+
+Create the foundational infrastructure for the RTU dashboard: Zustand state management, API client with mock fallback, port-based mode detection, and test infrastructure (Wave 0).
+
+Purpose: This infrastructure enables all subsequent dashboard features. Without these foundations, individual components cannot share data or communicate with the backend.
+
+Output: Working store, API client, mode detection, and test setup that subsequent plans build upon.
+
+
+
+@./.opencode/get-shit-done/workflows/execute-plan.md
+@./.opencode/get-shit-done/templates/summary.md
+
+
+
+@.planning/phases/01-foundation-dashboard/01-CONTEXT.md
+@.planning/phases/01-foundation-dashboard/01-RESEARCH.md
+@.planning/ROADMAP.md
+@.planning/REQUIREMENTS.md
+@sample_interface/src/app/App.tsx
+
+## Key Implementation Details
+
+**Mode Detection:**
+- Port 8080 → kiosk mode (fixed 1024x600)
+- Port 9090 → remote mode (responsive Full HD)
+- Use `window.location.port` for detection
+
+**Sensor Data Structure:**
+```typescript
+interface SensorData {
+ rainfall: {
+ today: number;
+ hourly: number;
+ monthlyAcc: number;
+ yearlyAcc: number;
+ };
+ voltage: {
+ solar: number;
+ battery: number;
+ batteryStatus: 'HIGH' | 'LOW';
+ };
+ station: {
+ id: string;
+ version: string;
+ };
+ communication: {
+ asu: number;
+ dBm: number;
+ percentage: number;
+ };
+ timestamp: string;
+}
+```
+
+**Polling Strategy:**
+- Default 5 second interval
+- Configurable via settings
+- Pause when document.hidden
+- Use AbortController for cleanup
+
+
+
+
+
+ task 1: Create Wave 0 test infrastructure
+ vitest.config.ts, src/test/setup.ts, package.json
+
+ - Test config extends Vite config
+ - Setup file provides test utilities
+ - Test command works: `npm test`
+
+
+ 1. Create vitest.config.ts extending vite.config.ts:
+ - Use @vitejs/plugin-react
+ - Configure test environment as jsdom
+ - Set setupFiles to src/test/setup.ts
+
+ 2. Create src/test/setup.ts:
+ - Import @testing-library/jest-dom matchers
+ - Configure cleanup after each test
+ - Add mock for window.matchMedia if needed
+
+ 3. Update package.json scripts:
+ - "test": "vitest run"
+ - "test:watch": "vitest"
+ - Add devDependencies: vitest, @testing-library/react, @testing-library/jest-dom, jsdom
+
+ 4. Run `pnpm install` to add dependencies
+
+
+ npm test -- --run 2>&1 | head -20
+
+ Test infrastructure ready, `npm test` runs successfully
+
+
+
+ task 2: Create Zustand sensor store
+ src/app/stores/sensorStore.ts, src/app/stores/__tests__/sensorStore.test.ts
+
+ - Store initializes with default/mock data
+ - setSensorData updates all fields
+ - Store can be subscribed to
+ - Selectors work for derived data
+
+
+ 1. Install zustand: `pnpm add zustand`
+
+ 2. Create src/app/stores/sensorStore.ts:
+ - Define SensorData interface per context
+ - Create store with initial state
+ - Export useSensorStore hook
+ - Add actions: setSensorData, updatePollingInterval
+ - Include selector for last update timestamp
+
+ 3. Create tests verifying:
+ - Store initializes with defaults
+ - setSensorData updates state
+ - Subscribers receive updates
+
+
+ npm test -- --run stores/sensorStore 2>&1 | grep -E "(PASS|FAIL|✓|✗)"
+
+ Zustand store created with SensorData interface, tests pass
+
+
+
+ task 3: Create API client with mock fallback
+ src/app/api/client.ts, src/app/api/__tests__/client.test.ts
+
+ - fetchSensorData returns SensorData
+ - On API failure, returns mock data
+ - Uses AbortController for cancellation
+ - Logs errors but doesn't throw
+
+
+ 1. Create src/app/api/client.ts:
+ - Define API endpoint URL (configurable)
+ - Create fetchSensorData function with AbortSignal
+ - Create mockSensorData generator with realistic values
+ - Implement fallback: try API → catch → return mock
+ - Add request timeout (5s)
+
+ 2. Create tests verifying:
+ - Returns data on successful fetch
+ - Returns mock on network error
+ - Respects AbortController
+ - Mock data has valid structure
+
+
+ npm test -- --run api/client 2>&1 | grep -E "(PASS|FAIL|✓|✗)"
+
+ API client with mock fallback created, tests pass
+
+
+
+ task 4: Implement port-based mode detection
+ src/app/App.tsx, src/app/hooks/useDisplayMode.ts
+
+ - Port 8080 → returns 'kiosk'
+ - Port 9090 → returns 'remote'
+ - Other ports → defaults to 'kiosk'
+ - Mode available via hook
+
+
+ 1. Create src/app/hooks/useDisplayMode.ts:
+ - Read window.location.port
+ - Return 'kiosk' for 8080, 'remote' for 9090
+ - Default to 'kiosk' for other ports
+ - Memoize result (port won't change)
+
+ 2. Update src/app/App.tsx:
+ - Import useDisplayMode hook
+ - Pass mode to DashboardLayout
+ - Initialize data polling on mount
+ - Clean up polling on unmount (useEffect cleanup)
+ - Add data-attr or class based on mode for CSS targeting
+
+
+ grep -n "useDisplayMode\|window.location.port\|displayMode" src/app/App.tsx src/app/hooks/useDisplayMode.ts 2>/dev/null | head -20
+
+ Mode detection implemented, App.tsx uses display mode hook
+
+
+
+ task 5: Set up data polling with cleanup
+ src/app/App.tsx, src/app/hooks/useSensorPolling.ts
+
+ - Polls every 5 seconds by default
+ - Pauses when document.hidden
+ - Resumes when visible again
+ - Properly cleans up on unmount
+
+
+ 1. Create src/app/hooks/useSensorPolling.ts:
+ - Accept polling interval (ms) parameter, default 5000
+ - Use setInterval for polling
+ - Listen for visibilitychange event
+ - Pause interval when document.hidden
+ - Resume when visible
+ - Use AbortController per fetch
+ - Return cleanup function
+
+ 2. Update App.tsx:
+ - Call useSensorPolling in component
+ - Pass polling interval from settings (or default)
+ - Ensure cleanup happens on unmount
+
+
+ grep -n "useSensorPolling\|setInterval\|visibilitychange\|AbortController" src/app/App.tsx src/app/hooks/useSensorPolling.ts 2>/dev/null | head -20
+
+ Polling implemented with visibility awareness and cleanup
+
+
+
+
+
+After completing all tasks:
+1. Run `npm test` — all tests should pass
+2. Check store exists: `ls src/app/stores/sensorStore.ts`
+3. Check API client exists: `ls src/app/api/client.ts`
+4. Check mode detection: `grep "useDisplayMode" src/app/App.tsx`
+5. Verify no console errors on build: `npm run build 2>&1 | grep -i error || echo "Build clean"`
+
+
+
+- Zustand store with SensorData interface exists and is tested
+- API client with mock fallback exists and is tested
+- Port-based mode detection works (8080=kiosk, 9090=remote)
+- Data polling with cleanup and visibility awareness implemented
+- Test infrastructure ready (`npm test` runs)
+- All files committed
+
+
+
diff --git a/.planning/phases/01-foundation-dashboard/01-02-PLAN.md b/.planning/phases/01-foundation-dashboard/01-02-PLAN.md
new file mode 100644
index 000000000..8ffa2bd8e
--- /dev/null
+++ b/.planning/phases/01-foundation-dashboard/01-02-PLAN.md
@@ -0,0 +1,271 @@
+---
+phase: 01-foundation-dashboard
+plan: 02
+type: execute
+wave: 1
+depends_on:
+ - 01-01
+files_modified:
+ - src/app/components/Header.tsx
+ - src/app/components/VoltageDisplay.tsx
+ - src/app/components/BatteryStatus.tsx
+ - src/app/components/__tests__/Header.test.tsx
+ - src/app/components/__tests__/VoltageDisplay.test.tsx
+ - src/app/components/__tests__/BatteryStatus.test.tsx
+autonomous: true
+requirements:
+ - DASH-02
+ - DASH-03
+ - DASH-04
+ - DASH-07
+ - UI-01
+ - UI-02
+must_haves:
+ truths:
+ - Header displays all status information
+ - Voltage displays show solar and battery readings
+ - Battery status shows HIGH/LOW indicator with color
+ - All touch targets are minimum 44px
+ - Header is compact for 1024x600 display
+ artifacts:
+ - path: src/app/components/Header.tsx
+ provides: Main header with status info
+ exports: Header
+ min_lines: 80
+ - path: src/app/components/VoltageDisplay.tsx
+ provides: Solar/battery voltage display
+ exports: VoltageDisplay
+ - path: src/app/components/BatteryStatus.tsx
+ provides: Battery status indicator
+ exports: BatteryStatus
+ key_links:
+ - from: Header
+ to: sensorStore
+ via: useSensorStore hook
+ - from: BatteryStatus
+ to: VoltageDisplay
+ via: Status color based on voltage level
+---
+
+
+Create the Header component and voltage monitoring components with modern styling, touch-friendly sizing, and status indicators.
+
+Purpose: The header is the primary information display area, showing station status, voltage, and login state. These components are used across all views.
+
+Output: Header.tsx, VoltageDisplay.tsx, BatteryStatus.tsx components with tests, all meeting touch target requirements.
+
+
+
+@./.opencode/get-shit-done/workflows/execute-plan.md
+@./.opencode/get-shit-done/templates/summary.md
+
+
+
+@.planning/phases/01-foundation-dashboard/01-CONTEXT.md
+@.planning/phases/01-foundation-dashboard/01-01-SUMMARY.md
+@sample_interface/src/app/components/Header.tsx
+
+## Key Implementation Details
+
+**Header Layout (compact for 1024x600):**
+```
+[TCK Logo] [Station ID] [Version] [Solar: XX.XV] [Battery: XX.XV] [Status]
+ [HH:MM:SS] [YYYY-MM-DD] [ASU: XX] [Login: ●]
+```
+
+**Voltage Display:**
+- Format: XX.XV (one decimal place)
+- Solar voltage: Always shown
+- Battery voltage: With status indicator
+
+**Battery Status:**
+- HIGH (≥12.0V): Green indicator
+- LOW (<12.0V): Red/orange indicator
+- Threshold: 12.0V configurable
+
+**Touch Targets:**
+- All interactive elements: minimum 44px height
+- Prefer 56px for primary actions
+- Use padding not margin for hit areas
+
+**Design Tokens:**
+- Use existing Tailwind classes
+- shadcn/ui Badge for status indicators
+- Lucide icons for visual elements
+
+
+
+
+
+ task 1: Create VoltageDisplay component
+ src/app/components/VoltageDisplay.tsx, src/app/components/__tests__/VoltageDisplay.test.tsx
+
+ - Displays voltage with one decimal place
+ - Shows label (Solar/Battery)
+ - Updates when sensor data changes
+ - Touch-friendly container (44px min)
+
+
+ 1. Create src/app/components/VoltageDisplay.tsx:
+ - Props: label (string), voltage (number), className (optional)
+ - Format voltage to XX.XV
+ - Use flex layout with gap-2
+ - Min-height 44px for touch
+ - Use text-sm for compact display
+ - Add voltage icon from Lucide (Zap or Battery)
+
+ 2. Create tests:
+ - Renders label and formatted voltage
+ - Updates on prop change
+ - Has minimum touch target size
+
+
+ npm test -- --run VoltageDisplay 2>&1 | grep -E "(PASS|FAIL|✓|✗)"
+
+ VoltageDisplay component created with tests passing
+
+
+
+ task 2: Create BatteryStatus component
+ src/app/components/BatteryStatus.tsx, src/app/components/__tests__/BatteryStatus.test.tsx
+
+ - Shows battery voltage with status badge
+ - HIGH status (≥12V): green badge
+ - LOW status (<12V): red/orange badge
+ - Status text visible: HIGH or LOW
+
+
+ 1. Create src/app/components/BatteryStatus.tsx:
+ - Props: voltage (number), threshold (optional, default 12.0)
+ - Calculate status based on threshold
+ - Use shadcn/ui Badge component
+ - Green variant for HIGH, destructive for LOW
+ - Display "XX.XV - HIGH/LOW" format
+ - Touch-friendly (44px min)
+
+ 2. Create tests:
+ - Shows HIGH for voltage ≥12.0
+ - Shows LOW for voltage <12.0
+ - Uses custom threshold if provided
+ - Updates when voltage changes
+
+
+ npm test -- --run BatteryStatus 2>&1 | grep -E "(PASS|FAIL|✓|✗)"
+
+ BatteryStatus component created with color-coded indicators
+
+
+
+ task 3: Modernize Header component
+ src/app/components/Header.tsx, src/app/components/__tests__/Header.test.tsx
+
+ - Displays TCK logo from /logo/
+ - Shows station ID and version
+ - Shows current time (HH:MM:SS) updating every second
+ - Shows current date (YYYY-MM-DD)
+ - Shows solar voltage
+ - Shows battery voltage with status
+ - Shows communication ASU
+ - Shows login status indicator
+ - All in compact layout for 1024x600
+
+
+ 1. Update/modernize src/app/components/Header.tsx:
+ - Import useSensorStore from stores
+ - Import VoltageDisplay and BatteryStatus
+ - Use useEffect for clock updates (1s interval)
+ - Layout: Logo | Station Info | Time/Date | Voltages | Comm | Login
+ - Use flexbox with justify-between
+ - Compact padding (py-2 px-4)
+ - Use text-sm throughout
+ - Login indicator: green dot when logged in
+ - Cleanup clock interval on unmount
+
+ 2. Create tests:
+ - Renders all required elements
+ - Time updates every second
+ - Uses sensor store data
+ - Has proper structure
+
+
+ npm test -- --run Header 2>&1 | grep -E "(PASS|FAIL|✓|✗)"
+
+ Header component displays all status info, tests pass
+
+
+
+ task 4: Verify touch target sizing
+ src/app/components/Header.tsx, src/app/components/VoltageDisplay.tsx, src/app/components/BatteryStatus.tsx
+
+ - All interactive elements ≥44px
+ - Verified via CSS inspection
+ - Document any exceptions
+
+
+ 1. Review all components for touch target compliance:
+ - Check min-height on containers
+ - Check padding on clickable elements
+ - Ensure no margins-only hit areas
+
+ 2. Add explicit classes where needed:
+ - `min-h-[44px]` for touch targets
+ - `flex items-center` for vertical centering
+
+ 3. Run visual verification:
+ - DevTools element picker
+ - Verify minimum dimensions
+
+
+ grep -n "min-h-\[44px\]\|min-h-11\|h-11\|h-\[44px\]" src/app/components/Header.tsx src/app/components/VoltageDisplay.tsx src/app/components/BatteryStatus.tsx 2>/dev/null | wc -l
+
+ All components have minimum 44px touch targets
+
+
+
+ task 5: Update DashboardLayout to use new Header
+ src/app/components/DashboardLayout.tsx
+
+ - DashboardLayout imports and renders new Header
+ - Layout works with Sidebar
+ - Content area scrollable
+
+
+ 1. Update src/app/components/DashboardLayout.tsx:
+ - Import new Header component
+ - Place Header above main content area
+ - Ensure flex layout works correctly
+ - Test with both kiosk and remote modes
+
+ 2. Verify integration:
+ - Header displays correctly
+ - Sidebar navigation still works
+ - Main content has proper padding
+
+
+ grep -n "import.*Header\|
+
+ DashboardLayout integrates new Header component
+
+
+
+
+
+After completing all tasks:
+1. Run `npm test` — all tests should pass
+2. Verify components exist: `ls src/app/components/Header.tsx src/app/components/VoltageDisplay.tsx src/app/components/BatteryStatus.tsx`
+3. Check touch targets: `grep -r "min-h-" src/app/components/*.tsx | grep -E "(Header|Voltage|Battery)"`
+4. Verify Header integration: `grep "
+
+
+- VoltageDisplay component shows formatted voltage (XX.XV)
+- BatteryStatus component shows color-coded HIGH/LOW status
+- Header displays all required info: Logo, Station ID, Version, Time, Date, Solar, Battery, Comm, Login
+- All touch targets minimum 44px
+- Components tested and passing
+- DashboardLayout uses new Header
+
+
+
diff --git a/.planning/phases/01-foundation-dashboard/01-03-PLAN.md b/.planning/phases/01-foundation-dashboard/01-03-PLAN.md
new file mode 100644
index 000000000..84795bc67
--- /dev/null
+++ b/.planning/phases/01-foundation-dashboard/01-03-PLAN.md
@@ -0,0 +1,287 @@
+---
+phase: 01-foundation-dashboard
+plan: 03
+type: execute
+wave: 2
+depends_on:
+ - 01-01
+ - 01-02
+files_modified:
+ - src/app/components/views/RainfallView.tsx
+ - src/app/components/RainfallCard.tsx
+ - src/app/components/StationInfo.tsx
+ - src/app/components/ClockDisplay.tsx
+ - src/app/components/CommStatus.tsx
+ - src/app/components/__tests__/RainfallView.test.tsx
+ - src/app/components/__tests__/RainfallCard.test.tsx
+autonomous: true
+requirements:
+ - DASH-01
+ - DASH-05
+ - DASH-06
+ - DASH-04
+ - UI-01
+ - UI-02
+ - UI-03
+must_haves:
+ truths:
+ - Dashboard shows 4 rainfall cards (Today/Hourly/MAR/Yearly)
+ - Cards update in real-time from sensor store
+ - Clock displays HH:MM:SS with seconds
+ - Comm status shows ASU/dBm/percentage
+ - Layout adapts to kiosk (1024x600) and remote (Full HD) modes
+ artifacts:
+ - path: src/app/components/views/RainfallView.tsx
+ provides: Main dashboard view
+ exports: RainfallView
+ min_lines: 60
+ - path: src/app/components/RainfallCard.tsx
+ provides: Individual rainfall metric card
+ exports: RainfallCard
+ - path: src/app/components/ClockDisplay.tsx
+ provides: Real-time clock component
+ exports: ClockDisplay
+ key_links:
+ - from: RainfallView
+ to: sensorStore
+ via: useSensorStore hook
+ - from: RainfallCard
+ to: RainfallView
+ via: Props passing rainfall data
+ - from: ClockDisplay
+ to: sensorStore
+ via: Optional sync with store timestamp
+---
+
+
+Create the main dashboard view with rainfall cards, clock display, communication status, and dual-mode responsive layout.
+
+Purpose: This is the primary user interface where operators view rainfall data and station status. Must work on both 7" touchscreen (1024x600) and Full HD remote displays.
+
+Output: RainfallView as main dashboard, RainfallCard components, ClockDisplay, and CommStatus with responsive layouts.
+
+
+
+@./.opencode/get-shit-done/workflows/execute-plan.md
+@./.opencode/get-shit-done/templates/summary.md
+
+
+
+@.planning/phases/01-foundation-dashboard/01-CONTEXT.md
+@.planning/phases/01-foundation-dashboard/01-01-SUMMARY.md
+@.planning/phases/01-foundation-dashboard/01-02-SUMMARY.md
+@sample_interface/src/app/components/views/RainfallView.tsx
+
+## Key Implementation Details
+
+**Rainfall Metrics (4 cards):**
+- Today: Current day's rainfall in mm
+- Hourly: Current hour's rainfall in mm
+- MAR Acc: Monthly accumulation in mm
+- Yearly Acc: Yearly accumulation in mm
+
+**Card Design:**
+- Color-coded: Today (blue), Hourly (cyan), MAR (green), Yearly (purple)
+- Large value display: XX.X mm
+- Label below value
+- Compact for grid layout
+- Touch-friendly (44px+ tap area)
+
+**Grid Layout:**
+- Kiosk (1024x600): 2x2 grid, compact cards
+- Remote (Full HD): 4x1 or 2x2 with larger cards
+- Use CSS Grid with responsive breakpoints
+
+**Clock Display:**
+- Format: HH:MM:SS (24-hour)
+- Updates every second
+- Also shown in Header (this is secondary/alternative)
+
+**Comm Status:**
+- ASU: Signal strength (0-31)
+- dBm: Signal power
+- Percentage: Derived from ASU
+
+
+
+
+
+ task 1: Create RainfallCard component
+ src/app/components/RainfallCard.tsx, src/app/components/__tests__/RainfallCard.test.tsx
+
+ - Displays rainfall value in mm (XX.X format)
+ - Shows label (Today/Hourly/MAR Acc/Yearly Acc)
+ - Color-coded border or background
+ - Large readable text
+ - Updates when value changes
+
+
+ 1. Create src/app/components/RainfallCard.tsx:
+ - Props: label (string), value (number), variant (today|hourly|monthly|yearly)
+ - Format value to one decimal place
+ - Color variants: today=blue, hourly=cyan, monthly=green, yearly=purple
+ - Use shadcn/ui Card component as base
+ - Min-height 100px for touch targets
+ - Value text: text-3xl font-bold
+ - Label text: text-sm text-muted-foreground
+ - Add rain icon from Lucide (CloudRain)
+
+ 2. Create tests:
+ - Renders label and formatted value
+ - Applies correct color variant
+ - Updates on value change
+ - Has minimum dimensions
+
+
+ npm test -- --run RainfallCard 2>&1 | grep -E "(PASS|FAIL|✓|✗)"
+
+ RainfallCard component with 4 color variants created
+
+
+
+ task 2: Create ClockDisplay component
+ src/app/components/ClockDisplay.tsx, src/app/components/__tests__/ClockDisplay.test.tsx
+
+ - Displays HH:MM:SS in 24-hour format
+ - Updates every second
+ - Shows date (YYYY-MM-DD) below or beside
+ - Cleans up interval on unmount
+
+
+ 1. Create src/app/components/ClockDisplay.tsx:
+ - Use useState for current time
+ - Use useEffect with setInterval (1000ms)
+ - Format with date-fns or native Intl
+ - Display: Time large, date smaller
+ - Optional: Sync with sensor store timestamp
+ - Cleanup interval in useEffect return
+ - Touch-friendly container
+
+ 2. Create tests:
+ - Renders time in correct format
+ - Renders date in correct format
+ - Cleanup works on unmount (no memory leaks)
+
+
+ npm test -- --run ClockDisplay 2>&1 | grep -E "(PASS|FAIL|✓|✗)"
+
+ ClockDisplay with real-time updates created
+
+
+
+ task 3: Create CommStatus component
+ src/app/components/CommStatus.tsx, src/app/components/__tests__/CommStatus.test.tsx
+
+ - Shows ASU (0-31), dBm, percentage
+ - Visual indicator for signal strength
+ - Updates from sensor store
+ - Compact for header placement
+
+
+ 1. Create src/app/components/CommStatus.tsx:
+ - Props: asu (number), dBm (number), percentage (number)
+ - Calculate percentage from ASU if not provided: (asu/31)*100
+ - Visual indicator: signal bars or progress
+ - Color: green (good), yellow (fair), red (poor)
+ - Compact display: icon + brief text
+ - Use Lucide Signal icon
+
+ 2. Create tests:
+ - Displays all three values
+ - Calculates percentage correctly
+ - Shows appropriate color indicator
+
+
+ npm test -- --run CommStatus 2>&1 | grep -E "(PASS|FAIL|✓|✗)"
+
+ CommStatus component with signal visualization
+
+
+
+ task 4: Create RainfallView dashboard
+ src/app/components/views/RainfallView.tsx, src/app/components/__tests__/RainfallView.test.tsx
+
+ - Displays 4 RainfallCards in grid
+ - Shows optional ClockDisplay
+ - Shows CommStatus
+ - Uses sensor store data
+ - Responsive layout for both modes
+
+
+ 1. Create/update src/app/components/views/RainfallView.tsx:
+ - Import useSensorStore
+ - Import RainfallCard, ClockDisplay, CommStatus
+ - Get rainfall data from store
+ - Grid layout: 2 cols on mobile/kiosk, 4 cols on desktop/remote
+ - Use CSS Grid: grid-cols-2 lg:grid-cols-4
+ - Gap: gap-4
+ - Padding: p-4
+ - Add section title: "Rainfall Monitor"
+ - Show last update timestamp
+ - Responsive font sizes
+
+ 2. Create tests:
+ - Renders all 4 rainfall cards
+ - Passes correct data to each card
+ - Has responsive grid layout
+ - Updates when store changes
+
+
+ npm test -- --run RainfallView 2>&1 | grep -E "(PASS|FAIL|✓|✗)"
+
+ RainfallView dashboard with responsive grid created
+
+
+
+ task 5: Implement dual-mode responsive layout
+ src/app/components/views/RainfallView.tsx, src/styles/responsive.css
+
+ - Port 8080: Compact layout for 1024x600
+ - Port 9090: Expanded layout for Full HD
+ - Cards resize appropriately
+ - Font sizes scale with mode
+
+
+ 1. Update RainfallView with mode-aware styling:
+ - Import useDisplayMode from hooks
+ - Apply conditional classes based on mode
+ - Kiosk: More compact padding, smaller gaps
+ - Remote: Larger spacing, bigger fonts
+
+ 2. Create src/styles/responsive.css if needed:
+ - Define mode-specific utilities
+ - Or use Tailwind classes inline
+
+ 3. Test responsive behavior:
+ - Verify grid adapts to viewport
+ - Cards maintain touch target size
+ - No horizontal scroll at 1024px
+
+
+ grep -n "useDisplayMode\|kiosk\|remote" src/app/components/views/RainfallView.tsx 2>/dev/null | head -10
+
+ Dual-mode layout responds to port number
+
+
+
+
+
+After completing all tasks:
+1. Run `npm test` — all tests should pass
+2. Verify dashboard exists: `ls src/app/components/views/RainfallView.tsx`
+3. Check responsive classes: `grep -E "grid-cols-|lg:grid-cols" src/app/components/views/RainfallView.tsx`
+4. Verify sensor store connection: `grep "useSensorStore" src/app/components/views/RainfallView.tsx`
+
+
+
+- RainfallCard component with 4 color variants exists
+- RainfallView displays 4 cards in responsive grid
+- ClockDisplay shows HH:MM:SS updating every second
+- CommStatus shows ASU/dBm with visual indicator
+- Layout adapts to kiosk (1024x600) and remote (Full HD) modes
+- All components tested and passing
+
+
+
diff --git a/.planning/phases/01-foundation-dashboard/01-04-PLAN.md b/.planning/phases/01-foundation-dashboard/01-04-PLAN.md
new file mode 100644
index 000000000..c050c197e
--- /dev/null
+++ b/.planning/phases/01-foundation-dashboard/01-04-PLAN.md
@@ -0,0 +1,357 @@
+---
+phase: 01-foundation-dashboard
+plan: 04
+type: execute
+wave: 3
+depends_on:
+ - 01-01
+ - 01-02
+ - 01-03
+files_modified:
+ - src/app/components/Sidebar.tsx
+ - src/app/components/LoginIndicator.tsx
+ - src/app/components/Navigation.tsx
+ - src/app/routes.ts
+ - vite.config.ts
+ - .bundlesize.json
+ - src/app/components/__tests__/Sidebar.test.tsx
+ - src/app/components/__tests__/LoginIndicator.test.tsx
+autonomous: true
+requirements:
+ - DASH-07
+ - UI-02
+ - UI-03
+ - UI-04
+ - UI-05
+must_haves:
+ truths:
+ - Sidebar navigation has Settings, Calibration, Flash Memory links
+ - Login indicator shows logged-in state
+ - Bundle size is under 170KB (target) / 200KB (limit)
+ - All routes defined and working
+ - Touch-friendly navigation (44px+ targets)
+ artifacts:
+ - path: src/app/components/Sidebar.tsx
+ provides: Navigation sidebar
+ exports: Sidebar
+ min_lines: 50
+ - path: src/app/components/LoginIndicator.tsx
+ provides: Login status indicator
+ exports: LoginIndicator
+ - path: src/app/routes.ts
+ provides: Route definitions
+ exports: router
+ - path: .bundlesize.json
+ provides: Bundle size budget config
+ key_links:
+ - from: Sidebar
+ to: routes.ts
+ via: Link components
+ - from: LoginIndicator
+ to: Header
+ via: Header composition
+ - from: routes.ts
+ to: RainfallView
+ via: Route configuration
+---
+
+
+Complete the dashboard with navigation, login indicator, route configuration, and verify performance budget (<170KB bundle size).
+
+Purpose: Navigation provides access to other sections (Settings, Calibration, Flash Memory). Login indicator shows authentication status. Performance verification ensures Pi Zero 2 W compatibility.
+
+Output: Working navigation, login indicator, routes configured, bundle size verified under budget.
+
+
+
+@./.opencode/get-shit-done/workflows/execute-plan.md
+@./.opencode/get-shit-done/templates/summary.md
+
+
+
+@.planning/phases/01-foundation-dashboard/01-CONTEXT.md
+@.planning/phases/01-foundation-dashboard/01-RESEARCH.md
+@sample_interface/src/app/components/Sidebar.tsx
+@sample_interface/src/app/routes.ts
+
+## Key Implementation Details
+
+**Navigation Items:**
+- Dashboard (home)
+- Settings (with sub-items)
+- Calibration
+- Flash Memory
+
+**Sidebar Design:**
+- Fixed width (64px icons-only or 240px expanded)
+- Touch-friendly items (44px+ height)
+- Active state highlighting
+- Collapsible on mobile
+
+**Login Indicator:**
+- Green dot/icon when logged in
+- Gray/red when logged out
+- Text label: "Logged In" / "Logged Out"
+- Small and compact for header
+
+**Routes:**
+- `/` → Dashboard (RainfallView)
+- `/utility/*` → Settings (placeholder for Phase 2)
+- `/calibration` → Calibration (placeholder)
+- `/flash-memory` → Flash Memory (placeholder)
+
+**Bundle Size Budget:**
+- Target: <170KB gzipped initial JS
+- Limit: <200KB gzipped
+- Tools: bundlesize, rollup-plugin-analyzer
+
+
+
+
+
+ task 1: Create LoginIndicator component
+ src/app/components/LoginIndicator.tsx, src/app/components/__tests__/LoginIndicator.test.tsx
+
+ - Shows green status when isLoggedIn=true
+ - Shows gray/red status when isLoggedIn=false
+ - Compact size for header placement
+ - Accessible (aria-label)
+
+
+ 1. Create src/app/components/LoginIndicator.tsx:
+ - Props: isLoggedIn (boolean), className (optional)
+ - Use shadcn/ui Badge or custom indicator
+ - Logged in: Green dot + "Logged In" text
+ - Logged out: Gray dot + "Login" text
+ - Compact: h-6, text-xs
+ - Use Lucide User icon
+ - Add aria-label for accessibility
+
+ 2. Create tests:
+ - Shows correct state for logged in/out
+ - Has accessible label
+ - Accepts custom className
+
+
+ npm test -- --run LoginIndicator 2>&1 | grep -E "(PASS|FAIL|✓|✗)"
+
+ LoginIndicator with accessible states created
+
+
+
+ task 2: Update Sidebar navigation
+ src/app/components/Sidebar.tsx, src/app/components/__tests__/Sidebar.test.tsx
+
+ - Shows Dashboard, Settings, Calibration, Flash Memory links
+ - Touch-friendly items (44px+ height)
+ - Active state for current route
+ - Collapsible sections
+
+
+ 1. Update src/app/components/Sidebar.tsx:
+ - Import NavLink from react-router
+ - Define navigation items array:
+ * Dashboard (Home icon) → /
+ * Settings (Settings icon) → /utility
+ * Calibration (Gauge icon) → /calibration
+ * Flash Memory (HardDrive icon) → /flash-memory
+ - Map items to NavLink components
+ - Item height: min-h-[44px]
+ - Active state: bg-accent class
+ - Icon + label layout
+ - Collapsible on mobile with hamburger
+
+ 2. Create tests:
+ - Renders all navigation items
+ - Items have correct links
+ - Touch target size adequate
+ - Active state applied correctly
+
+
+ npm test -- --run Sidebar 2>&1 | grep -E "(PASS|FAIL|✓|✗)"
+
+ Sidebar with 4 navigation items created
+
+
+
+ task 3: Configure routes for all views
+ src/app/routes.ts, src/app/components/views/SettingsView.tsx, src/app/components/views/CalibrationView.tsx, src/app/components/views/FlashMemoryView.tsx
+
+ - All routes defined in routes.ts
+ - Settings, Calibration, Flash Memory have placeholder views
+ - Routes use lazy loading where appropriate
+ - Navigation between routes works
+
+
+ 1. Create placeholder views:
+ - src/app/components/views/SettingsView.tsx: "Settings - Phase 2" placeholder
+ - src/app/components/views/CalibrationView.tsx: "Calibration - Phase 3" placeholder
+ - src/app/components/views/FlashMemoryView.tsx: "Flash Memory - Phase 3" placeholder
+
+ 2. Update src/app/routes.ts:
+ - Import all views
+ - Define routes array:
+ * path: "/", element: RainfallView
+ * path: "/utility/*", element: SettingsView
+ * path: "/calibration", element: CalibrationView
+ * path: "/flash-memory", element: FlashMemoryView
+ - Use createBrowserRouter
+ - Add error boundary for 404s
+
+
+ grep -n "SettingsView\|CalibrationView\|FlashMemoryView" src/app/routes.ts 2>/dev/null | head -10
+
+ Routes configured for all 4 main sections
+
+
+
+ task 4: Integrate LoginIndicator into Header
+ src/app/components/Header.tsx
+
+ - Header displays LoginIndicator
+ - Gets login state from sensor store
+ - Compact placement in header row
+
+
+ 1. Update src/app/components/Header.tsx:
+ - Import LoginIndicator
+ - Get isLoggedIn from sensor store (or mock for now)
+ - Place LoginIndicator in header layout
+ - Ensure proper spacing and alignment
+
+ 2. Test integration:
+ - Login indicator visible in header
+ - Updates when login state changes
+
+
+ grep -n "LoginIndicator" src/app/components/Header.tsx 2>/dev/null | head -5
+
+ LoginIndicator integrated into Header
+
+
+
+ task 5: Configure bundle size checking
+ .bundlesize.json, vite.config.ts, package.json
+
+ - Bundle size limit configured (170KB target, 200KB max)
+ - Build fails if bundle exceeds limit
+ - Size checking integrated in CI
+
+
+ 1. Create .bundlesize.json:
+ ```json
+ {
+ "files": [
+ {
+ "path": "dist/assets/*.js",
+ "maxSize": "200kb",
+ "compression": "gzip"
+ }
+ ]
+ }
+ ```
+
+ 2. Update package.json scripts:
+ - "build:check": "npm run build && bundlesize"
+ - Add bundlesize as devDependency
+
+ 3. Update vite.config.ts for optimization:
+ - Manual chunks for vendor code
+ - Drop console in production
+ - Minification settings
+
+ 4. Install bundlesize: `pnpm add -D bundlesize`
+
+
+ test -f .bundlesize.json && echo "Config exists" || echo "Config missing"
+
+ Bundle size checking configured
+
+
+
+ task 6: Build and verify bundle size
+ dist/
+
+ - Production build succeeds
+ - Bundle size under 200KB limit
+ - Ideally under 170KB target
+ - No build errors or warnings
+
+
+ 1. Run production build:
+ - `npm run build`
+ - Check for errors
+
+ 2. Analyze bundle size:
+ - `npm run build:check` or check dist/ folder
+ - Look at main JS file size
+ - Use rollup-plugin-visualizer if needed
+
+ 3. If over budget, investigate:
+ - Run `npx vite-bundle-visualizer`
+ - Identify large dependencies
+ - Consider code splitting
+
+ 4. Document actual size in SUMMARY
+
+
+ ls -lh dist/assets/*.js 2>/dev/null | awk '{print $5, $9}' | head -5
+
+ Bundle built and size verified (target: <170KB, limit: <200KB)
+
+
+
+ task 7: Final integration test
+ src/app/App.tsx
+
+ - Full app renders without errors
+ - Navigation works between routes
+ - Dashboard shows all data
+ - No console errors
+
+
+ 1. Verify App.tsx integration:
+ - RouterProvider with routes
+ - DashboardLayout with Header and Sidebar
+ - Outlet for route content
+
+ 2. Run all tests:
+ - `npm test` — should pass
+
+ 3. Verify no TypeScript errors:
+ - `npx tsc --noEmit`
+
+ 4. Check for console errors:
+ - Review any warnings in test output
+
+
+ npm test 2>&1 | tail -5
+
+ Full integration verified, all tests passing
+
+
+
+
+
+After completing all tasks:
+1. Run `npm test` — all tests should pass
+2. Run `npm run build` — should succeed
+3. Verify bundle size: Check dist/ folder, should be <200KB
+4. Check routes: `grep -r "path:" src/app/routes.ts | wc -l` should be 4+
+5. Verify navigation: `grep -r "NavLink\|Link" src/app/components/Sidebar.tsx`
+6. Check LoginIndicator in Header: `grep "LoginIndicator" src/app/components/Header.tsx`
+
+
+
+- LoginIndicator component shows logged-in state
+- Sidebar has navigation to Settings, Calibration, Flash Memory
+- All routes configured and working
+- Bundle size under 200KB (target <170KB)
+- No TypeScript errors
+- All tests passing
+- Full app integrates without errors
+
+
+
diff --git a/.planning/phases/01-foundation-dashboard/01-CONTEXT.md b/.planning/phases/01-foundation-dashboard/01-CONTEXT.md
new file mode 100644
index 000000000..404f0892c
--- /dev/null
+++ b/.planning/phases/01-foundation-dashboard/01-CONTEXT.md
@@ -0,0 +1,104 @@
+# Phase 1 Context - Foundation & Dashboard
+
+**Phase:** 1
+**Created:** 2026-03-13
+
+---
+
+## Decisions Made
+
+### Dashboard Layout
+
+- **Layout**: Keep current 3-panel layout (Sidebar | Header + Content)
+- **Header content**: All in header - Logo, Time, Date, Station ID, Comm Status, Version, Login Status, Solar Voltage, Battery Voltage
+- **Logo**: Use TCK logo from `/logo/` directory
+- **Rainfall cards**: Grid layout with color-coded cards (Today/Hourly/MAR/Yearly)
+
+### Real-time Data Strategy
+
+- **Approach**: Real API with mock fallback
+- **Polling interval**: User adjustable (stored in settings)
+- **Default polling**: 5 seconds
+- **Implementation**: Create API client with mock data fallback for demo/offline
+
+### Dual-Mode Display
+
+- **Detection**: By port number
+ - Port 8080 → Kiosk mode (1024x600)
+ - Port 9090 → Full HD remote mode
+- **Implementation**: `window.location.port` detection in app root
+- **Remote mode**: Expanded dashboard with larger fonts and more information visible
+
+### State Management
+
+- **Library**: Zustand (~1.1KB, research-recommended)
+- **Sensor data**: Global state in Zustand store
+- **Settings persistence**: localStorage + sync to backend when online
+- **Architecture**: Offline-first - settings persist locally, sync in background
+
+---
+
+## Code Context
+
+### Existing Assets
+
+**Routes** (`sample_interface/src/app/routes.ts`):
+- Dashboard at `/` (RainfallView)
+- Settings under `/utility/*`
+- Calibration at `/calibration`
+- Flash Memory at `/flash-memory`
+
+**Components**:
+- `DashboardLayout.tsx` - Main layout wrapper
+- `Header.tsx` - Header component (needs modernization)
+- `Sidebar.tsx` - Navigation sidebar
+- UI components in `components/ui/` - shadcn/ui components available
+
+**Guidelines**: `sample_interface/guidelines/Guidelines.md` - Full UI specification
+
+### Key Files to Modify
+
+- `src/app/App.tsx` - Add mode detection (port-based)
+- `src/app/components/Header.tsx` - Modernize for Phase 1
+- `src/app/components/views/RainfallView.tsx` - Main dashboard view
+- New: `src/app/stores/sensorStore.ts` - Zustand store for sensor data
+- New: `src/app/api/client.ts` - API client with mock fallback
+
+### Dependencies to Add
+
+- `zustand` - State management (research recommended)
+
+---
+
+## Prior Decisions (from PROJECT.md)
+
+- React/TypeScript stack
+- shadcn/ui components
+- Tailwind CSS
+- Two-port architecture (8080/9090)
+- <200KB bundle target for Pi Zero 2 W
+
+---
+
+## Success Criteria
+
+1. User sees real-time rainfall data (Today, Hourly, MAR Acc, Yearly Acc) updating automatically
+2. User sees solar and battery voltage with visual status indicators
+3. User sees station ID, version info, current time/date, and communication status
+4. User sees login status indicator on dashboard
+5. Dashboard loads in <2 seconds on Pi Zero 2 W
+6. All touch targets are minimum 44px (preferably 56px)
+7. Dashboard displays correctly in both 1024x600 (kiosk) and Full HD (remote) modes
+8. Navigation menu provides access to Settings, Calibration, and Flash Memory sections
+
+---
+
+## Out of Scope
+
+- CSV processing (Phase 3)
+- Network stack implementation (Phase 3)
+- Full settings implementation (Phase 2)
+
+---
+
+*Context for Phase 1 planning - decisions that guide research and planning*
diff --git a/.planning/phases/01-foundation-dashboard/01-RESEARCH.md b/.planning/phases/01-foundation-dashboard/01-RESEARCH.md
new file mode 100644
index 000000000..37b1b1c40
--- /dev/null
+++ b/.planning/phases/01-foundation-dashboard/01-RESEARCH.md
@@ -0,0 +1,176 @@
+# Phase 1: Foundation & Dashboard - Research
+
+**Phase:** 1
+**Researched:** 2026-03-13
+**Confidence:** HIGH
+
+---
+
+## Research Question
+
+What do we need to know to PLAN Phase 1 well? Phase 1 delivers a modern, performant dashboard for rainfall monitoring on Raspberry Pi Zero 2 W hardware.
+
+---
+
+## Key Findings
+
+### Dashboard Requirements (from CONTEXT.md + ROADMAP)
+
+**Must Display:**
+1. Real-time rainfall data (Today, Hourly, MAR Acc, Yearly Acc) — DASH-01
+2. Solar voltage reading — DASH-02
+3. Battery voltage with HIGH/LOW status indicator — DASH-03
+4. Station ID and version info — DASH-04
+5. Real-time clock (HH:MM:SS) and date (YYYY-MM-DD) — DASH-05
+6. Communication status (ASU/dBm/percentage) — DASH-06
+7. Login status indicator — DASH-07
+
+**UI Requirements:**
+- Modern, compact design optimized for 1024x600 — UI-01
+- Touch-friendly (44px+ touch targets) — UI-02
+- Responsive layout for Full HD remote access — UI-03
+- <200KB JS bundle for Pi Zero 2 W — UI-04
+- Navigation menu to Settings, Calibration, Flash Memory — UI-05
+
+### Implementation Decisions (Locked)
+
+**Layout:**
+- 3-panel layout: Sidebar | Header + Content
+- Header contains: Logo, Time, Date, Station ID, Comm Status, Version, Login Status, Solar Voltage, Battery Voltage
+- Rainfall cards in grid with color-coding
+
+**Dual-Mode Display:**
+- Port 8080 = Kiosk mode (1024x600)
+- Port 9090 = Full HD remote mode
+- Detection via `window.location.port`
+
+**State Management:**
+- Zustand (~1.1KB) for sensor data
+- localStorage + backend sync for settings persistence
+- Offline-first architecture
+
+**Real-time Data:**
+- Polling-based (default 5s, user adjustable)
+- API client with mock fallback for demo/offline
+
+### Technology Stack (from existing research)
+
+**Confirmed Stack:**
+- React 19.2.4 — 15-20% smaller than v18, required for shadcn/ui
+- Vite 8.0.0 — Fast HMR, superior tree-shaking
+- Tailwind CSS 4.2.0 — Zero-runtime CSS
+- React Router 7.x — Type-safe routing
+- Zustand 5.0.11 — ~1.1KB state management
+- shadcn/ui — Tree-shakeable components
+
+**Bundle Budget:** <170KB initial JS (200KB limit with buffer)
+
+### Critical Pitfalls to Avoid
+
+1. **Performance constraints** — Pi Zero 2 W has 1GHz quad-core, 512MB RAM
+2. **Layout thrashing** — Use React.memo, debounce updates
+3. **Unbounded data** — Circular buffers for historical data
+4. **Kiosk anti-patterns** — 44px+ touch targets, no hover-only states
+5. **Memory leaks** — Proper useEffect cleanup
+6. **Blocking operations** — Keep CSV/network for Phase 3
+
+### File Structure (from existing codebase)
+
+**Routes:**
+- `/` — Dashboard (RainfallView)
+- `/utility/*` — Settings
+- `/calibration` — Calibration
+- `/flash-memory` — Flash Memory
+
+**Components to Create/Modify:**
+- `App.tsx` — Add mode detection
+- `Header.tsx` — Modernize with all status info
+- `RainfallView.tsx` — Main dashboard view
+- `stores/sensorStore.ts` — New Zustand store
+- `api/client.ts` — API client with mock fallback
+
+### Dependencies to Add
+
+- `zustand` — State management
+
+---
+
+## Validation Architecture
+
+### Testable Behaviors
+
+1. **Dashboard displays all required data fields**
+ - Rainfall metrics (Today, Hourly, MAR, Yearly)
+ - Voltage readings (Solar, Battery with status)
+ - Station info (ID, Version)
+ - Time/Date display
+ - Communication status
+ - Login indicator
+
+2. **Touch targets meet minimum size**
+ - All interactive elements ≥44px
+ - Preferably 56px for primary actions
+
+3. **Dual-mode display works**
+ - Port 8080 renders kiosk layout
+ - Port 9090 renders expanded layout
+
+4. **Performance budget met**
+ - Initial JS <170KB gzipped
+ - LCP <2s on Pi Zero 2 W
+
+5. **Data updates in real-time**
+ - Polling interval configurable
+ - Updates visible without refresh
+
+6. **Navigation accessible**
+ - Sidebar shows Settings, Calibration, Flash Memory links
+ - Touch-friendly navigation
+
+### Observable Outputs
+
+| Artifact | Location | Verification |
+|----------|----------|--------------|
+| Dashboard component | `src/app/components/views/RainfallView.tsx` | Renders all 7 data fields |
+| Header component | `src/app/components/Header.tsx` | Shows status indicators |
+| Sensor store | `src/app/stores/sensorStore.ts` | Zustand store with mock data |
+| API client | `src/app/api/client.ts` | Fetch with fallback |
+| Mode detection | `src/app/App.tsx` | Port-based routing |
+| Bundle size | Build output | <170KB initial JS |
+
+### Key Connections
+
+- Sensor store → Dashboard view (data flow)
+- API client → Sensor store (update trigger)
+- Mode detection → Layout components (responsive behavior)
+- Header → Sensor store (status indicators)
+
+---
+
+## Standard Stack Recommendations
+
+### What to Use
+- React 19 + Vite 8 + Tailwind 4 (established)
+- Zustand for state (research-validated)
+- React Router 7 for navigation
+- shadcn/ui Card, Badge, Separator components
+- Lucide React for icons
+
+### What NOT to Use
+- Redux Toolkit (too heavy)
+- TanStack Query (overkill for polling)
+- Material UI (large CSS-in-JS)
+- Real-time libraries (WebSocket/SSE not needed for 5s polling)
+
+---
+
+## Gaps Requiring Decisions
+
+1. **API endpoint contracts** — Coordinate with Python backend team for exact data format
+2. **Color scheme for status indicators** — Use existing TCK brand colors?
+3. **Historical chart implementation** — Recharts is already in dependencies
+4. **Polling interval bounds** — Min/max acceptable values?
+
+---
+
+*Research synthesis for Phase 1 planning. Based on project CONTEXT.md, ROADMAP.md, and existing codebase research.*
diff --git a/.planning/phases/01-foundation-dashboard/01-VALIDATION.md b/.planning/phases/01-foundation-dashboard/01-VALIDATION.md
new file mode 100644
index 000000000..e123783f5
--- /dev/null
+++ b/.planning/phases/01-foundation-dashboard/01-VALIDATION.md
@@ -0,0 +1,90 @@
+---
+phase: 1
+slug: foundation-dashboard
+status: draft
+nyquist_compliant: false
+wave_0_complete: false
+created: 2026-03-13
+---
+
+# Phase 1 — Validation Strategy
+
+> Validation contract for Foundation & Dashboard phase. Focus on component rendering, bundle size, and touch-friendly interactions.
+
+---
+
+## Test Infrastructure
+
+| Property | Value |
+|----------|-------|
+| **Framework** | Vitest (bundled with Vite) |
+| **Config file** | `vitest.config.ts` — Wave 0 creates if missing |
+| **Quick run command** | `npm test` or `pnpm test` |
+| **Full suite command** | `npm run test:coverage` |
+| **Estimated runtime** | ~10 seconds (unit tests only) |
+
+---
+
+## Sampling Rate
+
+- **After every task commit:** Run `npm test`
+- **After every plan wave:** Run `npm run test:coverage`
+- **Before `/gsd-verify-work`:** Full suite must be green
+- **Max feedback latency:** 15 seconds
+
+---
+
+## Per-task Verification Map
+
+| Task ID | Plan | Wave | Requirement | Test Type | Automated Command | File Exists | Status |
+|---------|------|------|-------------|-----------|-------------------|-------------|--------|
+| 1-01-01 | 01 | 1 | UI-01 | component | `npm test -- Header.test.tsx` | ❌ W0 | ⬜ pending |
+| 1-01-02 | 01 | 1 | DASH-01 | component | `npm test -- RainfallView.test.tsx` | ❌ W0 | ⬜ pending |
+| 1-02-01 | 02 | 1 | DASH-02 | component | `npm test -- VoltageDisplay.test.tsx` | ❌ W0 | ⬜ pending |
+| 1-02-02 | 02 | 1 | DASH-03 | component | `npm test -- BatteryStatus.test.tsx` | ❌ W0 | ⬜ pending |
+| 1-03-01 | 03 | 2 | DASH-04 | component | `npm test -- StationInfo.test.tsx` | ❌ W0 | ⬜ pending |
+| 1-03-02 | 03 | 2 | DASH-05 | component | `npm test -- ClockDisplay.test.tsx` | ❌ W0 | ⬜ pending |
+| 1-03-03 | 03 | 2 | DASH-06 | component | `npm test -- CommStatus.test.tsx` | ❌ W0 | ⬜ pending |
+| 1-04-01 | 04 | 2 | DASH-07 | component | `npm test -- LoginIndicator.test.tsx` | ❌ W0 | ⬜ pending |
+| 1-04-02 | 04 | 2 | UI-02 | e2e/visual | Manual verification | N/A | ⬜ pending |
+| 1-04-03 | 04 | 2 | UI-03 | e2e/visual | Manual verification | N/A | ⬜ pending |
+| 1-04-04 | 04 | 3 | UI-04 | build | `npm run build && bundlesize` | ❌ W0 | ⬜ pending |
+| 1-04-05 | 04 | 3 | UI-05 | component | `npm test -- Navigation.test.tsx` | ❌ W0 | ⬜ pending |
+
+*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
+
+---
+
+## Wave 0 Requirements
+
+- [ ] `src/app/components/__tests__/Header.test.tsx` — Header component tests
+- [ ] `src/app/components/__tests__/RainfallView.test.tsx` — Dashboard view tests
+- [ ] `src/app/stores/__tests__/sensorStore.test.ts` — Zustand store tests
+- [ ] `vitest.config.ts` — Vitest configuration
+- [ ] `src/test/setup.ts` — Test utilities and mocks
+- [ ] `.bundlesize.json` — Bundle size budget configuration
+
+---
+
+## Manual-Only Verifications
+
+| Behavior | Requirement | Why Manual | Test Instructions |
+|----------|-------------|------------|-------------------|
+| Touch target sizing | UI-02 | Visual measurement | Verify all buttons ≥44px using DevTools inspector |
+| Kiosk mode rendering | UI-03 | Device-specific | Test on 1024x600 display, verify no horizontal scroll |
+| Remote mode rendering | UI-03 | Device-specific | Test on Full HD display, verify layout expands |
+| Pi Zero performance | UI-04 | Hardware-specific | Deploy to Pi, measure LCP with Chrome DevTools |
+| Real-time data updates | DASH-01 | Time-based | Verify polling updates display every 5 seconds |
+
+---
+
+## Validation Sign-Off
+
+- [ ] All tasks have `` verify or Wave 0 dependencies
+- [ ] Sampling continuity: no 3 consecutive tasks without automated verify
+- [ ] Wave 0 covers all MISSING references
+- [ ] No watch-mode flags
+- [ ] Feedback latency < 15s
+- [ ] `nyquist_compliant: true` set in frontmatter
+
+**Approval:** pending 2026-03-13