# 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*