388 lines
23 KiB
Markdown
388 lines
23 KiB
Markdown
# 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 (30–60s) 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 0–1s 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 ~8–10GB; Go backend + SQLite uses ~100MB; leave headroom for 26B A4B if tested |
|
||
| oMLX inference latency | Tier 1 photo analysis: 2–5s 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 ~2–5MB. 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*
|