Files
Remote-Terminal-for-MeshCore/CLAUDE.md
Jack Kingsman 888dd0f1a8 Update CLAUDEs
2026-01-13 14:48:32 -08:00

302 lines
13 KiB
Markdown

# RemoteTerm for MeshCore
A web interface for MeshCore mesh radio networks. The backend connects to a MeshCore-compatible radio over serial and exposes REST/WebSocket APIs. The React frontend provides real-time messaging and radio configuration.
**For detailed component documentation, see:**
- `app/CLAUDE.md` - Backend (FastAPI, database, radio connection, packet decryption)
- `frontend/CLAUDE.md` - Frontend (React, state management, WebSocket, components)
## Architecture Overview
```
┌─────────────────────────────────────────────────────────────────┐
│ Frontend (React) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ StatusBar│ │ Sidebar │ │MessageList│ │ MessageInput │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────────────┘ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ CrackerPanel (global collapsible, WebGPU cracking) │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │ │
│ useWebSocket ←──── Real-time updates │
│ │ │
│ api.ts ←──── REST API calls │
└───────────────────────────┼──────────────────────────────────────┘
│ HTTP + WebSocket (/api/*)
┌───────────────────────────┼──────────────────────────────────────┐
│ Backend (FastAPI) │
│ ┌──────────┐ ┌──────────────┐ ┌────────────┐ ┌───────────┐ │
│ │ Routers │→ │ Repositories │→ │ SQLite DB │ │ WebSocket │ │
│ └──────────┘ └──────────────┘ └────────────┘ │ Manager │ │
│ ↓ └───────────┘ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ RadioManager + Event Handlers │ │
│ └──────────────────────────────────────────────────────────┘ │
└───────────────────────────┼──────────────────────────────────────┘
│ Serial
┌──────┴──────┐
│ MeshCore │
│ Radio │
└─────────────┘
```
## Key Design Principles
1. **Store-and-serve**: Backend stores all packets even when no client is connected
2. **Parallel storage**: Messages stored both decrypted (when possible) and as raw packets
3. **Extended capacity**: Server stores contacts/channels beyond radio limits (~350 contacts, ~40 channels)
4. **Real-time updates**: WebSocket pushes events; REST for actions
5. **Offline-capable**: Radio operates independently; server syncs when connected
6. **Auto-reconnect**: Background monitor detects disconnection and attempts reconnection
## Data Flow
### Incoming Messages
1. Radio receives message → MeshCore library emits event
2. `event_handlers.py` catches event → stores in database
3. `ws_manager` broadcasts to connected clients
4. Frontend `useWebSocket` receives → updates React state
### Outgoing Messages
1. User types message → clicks send
2. `api.sendChannelMessage()` → POST to backend
3. Backend calls `radio_manager.meshcore.commands.send_chan_msg()`
4. Message stored in database with `outgoing=true`
5. For direct messages: ACK tracked; for channel: repeat detection
### ACK and Repeat Detection
**Direct messages**: Expected ACK code is tracked. When ACK event arrives, message marked as acked.
**Channel messages**: Flood messages echo back. The decoder identifies repeats by matching (channel_idx, text_hash, timestamp ±5s) and marks the original as "acked".
## Directory Structure
```
.
├── app/ # FastAPI backend
│ ├── CLAUDE.md # Backend documentation
│ ├── main.py # App entry, lifespan
│ ├── routers/ # API endpoints
│ ├── repository.py # Database CRUD
│ ├── event_handlers.py # Radio events
│ ├── decoder.py # Packet decryption
│ └── websocket.py # Real-time broadcasts
├── frontend/ # React frontend
│ ├── CLAUDE.md # Frontend documentation
│ ├── src/
│ │ ├── App.tsx # Main component
│ │ ├── api.ts # REST client
│ │ ├── useWebSocket.ts # WebSocket hook
│ │ └── components/
│ │ ├── CrackerPanel.tsx # WebGPU key cracking
│ │ ├── MapView.tsx # Leaflet map showing node locations
│ │ └── ...
│ └── vite.config.ts
├── references/meshcore_py/ # MeshCore Python library
├── tests/ # Backend tests (pytest)
├── data/ # SQLite database (runtime)
├── integration_test.html # Browser-based API tests
└── pyproject.toml # Python dependencies
```
## Development Setup
### Backend
```bash
# Install dependencies
uv sync
# Run server (auto-detects radio)
uv run uvicorn app.main:app --reload
# Or specify port
MESHCORE_SERIAL_PORT=/dev/cu.usbserial-0001 uv run uvicorn app.main:app --reload
```
### Frontend
```bash
cd frontend
npm install
npm run dev # http://localhost:5173, proxies /api to :8000
```
### Both Together (Development)
Terminal 1: `uv run uvicorn app.main:app --reload`
Terminal 2: `cd frontend && npm run dev`
### Production
In production, the FastAPI backend serves the compiled frontend:
```bash
cd frontend && npm run build && cd ..
uv run uvicorn app.main:app --host 0.0.0.0 --port 8000
```
Access at `http://localhost:8000`. All API routes are prefixed with `/api`.
## Testing
### Backend (pytest)
```bash
PYTHONPATH=. uv run pytest tests/ -v
```
Key test files:
- `tests/test_decoder.py` - Channel + direct message decryption, key exchange
- `tests/test_keystore.py` - Ephemeral key store
- `tests/test_event_handlers.py` - ACK tracking, repeat detection
- `tests/test_api.py` - API endpoints, read state tracking
- `tests/test_migrations.py` - Database migration system
### Frontend (Vitest)
```bash
cd frontend
npm run test:run
```
### Integration Tests
Open `integration_test.html` in a browser with the backend running.
### Before Completing Changes
**Always run both backend and frontend validation before finishing any changes:**
```bash
# From project root - run backend tests
PYTHONPATH=. uv run pytest tests/ -v
# From project root - run frontend tests and build
cd frontend && npm run test:run && npm run build
```
This catches:
- Type mismatches between frontend and backend (e.g., missing fields in TypeScript interfaces)
- Breaking changes to shared types or API contracts
- Runtime errors that only surface during compilation
## API Summary
All endpoints are prefixed with `/api` (e.g., `/api/health`).
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/health` | Connection status |
| GET | `/api/radio/config` | Radio configuration |
| PATCH | `/api/radio/config` | Update name, location, radio params |
| POST | `/api/radio/advertise` | Send advertisement |
| POST | `/api/radio/reconnect` | Manual radio reconnection |
| POST | `/api/radio/enable-server-decryption` | Export private key, enable decryption |
| GET | `/api/radio/decryption-status` | Check if decryption enabled |
| GET | `/api/contacts` | List contacts |
| POST | `/api/contacts/sync` | Pull from radio |
| POST | `/api/contacts/{key}/telemetry` | Request telemetry from repeater |
| POST | `/api/contacts/{key}/command` | Send CLI command to repeater |
| GET | `/api/channels` | List channels |
| POST | `/api/channels` | Create channel |
| GET | `/api/messages` | List with filters |
| POST | `/api/messages/direct` | Send direct message |
| POST | `/api/messages/channel` | Send channel message |
| POST | `/api/packets/decrypt/historical` | Decrypt stored packets |
| GET | `/api/packets/decrypt/progress` | Get historical decryption progress |
| POST | `/api/packets/maintenance` | Delete old packets (cleanup) |
| POST | `/api/packets/dedup` | Remove duplicate raw packets |
| GET | `/api/packets/dedup/progress` | Get deduplication progress |
| POST | `/api/contacts/{key}/mark-read` | Mark contact conversation as read |
| POST | `/api/channels/{key}/mark-read` | Mark channel as read |
| POST | `/api/read-state/mark-all-read` | Mark all conversations as read |
| GET | `/api/settings` | Get app settings |
| PATCH | `/api/settings` | Update app settings |
| WS | `/api/ws` | Real-time updates |
## Key Concepts
### Contact Public Keys
- Full key: 64-character hex string
- Prefix: 12-character hex (used for matching)
- Lookups use `LIKE 'prefix%'` for matching
### Contact Types
- `0` - Unknown
- `1` - Client (regular node)
- `2` - Repeater
- `3` - Room
### Channel Keys
- Stored as 32-character hex string (TEXT PRIMARY KEY)
- Hashtag channels: `SHA256("#name")[:16]` converted to hex
- Custom channels: User-provided or generated
### Message Types
- `PRIV` - Direct messages
- `CHAN` - Channel messages
- Both use `conversation_key` (user pubkey for PRIV, channel key for CHAN)
### Read State Tracking
Read state (`last_read_at`) is tracked **server-side** for consistency across devices:
- Stored as Unix timestamp in `contacts.last_read_at` and `channels.last_read_at`
- Updated via `POST /api/contacts/{key}/mark-read` and `POST /api/channels/{key}/mark-read`
- Bulk update via `POST /api/read-state/mark-all-read`
- Frontend compares `last_read_at` with message `received_at` to count unreads
**State Tracking Keys (Frontend)**: Generated by `getStateKey()` for message times (sidebar sorting):
- Channels: `channel-{channel_key}`
- Contacts: `contact-{12-char-pubkey-prefix}`
**Note:** These are NOT the same as `Message.conversation_key` (the database field).
### Server-Side Decryption
The server can decrypt historical packets if given the necessary keys:
**Channel messages**: Decrypted automatically using stored channel keys.
**Direct messages**: Requires the node's private key, which must be:
1. Exported from radio via `POST /radio/enable-server-decryption`
2. Stored **only in memory** (never persisted to disk)
3. Re-exported after every server restart
This allows decrypting messages from contacts whose public keys were learned after the message was received.
## MeshCore Library
The `meshcore_py` library provides radio communication. Key patterns:
```python
# Connection
mc = await MeshCore.create_serial(port="/dev/ttyUSB0")
# Commands
await mc.commands.send_msg(dst, msg)
await mc.commands.send_chan_msg(channel_idx, msg)
await mc.commands.get_contacts()
await mc.commands.set_channel(idx, name, key)
# Events
mc.subscribe(EventType.CONTACT_MSG_RECV, handler)
mc.subscribe(EventType.CHANNEL_MSG_RECV, handler)
mc.subscribe(EventType.ACK, handler)
```
## Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `MESHCORE_SERIAL_PORT` | auto-detect | Serial port for radio |
| `MESHCORE_DATABASE_PATH` | `data/meshcore.db` | SQLite database location |
| `MESHCORE_MAX_RADIO_CONTACTS` | `200` | Max recent contacts to keep on radio for DM ACKs |