13 KiB
RemoteRig — Project Context
Last updated: 2026-05-21 Repo:
CubeCraft-Creations/remote-rig| Host:code.cubecraftcreations.comLocal clone:/mnt/ai-storage/projects/remote-rig| Default branch:devDiscord:DISCORD_DEV_REMOTERIG_CHANNEL_IDLinear Epic: CUB-198
Overview
RemoteRig is a multi-camera remote monitoring system. It provides a camera grid dashboard for monitoring multiple GoPro cameras remotely. Cameras push status via MQTT/HTTP, the UI shows a live grid with SSE updates, and the system supports start/stop recording control.
Target hardware: Raspberry Pi Zero 2 W as the central hub, with ESP32 nodes attached to each GoPro camera for status collection and MQTT communication.
Tech Stack
| Layer | Technology | Notes |
|---|---|---|
| Backend | Go 1.25+ | Chi v5 router, SQLite (modernc.org/sqlite), go-yaml v3 |
| Frontend | React 19 + TypeScript 5.7 | Vite 6, Tailwind CSS 3.4 |
| State | Zustand 5 | Client-side camera state store |
| Icons | lucide-react 0.469 | |
| Real-time | SSE (Server-Sent Events) | /api/v1/events/stream |
| Messaging | MQTT | Mosquitto broker for ESP32 → hub communication |
| Database | SQLite | WAL mode, foreign keys enabled |
| Auth | API key (Bearer token) | Middleware, configurable in config.yaml |
| Testing | Vitest 4.1 + testing-library/react | Unit tests for React components |
| Linting | ESLint 9 | npm run lint |
| CI/CD | Gitea Actions | .gitea/workflows/ci.yaml, build-dev.yaml, deploy-dev.yaml |
Architecture
ESP32 Node (per camera)
│
├── HTTP POST /api/v1/cameras/:id/status (status push)
│
└── MQTT ──→ Mosquitto Broker ──→ MQTT Subscriber (Go hub)
│
▼
┌──────────────────────────────┐
│ Go Central Hub (Pi Zero 2 W)│
│ ┌────────────────────┐ │
│ │ SQLite Database │ │
│ └────────────────────┘ │
│ ┌────────────────────┐ │
│ │ SSE Hub │ │
│ └────────────────────┘ │
└──────────────────────────────┘
│
SSE /api/v1/events/stream
│
▼
React Dashboard (Browser)
Key Architecture Decisions
- SQLite over PostgreSQL: Single-node deployment on Pi Zero 2 W — no need for separate DB server
- SSE over WebSocket: Simpler server-side; unidirectional updates from hub → browser are sufficient
- MQTT for ESP32 → Hub communication: Lightweight, designed for IoT/embedded use
- API key auth: Simple bearer token middleware; configurable key in
config.yaml - Idempotent migrations: DB checks if tables exist before running migrations
Directory Layout
remote-rig/
├── cmd/server/main.go # Entry point — config load, router setup, graceful shutdown
├── config.yaml # Runtime configuration
├── go.mod / go.sum # Go dependencies
├── internal/
│ ├── api/
│ │ ├── api.go # Package doc
│ │ ├── cameras.go # GET /cameras, POST /cameras, GET /cameras/:id
│ │ ├── recording.go # POST /cameras/:id/start, POST /cameras/:id/stop
│ │ └── status.go # POST /cameras/:id/status (push from ESP32)
│ ├── auth/
│ │ └── middleware.go # API key auth middleware
│ ├── db/
│ │ ├── db.go # Open, migrations, WAL mode
│ │ └── migrations/
│ │ └── 001_create_tables.sql
│ └── events/
│ └── sse.go # SSE hub (subscribe, broadcast)
├── pkg/models/
│ └── camera.go # Camera, StatusLog, RecordingEvent, CameraStatus, Settings
├── src/ # React frontend
│ ├── App.tsx # Main app — header, camera grid, footer
│ ├── components/
│ │ ├── CameraCard.tsx # Single camera status card
│ │ ├── CameraCard.test.tsx # Unit tests
│ │ └── index.ts
│ ├── hooks/
│ │ ├── useSSE.ts # SSE connection hook
│ │ ├── useCameraStatus.ts # Camera status hook
│ │ ├── useSystemHealth.ts # System health hook
│ │ └── index.ts
│ ├── services/
│ │ └── api.ts # API client
│ ├── store/
│ │ ├── useCameraStore.ts # Zustand store
│ │ └── index.ts
│ ├── types/
│ │ └── index.ts # TypeScript interfaces
│ ├── utils/
│ │ └── index.ts
│ └── main.tsx
├── docs/
│ ├── CONTEXT.md # ← this file
│ └── plans/
│ └── 2026-05-21-cub-196-cameracard.md # CameraCard implementation plan
├── .gitea/workflows/
│ ├── ci.yaml # PR CI: lint → typecheck → test → build
│ ├── build-dev.yaml # Go binary build on dev push
│ └── deploy-dev.yaml # SCP + SSH deploy with rollback
├── .env.example # VITE_API_URL=http://localhost:8080/api
├── package.json
├── vite.config.ts
├── tailwind.config.js
└── tsconfig.json
Database Schema (SQLite)
cameras
| Column | Type | Notes |
|---|---|---|
| camera_id | TEXT PK | Unique camera identifier |
| friendly_name | TEXT NOT NULL | Human-readable name |
| mac_address | TEXT UNIQUE | MAC address (optional) |
| created_at | DATETIME | Default now |
| updated_at | DATETIME | Default now |
status_logs
| Column | Type | Notes |
|---|---|---|
| id | INTEGER PK AUTO | |
| camera_id | TEXT FK → cameras | |
| recorded_at | DATETIME | Default now |
| battery_pct | INTEGER | Nullable |
| video_remaining_sec | INTEGER | Nullable |
| recording_state | INTEGER | 0=idle, 1=recording |
| mode | TEXT | e.g. "video" |
| resolution | TEXT | e.g. "1080p" |
| fps | INTEGER | |
| online | INTEGER | 0=offline, 1=online |
| raw_battery_pct | REAL | Float precision |
Index: (camera_id, recorded_at DESC)
recording_events
| Column | Type | Notes |
|---|---|---|
| id | INTEGER PK AUTO | |
| camera_id | TEXT FK → cameras | |
| started_at | DATETIME NOT NULL | |
| stopped_at | DATETIME | Null while recording |
| reason | TEXT | e.g. "manual" |
| duration | INTEGER | Seconds |
Index: (camera_id, started_at DESC)
settings
| Column | Type | Notes |
|---|---|---|
| key | TEXT PK | |
| value | TEXT NOT NULL | |
| updated_at | DATETIME | Default now |
Default seeds: poll_interval_sec=30, low_battery_threshold=15, low_storage_alert_sec=300
API Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /health | No | Health check → {"status":"ok"} |
| GET | /api/v1/cameras | Yes | List all cameras with latest status |
| POST | /api/v1/cameras | Yes | Register a new camera |
| GET | /api/v1/cameras/:id | Yes | Camera detail + latest status + 24h history |
| POST | /api/v1/cameras/:id/start | Yes | Start recording + MQTT publish |
| POST | /api/v1/cameras/:id/stop | Yes | Stop recording |
| POST | /api/v1/cameras/:id/status | Yes | Push status from ESP32 node |
| GET | /api/v1/events/stream | No (SSE) | Real-time camera status stream |
Configuration (config.yaml)
db_path: "remoterig.db" # SQLite database path
api_key: "changeme" # Bearer token for API auth
port: 8080 # HTTP listen port
read_timeout: 5s
write_timeout: 10s
idle_timeout: 120s
mqtt:
broker: "localhost:1883"
client_id: "remoterig-hub"
platform:
type: "pi-zero-2w"
max_cameras: 16
Frontend Component Tree
App
├── Header
│ ├── Logo + Title ("RemoteRig Dashboard")
│ └── Stats bar (online count, recording count)
├── CameraGrid
│ └── CameraCard[] (responsive: 1→2→3→4 columns)
│ ├── Camera name + online/offline badge
│ ├── Resolution + FPS display
│ ├── Recording indicator (pulsing dot + REC/IDLE badge)
│ ├── Battery bar (color-coded: green/yellow/red)
│ └── Footer (Live/Last seen + video remaining)
└── Footer
└── "RemoteRig v0.1.0 — Multi-Camera Remote Monitoring System"
Empty state: "Waiting for cameras..." with pulsing radio icon when no cameras connected. Offline state: Camera card dimmed with dashed border, shows "Last seen Xm ago".
Color Palette (Tailwind — dark dashboard theme)
Custom theme in tailwind.config.js:
rig-dark-900(background),rig-dark-800,rig-dark-700(cards), etc.rig-accent(accent color)rig-success(green — battery ≥50%, online)rig-warning(yellow — battery 15-49%)rig-danger(red — battery <15%, offline, recording)
Linear Issue Map
| CUB | Title | Status |
|---|---|---|
| 198 | Epic: Multi-camera remote monitoring system | Backlog |
| 178 | UX/UI design: camera monitoring dashboard mockups | Backlog |
| 179 | Logging & persistence strategy | In Review |
| 180 | Risk mitigation checklist | Done |
| 181 | Scaffold Go module, directory layout, config, main.go | Done |
| 182 | Scaffold Go module (dup of 181) | Backlog |
| 183 | SQLite schema migration + DB init | Backlog |
| 184 | API key auth middleware | Backlog |
| 185 | Camera/StatusLog/RecordingEvent Go models | Backlog |
| 186 | GET /api/v1/cameras (list with live status) | Backlog |
| 187 | POST /api/v1/cameras/:id/start + MQTT publish | Done |
| 188 | POST /api/v1/cameras/:id/stop recording | Backlog |
| 189 | POST /api/v1/cameras (register new camera) | Backlog |
| 190 | GET /api/v1/cameras/:id (detail + history) | Backlog |
| 191 | POST /api/v1/cameras/:id/push-status (HTTP ingestion) | Backlog |
| 192 | MQTT subscriber for status ingestion + fan-out | Backlog |
| 193 | SSE /api/v1/events/stream endpoint | Backlog |
| 194 | Scaffold Vite + React + TypeScript + Tailwind project | Done |
| 195 | React SSE hook (useSSE.ts) | In Review |
| 196 | React CameraCard component with status display | Backlog |
| 197 | UX wireframe — main dashboard camera grid | In Review |
CI/CD Pipeline
ci.yaml (PR gate)
Runs on push/PR to dev and main:
lint-and-typecheck: npm ci → eslint → tsc --noEmittest(needs lint): npm test (vitest)build(needs test): npm run build → upload dist artifact
build-dev.yaml
Triggered by repository_dispatch: dev-build-success:
- Checks out, downloads artifact, builds Go binary
deploy-dev.yaml
Triggered by workflow_dispatch:
- SCP binary + deploy script to dev host
- Deploy with backup/rollback, systemctl restart
- Failure notification
Key Design Decisions
- SQLite chosen over PostgreSQL — Single-node Pi Zero 2 W deployment; no need for a separate DB server. WAL mode for concurrent read/write.
- SSE chosen over WebSocket — Unidirectional updates (hub → browser) are sufficient for status dashboard; SSE is simpler to implement and maintain.
- Chi router — Lightweight, idiomatic Go HTTP router with middleware support.
- Zustand over Redux — Minimal boilerplate for the camera status store.
- API key auth — Simple bearer token; hub is on a local network, not internet-facing.
- MQTT — Standard IoT protocol for ESP32 communication; Mosquitto broker runs locally on the Pi.
- Recording state tracking — Status push handler detects recording state changes and automatically opens/closes recording_events rows.
Known Limitations
- MQTT subscriber implementation is still in Backlog (CUB-192). Currently, only HTTP status push is wired up.
- SSE endpoint implementation is in Backlog (CUB-193) — frontend SSE hook exists but backend SSE hub is placeholder.
- CameraCard component plan exists (
docs/plans/2026-05-21-cub-196-cameracard.md) but implementation is pending. - CUB-182 is essentially a duplicate of CUB-181 (both scaffold Go module).
remoterig.db(SQLite DB file) is currently committed to the repo — should be in.gitignorefor a production config.
Getting Started
# Clone
cd /mnt/ai-storage/projects/remote-rig
git checkout dev
git pull origin dev
# Backend
go run cmd/server/main.go
# → runs on :8080
# Frontend
cp .env.example .env # edit if needed
npm install
npm run dev # → Vite dev server with API proxy
Default Agent Assignments
| Area | Agent | Notes |
|---|---|---|
| Backend (Go API, MQTT, SSE) | Dex | gitea-dex MCP |
| Database (SQLite schema/migrations) | Hex | gitea-hex MCP |
| Frontend (React, Tailwind) | Rex | gitea-rex MCP |
| Hardware (ESP32 firmware, GPIO) | Pip | gitea-pip MCP |
| Design (wireframes, UX) | Sketch |