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

8.5 KiB
Raw Blame History

phase plan subsystem tags dependency_graph tech_stack key_files decisions metrics
02-ai-pipeline 03 api, ai, netbox, queue, config
go
intake
multipart
orchestrator
netbox
waq
quick-add
tdd
requires provides affects
internal/ai.Orchestrator
internal/netbox.Client
internal/inventory.CatalogUpdater
internal/queue.WAQ
internal/queue.NewNetBoxOpHandler
internal/config.Config
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
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)
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)
created modified
internal/api/handlers/intake.go
internal/api/handlers/intake_test.go
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
id summary
INTAKE-01 IntakeHandler uses four interfaces (IntakeOrchestrator/IntakeNetBoxClient/IntakeCatalogUpdater/IntakeWAQ) rather than concrete types — allows complete unit testing without real NetBox or AI
id summary
INTAKE-02 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 summary
INTAKE-03 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 summary
INTAKE-04 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
duration completed tasks_completed files_created files_modified
~15 minutes 2026-04-10T06:08:00Z 2 2 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.AnalyzeAllocateNextHWIDCreateDevice
    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