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