--- phase: 06-lab-advisor plan: "02" subsystem: api tags: [go, sse, openrouter, claude, advisor, streaming, postgresql, chi] # Dependency graph requires: - phase: 06-lab-advisor-01 provides: store.Store, CreateConversation, AddMessage, GetConversation, ListConversations, RunMigrations - phase: 04-usb-events provides: SSE pattern (text/event-stream headers, Flush loop) provides: - POST /api/advisor/chat — SSE streaming chat backed by Claude Opus via OpenRouter - GET /api/advisor/conversations — list past conversations from PostgreSQL - GET /api/advisor/conversations/{id} — full message thread - InventoryContextBuilder — 60s-cached compact NetBox inventory summary for system prompt - Tier3 config field in AIConfig for OpenRouter lab-advisor model affects: [07-frontend-advisor, future-ai-tiers] # Tech tracking tech-stack: added: [] patterns: - "SSE streaming via go-openai CreateChatCompletionStream + http.Flusher.Flush per token" - "Nil-guard in router: advisorHandler==nil returns 503 instead of panic" - "Stale cache on NetBox error: return cached context rather than failing the chat request" - "Body size guard: http.MaxBytesReader(64KB) + message truncation at 8000 chars" key-files: created: - internal/advisor/context.go - internal/advisor/handler.go modified: - internal/ai/types.go - internal/config/config.go - internal/api/router.go - cmd/hwlab/main.go key-decisions: - "Tier3 config added to AIConfig struct (not a separate config) for consistency with Tier1/Tier2" - "advisorHandler nil-guard in router returns 503 — graceful degradation when DB not configured" - "Stale InventoryContext served on NetBox error rather than failing chat — availability over freshness" - "HWLAB_DATABASE_URL absence is non-fatal; advisor endpoints return 503 rather than crashing server" patterns-established: - "SSE pattern: Content-Type: text/event-stream + Cache-Control: no-cache + X-Accel-Buffering: no + Flush per write" - "Per-request openai.Client construction from Tier3 config allows model override without restart" requirements-completed: [ADV-01, ADV-02, ADV-03, ADV-05] # Metrics duration: 25min completed: 2026-04-10 --- # Phase 06 Plan 02: Lab Advisor Backend Summary **SSE streaming advisor chat backed by Claude Opus via OpenRouter, with 60s-cached NetBox inventory context injected into every system prompt and full PostgreSQL persistence of conversations and messages.** ## Performance - **Duration:** 25 min - **Started:** 2026-04-10T07:11:00Z - **Completed:** 2026-04-10T07:36:19Z - **Tasks:** 2 - **Files modified:** 6 (2 created, 4 modified) ## Accomplishments - InventoryContextBuilder: fetches up to 200 devices from NetBox, builds a compact text summary (total count, category breakdown, recent 20 items), caches 60s under sync.Mutex, falls back to stale cache on NetBox error - AdvisorHandler: StreamChat streams tokens from OpenRouter via go-openai CreateChatCompletionStream; each token delivered as `data: {"conversation_id":"...","token":"..."}` SSE event; body limited to 64KB, message truncated to 8000 chars; API key never echoed - Full persistence: CreateConversation + AddMessage(user) before stream; AddMessage(assistant) after stream completes - Three endpoints registered under /api/advisor/ with nil-guard for DB-unavailable graceful degradation - Tier3 TierConfig added to AIConfig with HWLAB_AI_TIER3_* env bindings and OpenRouter/claude-opus-4 defaults ## Task Commits 1. **Task 1: InventoryContextBuilder with 60s cache** - `7b02e67` (feat) 2. **Task 2: AdvisorHandler SSE streaming + router wiring** - `0190e85` (feat) ## Files Created/Modified - `internal/advisor/context.go` - InventoryContextBuilder with 60s cache and compact NetBox summary - `internal/advisor/handler.go` - AdvisorHandler: StreamChat, GetConversations, GetConversation - `internal/ai/types.go` - Added Tier3 TierConfig field to AIConfig - `internal/config/config.go` - Tier3 defaults (OpenRouter, claude-opus-4, 120s timeout) + env bindings - `internal/api/router.go` - /api/advisor routes with nil-guard; import advisor package - `cmd/hwlab/main.go` - Store init from HWLAB_DATABASE_URL, RunMigrations, AdvisorHandler wired ## Decisions Made - Tier3 config follows the same TierConfig struct as Tier1/Tier2 — no special advisor config type needed - Per-request openai.Client construction (not cached) allows model field in ChatRequest to override model without server restart, satisfying ADV-05 - advisorHandler is nil when HWLAB_DATABASE_URL is absent; router nil-guard returns 503 on all three endpoints rather than panicking - Stale inventory context returned on NetBox error (availability over freshness — homelab context) ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 2 - Missing Critical] Added Tier3 to AIConfig struct** - **Found during:** Task 2 (AdvisorHandler implementation) - **Issue:** Plan referenced `aiCfg.Tier3` but AIConfig only had Tier1 and Tier2 fields — handler would not compile - **Fix:** Added `Tier3 TierConfig` to AIConfig in internal/ai/types.go; added defaults and env bindings in config.go - **Files modified:** internal/ai/types.go, internal/config/config.go - **Verification:** go build ./... passes; Tier3 config loads from HWLAB_AI_TIER3_* env vars - **Committed in:** 0190e85 (Task 2 commit) **2. [Rule 2 - Missing Critical] Nil-guard in router for missing DB** - **Found during:** Task 2 (router wiring) - **Issue:** advisorHandler can be nil when HWLAB_DATABASE_URL is not set; registering nil method handlers would panic at request time - **Fix:** Added nil check in router; returns 503 with descriptive message when advisor is disabled - **Files modified:** internal/api/router.go - **Verification:** go build ./... passes; server starts without DB configured - **Committed in:** 0190e85 (Task 2 commit) --- **Total deviations:** 2 auto-fixed (2 missing critical) **Impact on plan:** Both fixes required for correctness and safe server startup. No scope creep. ## Issues Encountered - `.gitignore` entry `hwlab` matched the directory `cmd/hwlab/` — `git add cmd/hwlab/main.go` failed with "ignored file" warning. The file was already staged from a prior add, so commit proceeded normally. The gitignore entry targets the compiled binary, not the source directory; no fix needed. ## Known Stubs None — all three endpoints are fully wired. StreamChat requires HWLAB_AI_TIER3_API_KEY to be set with a valid OpenRouter key for actual AI responses; without it, OpenRouter returns 401 which is surfaced as a 502 to the client. This is expected behavior for a homelab tool. ## Threat Flags No new threat surface beyond what the plan's threat model covers. All mitigations from T-06-02-01 through T-06-02-03 are implemented (parameterized store calls, API key isolation, 64KB/8000-char input guards). ## Next Phase Readiness - All three /api/advisor/* endpoints ready for frontend integration - Set HWLAB_DATABASE_URL + HWLAB_AI_TIER3_API_KEY to enable advisor - Frontend advisor chat UI (Phase 07 or equivalent) can connect to POST /api/advisor/chat and consume the SSE token stream --- *Phase: 06-lab-advisor* *Completed: 2026-04-10*