docs(04-02): complete label rendering plan — QR code + bitmap renderers

This commit is contained in:
Mikkel Georgsen 2026-04-10 06:47:13 +00:00
parent 7800917500
commit bec54df2e0

View file

@ -0,0 +1,153 @@
---
phase: 04-usb-manager-label-printing
plan: "02"
subsystem: labels
tags: [go, qrcode, image, label-printing, bitmap]
# Dependency graph
requires:
- phase: 04-usb-manager-label-printing
provides: "Phase context, label dimensions, printer interface design"
- phase: 01-foundation
provides: "internal/netbox/types.go Device and CustomFields structs"
provides:
- "internal/labels package: RenderStandard (384x120) and RenderCable (384x180) bitmap renderers"
- "QR code generation encoding http://mac-mini.mg:8080/hw/HW-XXXXX URLs"
- "IsCableDevice() heuristic detection for cable records"
- "LabelData and CableLabelData structs as public API for printer driver"
affects:
- 04-03-printer-driver
- 04-05-intake-integration
# Tech tracking
tech-stack:
added:
- "github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e"
- "golang.org/x/image v0.39.0"
patterns:
- "Label renderers return image.Image — driver-agnostic; printer driver converts to bytes"
- "HWID format validated (HW-\\d{5}) before any qrcode.New() call (T-04-07 mitigation)"
- "TDD: failing tests committed first, implementation makes them pass"
key-files:
created:
- internal/labels/renderer.go
- internal/labels/renderer_test.go
- internal/labels/cable.go
- internal/labels/cable_test.go
modified:
- go.mod
- go.sum
key-decisions:
- "DisableBorder=true (not DisableBorderPadding — that field doesn't exist in skip2/go-qrcode v0.0.0-20200617195104)"
- "HWID format validated before qrcode.New() to mitigate T-04-07 DoS via malformed input"
- "IsCableDevice uses name/notes heuristic only — real device_type detection deferred to Phase 5"
- "Cable label is 180px tall (vs 120px standard) to fit 4 text lines"
patterns-established:
- "Label renderers: pure image.Image output, no USB/printer dependency — testable in isolation"
- "HWID validation: regexp.MustCompile at package level, checked at render entry points"
- "Text layout: TextOffsetX=112 (after QR block), y positions proportional to label height"
requirements-completed: [LBL-01, LBL-02, LBL-03]
# Metrics
duration: 15min
completed: 2026-04-10
---
# Phase 04 Plan 02: QR Code + Label Rendering Summary
**image.NRGBA label bitmaps at 203 DPI using skip2/go-qrcode — standard (384x120) and cable (384x180) templates with HWID-encoded QR codes and basicfont text**
## Performance
- **Duration:** ~15 min
- **Started:** 2026-04-10T06:30:00Z
- **Completed:** 2026-04-10T06:45:55Z
- **Tasks:** 2
- **Files modified:** 6 (4 created, 2 modified)
## Accomplishments
- Standard label renderer: 384x120px NRGBA bitmap with QR code (left 96x96), HW ID, name, spec line
- Cable label renderer: 384x180px bitmap with 4 text lines — HW ID, name, USB version/speed, wattage/test date
- IsCableDevice() heuristic returns true when device Name or AINotes contains "cable" (case-insensitive)
- HWID format validated before QR generation (T-04-07 mitigation — prevents DoS via malformed input)
- All 10 tests pass across both renderers and the cable detection helper
## Task Commits
Each task was committed atomically:
1. **Task 1: QR code generation + standard label renderer** - `8cbd840` (feat)
2. **Task 2: Cable label renderer + IsCableDevice detection helper** - `7800917` (feat)
## Files Created/Modified
- `internal/labels/renderer.go` — LabelData, RenderStandard(), drawText(), truncate(), HWID validation
- `internal/labels/renderer_test.go` — 5 tests: dimensions, QR non-blank, URL encoding, empty name fallback, white background
- `internal/labels/cable.go` — CableLabelData, RenderCable(), IsCableDevice()
- `internal/labels/cable_test.go` — 5 tests: dimensions, full data, IsCableDevice true/false, empty test date
- `go.mod` / `go.sum` — added github.com/skip2/go-qrcode and golang.org/x/image
## Decisions Made
- Used `DisableBorder = true` (not `DisableBorderPadding` — that field does not exist in the installed version of go-qrcode)
- HWID validated via `regexp.MustCompile("^HW-\\d{5}$")` at render entry points per T-04-07 threat disposition (mitigate)
- Cable detection kept as heuristic (name/notes contains "cable") — device_type-based detection is a Phase 5 concern
- go.mod upgraded from go 1.23.0 to 1.25.0 automatically when golang.org/x/image@v0.39.0 required it
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 1 - Bug] DisableBorderPadding field does not exist in go-qrcode**
- **Found during:** Task 1 (standard label renderer)
- **Issue:** Plan code snippet used `qr.DisableBorderPadding = true` but the installed version of skip2/go-qrcode has `DisableBorder bool` not `DisableBorderPadding`
- **Fix:** Changed to `qr.DisableBorder = true` (inspected package source)
- **Files modified:** internal/labels/renderer.go, internal/labels/cable.go
- **Verification:** All tests pass after fix
- **Committed in:** 8cbd840 (Task 1 commit)
**2. [Rule 2 - Missing Critical] Added HWID format validation (T-04-07)**
- **Found during:** Task 1 — threat model review
- **Issue:** Threat register marked T-04-07 (DoS via malformed HWID to qrcode.New) as `mitigate` — no validation was in the plan's code snippet
- **Fix:** Added `hwIDPattern = regexp.MustCompile("^HW-\\d{5}$")` and guard at top of both RenderStandard and RenderCable
- **Files modified:** internal/labels/renderer.go, internal/labels/cable.go
- **Verification:** Passing invalid HWIDs returns error rather than passing to qrcode.New
- **Committed in:** 8cbd840, 7800917 (both task commits)
---
**Total deviations:** 2 auto-fixed (1 bug, 1 missing critical security mitigation)
**Impact on plan:** Both fixes necessary for correctness and threat mitigation. No scope creep.
## Issues Encountered
- golang.org/x/image@v0.39.0 requires go 1.25+ — `go get` automatically upgraded go.mod from 1.23.0 to 1.25.0. This is acceptable for the project.
## User Setup Required
None — no external service configuration required.
## Next Phase Readiness
- `internal/labels` package is complete and fully tested
- Printer driver (04-03) can import `internal/labels` and call `RenderStandard` / `RenderCable` to get `image.Image`, then convert to bytes for the PRT Qutie
- Intake integration (04-05) can use `IsCableDevice()` to route to the correct label template
- No blockers
## Self-Check: PASSED
- internal/labels/renderer.go: FOUND
- internal/labels/renderer_test.go: FOUND
- internal/labels/cable.go: FOUND
- internal/labels/cable_test.go: FOUND
- Commit 8cbd840: FOUND
- Commit 7800917: FOUND
---
*Phase: 04-usb-manager-label-printing*
*Completed: 2026-04-10*