diff --git a/docs/architecture.md b/docs/architecture.md index 6e2af36..6c5ab12 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -80,7 +80,8 @@ The `DeviceManager` handles the connection to the MeshCore device via a direct s - **Per-channel region scope** - Before each channel send, the channel's mapped region scope key (16 bytes) is pushed to the firmware via `CMD_SET_FLOOD_SCOPE_KEY` (54). The scope-set + send pair is serialised under a `_send_lock` so concurrent sends on different channels can't swap each other's scope. Channels without a mapping get an all-zero key so a previously-set scope doesn't leak across channels - **Per-send channel-secret refresh** - Channel indices on the device compact down after a deletion, so the boot-time `_load_channel_secrets()` cache can drift. `send_channel_message` calls `_refresh_channel_secret(idx)` first (one extra `get_channel(idx)` round-trip) to fetch the current secret straight from firmware, update the in-memory cache and DB if they had drifted, and use it for the `pkt_payload` echo correlation - **Liveness telemetry** - Tracks `_last_rx_at` (bumped on every `RX_LOG_DATA` event) and `_consecutive_stats_failures` (incremented on `get_stats_*` / `get_bat` exceptions, cleared on success). Surfaced via `/health/strict` for the external watchdog -- **TCP self-heal** - A `_liveness_watcher_loop` task on the DM event loop calls `force_reconnect()` when no RX event has arrived for `HEALTH_STRICT_MAX_RX_STALE_SEC` (5 min). `send_channel_message` also detects empty-string `concurrent.futures.TimeoutError` from `set_flood_scope_key` (the symptom of a degraded long-lived TCP) and runs an in-place reconnect + one retry before failing. A 30 s cooldown and `_reconnect_lock` prevent churn; `_intentional_disconnect` keeps the DISCONNECTED handler from racing the reconnect +- **TCP self-heal** - A `_liveness_watcher_loop` task on the DM event loop calls `force_reconnect()` when no RX event has arrived for `HEALTH_STRICT_MAX_RX_STALE_SEC` (5 min). `send_channel_message` also detects empty-string `concurrent.futures.TimeoutError` from `set_flood_scope_key` (the symptom of a degraded long-lived TCP) and runs an in-place reconnect + one retry before failing. A 30 s cooldown and `_reconnect_lock` prevent churn; `_intentional_disconnect` keeps the DISCONNECTED handler from racing the reconnect. The watcher keeps re-checking staleness even after `_connected` has gone False, so a single failed reconnect (e.g. an empty `self_info`) no longer silently stops all further healing for non-BLE transports +- **Raw packet resend** - Own channel sends capture a full hex wire snapshot (`header + transport_codes + path_len + encrypted payload`) into `channel_messages.raw_packet`, rebuilt from the actual `pkt_payload` once echo correlation resolves it and honouring the device's cached `path_hash_mode`. `resend_channel_message()` re-broadcasts that snapshot verbatim via `CMD_SEND_RAW_PACKET` (0x41) so repeaters dedupe by packet hash (`Mesh::hasSeen`) and only previously-unreached nodes pick it up. Requires companion firmware ≥1.16 (`fw_ver_code` ≥ 13), gated via the cached `supports_raw_resend` flag captured from the connect-time `DEVICE_INFO` event. Self-echoes of a resend (the firmware seen-table can evict the hash within minutes on a busy mesh) are detected by recomputing the expected `pkt_payload` and matching an existing own row, so a resend never reappears as an inbound message from yourself --- @@ -143,7 +144,7 @@ Key tables: - `read_status` - Per-channel read counters and favorites (`is_favorite` column; used to pin channels in the sidebar/dropdown sort order) - `analyzers` - User-configured MeshCore Analyzer services (`name`, `url_template` with `{packetHash}` placeholder, `is_default`, `is_disabled`; partial unique index enforces a single default) -`direct_messages` gained a `delivery_path_hash_size` column (auto-migrated, defaults to 1) so reloaded DM bubbles render multi-byte routes correctly. The `path_len` column on `channel_messages`, `direct_messages`, and `paths` now stores the raw firmware byte (masked hop count plus path_hash_mode in the upper bits), recombined at write time via `pack_path_len()`; the API endpoints decode it back into `path_hash_size` on read. +`direct_messages` gained a `delivery_path_hash_size` column (auto-migrated, defaults to 1) so reloaded DM bubbles render multi-byte routes correctly. The `path_len` column on `channel_messages`, `direct_messages`, and `paths` now stores the raw firmware byte (masked hop count plus path_hash_mode in the upper bits), recombined at write time via `pack_path_len()`; the API endpoints decode it back into `path_hash_size` on read. `channel_messages` also gained a `raw_packet` column (the full hex wire snapshot captured at send time, indexed by `idx_cm_pkt` on `pkt_payload` for fast self-echo lookups) that powers raw resend; it is `NULL` for received and pre-migration rows, so the resend button stays disabled there. The use of SQLite allows for fast queries, reliable data storage, full-text search, and complex filtering (such as contact ignoring/blocking) without the risk of file corruption inherent to flat JSON files. @@ -169,6 +170,7 @@ The channels API reads from the `channels` DB table rather than iterating device | POST | `/api/messages` | Send message (`{text, channel_idx, reply_to?}`) | | GET | `/api/messages/updates` | Check for new messages (smart refresh) | | GET | `/api/messages//meta` | Get message metadata (echoes, paths) | +| POST | `/api/messages//resend` | Re-broadcast an own channel message verbatim via `CMD_SEND_RAW_PACKET` (same packet hash, so unreached repeaters pick it up). 400 for not-own / missing `raw_packet` snapshot / disconnected / firmware < 1.16, 404 for unknown id | | GET | `/api/messages/search` | Full-text search (`?q=`, `?channel_idx=`, `?limit=`) | ### Contacts @@ -249,7 +251,7 @@ The `PUT /api/channels//scope` endpoint accepts any `index` in `[0, devic | POST | `/api/analyzers//default` | Mark as default (enforced single-default via partial unique index) | | DELETE | `/api/analyzers/default` | Clear the default analyzer | -The backend no longer ships a pre-built `analyzer_url` per message — channel-message payloads include `packet_hash` instead, and the frontend substitutes `{packetHash}` in the chosen URL template at click time. +The backend no longer ships a pre-built `analyzer_url` per message — channel-message payloads include `packet_hash` instead, and the frontend substitutes `{packetHash}` in the chosen URL template at click time. The Letsmesh Analyzer is not a hardcoded pseudo-row: `seed_default_analyzers()` inserts it as an ordinary `analyzers` row exactly once per install (guarded by the `analyzer_letsmesh_seeded` flag in `app_settings`), so it can be renamed, disabled, or deleted like any user entry. The row stores `is_disabled` (not `is_enabled`); the UI inverts it on read/write so the switch reads "Enabled", which is why no data migration was needed. ### Direct Messages @@ -266,7 +268,7 @@ The backend no longer ships a pre-built `analyzer_url` per message — channel-m | Method | Endpoint | Description | |--------|----------|-------------| -| GET | `/api/status` | Connection status (device name, transport type, serial port / BLE address) | +| GET | `/api/status` | Connection status (device name, transport type, serial port / BLE address). When connected also surfaces `fw_ver_code`, `supports_raw_resend`, `path_hash_mode`, and `path_hash_size` so the frontend can show/hide the raw-resend button and verify the resend snapshot's hash size | | GET | `/api/device/info` | Device information | | GET | `/api/device/stats` | Device statistics | | GET | `/api/device/settings` | Get device settings | @@ -362,6 +364,8 @@ Real-time log streaming via Socket.IO. **Server → Client:** - `log_line` - New log line +The `MemoryLogHandler` filters werkzeug access-log records for `/socket.io/` and `/api/logs/` paths before buffering/broadcasting. With `async_mode='threading'` Socket.IO falls back to long-polling; without this filter every poll is logged, the broadcast wakes the pending poll, the client re-polls immediately, and an open System Log tab spins at 10+ requests/sec. + --- ## Offline Support diff --git a/docs/user-guide.md b/docs/user-guide.md index 813a46c..31ac154 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -155,10 +155,19 @@ Archives are created automatically at midnight (00:00 UTC) each day. The live vi **Message limit:** 140 bytes (LoRa limitation) +**On phones and tablets:** pressing Enter inserts a new line instead of sending — tap the **Send** button to publish. This prevents a mistapped Enter on the on-screen keyboard from firing off a half-typed message. On desktop, Enter still sends. + ### Replying to Users Click the reply button on any message to insert `@[UserName]` into the text field, then type your reply. +### Message Actions + +Your own messages carry a small row of action buttons: + +- **Edit message** (pencil icon) - Copies the message text back into the composer so you can tweak it and send it again as a new message. +- **Resend** (repeat-arrow icon) - Re-broadcasts the *same* packet. Repeaters that already forwarded the original ignore the duplicate, but nodes that never heard it can still pick it up — so the resend extends the repeater list on the existing message's badge instead of creating a new message. Handy right after sending when the delivery badge shows only partial coverage. This button only appears when your device runs companion firmware **1.16 or newer**; on older firmware it is hidden. + --- ## Message Content Features @@ -209,7 +218,7 @@ Access the Direct Messages feature: - Use the (x) button to clear the search and select a different contact 2. Type your message in the input field (max 140 bytes, same as channels) 3. Use the emoji picker button to insert emojis -4. Press Enter or click Send +4. Press Enter or click Send (on phones and tablets, Enter inserts a new line — tap Send to deliver) 5. Click "Back" button to return to the main chat view ### Persistence @@ -679,16 +688,17 @@ Manage MeshCore region scopes (also called flood scopes). See [Region Scopes](#r ### Analyzer Tab -Configure MeshCore Analyzer services used by the chart icon under each group-chat message. The icon resolves at click time depending on what you configure here: +Configure MeshCore Analyzer services used by the chart icon under each group-chat message. The **Letsmesh Analyzer** is added for you on first startup as an ordinary entry — you can rename, disable, star, or delete it just like any service you add yourself. The chart icon resolves at click time depending on what you have enabled here: -- **No custom analyzers (or all disabled)** → opens the built-in Letsmesh analyzer -- **One default analyzer set** → opens that service directly -- **Multiple enabled analyzers, no default** → opens a chooser modal +- **Nothing enabled** (everything disabled or deleted) → a toast tells you "No analyzer configured" and points you back to this tab +- **A default is set** → opens that service directly +- **Exactly one enabled, no default** → opens it directly +- **Several enabled, no default** → opens a chooser modal so you can pick Each row supports: +- **Enabled switch** — the switch reads as **Enabled** when it is on (checked = active); flip it off to keep a service configured but out of the picker - **Star toggle** — mark this analyzer as the default. Only one default is allowed -- **Enabled switch** — temporarily disable a service without deleting it - **Edit / Delete** buttons When adding or editing, the URL template must contain the placeholder `{packetHash}` — it is substituted with the message's packet hash at click time. diff --git a/docs/whatsnew.md b/docs/whatsnew.md index 18ccb74..fae3c8c 100644 --- a/docs/whatsnew.md +++ b/docs/whatsnew.md @@ -6,10 +6,31 @@ For deep technical notes, see [architecture.md](architecture.md). For the full g --- -## Unreleased (since fd2b3d0) +## Unreleased (since debb711) + +_Nothing yet — the next change to land on `dev` goes here._ + +--- + +## 2026-07-02 ### New features +- **Explicit Send on phones and tablets.** On touch devices, pressing Enter now inserts a new line instead of sending — you tap the **Send** button to publish. This stops a mistapped Enter on the on-screen keyboard from firing off a half-typed message. Desktop keeps Enter-to-send. Applies to both group chat and direct messages. +- **Manage the Letsmesh analyzer like any other entry.** The built-in Letsmesh Analyzer is now a normal row in **Settings → Analyzer** — rename it, disable it, star it as the default, or delete it, just like a service you add yourself. The chart icon under a message resolves at click time: nothing enabled shows a "No analyzer configured" hint, a starred default (or a single enabled service) opens directly, and several enabled without a default show a chooser. The row's switch now reads **Enabled** when it's on. + +### Reliability & polish + +- **Multi-line direct messages keep their line breaks.** A DM you sent across several lines showed correctly to the recipient but collapsed to a single line in your own copy. Your own bubble now preserves the line breaks too. + +--- + +## 2026-06-26 + +### New features + +- **Resend a channel message (same packet).** Your own group-chat messages now carry a repeat-arrow button that re-broadcasts the *exact same* packet. Repeaters that already forwarded it stay quiet, but nodes that never heard it can still pick it up — so a resend fills in coverage on the existing message's delivery badge instead of posting a duplicate. Requires companion firmware 1.16 or newer; the button is hidden on older devices. +- **Clearer "Edit message" button.** The button that copies a message back into the composer for hand-editing used to be mislabeled "Resend." It's now a pencil **Edit message** button on both channel and direct messages, clearly separate from the real Resend above. - **Custom Analyzer services.** A new **Settings → Analyzer** tab lets you register your own MeshCore Analyzer services. Each entry has an enable/disable switch, a "star" toggle to mark it as the default, and an Edit/Delete pair. The chart icon under each group-chat message now resolves at click time: built-in Letsmesh if you haven't configured anything, the default service when one is set, or a chooser modal when several are enabled. URL templates use `{packetHash}` as a placeholder. - **Apply a saved path straight from Contact Info.** Each entry in the **Paths** list inside the DM Contact Info modal gained an upload-arrow button. Click it to push that configured path to the device as the active route — no more switching to the console to run `change_path`. - **Database "Optimize now" button + live size.** The Backup modal now shows the current DB size and exposes an **Optimize now** button that runs SQLite `VACUUM` on demand. Useful after a big retention pass when you want to reclaim space without waiting for the nightly job. @@ -18,6 +39,9 @@ For deep technical notes, see [architecture.md](architecture.md). For the full g ### Reliability & polish +- **System Log tab no longer floods the server.** Opening the System Log could trigger a feedback loop that hammered the server with 10+ requests a second; the log noise that caused it is now filtered out, so the tab stays quiet. +- **The connection badge stops lying about device state.** The status badge could flip back to "Connected" on a routine message refresh even while the device was actually disconnected. Device status is now driven only by real device connectivity, with a 60-second fallback check so a long-open tab stays accurate. +- **Automatic recovery after a failed reconnect.** A reconnect that failed quietly used to leave the app stuck "disconnected" until the container was restarted by hand. The background liveness watcher now keeps retrying instead of giving up after a single failure. - **No more 10–15 s freezes on app load.** The realtime channel used a transport that the dev server couldn't upgrade; we now stay on long-polling, which keeps real-time pushes working without the reconnect loop. - **Channel list stays complete when the device is slow.** Channels are now read from the local cache rather than re-queried slot-by-slot, so a brief device stall no longer leaves you with just the Public channel after a refresh. - **Sending on a re-used channel slot now works after a deletion.** When you delete a channel, the device compacts the remaining slots — until now the app kept using the old keys for that slot. We refresh the secret from the device just before each send.