--- phase: 04-usb-manager-label-printing plan: "05" type: execute wave: 3 depends_on: [04-03] files_modified: - web/src/components/USBStatusBar.tsx - web/src/hooks/useUSBEvents.ts - web/src/pages/DashboardPage.tsx autonomous: true requirements: [USB-04] must_haves: truths: - "USB device connect/disconnect events appear in the frontend in real time" - "Dashboard shows a USB status bar with connected device names and roles" - "Print Label button on item cards calls POST /api/labels/:id/print and shows a toast on success/failure" - "USB status bar updates without a page reload when a device is plugged or unplugged" artifacts: - path: web/src/hooks/useUSBEvents.ts provides: "React hook subscribing to GET /api/usb/events SSE stream, returns DeviceEvent[]" exports: [useUSBEvents, DeviceEvent, DeviceState] - path: web/src/components/USBStatusBar.tsx provides: "USBStatusBar component showing connected USB devices with role icons" exports: [USBStatusBar] - path: web/src/pages/DashboardPage.tsx provides: "Updated dashboard with USBStatusBar in header area and Print Label quick action on item cards" key_links: - from: web/src/hooks/useUSBEvents.ts to: GET /api/usb/events via: "new EventSource('/api/usb/events') with onmessage handler" pattern: "EventSource" - from: web/src/pages/DashboardPage.tsx to: POST /api/labels/:id/print via: "fetch('/api/labels/${id}/print', {method:'POST'}) in handlePrintLabel" pattern: "api/labels" --- Build the frontend USB status bar and wire the print label quick action on dashboard item cards. Purpose: Delivers the user-visible half of USB-04 — device events arrive from the SSE stream and update the UI without polling or page reloads. Also surfaces the print action from the dashboard. Output: `useUSBEvents` hook, `USBStatusBar` component, updated `DashboardPage` with print button. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/phases/04-usb-manager-label-printing/04-CONTEXT.md @web/src/pages/DashboardPage.tsx SSE message format — each event is `data: {JSON}\n\n` where JSON is: ```typescript interface DeviceEvent { VIDPID: string; // e.g. "0525:a4a7" Spec: { VID: string; PID: string; Name: string; // e.g. "PRT Qutie" Role: number; // 0=Printer, 1=CableTester, 2=Unknown BaudRate: number; }; State: number; // 0=Disconnected, 1=Connected } type DeviceState = 0 | 1; const StateDisconnected: DeviceState = 0; const StateConnected: DeviceState = 1; ``` Print endpoint response: ```typescript // POST /api/labels/:deviceID/print // 200: { status: "printed" } // 200 with print_skipped: { status: "ok", print_skipped: true } (printer unavailable) // 404: { error: "device not found" } // 500: { error: "..." } ``` ClickHouse design system (from CLAUDE.md global): - Background: #000000 (pure black) - Accent: #faff69 (neon volt) - Status indicators: connected = #22c55e (green-500), disconnected = #ef4444 (red-500) - Font: existing Tailwind classes, no custom CSS Existing DashboardPage patterns (read the file before editing): - Uses TanStack Query for inventory data - Uses react-hot-toast for notifications - Item cards are rendered via ItemCard component or inline map - Quick actions are icon buttons in the card's action area Task 1: useUSBEvents hook + USBStatusBar component web/src/hooks/useUSBEvents.ts, web/src/components/USBStatusBar.tsx Read `web/src/pages/DashboardPage.tsx` to understand the existing component patterns before writing. Create `web/src/hooks/useUSBEvents.ts`: ```typescript import { useEffect, useState } from 'react'; export const StateDisconnected = 0; export const StateConnected = 1; export interface DeviceSpec { VID: string; PID: string; Name: string; Role: number; // 0=Printer, 1=CableTester, 2=Unknown BaudRate: number; } export interface DeviceEvent { VIDPID: string; Spec: DeviceSpec; State: number; } /** * useUSBEvents subscribes to the GET /api/usb/events SSE stream and maintains * the current map of connected USB devices. * * Returns: connectedDevices (map of VIDPID → DeviceSpec) and the last raw event. */ export function useUSBEvents(): { connectedDevices: Map; lastEvent: DeviceEvent | null; } { const [connectedDevices, setConnectedDevices] = useState>(new Map()); const [lastEvent, setLastEvent] = useState(null); useEffect(() => { const source = new EventSource('/api/usb/events'); source.onmessage = (e) => { try { const event: DeviceEvent = JSON.parse(e.data); setLastEvent(event); setConnectedDevices((prev) => { const next = new Map(prev); if (event.State === StateConnected) { next.set(event.VIDPID, event.Spec); } else { next.delete(event.VIDPID); } return next; }); } catch { // Malformed SSE data — ignore, keep existing state } }; source.onerror = () => { // SSE error (server down, network issue) — EventSource auto-reconnects // No state change needed; devices remain as last known }; return () => { source.close(); }; }, []); return { connectedDevices, lastEvent }; } ``` Create `web/src/components/USBStatusBar.tsx`: ```tsx import { Printer, Cable, Usb } from 'lucide-react'; import { useUSBEvents, DeviceSpec } from '../hooks/useUSBEvents'; const roleIcon = (role: number) => { switch (role) { case 0: return ; case 1: return ; default: return ; } }; /** * USBStatusBar displays connected USB devices in a compact horizontal bar. * Shows a dot indicator per device; empty state shows "No USB devices". * Uses ClickHouse design system: black bg, neon volt accent, green for connected. */ export function USBStatusBar() { const { connectedDevices } = useUSBEvents(); const devices = Array.from(connectedDevices.entries()); if (devices.length === 0) { return (
No USB devices
); } return (
{devices.map(([vidpid, spec]: [string, DeviceSpec]) => (
{roleIcon(spec.Role)} {spec.Name}
))}
); } ```
cd /home/mikkel/homelabby/web && npx tsc --noEmit 2>&1 | tail -20 TypeScript compilation clean. useUSBEvents hook and USBStatusBar component created with no type errors.
Task 2: Wire USBStatusBar + Print Label button into DashboardPage web/src/pages/DashboardPage.tsx Read `web/src/pages/DashboardPage.tsx` fully before editing. Make two targeted additions to DashboardPage: **1. Import and place USBStatusBar** in the page header area (above the inventory grid, alongside the existing filter controls). Find the header/toolbar section and add: ```tsx import { USBStatusBar } from '../components/USBStatusBar'; // In the header JSX, alongside existing controls: ``` **2. Add Print Label quick action button** to each item card. Find where item quick actions are rendered (look for the existing "Print Label" or "View in NetBox" / "Edit" action area). If a print action already exists as a stub, wire it; if not, add a Printer icon button: ```tsx import toast from 'react-hot-toast'; import { Printer } from 'lucide-react'; // In the item card action area (per item): const handlePrintLabel = async (deviceId: number) => { const toastId = toast.loading('Printing label...'); try { const res = await fetch(`/api/labels/${deviceId}/print`, { method: 'POST' }); const data = await res.json(); if (!res.ok) { toast.error(data.error ?? 'Print failed', { id: toastId }); return; } if (data.print_skipped) { toast('Label queued — printer not connected', { id: toastId, icon: '⚠️' }); } else { toast.success('Label printed', { id: toastId }); } } catch { toast.error('Network error', { id: toastId }); } }; // Button (in each card's action area): ``` Move `handlePrintLabel` outside the map callback (define once at component level, not per-render). Do not restructure the existing DashboardPage layout — make additive changes only. Preserve all existing functionality (grid/list toggle, filters, item navigation). cd /home/mikkel/homelabby/web && npx tsc --noEmit && npx eslint src/pages/DashboardPage.tsx --max-warnings 0 2>&1 | tail -20 TypeScript and ESLint clean. USBStatusBar renders in dashboard header. Print Label button present on each item card.
## Trust Boundaries | Boundary | Description | |----------|-------------| | SSE stream → React state | Parsed JSON from server updates UI state | | Print button → POST /api/labels | User-triggered HTTP call from browser | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-04-14 | Tampering | SSE JSON parse | mitigate | Wrap JSON.parse in try/catch; malformed data is ignored, state unchanged | | T-04-15 | Denial of Service | EventSource reconnect loop | accept | Browser EventSource auto-reconnects with backoff; solo-operator, not a threat vector | | T-04-16 | Information Disclosure | Device names in SSE | accept | Device names visible on LAN only; PRT Qutie name is not sensitive | 1. `npx tsc --noEmit` in web/ — clean 2. `npx eslint src/` — no errors or warnings on new files 3. `npm run build` — production build succeeds - useUSBEvents hook subscribes to /api/usb/events SSE and maintains connected device map - USBStatusBar shows connected devices with role icons and green status dots; empty state when none - DashboardPage header includes USBStatusBar - Each item card has a Printer icon button wired to POST /api/labels/:id/print - Print success shows success toast; printer unavailable shows warning toast; error shows error toast - No TypeScript errors, no ESLint errors After completion, create `.planning/phases/04-usb-manager-label-printing/04-05-SUMMARY.md`