5 plans across 3 waves covering USB-01 through USB-04 and LBL-01 through LBL-05. Mock drivers and goroutine-leak harness tests enable full TDD before hardware arrives. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
9.5 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 04-usb-manager-label-printing | 04 | execute | 3 |
|
|
true |
|
|
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.
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/phases/04-usb-manager-label-printing/04-CONTEXT.md @internal/api/handlers/intake.go @internal/api/handlers/intake_test.goFrom internal/api/handlers/intake.go (existing, must read before editing):
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):
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):
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):
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:
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:
- New interface (follow the existing interface pattern in this file):
// 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
}
-
Add
printer IntakePrinterfield toIntakeHandlerstruct (afterwaq). -
Update
NewIntakeHandlersignature — addp IntakePrinteras the last parameter:
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
- Update
IntakeResponse— addPrintSkipped bool:
type IntakeResponse struct {
// ... existing fields unchanged ...
PrintSkipped bool `json:"print_skipped"`
}
- In
ServeHTTP, after the NetBox record is successfully created (aftercatalogUpdater.UpdateCatalogStatussucceeds), add the auto-print step:
// 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.
-
Update
cmd/hwlab/main.goto pass the MockDriver instance toNewIntakeHandleras the last argument. -
Update all existing
newHandler(...)calls inintake_test.goto passnilas 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:
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.
<threat_model>
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 |
| </threat_model> |
<success_criteria>
- 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 </success_criteria>