From a892ae1c38cf308a9652370e0c7c2e22a6d9283c Mon Sep 17 00:00:00 2001 From: Mikkel Georgsen Date: Fri, 10 Apr 2026 06:16:09 +0000 Subject: [PATCH] docs(03-02): complete inventory endpoints plan summary --- .../03-dashboard-intake-ui/03-02-SUMMARY.md | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 .planning/phases/03-dashboard-intake-ui/03-02-SUMMARY.md diff --git a/.planning/phases/03-dashboard-intake-ui/03-02-SUMMARY.md b/.planning/phases/03-dashboard-intake-ui/03-02-SUMMARY.md new file mode 100644 index 0000000..db637db --- /dev/null +++ b/.planning/phases/03-dashboard-intake-ui/03-02-SUMMARY.md @@ -0,0 +1,121 @@ +--- +phase: 03-dashboard-intake-ui +plan: "02" +subsystem: api, netbox +tags: [go, inventory, rest-api, interface, tdd, chi] +dependency_graph: + requires: + - internal/netbox.Client (ListDevices + GetDevice) + provides: + - 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 + affects: + - internal/api/router.go (NewRouter signature extended) + - cmd/hwlab/main.go (InventoryHandler constructed and wired) +tech_stack: + 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) +key_files: + created: + - internal/api/handlers/inventory.go + - internal/api/handlers/inventory_test.go + modified: + - internal/api/router.go + - cmd/hwlab/main.go +decisions: + - id: INV-01 + summary: "InventoryNetBoxClient interface wraps only ListDevices+GetDevice — keeps the handler narrow and fully testable without a live NetBox connection" + - id: INV-02 + summary: "nil PhotoURLs from netbox.CustomFields coerced to []string{} in deviceToResponse to guarantee JSON encodes as [] not null — prevents frontend null-handling bugs" + - id: INV-03 + summary: "Hard-coded limit=200 in ListDevices call (T-03-06) — caller cannot request unbounded result sets; pagination deferred to a future plan" +metrics: + duration: "~10 minutes" + completed: "2026-04-10T00:00:00Z" + tasks_completed: 2 + files_created: 2 + files_modified: 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.Device` → `InventoryItemResponse`, 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