homelabby/.planning/research/ARCHITECTURE.md

388 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Architecture Research
**Domain:** AI-powered hardware inventory management with USB peripheral control
**Researched:** 2026-04-09
**Confidence:** HIGH (project stack is well-defined; patterns are established Go idioms)
## Standard Architecture
### System Overview
```
┌─────────────────────────────────────────────────────────────────────┐
│ Browser (React SPA) │
│ ┌────────────┐ ┌───────────────┐ ┌──────────────┐ │
│ │ Inventory │ │ Lab Advisor │ │ USB Testing │ │
│ │ Dashboard │ │ Chat View │ │ Workflow UI │ │
│ └─────┬──────┘ └──────┬────────┘ └──────┬───────┘ │
└────────┼────────────────┼────────────────────┼───────────────────────┘
│ │ │ HTTP/REST + SSE
┌────────▼────────────────▼────────────────────▼───────────────────────┐
│ Go Backend (Single Binary) │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ HTTP Layer │ │ AI Orch. │ │ USB Manager │ │
│ │ (Handlers) │ │ (3-tier │ │ (Serial/USB │ │
│ │ │ │ pipeline) │ │ devices) │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ ┌──────▼─────────────────▼──────────────────▼───────┐ │
│ │ Service Layer │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ │
│ │ │ Inventory │ │ Advisor │ │ Label / Testing │ │ │
│ │ │ Service │ │ Service │ │ Service │ │ │
│ │ └──────┬────┘ └────┬─────┘ └────────┬─────────┘ │ │
│ └─────────┼────────────┼──────────────────┼───────────┘ │
│ │ │ │ │
│ ┌─────────▼────────────▼──────────────────▼───────┐ │
│ │ Client / Adapter Layer │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────────────┐│ │
│ │ │ NetBox │ │ AI │ │ SQLite (chat ││ │
│ │ │ Client │ │ Client │ │ history only) ││ │
│ │ └──────┬────┘ └────┬─────┘ └──────────────────┘│ │
│ └─────────┼────────────┼─────────────────────────────┘ │
└────────────┼────────────┼──────────────────────────────────────────────┘
│ │
┌───────▼───┐ ┌─────▼──────────────────────────────┐
│ NetBox │ │ AI Inference Layer │
│ LXC 130 │ │ ┌────────────┐ ┌───────────────┐ │
│ REST API │ │ │ oMLX │ │ OpenRouter │ │
└───────────┘ │ │ (Gemma 4 │ │ (Tier 2/3 │ │
│ │ local) │ │ remote) │ │
│ └────────────┘ └───────────────┘ │
└────────────────────────────────────────┘
```
### Component Responsibilities
| Component | Responsibility | Notes |
|-----------|----------------|-------|
| React SPA | All user interaction — intake, search, chat, USB workflows | Served as static files by Go; communicates via REST + SSE |
| HTTP Layer (Go handlers) | Route requests, validate input, marshal/unmarshal JSON | Thin — no business logic |
| AI Orchestrator | 3-tier pipeline: route task to local/remote model, aggregate results | Owns escalation logic, prompt construction |
| USB Manager | Own goroutines per device, serial protocol handling, event fan-out | Manages PRT Qutie, 3x Treedix, FNIRSI FNB58 |
| Inventory Service | Photo intake flow, NetBox record creation, quality gate transitions | The core domain service |
| Advisor Service | Chat session management, context assembly from NetBox, response streaming | Wraps AI Orchestrator with lab-context injection |
| Label/Testing Service | Print job construction, cable test sequencing, results capture | Coordinates USB Manager with inventory records |
| NetBox Client | All NetBox REST calls, custom field mapping, HW-XXXXX ID allocation | Single integration point — no direct NetBox calls elsewhere |
| AI Client | OpenAI-compatible interface pointed at oMLX or OpenRouter | Swappable base URL = swappable model tier |
| SQLite store | Chat history, config, local-only ephemeral state | NOT inventory data — NetBox owns that |
| oMLX (Mac Mini) | Local Gemma 4 inference via MLX, OpenAI-compatible endpoint | Tier 1 — always tried first |
| OpenRouter | Remote model routing to Claude Opus, GPT-4o, etc. | Tier 2/3 — escalated only |
| NetBox (LXC 130) | Sole inventory data store, REST API at 10.5.0.130:8000 | Source of truth |
| SearXNG (LXC 129) | Product research search API at 10.5.0.129:8080 | Called by AI Orchestrator during research agent tasks |
## Recommended Project Structure
```
hwlab/
├── cmd/
│ └── hwlab/
│ └── main.go # Wire-up only: init config, start server
├── internal/
│ ├── api/
│ │ ├── handlers/ # HTTP handlers — thin, delegate to services
│ │ │ ├── inventory.go
│ │ │ ├── advisor.go
│ │ │ └── usb.go
│ │ ├── middleware/ # Auth (if any), logging, CORS
│ │ └── router.go # Route registration
│ ├── ai/
│ │ ├── orchestrator.go # 3-tier routing, escalation logic
│ │ ├── client.go # OpenAI-compatible HTTP client (base URL configurable)
│ │ └── prompts/ # Prompt templates per task type
│ ├── inventory/
│ │ ├── service.go # Intake, quality gate, search
│ │ └── types.go # Domain types for inventory items
│ ├── advisor/
│ │ ├── service.go # Chat session, context assembly
│ │ └── history.go # SQLite persistence for chat
│ ├── usb/
│ │ ├── manager.go # Device lifecycle, goroutine-per-device
│ │ ├── printer/ # PRT Qutie protocol
│ │ ├── tester/ # Treedix cable tester protocol
│ │ └── powermeter/ # FNIRSI FNB58 protocol
│ ├── label/
│ │ └── service.go # QR code generation, print job construction
│ ├── netbox/
│ │ └── client.go # All NetBox REST calls, field mapping
│ ├── search/
│ │ └── client.go # SearXNG JSON API client
│ └── config/
│ └── config.go # App config (env/file), secrets
├── web/ # React SPA build output (embedded via go:embed)
│ └── dist/
├── frontend/ # React TypeScript source
│ ├── src/
│ │ ├── views/
│ │ ├── components/
│ │ └── api/ # Typed fetch wrappers
│ ├── package.json
│ └── vite.config.ts
└── Makefile # build, dev, test, embed-frontend
```
### Structure Rationale
- **internal/:** Go convention — prevents external import of application internals
- **internal/ai/:** Keeps all AI concerns (prompts, routing, client) co-located; orchestrator is the only caller of the AI client
- **internal/usb/:** Each device gets its own sub-package; USB Manager owns the goroutines and exposes typed channels upward
- **internal/netbox/:** Single client prevents NetBox calls from leaking into service or handler layers
- **web/dist/:** Embedded into the Go binary at build time via `go:embed` — single binary deployment, no separate static server needed
- **cmd/hwlab/:** Only wire-up code; no business logic in main
## Architectural Patterns
### Pattern 1: Single Binary with Embedded Frontend
**What:** The React SPA build output is embedded into the Go binary using `go:embed`. The Go HTTP server serves static assets on `/` and API routes on `/api/`.
**When to use:** Single-operator homelab tool where deployment simplicity matters more than independent frontend deploys.
**Trade-offs:** Rebuild binary to update frontend. Acceptable for solo use; would need rethinking for teams.
**Example:**
```go
//go:embed web/dist
var staticFiles embed.FS
mux.Handle("/", http.FileServerFS(staticFiles))
mux.Handle("/api/", apiRouter)
```
### Pattern 2: Goroutine-Per-USB-Device with Channel Fan-Out
**What:** Each USB device runs in a dedicated goroutine. The USB Manager holds typed channels for commands (in) and events (out). HTTP handlers send commands; SSE endpoint subscribes to event stream.
**When to use:** Any time you need concurrent, non-blocking I/O to physical devices without coordination complexity.
**Trade-offs:** Simple but requires careful channel sizing and shutdown sequencing via `context.Context`.
**Example:**
```go
type USBManager struct {
printer *PrinterDevice
testers [3]*TesterDevice
powerMeter *PowerMeterDevice
}
// Each device goroutine:
func (d *PrinterDevice) run(ctx context.Context, cmds <-chan PrintCmd, events chan<- DeviceEvent) {
for {
select {
case cmd := <-cmds:
// send to serial port
case <-ctx.Done():
return
}
}
}
```
### Pattern 3: Three-Tier AI Orchestrator with Eager Local, Lazy Remote
**What:** All AI tasks are attempted first with oMLX (Tier 1 — local Gemma 4). If confidence is below threshold or the task type explicitly requires it, the orchestrator escalates to OpenRouter (Tier 2/3 — Claude Opus or similar). The AI Client struct accepts a base URL, so switching tiers is a URL swap, not a code change.
**When to use:** Local inference is available but not always sufficient; cloud costs must be minimized.
**Trade-offs:** Adds latency on escalation paths. Confidence scoring for vision tasks (photo intake) needs calibration.
**Example:**
```go
type AIClient struct {
BaseURL string // "http://localhost:11434/v1" or "https://openrouter.ai/api/v1"
Model string
APIKey string
}
func (o *Orchestrator) Run(ctx context.Context, task Task) (Result, error) {
result, err := o.tier1.Complete(ctx, task)
if err != nil || result.Confidence < o.threshold {
return o.tier2.Complete(ctx, task)
}
return result, nil
}
```
### Pattern 4: NetBox as External State — Repository Pattern Wrapper
**What:** The `internal/netbox` package implements a typed repository interface over the NetBox REST API. Services call the repository; they never construct raw HTTP calls to NetBox. The repository maps between NetBox's JSON schema and HWLab domain types.
**When to use:** Any time an external service is the source of truth and its API shape should not leak into domain logic.
**Trade-offs:** Extra mapping layer. Worth it: if NetBox API changes, only `internal/netbox` needs updating.
## Data Flow
### Photo Intake Flow (Primary Flow)
```
User uploads photo
POST /api/intake (handler)
InventoryService.Intake(photo)
AIOrchestrator.AnalyzeHardware(photo)
↓ (Tier 1: oMLX/Gemma 4 vision)
→ if low confidence → Tier 2: OpenRouter (Claude Opus vision)
Structured item data (name, category, specs)
AIOrchestrator.Research(item) [optional, if quality gate requires]
↓ (SearXNG search + AI synthesis)
NetBoxClient.CreateItem(item)
↓ → NetBox LXC (REST POST)
HW-XXXXX ID assigned
LabelService.Print(item)
USBManager.Printer.Print(label)
↓ → PRT Qutie (USB serial)
SSE event → frontend: "intake complete, HW-XXXXX"
```
### Cable Testing Flow
```
User initiates test (selects item HW-XXXXX, selects tester port)
POST /api/test (handler)
TestingService.RunTest(itemID, testerIndex)
USBManager.Tester[n].RunTest(cableType)
↓ → Treedix device (USB serial command)
DeviceEvent (test result bytes)
TestingService.ParseResult(bytes)
NetBoxClient.UpdateItemField(itemID, "cable_test_result", result)
SSE event → frontend: test result display
```
### Lab Advisor Chat Flow
```
User sends message
POST /api/advisor/chat (handler, streams response)
AdvisorService.Chat(sessionID, message)
NetBoxClient.GetInventoryContext() [fetch relevant items for context]
AIOrchestrator.Chat(systemPrompt + context + history + message)
↓ (Tier 3: OpenRouter/Claude Opus — always remote for advisor)
Streamed tokens → SSE → frontend chat view
AdvisorService.SaveMessage(sessionID, message, response) → SQLite
```
### Key Data Flow Rules
1. **NetBox is write-through:** Any state change to an inventory item goes to NetBox immediately — no local cache of inventory records.
2. **USB events are push, not poll:** Device goroutines push events onto channels; the SSE handler fans them out to connected browser clients.
3. **AI calls are always async from the user's perspective:** Handlers return immediately with a job ID; progress arrives via SSE.
4. **SQLite is append-only for chat:** No inventory data in SQLite. Chat history rows are never updated, only inserted and read.
## Build Order (Phase Dependencies)
The architecture has a clear dependency DAG that should drive phase sequencing:
```
Phase 1: Go scaffold + NetBox client + basic CRUD
↓ (NetBox client is a dependency of everything)
Phase 2: AI client + local oMLX integration + photo intake pipeline
↓ (AI pipeline needed before intake flow is useful)
Phase 3: React SPA + inventory dashboard (reads from NetBox via Go API)
↓ (frontend needs backend API to be stable)
Phase 4: USB Manager + Printer + label printing flow
↓ (USB hardware arrives 2026-04-13; serial protocols need reverse-engineering)
Phase 5: Cable tester integration + testing workflows
↓ (builds on USB Manager foundation)
Phase 6: Lab Advisor chat (OpenRouter, streaming SSE, SQLite history)
↓ (can be parallel with Phase 4/5 but needs frontend)
Phase 7: SearXNG research agent + quality gate automation
↓ (enhancement layer — all primitives exist by this point)
```
**Critical dependency:** The NetBox client and AI client must be stable before building services that depend on them. Building the NetBox client first (with a thin mock layer for tests) unblocks parallel work on AI and USB.
**USB hardware blocker:** USB device protocols (printer, testers, power meter) cannot be finalized until the hardware arrives (2026-04-13). Phase 4+ USB work should start with a stub/simulator interface that gets replaced once protocols are reverse-engineered.
## Anti-Patterns
### Anti-Pattern 1: Storing Inventory Data Locally
**What people do:** Cache NetBox responses in SQLite or an in-memory map for "performance."
**Why it's wrong:** Creates a second source of truth. NetBox records updated via other tools (direct API, NetBox UI) silently diverge from the local cache. Debugging inventory discrepancies is brutal.
**Do this instead:** Accept the latency of NetBox REST calls. If performance becomes an issue post-MVP, add a read-through cache with short TTL (3060s) and an explicit invalidation hook.
### Anti-Pattern 2: USB Device Logic in HTTP Handlers
**What people do:** Open serial port in the HTTP handler, write bytes, read response synchronously, close port.
**Why it's wrong:** Serial devices are stateful (connection must persist), blocking (reads block the goroutine), and shared (multiple requests may target the same device). Handler-level serial access causes race conditions and hangs.
**Do this instead:** USB Manager owns all device connections. Handlers send typed commands on a channel and wait for a response on a reply channel (or SSE events for streaming results).
### Anti-Pattern 3: Mixing AI Tiers in Service Logic
**What people do:** Hard-code "use Claude for advisor, use Gemma for intake" calls in service layer code.
**Why it's wrong:** Ties business logic to specific model names. Swapping models (e.g., Gemma 4 → Gemma 4 26B) requires touching service code.
**Do this instead:** Services call the AI Orchestrator with a task type and confidence requirement. The Orchestrator owns model selection. Config file maps task types to model preferences.
### Anti-Pattern 4: Polling USB Devices from the Frontend
**What people do:** Frontend polls `GET /api/usb/status` every second to get device state.
**Why it's wrong:** Creates 1 HTTP request/second per connected client, adds 01s latency to test result display, and hammers the serial port with unnecessary reads.
**Do this instead:** Use Server-Sent Events (SSE). USB Manager emits events; Go SSE handler fans out to subscribed browser clients. Instant updates, one persistent connection per client.
## Integration Points
### External Services
| Service | Integration Pattern | Notes |
|---------|---------------------|-------|
| NetBox (LXC 130) | REST API via `internal/netbox` typed client | go-netbox official client or hand-rolled against v4 OpenAPI spec; needs custom fields for HW IDs and quality gate status |
| oMLX (local) | OpenAI-compatible `/v1/chat/completions` + `/v1/completions` with vision | Base URL: `http://localhost:PORT/v1`; Gemma 4 E4B confirmed fits in 16GB |
| OpenRouter | OpenAI-compatible API with `Authorization: Bearer` | Base URL: `https://openrouter.ai/api/v1`; model specified per request |
| SearXNG (LXC 129) | HTTP GET `http://10.5.0.129:8080/search?q=...&format=json` | No auth; called by AI Orchestrator during research tasks |
| PRT Qutie | USB serial (CDC-ACM or HID) | Protocol unknown until hardware arrives; need to sniff USB traffic from official macOS app |
| Treedix testers (x3) | USB serial (likely CDC-ACM) | 3 independent serial ports; USB Manager assigns by device serial number |
| FNIRSI FNB58 | USB HID or CDC-ACM | Power meter; likely streams measurement data continuously |
### Internal Boundaries
| Boundary | Communication | Notes |
|----------|---------------|-------|
| Handler → Service | Direct Go function call | Handlers are thin wrappers; no business logic |
| Service → AI Orchestrator | Direct Go function call; returns via channel for streaming | Orchestrator manages concurrency internally |
| Service → NetBox Client | Direct Go function call (sync); client handles HTTP retries | Client is the only thing that knows about NetBox |
| Service → USB Manager | Typed command channel (in), event channel (out) | Decouples HTTP request lifecycle from serial I/O timing |
| AI Orchestrator → AI Client | Direct Go function call; streaming via `io.Reader` | Client is a thin HTTP wrapper; Orchestrator does prompt assembly |
| HTTP Handler → SSE | Go channel per client connection, fan-out goroutine | SSE handler registers a channel, USB Manager / AI Orchestrator write to it |
| Frontend → Backend | REST JSON for commands, SSE for async results | No WebSocket — SSE is simpler and sufficient for unidirectional push |
## Scaling Considerations
This is a single-operator homelab tool. Scaling beyond one concurrent user is out of scope. The relevant concerns are:
| Concern | Approach |
|---------|----------|
| Memory (16GB shared with oMLX) | Gemma 4 E4B uses ~810GB; Go backend + SQLite uses ~100MB; leave headroom for 26B A4B if tested |
| oMLX inference latency | Tier 1 photo analysis: 25s expected. Use async + SSE, never block the HTTP response. |
| NetBox API latency | LXC on same network; expect <10ms. No caching needed initially. |
| USB serial reliability | Serial devices disconnect. USB Manager must handle reconnect gracefully without crashing. |
| Single binary size | go:embed of React build adds ~25MB. Acceptable. |
## Sources
- [go-netbox official Go client](https://github.com/netbox-community/go-netbox) MEDIUM confidence (official but may lag NetBox v4.2 custom field API)
- [NetBox REST API Overview](https://netboxlabs.com/docs/netbox/integrations/rest-api/) HIGH confidence
- [OpenAI Go library (official)](https://github.com/openai/openai-go) HIGH confidence; base URL override works for any OpenAI-compatible server
- [go.bug.st/serial for USB serial](https://pkg.go.dev/github.com/bugst/go-serial) HIGH confidence; standard library for serial in Go
- [Go modular monolith patterns](https://medium.com/@hocineelhadj/building-a-simple-backend-with-modular-monolith-architecture-in-go-e2ec7b59bc58) MEDIUM confidence
- [Azure AI Agent Orchestration Patterns](https://learn.microsoft.com/en-us/azure/architecture/ai-ml/guide/ai-agent-design-patterns) HIGH confidence for orchestration pattern naming and structure
- [Alex Edwards: Fat Service Pattern for Go](https://www.alexedwards.net/blog/the-fat-service-pattern) HIGH confidence; well-regarded Go architecture blog
---
*Architecture research for: HWLab — AI-powered homelab hardware inventory management*
*Researched: 2026-04-09*