# RemoteRig — Project Context

> **Last updated:** 2026-05-21 (evening — post-planning sync)
> **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)
> **MQTT Contract:** [docs/MQTT_CONTRACT.md](./docs/MQTT_CONTRACT.md)

---

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

```
                   ┌──────────────────────────────────────────┐
                   │     Travel Router (self-contained LAN)    │
                   │         Subnet: 192.168.4.0/24            │
                   │         DHCP pool: .100-.200              │
                   └──────┬──────────┬──────────┬──────────────┘
                          │          │          │
          ┌───────────────┘          │          └───────────────┐
          ▼                          ▼                          ▼
   ┌──────────────┐         ┌──────────────┐         ┌──────────────────┐
   │   ESP32 #1   │         │   ESP32 #N   │         │  Pi Zero 2 W     │
   │  DHCP addr   │         │  DHCP addr   │         │  192.168.4.10    │
   │              │         │              │         │  (static IP)     │
   │ STA→GoPro AP │         │ STA→GoPro AP │         │                  │
   │ STA→Router   │         │ STA→Router   │         │  Mosquitto :1883 │
   │              │         │              │         │  Go API :8080    │
   │ MQTT→:1883   │         │ MQTT→:1883   │         │  React UI        │
   └──────┬───────┘         └──────┬───────┘         │  SQLite DB       │
          │                        │                  └──────────────────┘
          ▼                        ▼                           │
   ┌──────────────┐         ┌──────────────┐                  │
   │ GoPro Hero 3 │         │ GoPro Hero 3 │     SSE /api/v1/events/stream
   │ AP: 10.5.5.1 │         │ AP: 10.5.5.1 │                  │
   │ Wi-Fi only   │         │ Wi-Fi only   │                  ▼
   └──────────────┘         └──────────────┘         ┌──────────────────┐
                                                     │  User Device     │
                                                     │  (laptop/kiosk)  │
                                                     │  http://.4.10    │
                                                     └──────────────────┘
```

**Network is fully self-contained — no internet dependency.** The travel router creates the LAN. All devices connect to it. The Pi runs all services (Mosquitto, Go API, React UI, SQLite). ESP32s bridge the GoPro's AP to the LAN via MQTT.

### Key Architecture Decisions (revised)
- **Closed travel router network** — No venue Wi-Fi dependency. User brings their own router. All devices on `192.168.4.0/24`.
- **ESP32 dual-STA** — One STA to GoPro AP (10.5.5.1), one STA to travel router. No channel-hopping concerns on closed network.
- **ESP32 → GoPro over Wi-Fi** — Bacpac I²C route rejected (30-pin Herobus connector too complex). HTTP to GoPro AP is proven and reliable.
- **MQTT for ESP32 → Hub** — Lightweight, designed for IoT. Mosquitto on Pi. QoS 1 for status, QoS 2 for commands. Full contract: [docs/MQTT_CONTRACT.md](./docs/MQTT_CONTRACT.md)
- **SQLite over PostgreSQL** — Single-node Pi Zero 2 W deployment. WAL mode for concurrent read/write.
- **SSE over WebSocket** — Unidirectional hub → browser updates. Simpler, sufficient for status dashboard.
- **Chi router** — Lightweight Go HTTP router with middleware support.
- **Zustand over Redux** — Minimal boilerplate for camera status store.
- **API key auth** — Simple bearer token; closed LAN, not internet-facing.
- **Camera auto-discovery** — ESP32 publishes `announce` message on first MQTT connect. Hub auto-registers by MAC, assigns sequential `cam-NNN` ID.

## 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"      # Mosquitto on Pi Zero 2 W
  client_id: "remoterig-hub"
platform:
  type: "pi-zero-2w"
  max_cameras: 16
network:
  subnet: "192.168.4.0/24"     # Travel router subnet
  hub_ip: "192.168.4.10"       # Pi Zero 2 W static IP
