8.5 KiB
| phase | plan | subsystem | tags | dependency_graph | tech_stack | key_files | decisions | metrics | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 02-ai-pipeline | 03 | api, ai, netbox, queue, config |
|
|
|
|
|
|
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 IntakeHandlerstruct andNewIntakeHandlerconstructor accepting all four interfaces plus NetBox default IDs and quick-add configServeHTTPflow:- ParseMultipartForm (32 MB cap — T-02-09 DoS mitigation)
- Validate 1–3 photos (T-02-12 count check)
- Read + base64-encode each photo with MIME detection
orchestrator.Analyze→AllocateNextHWID→CreateDevice- On NetBox failure: enqueue
OpNetBoxCreateDeviceto WAQ → 202; nil WAQ → 503 - On success:
PatchCustomFields+SyncTags+UpdateCatalogStatus(all non-fatal) → 201
IntakeResponseJSON struct withhw_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
NewRoutersignature extended:NewRouter(staticFiles fs.FS, intakeHandler http.Handler) http.Handlerr.Post("/intake", intakeHandler.ServeHTTP)registered under/api
internal/config/config.go
- Three new fields:
NetBoxDefaultDeviceTypeID,NetBoxDefaultRoleID,NetBoxDefaultSiteID(allint32) - 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.IntakeWAQtyped interface variable — only assigned when WAQ init succeeds, ensuring nil interface (not nil-wrapped-in-interface) when unavailable - Replaces
queue.NoOpHandlerwithqueue.NewNetBoxOpHandler(nbClient)for WAQ worker - Passes
intakeHandlertoapi.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.godeclaredPatchCustomFields(ctx, deviceID int, ...)butqueue.NetBoxOpsClientinterface (from Plan 02) declared it asint64. The*netbox.Clientwould not satisfyNetBoxOpsClientat compile time.UpdateCatalogStatusincatalog_updater.gosimilarly usedint, inconsistent withCreateDevicereturningint64. - Fix: Changed both signatures to
int64. The API call site casts toint32(deviceID)(fits; NetBox IDs are 32-bit). Updatedcustom_fields_test.gointegration 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.NewWAQfails andwaqremains a nil*queue.WAQ, passing it directly toNewIntakeHandler(which takesIntakeWAQ) creates a non-nil interface wrapping a nil pointer. The nil check inServeHTTP(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 assignedwaqInstanceto 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 wiring59aa89b: 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