homelabby/.planning/phases/02-ai-pipeline/02-03-SUMMARY.md

164 lines
8.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
phase: 02-ai-pipeline
plan: "03"
subsystem: api, ai, netbox, queue, config
tags: [go, intake, multipart, orchestrator, netbox, waq, quick-add, tdd]
dependency_graph:
requires:
- internal/ai.Orchestrator
- internal/netbox.Client
- internal/inventory.CatalogUpdater
- internal/queue.WAQ
- internal/queue.NewNetBoxOpHandler
- internal/config.Config
provides:
- internal/api/handlers.IntakeHandler
- internal/api/handlers.NewIntakeHandler
- internal/api/handlers.IntakeOrchestrator (interface)
- internal/api/handlers.IntakeNetBoxClient (interface)
- internal/api/handlers.IntakeCatalogUpdater (interface)
- internal/api/handlers.IntakeWAQ (interface)
- POST /api/intake route
affects:
- internal/api/router.go (NewRouter signature extended)
- internal/config/config.go (NetBoxDefault* fields added)
- cmd/hwlab/main.go (full wiring, NoOpHandler replaced)
- internal/netbox/custom_fields.go (PatchCustomFields int→int64)
- internal/inventory/catalog_updater.go (UpdateCatalogStatus int→int64)
tech_stack:
added: []
patterns:
- interface-for-testability (IntakeOrchestrator/IntakeNetBoxClient/IntakeCatalogUpdater/IntakeWAQ)
- typed-interface-nil-guard (waqForHandler avoids nil-interface-wrapping bug)
- graceful-degradation (WAQ enqueue on NetBox failure; 503 if both unavailable)
- tdd-red-green (6 tests written before handler implemented)
key_files:
created:
- internal/api/handlers/intake.go
- internal/api/handlers/intake_test.go
modified:
- internal/api/router.go
- internal/config/config.go
- cmd/hwlab/main.go
- internal/netbox/custom_fields.go
- internal/netbox/custom_fields_test.go
- internal/inventory/catalog_updater.go
decisions:
- id: INTAKE-01
summary: "IntakeHandler uses four interfaces (IntakeOrchestrator/IntakeNetBoxClient/IntakeCatalogUpdater/IntakeWAQ) rather than concrete types — allows complete unit testing without real NetBox or AI"
- id: INTAKE-02
summary: "Nil WAQ passes as typed interface nil (not nil *WAQ) to avoid Go nil-interface-wrapping bug — main.go declares var waqForHandler handlers.IntakeWAQ and only assigns when WAQ init succeeds"
- id: INTAKE-03
summary: "PatchCustomFields and UpdateCatalogStatus signatures changed from int to int64 to match CreateDevice return type and queue.NetBoxOpsClient interface — int was inconsistent with the rest of the NetBox ID surface"
- id: INTAKE-04
summary: "Standard path and quick-add path both call CreateDevice immediately — the review step is a UI concern handled by the frontend; the handler always attempts immediate creation"
metrics:
duration: "~15 minutes"
completed: "2026-04-10T06:08:00Z"
tasks_completed: 2
files_created: 2
files_modified: 6
---
# Phase 2 Plan 03: POST /api/intake Handler, Router Wiring, Quick Add Mode Summary
**One-liner:** Multipart photo upload handler wiring orchestrator → HW-ID allocation → NetBox create (or WAQ enqueue on failure), with quick-add bypass and six TDD unit tests covering all code paths.
## What Was Built
### `internal/api/handlers/intake.go`
- Four interfaces (`IntakeOrchestrator`, `IntakeNetBoxClient`, `IntakeCatalogUpdater`, `IntakeWAQ`) for full testability without concrete dependencies
- `IntakeHandler` struct and `NewIntakeHandler` constructor accepting all four interfaces plus NetBox default IDs and quick-add config
- `ServeHTTP` flow:
1. ParseMultipartForm (32 MB cap — T-02-09 DoS mitigation)
2. Validate 13 photos (T-02-12 count check)
3. Read + base64-encode each photo with MIME detection
4. `orchestrator.Analyze``AllocateNextHWID``CreateDevice`
5. On NetBox failure: enqueue `OpNetBoxCreateDevice` to WAQ → 202; nil WAQ → 503
6. On success: `PatchCustomFields` + `SyncTags` + `UpdateCatalogStatus` (all non-fatal) → 201
- `IntakeResponse` JSON struct with `hw_id`, `model`, `manufacturer`, `category`, `specs`, `suggested_tags`, `ai_notes`, `confidence`, `catalog_status`, `netbox_id`, `queued`
### `internal/api/handlers/intake_test.go`
Six TDD tests:
| Test | Scenario | Expected |
|------|----------|----------|
| TestIntakeHandlerRejectsZeroPhotos | 0 files | 400 |
| TestIntakeHandlerRejectsFourPhotos | 4 files | 400 |
| TestIntakeHandlerHighConfidence | confidence 0.95, NetBox OK | 201, catalog_status=indexed |
| TestIntakeHandlerLowConfidence | confidence 0.40 | 201, catalog_status=needs_research |
| TestIntakeHandlerQuickAdd | quick_add=true, confidence 0.95 | 201, CreateDevice called once |
| TestIntakeHandlerNetBoxDown | CreateDevice error | 202, queued=true, 1 WAQ op enqueued |
### `internal/api/router.go`
- `NewRouter` signature extended: `NewRouter(staticFiles fs.FS, intakeHandler http.Handler) http.Handler`
- `r.Post("/intake", intakeHandler.ServeHTTP)` registered under `/api`
### `internal/config/config.go`
- Three new fields: `NetBoxDefaultDeviceTypeID`, `NetBoxDefaultRoleID`, `NetBoxDefaultSiteID` (all `int32`)
- Defaults: all `1` (placeholder IDs provisioned in NetBox Phase 1)
- Env bindings: `HWLAB_NETBOX_DEFAULT_DEVICE_TYPE_ID`, `_ROLE_ID`, `_SITE_ID`
### `cmd/hwlab/main.go`
- Creates `netbox.NewClient`, `ai.NewTierClient` (x2), `ai.NewOrchestrator`, `inventory.NewCatalogUpdater`
- WAQ init: uses `var waqForHandler handlers.IntakeWAQ` typed interface variable — only assigned when WAQ init succeeds, ensuring nil interface (not nil-wrapped-in-interface) when unavailable
- Replaces `queue.NoOpHandler` with `queue.NewNetBoxOpHandler(nbClient)` for WAQ worker
- Passes `intakeHandler` to `api.NewRouter`
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 1 - Bug] PatchCustomFields and UpdateCatalogStatus used `int` instead of `int64`**
- **Found during:** Task 1, implementing IntakeNetBoxClient interface
- **Issue:** `custom_fields.go` declared `PatchCustomFields(ctx, deviceID int, ...)` but `queue.NetBoxOpsClient` interface (from Plan 02) declared it as `int64`. The `*netbox.Client` would not satisfy `NetBoxOpsClient` at compile time. `UpdateCatalogStatus` in `catalog_updater.go` similarly used `int`, inconsistent with `CreateDevice` returning `int64`.
- **Fix:** Changed both signatures to `int64`. The API call site casts to `int32(deviceID)` (fits; NetBox IDs are 32-bit). Updated `custom_fields_test.go` integration test variable type accordingly.
- **Files modified:** `internal/netbox/custom_fields.go`, `internal/netbox/custom_fields_test.go`, `internal/inventory/catalog_updater.go`
- **Commits:** 4fc9362
**2. [Rule 2 - Missing nil guard] Nil WAQ must not be passed as non-nil interface**
- **Found during:** Task 2, wiring main.go
- **Issue:** If `queue.NewWAQ` fails and `waq` remains a nil `*queue.WAQ`, passing it directly to `NewIntakeHandler` (which takes `IntakeWAQ`) creates a non-nil interface wrapping a nil pointer. The nil check in `ServeHTTP` (`if h.waq != nil`) would always pass, causing a nil pointer panic on WAQ error.
- **Fix:** Declared `var waqForHandler handlers.IntakeWAQ` (interface type, zero value is true nil interface) and only assigned `waqInstance` to it when WAQ init succeeded.
- **Files modified:** `cmd/hwlab/main.go`
- **Commit:** 59aa89b
## Known Stubs
None — all data paths are wired end-to-end. The handler calls real orchestrator, real NetBox client, and real WAQ in production.
## Threat Surface Coverage
All five threats from the plan's threat register are mitigated:
| Threat | Mitigation | Where |
|--------|-----------|-------|
| T-02-09: DoS via upload size | `r.ParseMultipartForm(32 << 20)` — 32 MB hard cap | intake.go:ServeHTTP |
| T-02-10: AI-extracted device name tampering | Go string passed to go-netbox JSON marshal; no SQL path | intake.go, client.go |
| T-02-11: AI-extracted tag injection | normalizeTags in SyncTags (Phase 1 T-04-02) | tags.go |
| T-02-12: Photo count bypass | `len(files) == 0` and `len(files) > 3` checked before any processing | intake.go:ServeHTTP |
| T-02-13: hw_id spoofing | AllocateNextHWID assigns ID server-side; caller cannot control it | hwid.go |
## Self-Check
Files created:
- internal/api/handlers/intake.go: FOUND
- internal/api/handlers/intake_test.go: FOUND
Commits:
- 4fc9362: feat(02-03): POST /api/intake handler with orchestrator and NetBox wiring
- 59aa89b: feat(02-03): wire POST /api/intake route, real WAQ handler, and NetBox defaults in config
`go build ./...`: PASS
`go test ./internal/api/... -v`: 7/7 PASS (6 intake + 1 health)
`go test ./...`: all packages PASS
`grep "Post.*intake" internal/api/router.go`: PRESENT
`grep "NewNetBoxOpHandler" cmd/hwlab/main.go`: PRESENT
`grep "NoOpHandler" cmd/hwlab/main.go`: NOT PRESENT
## Self-Check: PASSED