| 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.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