```

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

**Last synced:** 2026-05-21 (evening)

| CUB | Title | Status | Agent |
|-----|-------|--------|-------|
| 198 | **Epic: Multi-camera remote monitoring system** | Backlog | — |
| 238 | Define MQTT message format contract | ✅ Done | Dex |
| 228 | Add battery_calibration_offset to cameras table | Backlog | Hex |
| 230 | ESP32 offline status buffering and replay | Backlog | Pip |
| 232 | Implement MQTT subscriber in Go hub | Backlog | Dex |
| 229 | Design camera auto-discovery and registration flow | Backlog | Dex |
| 231 | Mosquitto MQTT broker setup on Pi Zero 2 W | Backlog | Dex |
| 233 | Verify and harden SSE endpoint | Backlog | Dex |
| 234 | Verify and harden all camera API endpoints | Backlog | Dex |
| 235 | Implement GET /api/v1/cameras/:id with 24h history | Backlog | Dex |
| 236 | Implement POST /api/v1/cameras registration | Backlog | Dex |
| 237 | Update CONTEXT.md to actual state | Backlog | Otto |
| — | — | — | — |
| 173 | Confirm GoPro Hero 3 Wi‑Fi control API | ✅ Done | Otto |
| 174 | ESP32 firmware baseline | ✅ Done | Pip |
| 175 | Central hub backend (Go service) | ✅ Done | Dex |
| 177 | Database schema (cameras, events, status_logs) | ✅ Done | Hex |
| 180 | Risk mitigation checklist | ✅ Done | — |
| 181 | Scaffold Go module, directory layout, config | ✅ Done | — |
| 187 | POST start recording + MQTT publish | ✅ Done | Dex |
| 194 | Scaffold Vite + React + TypeScript + Tailwind | ✅ Done | — |
| 195 | React SSE hook (useSSE.ts) + Zustand store | ✅ Done | Rex |
| 196 | CameraCard component + 16 unit tests | ✅ Done | Rex |
| 179 | Logging & persistence strategy | ✅ Done | — |
| 208 | Add README with project overview | ✅ Done | Hermes |
| 182 | ~~Duplicate of CUB-181~~ | ❌ Canceled | — |
| — | — | — | — |
| 183 | SQLite schema migration + DB init | Backlog | Hex |
| 184 | API key auth middleware | Backlog | Dex |
| 185 | Camera/StatusLog/RecordingEvent Go models | Backlog | Hex |
| 186 | GET /api/v1/cameras (list with live status) | Backlog | Dex |
| 188 | POST stop recording | Backlog | Dex |
| 189 | POST register new camera | Backlog | Dex |
| 190 | GET camera detail + history | Backlog | Dex |
| 191 | POST push-status (HTTP ingestion) | Backlog | Dex |
| 192 | MQTT subscriber | Backlog | Dex |
| 193 | SSE /api/v1/events/stream endpoint | Backlog | Dex |
| 178 | UX/UI design mockups | Backlog | Sketch |
| 197 | Dashboard camera grid wireframe | In Review | Sketch |
| 176 | Frontend umbrella (React + Tailwind) | Backlog | Rex |

## 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 not yet implemented** (CUB-232) — ESP32→hub communication backbone is designed (see [MQTT_CONTRACT.md](./docs/MQTT_CONTRACT.md)) but not built. Currently only HTTP status push is wired up.
- **SSE endpoint needs verification** (CUB-233) — Frontend SSE hook exists (merged PR #2), backend SSE hub code exists (140 lines) but needs heartbeat, reconnection, and integration testing.
- **No camera auto-discovery** (CUB-229) — Cameras must be manually registered. ESP32 announce protocol designed in MQTT contract but not implemented.
- **No battery calibration** (CUB-228) — GoPro Hero 3 reports raw byte; per-camera calibration offset column not yet added to schema.
- **No offline buffering on ESP32** (CUB-230) — If travel router Wi-Fi drops, status data is lost. SPIFFS buffer designed but not implemented.
- **CameraCard wireframe pending** (CUB-197) — Dashboard UI has live code but needs UX review/wireframe sign-off.
- **remoterig.db committed to repo** — Should be in `.gitignore` for production. Low priority (convenient for dev).
- **Time sync TBD** — ESP32s need accurate timestamps without internet. Options: Pi as NTP server, GPS module, or HTTP time endpoint. See MQTT contract open questions.

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