docs(02-03): complete intake handler plan summary
This commit is contained in:
parent
59aa89b199
commit
16cfc48644
1 changed files with 164 additions and 0 deletions
164
.planning/phases/02-ai-pipeline/02-03-SUMMARY.md
Normal file
164
.planning/phases/02-ai-pipeline/02-03-SUMMARY.md
Normal 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 1–3 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
|
||||
Loading…
Add table
Reference in a new issue