diff --git a/.planning/phases/04-usb-manager-label-printing/04-03-SUMMARY.md b/.planning/phases/04-usb-manager-label-printing/04-03-SUMMARY.md new file mode 100644 index 0000000..35b0339 --- /dev/null +++ b/.planning/phases/04-usb-manager-label-printing/04-03-SUMMARY.md @@ -0,0 +1,146 @@ +--- +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-.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 | 43–50 | `Print()` is a no-op | PRT Qutie protocol unknown — requires Wireshark capture on hardware day | +| internal/api/handlers/label.go | 92–99 | `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