--- phase: 05-cable-test-integration plan: "03" type: execute wave: 3 depends_on: ["05-02"] files_modified: - web/src/pages/CableTestPage.tsx - web/src/pages/CableTestPage.test.tsx - web/src/api/test.ts - web/src/router.tsx autonomous: true requirements: [CBL-06] must_haves: truths: - "Navigating to /test renders the Cable Test Station page" - "Left panel shows active tester readout (or 'No tester connected' placeholder)" - "Right panel shows last 20 tests in a scrollable list" - "Center panel shows label preview for the most recent test" - "Print & Next button POSTs the current test result and clears the form for the next cable" - "Live FNB58 data streams into the tester readout panel via GET /api/test/events SSE" - "Page is usable on mobile screen (single column stacked layout below lg breakpoint)" artifacts: - path: "web/src/pages/CableTestPage.tsx" provides: "Cable Test Station page with three panels" - path: "web/src/api/test.ts" provides: "submitCableTest(), streamTestEvents(), getRecentTests() API helpers" - path: "web/src/router.tsx" provides: "/test route registered" key_links: - from: "Print & Next button" to: "POST /api/test/cable" via: "submitCableTest() mutation (TanStack Query useMutation)" - from: "Live readout panel" to: "GET /api/test/events" via: "EventSource in useEffect, closed on unmount" - from: "Recent tests panel" to: "GET /api/test/recent" via: "useQuery with 5s refetch interval" --- Build the Cable Test Station page at /test: three-panel layout (tester readout, label preview, recent tests), Print & Next workflow, live SSE updates, mobile-responsive. Purpose: Operator can test cables and print labels from a single page without leaving the workflow. Output: web/src/pages/CableTestPage.tsx, web/src/api/test.ts, router update. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/05-cable-test-integration/05-CONTEXT.md @.planning/phases/05-cable-test-integration/05-02-SUMMARY.md @.planning/phases/03-dashboard-intake-ui/03-01-SUMMARY.md @web/src/router.tsx @web/src/pages/DashboardPage.tsx POST /api/test/cable Request body: ```json { "cable_type": 0, "usb_version": "USB 3.2 Gen 2", "dp_version": "", "hdmi_version": "", "speed_gbps": 10.0, "max_watts": 100, "pin_continuity": true, "has_emarker": true, "resistance_ohm": 0.12, "hw_id": "HW-00042" } ``` Response 201: ```json { "hw_id": "HW-00042", "netbox_id": 7, "print_skipped": false } ``` GET /api/test/recent Response 200: array of TestResult objects (same shape as POST body) GET /api/test/events (SSE) event stream: `data: {"voltage":5.1,"current_amps":3.0,"power_watts":15.3,"pd_protocol":"PD3.0","timestamp":"..."}\n\n` Design system tokens (from CLAUDE.md / ClickHouse design): - Background: #000000 - Accent: #faff69 (neon volt) - Card background: #111111 - Border: #222222 - Text primary: #ffffff - Text muted: #888888 - Tailwind classes: bg-black, text-[#faff69], bg-[#111], border-[#222] Task 1: API helpers + Cable Test Station page web/src/api/test.ts, web/src/pages/CableTestPage.tsx, web/src/router.tsx Create web/src/api/test.ts: ```typescript export interface TestResult { cable_type: 0 | 1 | 2; // 0=USB, 1=DP, 2=HDMI usb_version: string; dp_version: string; hdmi_version: string; speed_gbps: number; max_watts: number; pin_continuity: boolean; has_emarker: boolean; resistance_ohm: number; hw_id: string; } export interface SubmitResponse { hw_id: string; netbox_id: number; print_skipped: boolean; } export interface LiveReading { voltage: number; current_amps: number; power_watts: number; pd_protocol: string; timestamp: string; } export async function submitCableTest(result: TestResult): Promise { const res = await fetch('/api/test/cable', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(result), }); if (!res.ok) throw new Error(`submit failed: ${res.status}`); return res.json(); } export async function getRecentTests(): Promise { const res = await fetch('/api/test/recent'); if (!res.ok) throw new Error(`recent tests failed: ${res.status}`); return res.json(); } ``` Create web/src/pages/CableTestPage.tsx: Layout: Three-column grid on lg+, single stacked column on mobile. - Column 1 (tester readout): "Cable Test Station" heading; if liveReading state is set show Voltage/Current/Power/Protocol values with neon volt accent; else show "No tester connected" in muted text. Use EventSource in useEffect — close on component unmount. On each SSE message parse JSON into liveReading state. - Column 2 (center — label preview + print): Show a card with current test fields as text (HW ID, type, version, speed, power, continuity, eMarker, resistance). "Print & Next" button (bg-[#faff69] text-black font-bold): calls submitCableTest mutation; on success show toast "Label printed for {hw_id}" (react-hot-toast) then reset form. If print_skipped=true toast says "Saved — printer not available". Loading state: button disabled + spinner. - Column 3 (recent tests): "Recent Tests" heading; useQuery fetching getRecentTests() every 5000ms; render as list rows showing cable_type icon (USB/DP/HDMI), hw_id, pin_continuity chip (green tick / red X), speed_gbps. Use lucide-react icons: Usb, Monitor, Tv2 for cable types. CheckCircle2 and XCircle for continuity. For the "current test" form: a simple controlled form with number/text inputs for each TestResult field. Pre-populate with mock data when liveReading arrives (voltage/current into the readout; the test fields remain manual until real driver parsing is implemented). Register /test route in web/src/router.tsx. Add "Test" nav link in the site nav (alongside Dashboard / Intake / Scan). Design tokens: all backgrounds bg-black, cards bg-[#111] border border-[#222] rounded-lg p-4, headings text-white, muted text-[#888], accent text-[#faff69]. cd /home/mikkel/homelabby/web && npm run build 2>&1 | tail -20 npm run build exits 0; /test route exists in router; CableTestPage renders three panels; Print & Next button is present. Task 2: Unit tests for CableTestPage web/src/pages/CableTestPage.test.tsx Create web/src/pages/CableTestPage.test.tsx using Vitest + React Testing Library (already in the project from Phase 3). Mock fetch globally with vi.stubGlobal. Tests: 1. "renders 'No tester connected' when no SSE data" — render CableTestPage, assert text present. 2. "renders recent tests list" — mock getRecentTests() returning 2 USB items; assert two rows visible. 3. "Print & Next calls submitCableTest and shows toast" — mock submitCableTest to resolve {hw_id:"HW-00001",netbox_id:1,print_skipped:false}; fill hw_id input; click Print & Next; await toast text "Label printed". 4. "Print & Next shows print_skipped toast" — mock returns print_skipped:true; assert toast says "Saved — printer not available". Mock EventSource in test setup: ```typescript class MockEventSource { addEventListener = vi.fn(); removeEventListener = vi.fn(); close = vi.fn(); } vi.stubGlobal('EventSource', MockEventSource); ``` Wrap component in necessary providers (QueryClientProvider, MemoryRouter, Toaster). cd /home/mikkel/homelabby/web && npm test -- --run CableTestPage 2>&1 | tail -30 All 4 CableTestPage tests pass; npm run build still exits 0. ## Trust Boundaries | Boundary | Description | |----------|-------------| | browser → POST /api/test/cable | User-submitted form data; validated server-side in 05-02 | | SSE stream → browser | Server push only; EventSource is read-only, no injection risk | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-05-07 | XSS | TestResult fields rendered in DOM | mitigate | Use React JSX (auto-escaped); no dangerouslySetInnerHTML | | T-05-08 | DoS | EventSource leak on unmount | mitigate | useEffect cleanup closes EventSource; verified in test 1 | ``` cd web && npm run build # exits 0, no TypeScript errors cd web && npm test -- --run CableTestPage # all 4 tests pass # Manual smoke test: navigate to http://localhost:5173/test # Verify three panels render, Print & Next button present, /test in nav ``` - /test route renders Cable Test Station page with three panels - Print & Next submits POST /api/test/cable, shows success toast, resets form - SSE live readings update the tester readout panel; EventSource closed on unmount - Recent tests list auto-refreshes every 5s - Layout is single-column on mobile, three-column on lg+ - All 4 unit tests pass; npm run build exits 0 - ClickHouse design tokens applied throughout (#000000 background, #faff69 accent) After completion, create `.planning/phases/05-cable-test-integration/05-03-SUMMARY.md`