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

13 KiB

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

# 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

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:

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)

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)

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:

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

# 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