250 lines
9.7 KiB
Markdown
250 lines
9.7 KiB
Markdown
---
|
|
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"
|
|
---
|
|
|
|
<objective>
|
|
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.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.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
|
|
</context>
|
|
|
|
<interfaces>
|
|
<!-- Backend API contracts this page consumes. -->
|
|
|
|
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]
|
|
</interfaces>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: API helpers + Cable Test Station page</name>
|
|
<files>web/src/api/test.ts, web/src/pages/CableTestPage.tsx, web/src/router.tsx</files>
|
|
<action>
|
|
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<SubmitResponse> {
|
|
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<TestResult[]> {
|
|
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].
|
|
</action>
|
|
<verify>
|
|
<automated>cd /home/mikkel/homelabby/web && npm run build 2>&1 | tail -20</automated>
|
|
</verify>
|
|
<done>npm run build exits 0; /test route exists in router; CableTestPage renders three panels; Print & Next button is present.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Unit tests for CableTestPage</name>
|
|
<files>web/src/pages/CableTestPage.test.tsx</files>
|
|
<action>
|
|
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).
|
|
</action>
|
|
<verify>
|
|
<automated>cd /home/mikkel/homelabby/web && npm test -- --run CableTestPage 2>&1 | tail -30</automated>
|
|
</verify>
|
|
<done>All 4 CableTestPage tests pass; npm run build still exits 0.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<threat_model>
|
|
## 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 |
|
|
</threat_model>
|
|
|
|
<verification>
|
|
```
|
|
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
|
|
```
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- /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)
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/05-cable-test-integration/05-03-SUMMARY.md`
|
|
</output>
|