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