docs(02-03): complete intake handler plan summary

This commit is contained in:
Mikkel Georgsen 2026-04-10 05:56:50 +00:00
parent 59aa89b199
commit 16cfc48644

View file

@ -0,0 +1,164 @@
---
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