homelabby/.planning/phases/04-usb-manager-label-printing/04-03-SUMMARY.md

146 lines
7.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
phase: 04-usb-manager-label-printing
plan: "03"
subsystem: printer
tags: [go, printer, serial, sse, http, tdd]
dependency_graph:
requires:
- internal/usb/manager.go (DeviceEvent, Manager.Events)
- internal/labels/renderer.go (RenderStandard, RenderCable, IsCableDevice)
- internal/netbox/types.go (Device, CustomFields)
provides:
- internal/printer package (PrinterDriver, MockDriver, PrtQutieDriver)
- POST /api/labels/:deviceID/print endpoint
- GET /api/usb/events SSE endpoint
affects:
- cmd/hwlab/main.go (USB Manager start, MockDriver, new handler wiring)
- internal/api/router.go (two new parameters + routes)
tech_stack:
added: []
patterns:
- "PrinterDriver interface: Connect/Print(bitmap,w,h)/Disconnect — driver-agnostic bitmap protocol"
- "MockDriver: saves PNG to SaveDir for visual inspection without hardware"
- "PrtQutieDriver: stub returns ErrNoDevice — safe before hardware arrives"
- "SSE handler: select on r.Context().Done() for goroutine-leak-safe teardown"
- "Rate limiter: sync.Mutex + lastPrintTime guards 1/s print cadence (T-04-09)"
- "TDD: RED commit → GREEN commit for both tasks"
key_files:
created:
- internal/printer/driver.go
- internal/printer/mock_driver.go
- internal/printer/prt_qutie.go
- internal/printer/driver_test.go
- internal/api/handlers/label.go
- internal/api/handlers/label_test.go
- internal/api/handlers/usb_events.go
modified:
- internal/api/router.go
- cmd/hwlab/main.go
decisions:
- "Print() signature is Print(bitmap []byte, width, height int) not Print(image.Image) — driver-agnostic; bitmap conversion happens in handler via ImageToRawBitmap()"
- "Rate limit implemented inline (sync.Mutex + lastPrintTime) rather than chi middleware — simpler for single-driver use case, avoids middleware ordering complexity"
- "MockDriver.SaveDir is exported so tests can inject t.TempDir() without /tmp pollution"
- "PrtQutieDriver.Connect() returns ErrNoDevice stub — real port resolution deferred to hardware arrival day 2026-04-13"
- "USBEventsHandler uses 30s ticker keepalive to prevent proxy timeouts on quiet periods"
metrics:
duration_minutes: 4
completed_date: "2026-04-10"
tasks_completed: 2
tasks_total: 2
files_created: 7
files_modified: 2
---
# Phase 4 Plan 03: Printer Driver Interface + HTTP Endpoints Summary
**One-liner:** PrinterDriver interface with MockDriver (PNG-to-/tmp) and PrtQutieDriver stub, plus POST /api/labels/:id/print (rate-limited) and GET /api/usb/events (SSE with goroutine-safe teardown).
## What Was Built
### internal/printer/driver.go
`PrinterDriver` interface with `Connect() error`, `Print(bitmap []byte, width, height int) error`, `Disconnect() error`. `ImageToRawBitmap(img image.Image) ([]byte, int, int)` converts `image.Image` to 1-bit packed row-major bitmap (dark pixels → 1). Sentinel errors: `ErrNoDevice`, `ErrNotConnected`, `ErrEmptyBitmap`.
### internal/printer/mock_driver.go
`MockDriver` implements `PrinterDriver`. `Print()` reconstructs a grayscale PNG from the 1-bit bitmap and saves it to `SaveDir` (default `/tmp`) as `hwlab-label-<timestamp>.png`. Logs path to stdout. No hardware required — default driver until PRT Qutie arrives.
### internal/printer/prt_qutie.go
`PrtQutieDriver` implements `PrinterDriver` using `go.bug.st/serial`. `Connect()` returns `ErrNoDevice` (stub) — real port resolution deferred to hardware characterization day 2026-04-13. Print protocol unknown; `TODO(hardware)` comments reference Wireshark capture approach.
### internal/api/handlers/label.go
`LabelHandler` handles `POST /api/labels/:deviceID/print`. Fetches device from NetBox, routes to `labels.RenderCable` or `labels.RenderStandard` based on `IsCableDevice()`, converts to bitmap via `printer.ImageToRawBitmap`, calls `LabelPrinter.Print()`. Rate-limited to 1 print/second via `sync.Mutex + lastPrintTime` (T-04-09 mitigation).
### internal/api/handlers/usb_events.go
`USBEventsHandler` handles `GET /api/usb/events`. Streams `usb.DeviceEvent` values as Server-Sent Events. 30-second keepalive ticker prevents proxy timeouts. Returns on `r.Context().Done()` — no goroutine leak (T-04-11 mitigation verified by Test 5).
### internal/api/router.go
Updated `NewRouter` signature to accept `*handlers.LabelHandler` and `*handlers.USBEventsHandler`. Routes: `POST /api/labels/{deviceID}/print` and `GET /api/usb/events`.
### cmd/hwlab/main.go
USB Manager created (`usb.NewManager(2*time.Second)`), started via `go usbManager.Start(ctx)`, stopped via `defer usbManager.Stop()`. `printer.NewMockDriver()` constructed and connected. `LabelHandler` and `USBEventsHandler` constructed and passed to `NewRouter`.
## Commits
| Task | Commit | Description |
|------|---------|-------------|
| Task 1 RED | b223b54 | Failing printer driver tests (TDD RED) |
| Task 1 GREEN | 1156eff | PrinterDriver interface, MockDriver, PrtQutieDriver stub |
| Task 2 RED | dd381ee | Failing handler tests for label print and USB SSE (TDD RED) |
| Task 2 GREEN | 9f57cbd | LabelHandler, USBEventsHandler, router wiring, main.go init |
## Test Results
```
=== RUN TestMockDriverPrintSavesPNG PASS
=== RUN TestMockDriverPrintNilBitmap PASS
=== RUN TestMockDriverConnectDisconnect PASS
=== RUN TestImageToRawBitmapLength PASS
=== RUN TestPrtQutieConnectReturnsErrNoDevice PASS
=== RUN TestMockDriverSaveDirHonoured PASS
=== RUN TestLabelPrintSuccess PASS
=== RUN TestLabelPrintDeviceNotFound PASS
=== RUN TestLabelPrintPrinterError PASS
=== RUN TestUSBEventsSSEContentType PASS
=== RUN TestUSBEventsNoGoroutineLeak PASS
PASS — go build ./... PASS
```
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 2 - Missing Critical] Rate limiting on print endpoint (T-04-09)**
- **Found during:** Task 2 — threat model review
- **Issue:** Threat register marks T-04-09 (DoS via runaway print calls / label waste) as `mitigate`. Plan code snippet had no rate limiting.
- **Fix:** Added `sync.Mutex + lastPrintTime + printCooldown(1s)` to `LabelHandler`. Returns HTTP 429 when violated.
- **Files modified:** internal/api/handlers/label.go
- **Commit:** 9f57cbd
## Known Stubs
| File | Line | Item | Reason |
|------|------|------|--------|
| internal/printer/prt_qutie.go | 35 | `Connect()` returns `ErrNoDevice` | Real VID/PID port enumeration deferred to hardware arrival 2026-04-13 |
| internal/printer/prt_qutie.go | 4350 | `Print()` is a no-op | PRT Qutie protocol unknown — requires Wireshark capture on hardware day |
| internal/api/handlers/label.go | 9299 | `USBVersion: "Unknown"` in cable label | Cable field parsing from `TestData` JSON deferred to Phase 5 |
These stubs do not prevent the plan's goal from being achieved: MockDriver renders and saves real PNG labels; the PrtQutieDriver stub is intentionally non-functional until hardware arrives.
## Threat Flags
None — no new network endpoints beyond the two planned (`POST /api/labels/:id/print`, `GET /api/usb/events`). Both are LAN-only per project constraints.
## Self-Check: PASSED
- internal/printer/driver.go: FOUND
- internal/printer/mock_driver.go: FOUND
- internal/printer/prt_qutie.go: FOUND
- internal/printer/driver_test.go: FOUND
- internal/api/handlers/label.go: FOUND
- internal/api/handlers/label_test.go: FOUND
- internal/api/handlers/usb_events.go: FOUND
- internal/api/router.go: FOUND (modified)
- cmd/hwlab/main.go: FOUND (modified)
- Commit b223b54: FOUND
- Commit 1156eff: FOUND
- Commit dd381ee: FOUND
- Commit 9f57cbd: FOUND