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>
254 lines
9.5 KiB
Markdown
254 lines
9.5 KiB
Markdown
---
|
|
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"
|
|
---
|
|
|
|
<objective>
|
|
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.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/phases/04-usb-manager-label-printing/04-CONTEXT.md
|
|
@internal/api/handlers/intake.go
|
|
@internal/api/handlers/intake_test.go
|
|
</context>
|
|
|
|
<interfaces>
|
|
<!-- Key contracts from prior plans. -->
|
|
|
|
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 }
|
|
```
|
|
</interfaces>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 1: Add IntakePrinter interface + auto-print to intake handler</name>
|
|
<files>internal/api/handlers/intake.go, internal/api/handlers/intake_test.go</files>
|
|
<behavior>
|
|
- 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)
|
|
</behavior>
|
|
<action>
|
|
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
|
|
}
|
|
```
|
|
</action>
|
|
<verify>
|
|
<automated>cd /home/mikkel/homelabby && go test ./internal/api/handlers/... -v -run "TestIntake" 2>&1 | tail -30</automated>
|
|
</verify>
|
|
<done>All intake tests pass (existing 6 + new 4). `go build ./cmd/hwlab/...` succeeds. IntakePrinter interface accepted as nil safely.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<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>
|
|
|
|
<verification>
|
|
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
|
|
</verification>
|
|
|
|
<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>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/04-usb-manager-label-printing/04-04-SUMMARY.md`
|
|
</output>
|