---
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 componentweb/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] = useStatecd /home/mikkel/homelabby/web && npx tsc --noEmit 2>&1 | tail -20TypeScript compilation clean. useUSBEvents hook and USBStatusBar component created with no type errors.Task 2: Wire USBStatusBar + Print Label button into DashboardPageweb/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 -20TypeScript 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