homelabby/.planning/research/ARCHITECTURE.md

23 KiB
Raw Blame History

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

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:

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


Architecture research for: HWLab — AI-powered homelab hardware inventory management Researched: 2026-04-09