generated from CubeCraft-Creations/Tracehound
docs: add comprehensive project context file (CONTEXT.md) for agent reference
This commit is contained in:
+322
@@ -0,0 +1,322 @@
|
||||
# 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 | |
|
||||
Reference in New Issue
Block a user