homelabby/.planning/phases/03-dashboard-intake-ui/03-02-SUMMARY.md

5.5 KiB

phase plan subsystem tags dependency_graph tech_stack key_files decisions metrics
03-dashboard-intake-ui 02 api, netbox
go
inventory
rest-api
interface
tdd
chi
requires provides affects
internal/netbox.Client (ListDevices + GetDevice)
internal/api/handlers.InventoryHandler
internal/api/handlers.NewInventoryHandler
internal/api/handlers.InventoryNetBoxClient (interface)
internal/api/handlers.InventoryItemResponse
GET /api/inventory route
GET /api/inventory/{id} route
internal/api/router.go (NewRouter signature extended)
cmd/hwlab/main.go (InventoryHandler constructed and wired)
added patterns
interface-for-testability (InventoryNetBoxClient mirrors IntakeNetBoxClient pattern)
nil-safe-slice (nil PhotoURLs coerced to [] before JSON encoding)
tdd-red-green (7 tests written failing, then handler implemented to pass)
created modified
internal/api/handlers/inventory.go
internal/api/handlers/inventory_test.go
internal/api/router.go
cmd/hwlab/main.go
id summary
INV-01 InventoryNetBoxClient interface wraps only ListDevices+GetDevice — keeps the handler narrow and fully testable without a live NetBox connection
id summary
INV-02 nil PhotoURLs from netbox.CustomFields coerced to []string{} in deviceToResponse to guarantee JSON encodes as [] not null — prevents frontend null-handling bugs
id summary
INV-03 Hard-coded limit=200 in ListDevices call (T-03-06) — caller cannot request unbounded result sets; pagination deferred to a future plan
duration completed tasks_completed files_created files_modified
~10 minutes 2026-04-10T00:00:00Z 2 2 2

Phase 3 Plan 02: GET /api/inventory + GET /api/inventory/:id Summary

One-liner: Inventory list and detail REST endpoints behind a narrow InventoryNetBoxClient interface, with 7 TDD unit tests covering all HTTP status paths and routes wired into the chi router.

What Was Built

internal/api/handlers/inventory.go

  • InventoryNetBoxClient interface with ListDevices and GetDevice*netbox.Client satisfies it without any changes
  • InventoryHandler struct and NewInventoryHandler(nb InventoryNetBoxClient) *InventoryHandler constructor
  • InventoryItemResponse JSON struct: id, name, asset_tag, hw_id, catalog_status, product_url, firmware_version, test_date, test_data, ai_notes, photo_urls
  • deviceToResponse helper: maps netbox.DeviceInventoryItemResponse, coerces nil PhotoURLs to []string{}
  • ListInventory: GET /api/inventory — calls ListDevices(ctx, 200), returns 200 JSON array or 502 on error
  • GetInventoryItem: GET /api/inventory/{id} — validates ID with strconv.Atoi (400 on failure), calls GetDevice, returns 200/404/502

internal/api/handlers/inventory_test.go

Seven TDD unit tests using mockInventoryNetBoxClient:

Test Scenario Expected
TestListInventoryEmpty mock returns [] 200, empty JSON array
TestListInventoryItems mock returns 2 devices 200, 2-item array with hw_id/name/catalog_status/asset_tag/photo_urls
TestListInventoryNetBoxError mock returns error 502, {"error": "..."}
TestGetInventoryItemFound mock returns device id=42 200, device JSON with correct fields
TestGetInventoryItemNotFound mock returns "404 not found" error 404, {"error": "item not found"}
TestGetInventoryItemInvalidID path param = "abc" 400, {"error": "id must be an integer"}
TestGetInventoryItemNetBoxError mock returns server error 502, {"error": "..."}

internal/api/router.go

  • NewRouter signature extended: NewRouter(staticFiles fs.FS, intakeHandler http.Handler, inventoryHandler *handlers.InventoryHandler) http.Handler
  • Two new routes in /api group: GET /inventory and GET /inventory/{id}

cmd/hwlab/main.go

  • inventoryHandler := handlers.NewInventoryHandler(nbClient) constructed after NetBox client
  • Passed as third argument to api.NewRouter

Deviations from Plan

None — plan executed exactly as written.

Known Stubs

None — both endpoints call real netbox.Client methods (ListDevices, GetDevice). No mock or placeholder data flows to responses in production.

Threat Surface Coverage

All four threats from the plan's threat register are addressed:

Threat Mitigation Where
T-03-04: URL param tampering strconv.Atoi rejects non-integer IDs → 400 inventory.go:GetInventoryItem
T-03-05: Error message info disclosure Errors describe NetBox connectivity only; no credentials or paths exposed inventory.go error paths
T-03-06: DoS via unbounded list Hard-coded limit=200 in ListDevices call inventory.go:ListInventory
T-03-07: Unauthenticated endpoints Accepted for Phase 3 homelab single-operator; Phase 4+ can add auth middleware router.go

Self-Check

Files created:

  • internal/api/handlers/inventory.go: FOUND
  • internal/api/handlers/inventory_test.go: FOUND

Commits:

  • b0b6153: feat(03-02): InventoryHandler with list+detail endpoints and 7 unit tests
  • 743611f: feat(03-02): wire GET /api/inventory and GET /api/inventory/{id} routes

go build ./...: PASS go test ./internal/api/handlers/... -run "TestListInventory|TestGetInventory" -v: 7/7 PASS go test ./...: all packages PASS grep "inventory" internal/api/router.go: both GET routes PRESENT grep "NewInventoryHandler" cmd/hwlab/main.go: PRESENT

Self-Check: PASSED