diff --git a/AGENTS.md b/AGENTS.md index c8a2cd4..0ae35af 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -61,6 +61,30 @@ A web interface for MeshCore mesh radio networks. The backend connects to a Mesh └─────────────┘ ``` +## Feature Priority + +**Primary (must work correctly):** +- Sending and receiving direct messages and channel messages +- Accurate message display: correct ordering, deduplication, pagination/history loading, and real-time updates without data loss or duplicates +- Accurate ACK tracking, repeat/echo counting, and path display +- Historical packet decryption (recovering incoming messages using newly-added keys) +- Outgoing DMs are stored as plaintext by the send endpoint — no decryption needed + +**Secondary:** +- Channel key cracker (WebGPU brute-force) +- Repeater management (telemetry, CLI commands, ACL) + +**Tertiary (best-effort, quality-of-life):** +- Raw packet feed — a debug/observation tool ("radio aquarium"); interesting to watch or copy packets from, but not critical infrastructure +- Map view — visual display of node locations from advertisements +- Network visualizer — force-directed graph of mesh topology +- Bot system — automated message responses +- Read state tracking / mark-all-read — convenience feature for unread badges; no need for transactional atomicity or race-condition hardening + +## Error Handling Philosophy + +**Background tasks** (WebSocket broadcasts, periodic sync, contact auto-loading, etc.) use fire-and-forget `asyncio.create_task`. Exceptions in these tasks are logged to the backend logs, which is sufficient for debugging. There is no need to track task references or add done-callbacks purely for error visibility. If there's a convenient way to bubble an error to the frontend (e.g., via `broadcast_error` for user-actionable problems), do so, but this is minor and best-effort. + ## Key Design Principles 1. **Store-and-serve**: Backend stores all packets even when no client is connected @@ -99,7 +123,7 @@ The following are **deliberate design choices**, not bugs. They are documented i **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". +**Channel messages**: Flood messages echo back through repeaters. Repeats are identified by the database UNIQUE constraint on `(type, conversation_key, text, sender_timestamp)` — when an INSERT hits a duplicate, `_handle_duplicate_message()` in `packet_processor.py` increments the ack count on the original and adds the new path. There is no timestamp-windowed matching; deduplication is exact-match only. ## Directory Structure @@ -217,29 +241,39 @@ All endpoints are prefixed with `/api` (e.g., `/api/health`). | 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/reboot` | Reboot radio or reconnect if disconnected | | PUT | `/api/radio/private-key` | Import private key to radio | +| POST | `/api/radio/advertise` | Send advertisement | +| POST | `/api/radio/reboot` | Reboot radio or reconnect if disconnected | +| POST | `/api/radio/reconnect` | Manual radio reconnection | | GET | `/api/contacts` | List contacts | +| GET | `/api/contacts/{key}` | Get contact by public key or prefix | | POST | `/api/contacts` | Create contact (optionally trigger historical DM decrypt) | +| DELETE | `/api/contacts/{key}` | Delete contact | | POST | `/api/contacts/sync` | Pull from radio | +| POST | `/api/contacts/{key}/add-to-radio` | Push contact to radio | +| POST | `/api/contacts/{key}/remove-from-radio` | Remove contact from radio | +| POST | `/api/contacts/{key}/mark-read` | Mark contact conversation as read | | POST | `/api/contacts/{key}/telemetry` | Request telemetry from repeater | | POST | `/api/contacts/{key}/command` | Send CLI command to repeater | +| POST | `/api/contacts/{key}/trace` | Trace route to contact | | GET | `/api/channels` | List channels | +| GET | `/api/channels/{key}` | Get channel by key | | POST | `/api/channels` | Create channel | +| DELETE | `/api/channels/{key}` | Delete channel | +| POST | `/api/channels/sync` | Pull from radio | +| POST | `/api/channels/{key}/mark-read` | Mark channel as read | | GET | `/api/messages` | List with filters | | POST | `/api/messages/direct` | Send direct message | | POST | `/api/messages/channel` | Send channel message | +| GET | `/api/packets/undecrypted/count` | Count of undecrypted packets | | 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/contacts/{key}/mark-read` | Mark contact conversation as read | -| POST | `/api/channels/{key}/mark-read` | Mark channel as read | +| POST | `/api/packets/maintenance` | Delete old packets and vacuum | | GET | `/api/read-state/unreads` | Server-computed unread counts, mentions, last message times | | POST | `/api/read-state/mark-all-read` | Mark all conversations as read | | GET | `/api/settings` | Get app settings | | PATCH | `/api/settings` | Update app settings | +| POST | `/api/settings/favorites/toggle` | Toggle favorite status | +| POST | `/api/settings/migrate` | One-time migration from frontend localStorage | | WS | `/api/ws` | Real-time updates | ## Key Concepts @@ -321,6 +355,7 @@ mc.subscribe(EventType.ACK, handler) | `MESHCORE_BLE_ADDRESS` | *(none)* | BLE device address (mutually exclusive with serial/TCP) | | `MESHCORE_BLE_PIN` | *(required with BLE)* | BLE PIN code | | `MESHCORE_DATABASE_PATH` | `data/meshcore.db` | SQLite database location | -| `MESHCORE_MAX_RADIO_CONTACTS` | `200` | Max recent contacts to keep on radio for DM ACKs | + +**Note:** `max_radio_contacts` is a runtime setting stored in the database (`app_settings` table), not an environment variable. It is configured via `PATCH /api/settings`. **Transport mutual exclusivity:** Only one of `MESHCORE_SERIAL_PORT`, `MESHCORE_TCP_HOST`, or `MESHCORE_BLE_ADDRESS` may be set. If none are set, serial auto-detection is used. diff --git a/app/AGENTS.md b/app/AGENTS.md index a102a6e..214ac41 100644 --- a/app/AGENTS.md +++ b/app/AGENTS.md @@ -232,7 +232,7 @@ messages ( text TEXT NOT NULL, sender_timestamp INTEGER, received_at INTEGER NOT NULL, - path TEXT, -- Hex-encoded routing path (2 chars per hop), null for outgoing + paths TEXT, -- JSON array of {path, received_at} for multiple delivery paths txt_type INTEGER DEFAULT 0, signature TEXT, outgoing INTEGER DEFAULT 0, @@ -244,10 +244,8 @@ raw_packets ( id INTEGER PRIMARY KEY, timestamp INTEGER NOT NULL, data BLOB NOT NULL, -- Raw packet bytes - decrypted INTEGER DEFAULT 0, message_id INTEGER, -- FK to messages if decrypted - decrypt_attempts INTEGER DEFAULT 0, - last_attempt INTEGER, + payload_hash TEXT, -- SHA256 of payload for deduplication (UNIQUE index) FOREIGN KEY (message_id) REFERENCES messages(id) ) @@ -364,8 +362,16 @@ packet, `packet_processor.py` handles the complete flow: export, unknown contact), `on_contact_message` handles DMs from the MeshCore library's `CONTACT_MSG_RECV` event. DB deduplication prevents double-storage when both paths fire. +**Outgoing DMs**: Outgoing direct messages are only sent via the app's REST API +(`POST /api/messages/direct`), which stores the plaintext directly in the database. +No decryption is needed for outgoing DMs. The real-time packet processor may also see +the outgoing packet via `RX_LOG_DATA`, but the DB UNIQUE constraint deduplicates it +against the already-stored plaintext. Historical decryption intentionally skips outgoing +packets for the same reason — the app already has the plaintext. + **Historical decryption**: When creating a contact with `try_historical=True`, the server -attempts to decrypt all stored `TEXT_MESSAGE` packets for that contact. +attempts to decrypt all stored `TEXT_MESSAGE` packets for that contact. This only recovers +**incoming** messages; outgoing DMs are already stored as plaintext by the send endpoint. **Direction detection**: The decoder uses the 1-byte dest_hash and src_hash to determine if a message is incoming or outgoing. Edge case: when both bytes match (1/256 chance), @@ -443,10 +449,11 @@ When ACK event arrives, the message's ack count is incremented. ### Channel Message Repeats -Flood messages echo back through repeaters. Detection uses: -- Channel key -- Text hash -- Timestamp (±5 second window) +Flood messages echo back through repeaters. Repeats are detected via the database +UNIQUE constraint on `(type, conversation_key, text, sender_timestamp)`. When an INSERT +hits a duplicate, `_handle_duplicate_message()` in `packet_processor.py` increments the +ack count and adds the new delivery path. There is no timestamp-windowed matching; +deduplication is exact-match only. Each repeat increments the ack count. The frontend displays: - `?` = no acks diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index e382f46..6208565 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -47,7 +47,7 @@ frontend/ │ │ ├── MessageList.tsx # Message display, avatars, clickable senders │ │ ├── MessageInput.tsx # Text input with imperative handle │ │ ├── ContactAvatar.tsx # Contact profile image component -│ │ ├── RawPacketList.tsx # Raw packet feed display +│ │ ├── RawPacketList.tsx # Raw packet feed (tertiary debug/observation tool) │ │ ├── MapView.tsx # Leaflet map showing node locations │ │ ├── CrackerPanel.tsx # WebGPU channel key cracker (lazy-loads wordlist) │ │ ├── NewMessageModal.tsx