docs(04-03): complete printer driver + HTTP endpoints plan — MockDriver, SSE, rate limiter

This commit is contained in:
Mikkel Georgsen 2026-04-10 06:53:59 +00:00
parent 9f57cbdf6c
commit 4d2d35e277

View file

@ -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-<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