# RemoteRig — Project Context > **Last updated:** 2026-05-21 > **Repo:** `CubeCraft-Creations/remote-rig` | **Host:** `code.cubecraftcreations.com` > **Local clone:** `/mnt/ai-storage/projects/remote-rig` | **Default branch:** `dev` > **Discord:** `DISCORD_DEV_REMOTERIG_CHANNEL_ID` > **Linear Epic:** [CUB-198](https://linear.app/cubecraft-creations/issue/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`) ```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`: 1. `lint-and-typecheck`: npm ci → eslint → tsc --noEmit 2. `test` (needs lint): npm test (vitest) 3. `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 1. **SQLite chosen over PostgreSQL** — Single-node Pi Zero 2 W deployment; no need for a separate DB server. WAL mode for concurrent read/write. 2. **SSE chosen over WebSocket** — Unidirectional updates (hub → browser) are sufficient for status dashboard; SSE is simpler to implement and maintain. 3. **Chi router** — Lightweight, idiomatic Go HTTP router with middleware support. 4. **Zustand over Redux** — Minimal boilerplate for the camera status store. 5. **API key auth** — Simple bearer token; hub is on a local network, not internet-facing. 6. **MQTT** — Standard IoT protocol for ESP32 communication; Mosquitto broker runs locally on the Pi. 7. **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 `.gitignore` for a production config. ## Getting Started ```bash # 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 | |