--- phase: 04-usb-manager-label-printing plan: "04" type: execute wave: 3 depends_on: [04-03] files_modified: - internal/api/handlers/intake.go - internal/api/handlers/intake_test.go autonomous: true requirements: [LBL-05] must_haves: truths: - "After a successful intake that creates a NetBox record, a label is automatically printed" - "If the printer is unavailable (ErrNoDevice or ErrNotConnected), intake succeeds without printing and the response includes a print_skipped flag" - "Auto-print does not block intake — printer errors are logged and surfaced in response, not returned as HTTP 500" - "Existing intake tests continue to pass unchanged (nil printer = no-op)" artifacts: - path: internal/api/handlers/intake.go provides: "Updated IntakeHandler accepting optional IntakePrinter; auto-prints after record creation" exports: [IntakeHandler, IntakePrinter] - path: internal/api/handlers/intake_test.go provides: "Updated tests covering auto-print success, printer error (non-fatal), nil printer (no-op)" key_links: - from: internal/api/handlers/intake.go to: internal/printer/driver.go via: "IntakePrinter interface satisfied by *printer.MockDriver or *printer.PrtQutieDriver" pattern: "IntakePrinter" - from: internal/api/handlers/intake.go to: internal/labels/renderer.go via: "calls labels.RenderStandard(d) after device creation" pattern: "labels\\.RenderStandard" --- Integrate auto-print into the intake flow: after a NetBox record is created successfully, the intake handler calls the label renderer and printer driver. Printer failures must not fail the intake — they are non-fatal and surfaced in the response. Purpose: Delivers LBL-05 — label printing as the final step of AI intake without leaving the intake screen. The intake handler already has a clean dependency injection pattern (IntakeOrchestrator, IntakeNetBoxClient, etc.) — follow the same pattern to add IntakePrinter. Output: Updated intake handler with optional auto-print, updated tests. @$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 @internal/api/handlers/intake.go @internal/api/handlers/intake_test.go From internal/api/handlers/intake.go (existing, must read before editing): ```go type IntakeHandler struct { orchestrator IntakeOrchestrator netboxClient IntakeNetBoxClient catalogUpdater IntakeCatalogUpdater waq IntakeWAQ // may be nil deviceTypeID int32 roleID int32 siteID int32 quickAddEnabled bool quickAddThresh float64 } func NewIntakeHandler( orch IntakeOrchestrator, nb IntakeNetBoxClient, cu IntakeCatalogUpdater, waq IntakeWAQ, deviceTypeID, roleID, siteID int32, quickAddEnabled bool, quickAddThresh float64, ) *IntakeHandler ``` IntakeResponse (existing, to be extended with PrintSkipped): ```go type IntakeResponse struct { DeviceID int64 `json:"device_id"` HWID string `json:"hw_id"` CatalogStatus string `json:"catalog_status"` // ... other fields } ``` From internal/printer/driver.go (Plan 04-03): ```go type PrinterDriver interface { Connect() error Print(bitmap []byte, width, height int) error Disconnect() error } var ErrNoDevice = errors.New("printer: no device found matching VID/PID") var ErrNotConnected = errors.New("printer: not connected") func ImageToRawBitmap(img image.Image) ([]byte, int, int) ``` From internal/labels/renderer.go (Plan 04-02): ```go type LabelData struct { HWID, Name, SpecLine string } func RenderStandard(d LabelData) (image.Image, error) func IsCableDevice(d netbox.Device) bool ``` From internal/netbox/types.go: ```go type Device struct { ID int; Name string; AssetTag string; CustomFields CustomFields } ``` Task 1: Add IntakePrinter interface + auto-print to intake handler internal/api/handlers/intake.go, internal/api/handlers/intake_test.go - Test 1: Successful intake with a mock printer that returns nil — response contains print_skipped=false - Test 2: Successful intake with a mock printer that returns ErrNoDevice — response contains print_skipped=true, HTTP still 201 - Test 3: Successful intake with nil printer — response contains print_skipped=true (no-op path), no panic - Test 4: Printer error does NOT cause intake to return 500 — device record was created, so 201 is returned - Test 5: All 6 existing intake tests continue to pass (nil printer = existing behavior preserved) Read `internal/api/handlers/intake.go` fully before editing. Add to `internal/api/handlers/intake.go`: 1. New interface (follow the existing interface pattern in this file): ```go // IntakePrinter is the optional printer used to auto-print a label after intake. // If nil, label printing is skipped. Printer errors are non-fatal. type IntakePrinter interface { Print(bitmap []byte, width, height int) error } ``` 2. Add `printer IntakePrinter` field to `IntakeHandler` struct (after `waq`). 3. Update `NewIntakeHandler` signature — add `p IntakePrinter` as the last parameter: ```go func NewIntakeHandler( orch IntakeOrchestrator, nb IntakeNetBoxClient, cu IntakeCatalogUpdater, waq IntakeWAQ, deviceTypeID, roleID, siteID int32, quickAddEnabled bool, quickAddThresh float64, p IntakePrinter, // NEW — may be nil ) *IntakeHandler ``` 4. Update `IntakeResponse` — add `PrintSkipped bool`: ```go type IntakeResponse struct { // ... existing fields unchanged ... PrintSkipped bool `json:"print_skipped"` } ``` 5. In `ServeHTTP`, after the NetBox record is successfully created (after `catalogUpdater.UpdateCatalogStatus` succeeds), add the auto-print step: ```go // Auto-print label (non-fatal: printer errors skip printing but don't fail intake) resp.PrintSkipped = true if h.printer != nil { // Fetch the created device to build LabelData // Use the deviceID and HWID already available in the response labelData := labels.LabelData{ HWID: hwid, // already allocated earlier in the handler Name: result.Name, // from AI IntakeResult SpecLine: result.SpecLine, // from AI IntakeResult (top spec summary) } img, renderErr := labels.RenderStandard(labelData) if renderErr == nil { bitmap, w, h := printer.ImageToRawBitmap(img) printErr := h.printer.Print(bitmap, w, h) if printErr == nil { resp.PrintSkipped = false } else { log.Printf("auto-print skipped: %v", printErr) } } else { log.Printf("label render failed: %v", renderErr) } } ``` Note: `result.SpecLine` may not exist yet on `ai.IntakeResult`. Check the struct definition in `internal/ai/` — if there is no SpecLine field, use `result.AINotes` or the first spec from `result.Specs` as a fallback. Do NOT add a field to IntakeResult if it does not already exist — use what is there. 6. Update `cmd/hwlab/main.go` to pass the MockDriver instance to `NewIntakeHandler` as the last argument. 7. Update all existing `newHandler(...)` calls in `intake_test.go` to pass `nil` as the last argument — preserving the 6 existing test behaviors exactly. Write 4 new tests (Tests 1-4) in `intake_test.go` using a `mockPrinter` type: ```go type mockPrinter struct { returnErr error called bool } func (m *mockPrinter) Print(bitmap []byte, w, h int) error { m.called = true return m.returnErr } ``` cd /home/mikkel/homelabby && go test ./internal/api/handlers/... -v -run "TestIntake" 2>&1 | tail -30 All intake tests pass (existing 6 + new 4). `go build ./cmd/hwlab/...` succeeds. IntakePrinter interface accepted as nil safely. ## Trust Boundaries | Boundary | Description | |----------|-------------| | intake flow → printer | Label printed after record creation; print failure must not corrupt the intake record | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-04-12 | Tampering | auto-print data | accept | LabelData derived from NetBox-confirmed record; no user-supplied raw input flows to printer at this stage | | T-04-13 | Denial of Service | print loop in intake | mitigate | Print is called once per intake, synchronous, with its own error boundary — cannot loop or recurse | 1. `go test ./internal/api/handlers/... -v` — all tests pass (old + new) 2. `go build ./cmd/hwlab/...` — compiles with updated NewIntakeHandler signature 3. Test with nil printer: intake returns 201 with print_skipped=true 4. Test with mock printer returning ErrNoDevice: intake returns 201 with print_skipped=true - IntakePrinter interface added to intake.go - Auto-print step fires after NetBox record creation - Printer failure (any error) sets print_skipped=true and logs — never 500 - nil printer behaves as no-op (print_skipped=true) - All existing intake tests pass unchanged (nil passed as printer) - New tests cover the 4 printer scenarios After completion, create `.planning/phases/04-usb-manager-label-printing/04-04-SUMMARY.md`