Skip to content

MeshCore MQTT broker presets, LoRa reconnect refactor, and Reticulum games/session fixes - #821

Merged
rinchen merged 9 commits into
mainfrom
deps
Aug 8, 2026
Merged

rinchen merged 9 commits into
mainfrom
deps

Conversation

@rinchen

@rinchen rinchen commented Aug 8, 2026

Copy link
Copy Markdown
Member

Overview

The deps branch bundles a dependency refresh with feature and hardening work across MeshCore MQTT, LoRa (Meshtastic + MeshCore) reconnect, and Reticulum games. 69 files changed (+4,128 / −2,154). Commits, oldest → newest:

Commit Summary
2c8ffba chore: bump deps
0dfbc15 feat(reticulum): games win confetti and LXMF FIELD_REACTION interop
e8efcea fix(meshcore): preserve on_radio during path-updated contact rebuild
1d67711 refactor: shared LoRa reconnect attempt + drop panel RF auto-connect
981ed75 chore: track rsReticulum bond-desync upstream PR #21
600561e feat(mqtt): dropdown preset picker with new MeshCore brokers
a154e35 fix(mqtt): harden device-signing MeshCore broker guards and dedupe presets
9480e42 fix: address PR review findings (device-signing MQTT, reactions, reconnect)
dae9d72 fix(reticulum): handle expired LRGP game sessions gracefully

1. MeshCore MQTT broker presets (600561e + a154e35)

New brokers + shared picker

  • Replaced the row of preset buttons with a shared, presentational MqttNetworkPresetSelect dropdown (src/renderer/components/MqttNetworkPresetSelect.tsx, used by both Meshtastic and MeshCore panels).
  • Added four device-signing brokers alongside LetsMesh / MeshMapper / Colorado: Waev, Meshat.se, MeshCore.CA (with a Primary/Backup broker toggle), and EastMesh. Host constants + JWT aud live in letsMeshJwt.ts; preset field blocks in meshcoreMqttPresets.ts.

Guard hardening (a154e35)

  • wsPath + TLS enforcement. validateLetsMeshPresetConnect / letsMeshPresetConfigurationDeviation (letsMeshConnectionGuards.ts) now require the broker's expected WebSocket path (/ws for LetsMesh/MeshMapper/Colorado, /mqtt for Waev/Meshat.se/MeshCore.CA/EastMesh) and tlsEnabled. Expected path comes from a single source of truth, DEVICE_SIGNING_HOST_WS_PATHS in letsMeshJwt.ts, from which the host allowlist (isLetsMeshSettings) is now derived — the two can no longer drift. New helper: deviceSigningWsPathForHost().
  • Toggle repair. The MeshCore.CA Primary/Backup and LetsMesh US/EU toggles set tlsEnabled/wsPath (and keepalive) so switching a stale saved config repairs the structural fields while preserving the user's topic prefix.
  • Custom vs auto-launch parity. Manual Connect and startup auto-launch now share one JWT gate, usesMeshcoreDeviceSigningMqtt(preset, settings) (named preset or device-signing host). Previously manual Connect gated on the preset while auto-launch gated on the host, so Custom-pointed-at-a-known-broker would JWT on auto-launch but not manual Connect. The identity banner also shows in the host-based case.
  • Keepalive false positive. The LetsMesh Region (US/EU) buttons set a 60s keepalive; the deviation guard now accepts 30 or 60 so following "Use Region" no longer raises an amber banner.

De-duplication (a154e35)

  • Exported a single DEVICE_SIGNING_MESHCORE_PRESETS set (replacing three copies: the presets module, IATA scoping in meshcoreMqttTopicPrefix.ts, and the reconcile list in connectionPanelStorageMigrations.ts).
  • Added deviceSigningWssFields(server, wsPath) to collapse the identical WSS field blocks for Waev/Meshat.se/EastMesh/etc.

i18n

  • Generalized the LetsMesh-branded connect errors to device-signing / public-broker wording; added letsMeshRequiresTls and letsMeshRequiresWsPath; keyed the previously hardcoded US/EU region labels and added aria-pressed to the region/broker toggles.
  • Filled all 15 non-English locales for the new keys and hand-fixed machine-translation artifacts (it NBSP, ja v1_ literal, uk apostrophe). pnpm run check:i18n:branch passes.

Docs

  • README.md (picker + four brokers), docs/letsmesh-mqtt-auth.md (aud = connect host, /ws vs /mqtt, 30/60 keepalive), docs/meshcore-meshtastic-parity.md (device-signing Authentication subsection + MeshMapper .cc.net note), docs/troubleshooting.md (stale wsPath block + generalized token sections), docs/key-backup-and-crypto.md (device-signing wording).

2. LoRa reconnect refactor (1d67711)

  • New shared module src/renderer/lib/loraRfReconnectAttempt.ts (+ tests) centralizes the single BLE reconnect attempt lifecycle (open + handshake budget, deferred-disconnect flush) used by both useMeshtasticRuntime.ts and useMeshcoreRuntime.ts, which were each ~500 lines and largely duplicated. Both runtimes shrank substantially.
  • Dropped the panel-owned RF auto-connect path in favor of the coordinator/gate (protocolRfAutoConnectGate.ts, useProtocolRfAutoConnect); removed the corresponding wiring from App.tsx and ConnectionPanel.tsx (large deletions in ConnectionPanel.tsx/.test.tsx are this removal, not the MQTT work).
  • Parity preserved via loraRfReconnectParity.contract.test.ts, useMeshcoreRuntime.reconnect.test.ts, and useMeshtasticRuntime.reconnect-hardening.test.ts (manual-disconnect-must-not-auto-reconnect contracts intact).

3. Reticulum games (0dfbc15)

  • Win-state confetti: new src/renderer/lib/confettiBurst.ts (+ tests) and GamesPanel.tsx wiring.
  • LXMF FIELD_REACTION interop: reticulumGamesMetadata.ts (+ tests) and sidecar changes in reticulum-sidecar/src/stack/live.rs; parity doc docs/reticulum-games-parity.md updated.

4. MeshCore on_radio fix (e8efcea)

  • meshcorePathUpdatedRuntime.ts preserves the on_radio presence flag when rebuilding a contact from a path-updated (129) event, so a path refresh no longer drops radio presence. Covered by meshcorePathUpdatedRuntime.test.ts.

5. Dependencies & tracking (2c8ffba, 981ed75)

  • package.json / pnpm-lock.yaml dependency refresh.
  • scripts/update.sh watch entry for rsReticulum bond-desync upstream PR Update ConnectionPanel.tsx text #21; minor Reticulum patch/README/doc touch-ups.

6. PR review follow-ups (9480e42)

Addressed inline review findings from the earlier commits, keeping changes minimal and adding coverage:

  • MeshCore MQTT: startup preset reconcile rewrites only transport fields, preserving a manually stored password/username; deviceSigningWssFields resets tlsInsecure: false so leaving Ripple restores cert verification; shouldAutoLaunchMeshcoreMqttAtStartup + the deviation banner both use the shared usesMeshcoreDeviceSigningMqtt predicate; cancelling the Ripple/Colorado preset confirm remounts the controlled select so it snaps back; removed the unused autoConnectCancelRef.
  • Reticulum sidecar (live.rs): send_reaction emits a structured reaction_target only when the target hash parses (content-only fallback omits it); reaction_fields_from_message rejects a FIELD_REACTION with trailing bytes.
  • Games / reconnect: burstConfetti reports whether a burst started; GamesPanel records a win as celebrated only on a real start and retries a win queued during an active burst; useMeshcoreRuntime setup-abort branch cleans up any late-opened transport before deferring.
  • Docs/tests: README/parity Custom clarification; axe on MqttNetworkPresetSelect; execution-order assertion in useProtocolRfAutoConnect; omitted-wsPath guard; shared source helper.

7. Reticulum LRGP game session expiry (dae9d72)

An idle LRGP session passes its TTL (tic-tac-toe 24h; chess and four-in-a-row 7d active, 24h pending — all values upstream in ratspeak/lrgp-rs) and the protocol rejects any outbound action, so resign/move/draw failed with a raw dispatch_error: session expired toast while the stored row stayed active — leaving the game stuck with controls that always fail.

  • Sidecar (games_session.rs): on LrgpError::SessionExpired, persist the session's stored status to expired (so list_sessions and future refreshes stop showing a dead game as active) and return the stable session_expired code instead of the raw dispatch_error string. New Rust regression test ages a session past its TTL and asserts both the code and the persisted status.
  • Renderer (reticulumGamesSession.ts): recognize session_expired (and the legacy dispatch_error wrapper), flip the local session to expired so the resign/draw controls disappear and only Delete remains, and show a friendly, duration-agnostic toast. Tests cover the flip, the toast, and the reason matcher.
  • In-game notice (GamesPanel.tsx): a per-category expiry notice under New challenge — unanswered challenges 24h (all games); active tic-tac-toe 24h; chess and four-in-a-row 7 days. New i18n keys translated to all 15 locales.

Risk / compatibility

  • Guard changes are stricter: a stale saved config with the wrong wsPath/TLS now blocks Connect with a clear message (previously an opaque broker failure). Re-selecting the preset or using a broker/region toggle repairs it; connectionPanelStorageMigrations.ts also reconciles on startup.
  • Deviation is host-aware (expected wsPath follows the actual broker host) rather than threaded through the selected preset — same guard accuracy with fewer call-site changes.
  • The games-expiry sidecar change requires a sidecar rebuild to take effect; the renderer notice/toast are live on the next renderer build.
  • No wire-format or storage-schema changes; preset storage keys and MQTT settings shape are unchanged.

Test plan

  • pnpm run typecheck (base, tsconfig.main.json, tsconfig.e2e.json)
  • ESLint on touched source files (clean)
  • Vitest — MQTT guards/JWT/presets/topic-prefix/storage migrations/auto-launch/ConnectionPanel; LoRa reconnect parity; games confetti/reaction/session-expiry; reticulumGamesSession expired flip + matcher
  • Rust — cargo test --features rns-stack games_session (incl. new expiry regression) + reaction unit tests
  • pnpm run check:i18n (all 15 locales verified for the new games keys)
  • Full pnpm run check:pr in CI (full lint + typecheck + test:run + sidecar)
  • Manual — device-signing presets: Connect mints JWT with aud = connect host and correct wsPath; MeshCore.CA Primary/Backup + LetsMesh US/EU toggles; Colorado region gate; Ripple confirm; IATA topic normalize
  • Manual — LoRa BLE reconnect on Meshtastic + MeshCore (edge-of-range drop retries; manual disconnect does not auto-reconnect)
  • Manual — Reticulum game win confetti + reaction interop; resign an idle-expired game → friendly toast, game flips to expired, Delete removes it

rinchen added 7 commits August 8, 2026 05:51
Adds a lightweight, dependency-free canvas confetti burst (confettiBurst.ts)
fired once per session_id on a completed local Games win, gated by the pure
isGamesWinForSelf helper and suppressed under app/OS reduced motion.

Stamps and decodes the standard LXMF FIELD_REACTION (0x40) so Reticulum
tapbacks interoperate with Ratspeak/Sideband: send_reaction adds the msgpack
REACTION_TO/REACTION_CONTENT map before sign while keeping the emoji as
content, and inbound decode is fail-open (accepts 32-byte binary or 64-hex
target, wins over a reply, and leaves reply/plain paths untouched when the
field is absent or malformed).

Documents the v1.0.25 Games review (CSS-only, no protocol delta), marks Win
celebration done, and tracks Four in a Row as a follow-up.
The debounced path-updated (129) rebuild called buildNodesFromContacts
without contactsFromRadio, re-saving live radio contacts with on_radio=0
after a successful sync (only self stayed on-radio). Pass
contactsFromRadio: true so on-radio state and diagnostics survive the
rebuild, and cover it with unit tests.
Extract the Meshtastic/MeshCore reconnect attempt skeleton behind
runLoraRfReconnectAttempt and delete the dead ConnectionPanel mount
auto-connect path now that ProtocolAutoConnectCoordinator owns cold-start.
Point RATSPEAK_PATCH_ENTRIES and the patches README Upstream PR field at
ratspeak/rsReticulum#21 (was empty / "no upstream PR yet") so
`pnpm run update` reports the overlay as tracked.
Replace the MeshCore and Meshtastic MQTT network-preset pill rows with a
shared MqttNetworkPresetSelect dropdown so both protocols stay uniform.
Add Waev, Meshat.se, MeshCore.CA (Primary/Backup), and EastMesh as MeshCore
device-signing JWT presets alongside the existing ones, and rename the
Meshtastic "MQTT :1883" label to "Official".
…esets

Enforce broker wsPath (/ws vs /mqtt) and TLS in the device-signing connect
and deviation guards, driven by a single host->wsPath map in letsMeshJwt.
Repair wsPath/tlsEnabled via the MeshCore.CA and LetsMesh region toggles.

Align the manual Connect and startup auto-launch JWT gate on
usesMeshcoreDeviceSigningMqtt (preset or device-signing host) so Custom
settings pointed at a known broker mint a JWT consistently. Accept a 30s or
60s LetsMesh keepalive without a false deviation banner.

Export one DEVICE_SIGNING_MESHCORE_PRESETS set for IATA scoping + reconcile,
add a shared deviceSigningWssFields() helper, generalize the LetsMesh-branded
error copy, key the US/EU region labels, and fill all locales.

Add guard/aud/wsPath, Colorado region-ack, Colorado Stay, and IATA UI tests;
refresh README, auth, parity, troubleshooting, and key-backup docs.
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds four MeshCore MQTT presets and generalized device-signing authentication, centralizes RF reconnect handling, adds structured LXMF reactions, implements local-win confetti, preserves radio-origin contact metadata, and updates related tests and documentation.

Changes

MeshCore MQTT presets and authentication

Layer / File(s) Summary
Preset and authentication contracts
src/renderer/lib/meshcoreMqttPresets.ts, src/renderer/lib/letsMeshJwt.ts, docs/..., README.md
Adds Waev, Meshat.se, MeshCore.CA, and EastMesh presets. Adds host-specific JWT audiences, WebSocket paths, keepalive rules, and documentation.
Preset picker and connection integration
src/renderer/components/MqttNetworkPresetSelect.tsx, src/renderer/components/ConnectionPanel.tsx, src/renderer/components/ConnectionPanel.test.tsx
Replaces preset button groups with a controlled select and applies the expanded broker, identity, TLS, path, and region settings.
Validation and auto-launch wiring
src/renderer/lib/letsMeshConnectionGuards.ts, src/renderer/lib/mqttAutoLaunch.ts, src/renderer/lib/connectionPanelStorageMigrations.ts
Validates broker-specific TLS and paths and applies device-signing behavior to presets and stored settings.

RF reconnect and auto-connect ownership

Layer / File(s) Summary
Shared reconnect-attempt runner
src/renderer/lib/loraRfReconnectAttempt.ts, src/renderer/lib/loraRfReconnectAttempt.test.ts
Adds shared reconnect budgets, backoff, suspension, overlap handling, deadlines, cleanup, retries, and deferred scheduling.
Runtime reconnect integration
src/renderer/runtime/useMeshcoreRuntime.ts, src/renderer/runtime/useMeshtasticRuntime.ts, src/renderer/runtime/*reconnect*.test.ts
Moves MeshCore and Meshtastic reconnect state machines to the shared runner while retaining transport-specific callbacks and outcomes.
Coordinator-owned cold-start behavior
src/renderer/components/ConnectionPanel.tsx, src/renderer/hooks/useProtocolRfAutoConnect.test.tsx, src/renderer/App.tsx
Removes panel-owned mount auto-connect and updates coordinator, cancellation, fallback, and platform tests.

Structured LXMF reactions

Layer / File(s) Summary
Reaction encoding and decoding
reticulum-sidecar/src/stack/live.rs, docs/reticulum.md, docs/reticulum-sidecar-ipc.md
Encodes reactions with FIELD_REACTION, decodes binary or hexadecimal targets, preserves emoji content, and prioritizes valid reactions over replies.

Local-win game celebration

Layer / File(s) Summary
Win detection and confetti rendering
src/renderer/lib/reticulum/reticulumGamesMetadata.ts, src/renderer/lib/confettiBurst.ts, src/renderer/components/GamesPanel.tsx, src/renderer/components/*test.tsx
Detects completed local wins and triggers session-deduplicated, board-positioned confetti with reduced-motion and cleanup handling.

MeshCore contact rebuild metadata

Layer / File(s) Summary
Radio-origin rebuild propagation
src/renderer/lib/meshcore/meshcorePathUpdatedRuntime.ts, src/renderer/lib/meshcore/meshcorePathUpdatedRuntime.test.ts
Passes contactsFromRadio: true when rebuilding nodes from live contacts and verifies path-history behavior.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 72.31% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the three main change areas: MeshCore MQTT presets, LoRa reconnect refactoring, and Reticulum games and session updates.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch deps

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/renderer/lib/mqttAutoLaunch.ts (1)

62-76: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Inconsistent predicates for "uses MeshCore device-signing MQTT". This PR introduced usesMeshcoreDeviceSigningMqtt(preset, settings) as the shared predicate, but only some call sites adopted it. The remaining sites still use isDeviceSigningMeshcorePreset alone or isLetsMeshSettings(server) alone, so the new Waev, Meshat.se, MeshCore.CA, and EastMesh presets are treated differently depending on which code path runs.

  • src/renderer/lib/mqttAutoLaunch.ts#L62-L76: replace isLetsMeshSettings(settings.server) in shouldAutoLaunchMeshcoreMqttAtStartup with usesMeshcoreDeviceSigningMqtt(readStoredMeshcoreMqttPreset(), settings) so the startup gate matches the tryAutoLaunchMqtt gate at line 76.
  • src/renderer/components/ConnectionPanel.tsx#L2440-L2447: use usesMeshcoreDeviceSigningMqtt(meshcorePreset, meshcoreMqttSettings) for the deviation banner so a Custom configuration pointed at a device-signing host shows the same banner it is validated against at line 2704, and confirm that isLetsMeshSettings recognizes every host in DEVICE_SIGNING_HOST_WS_PATHS.

Pair the change with tests for a Waev preset and for a Custom configuration pointed at mqtt.waev.app.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/lib/mqttAutoLaunch.ts` around lines 62 - 76, Align both MQTT
call sites with the shared usesMeshcoreDeviceSigningMqtt predicate: in
src/renderer/lib/mqttAutoLaunch.ts lines 62-76, update
shouldAutoLaunchMeshcoreMqttAtStartup to use the stored preset and settings
instead of isLetsMeshSettings alone; in
src/renderer/components/ConnectionPanel.tsx lines 2440-2447, use
usesMeshcoreDeviceSigningMqtt(meshcorePreset, meshcoreMqttSettings) for the
deviation banner. Ensure isLetsMeshSettings covers every host in
DEVICE_SIGNING_HOST_WS_PATHS, and add tests for a Waev preset and Custom
settings targeting mqtt.waev.app.
🟡 Other comments (6)
src/renderer/components/GamesPanel.tsx-117-126 (1)

117-126: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Record the session only after its confetti burst starts.

If a user selects a second completed local win during an active burst, burstConfetti returns without rendering. Line 124 still records that second session_id. The effect then has no dependency change that retries the celebration.

Make burstConfetti report whether it started. Queue or retry the second celebration after the active burst ends. Add a test for two local-win sessions selected during one burst.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/components/GamesPanel.tsx` around lines 117 - 126, Update the
celebration flow in the GamesPanel useEffect and burstConfetti so burstConfetti
reports whether a burst actually started, recording selectedSession.session_id
in celebratedWinsRef only after a successful start. When a second completed
local win is selected during an active burst, queue or retry it after the
current burst ends, and add coverage for two local-win sessions selected during
one burst.

Sources: Coding guidelines, Path instructions

README.md-509-509 (1)

509-509: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Document host-based device-signing behavior for Custom settings.

usesMeshcoreDeviceSigningMqtt() enables device-signing validation when Custom settings target a known device-signing hostname. The statement that Custom is excluded can cause users to expect a non-JWT flow for a hand-tuned LetsMesh-compatible broker.

State that named presets use device signing and that Custom settings also use it when their server matches a known device-signing broker host.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` at line 509, Update the MQTT documentation around the
device-signing presets to state that Custom settings also use device signing
when the configured server matches a known device-signing broker hostname.
Preserve the existing distinction for Ripple and Custom while clarifying that
Custom is only non-device-signing for unmatched hosts.
docs/meshcore-meshtastic-parity.md-180-180 (1)

180-180: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the Port cell for the Meshtastic Custom preset.

The Port column contains ;. The MeshCore table uses for the same case (line 208). Use the same placeholder here.

📝 Proposed fix
-| Custom   | (user)                           | ;    | No automatic changes; use for private brokers                                                                                |
+| Custom   | (user)                           | —    | No automatic changes; use for private brokers                                                                                |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/meshcore-meshtastic-parity.md` at line 180, Update the Port cell for the
Meshtastic Custom preset in the parity table from `;` to the em dash placeholder
`—`, matching the MeshCore table’s corresponding Custom entry.
src/renderer/components/ConnectionPanel.tsx-2218-2231 (1)

2218-2231: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Cancelling the Ripple or Colorado confirm leaves the picker showing the wrong preset.

MqttNetworkPresetSelect is a controlled <select> bound to meshcorePreset. When the user picks ripple or coloradomesh and then cancels window.confirm, the handler returns before any setState call. No re-render is scheduled, so the DOM <select> keeps the cancelled option while meshcorePreset and meshcoreMqttSettings still hold the previous preset. The user then sees a preset that is not applied, and Connect uses the old broker.

Force a re-render on cancel so React restores the controlled value.

🐛 Proposed fix
+  const [presetSelectNonce, setPresetSelectNonce] = useState(0);
                 onSelect={(value) => {
                   const id = value as MeshcoreMqttPreset;
                   if (id === 'custom') {
                     setMeshcorePreset(id);
                     return;
                   }
                   if (id === 'ripple') {
-                    if (!window.confirm(t('connectionPanel.ripplePresetConfirm'))) return;
+                    if (!window.confirm(t('connectionPanel.ripplePresetConfirm'))) {
+                      setPresetSelectNonce((n) => n + 1);
+                      return;
+                    }
                   }
                   if (id === 'coloradomesh') {
-                    if (!window.confirm(t('connectionPanel.coloradoPresetConfirm'))) return;
+                    if (!window.confirm(t('connectionPanel.coloradoPresetConfirm'))) {
+                      setPresetSelectNonce((n) => n + 1);
+                      return;
+                    }
                     localStorage.setItem(COLORADO_MQTT_REGION_ACK_KEY, '1');
                   }

Pair the fix with a test that cancels the Colorado confirm and asserts the combobox value stays letsmesh.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/components/ConnectionPanel.tsx` around lines 2218 - 2231, Update
the onSelect handler for MqttNetworkPresetSelect so cancelling either the Ripple
or Colorado confirmation triggers a state update that preserves the current
meshcorePreset, allowing React to restore the controlled select value. Add a
test covering cancellation of the Colorado confirmation and assert the combobox
remains set to letsmesh.
reticulum-sidecar/src/stack/live.rs-4761-4764 (1)

4761-4764: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject incomplete FIELD_REACTION payloads.

read_value decodes one MessagePack value and does not require the whole field buffer to be consumed. A field with a valid reaction map plus trailing bytes is malformed, but it is still treated as a reaction. Use the decoded value in a way that only accepts a complete map—either a cursor/position check with an owned read, or read_value_ref with direct ValueRef::Map handling.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@reticulum-sidecar/src/stack/live.rs` around lines 4761 - 4764, Update
reaction_fields_from_message to require complete consumption of FIELD_REACTION
after decoding: use an owned cursor and verify its position reaches the raw
buffer length, or use read_value_ref with direct map handling. Return None for
valid maps followed by trailing bytes, while preserving acceptance of complete
reaction maps.
src/renderer/runtime/useMeshcoreRuntime.ts-3479-3490 (1)

3479-3490: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Cleanup the MeshCore reconnect transport before returning the deferred restart.

When a setup abort occurs after openMeshCoreTransport succeeds, onAttemptError returns 'defer' and skips lateTransport.cleanup(openedDriverIdentityId). Add the same timeout-cleanup call to the setup-abort block, or handle openedDriverIdentityId there, so an opened BLE/TCP transport cannot stay alive until the later follow-up attempt.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/runtime/useMeshcoreRuntime.ts` around lines 3479 - 3490, The
setup-abort branch in onAttemptError must clean up any transport opened by the
current attempt before returning 'defer'. Reuse the existing
lateTransport.cleanup path with openedDriverIdentityId (including its timeout
behavior), then preserve the deferred reconnect flag and return value.
🧹 Nitpick comments (4)
src/renderer/lib/letsMeshConnectionGuards.test.ts (1)

94-131: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a case for a missing wsPath.

The upgrade path described in docs/troubleshooting.md is stale settings that carry no wsPath. validateLetsMeshPresetConnect maps undefined to '' and rejects it. No test pins that behavior.

🧪 Proposed test
+  it('rejects a missing wsPath on a device-signing broker', () => {
+    const { wsPath: _omitted, ...noPath } = base;
+    expect(validateLetsMeshPresetConnect(noPath)).toContain('/ws');
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/lib/letsMeshConnectionGuards.test.ts` around lines 94 - 131, Add
a test in the validateLetsMeshPresetConnect cases asserting that an omitted
wsPath (undefined) is rejected and the validation message reflects the expected
/ws path, covering stale settings without wsPath.
src/renderer/components/ConnectionPanel.tsx (1)

713-714: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Delete the unused autoConnectCancelRef dead store.

autoConnectCancelRef is only written in handleConnect, handleCancelConnection, and handleReconnect, and no reader remains after the mount auto-connect effect was removed. Keep cancelProtocolRfAutoConnect(protocol) as the single cancellation path instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/components/ConnectionPanel.tsx` around lines 713 - 714, Remove
the unused autoConnectCancelRef declaration and all writes to it in
handleConnect, handleCancelConnection, and handleReconnect. Retain
cancelProtocolRfAutoConnect(protocol) as the sole deferred BLE reconnect
cancellation path.
src/renderer/lib/loraRfReconnectAttempt.test.ts (1)

293-309: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse loadRendererLibSource for the source-contract read.

This PR adds loadRendererLibSource in src/renderer/lib/sourceContractTestHelpers.ts, and the three other reconnect contract tests already use it. This block duplicates that logic with a dynamic node:fs import and a bare __dirname. Use the shared helper so the path resolution stays in one place.

♻️ Proposed refactor
-describe('loraRfReconnectAttempt source contracts', () => {
-  it('owns raceWithDeadline budget and finally schedule flush', async () => {
-    const { readFileSync } = await import('node:fs');
-    const { join } = await import('node:path');
-    const source = readFileSync(join(__dirname, 'loraRfReconnectAttempt.ts'), 'utf8');
+describe('loraRfReconnectAttempt source contracts', () => {
+  it('owns raceWithDeadline budget and finally schedule flush', () => {
+    const source = loadRendererLibSource('loraRfReconnectAttempt.ts');

Add the import at the top of the file:

import { loadRendererLibSource } from './sourceContractTestHelpers';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/lib/loraRfReconnectAttempt.test.ts` around lines 293 - 309,
Update the “loraRfReconnectAttempt source contracts” test to import and use the
shared loadRendererLibSource helper when reading loraRfReconnectAttempt.ts.
Remove the duplicated node:fs/node:path imports and __dirname-based path
resolution, while preserving all existing source assertions.
src/renderer/hooks/useProtocolRfAutoConnect.test.tsx (1)

464-491: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the ordering that this test names.

The test name states that the secondary waits for primary settle and then protocol settle before the BLE connect. The assertions only check that each mock was called. All three mocks resolve immediately, so the test still passes if the hook reorders the waits after connectAutomatic. Assert the call order so the sequencing regression is caught.

💚 Proposed assertion
       expect(mocks.awaitNobleBlePrimaryAutoConnectSettled).toHaveBeenCalled();
       expect(mocks.awaitNobleBleProtocolSettle).toHaveBeenCalledWith(
         'meshtastic',
         expect.any(Number),
       );
+      const primarySettleOrder =
+        mocks.awaitNobleBlePrimaryAutoConnectSettled.mock.invocationCallOrder[0];
+      const protocolSettleOrder = mocks.awaitNobleBleProtocolSettle.mock.invocationCallOrder[0];
+      const connectOrder = connectAutomatic.mock.invocationCallOrder[0];
+      expect(primarySettleOrder).toBeLessThan(protocolSettleOrder);
+      expect(protocolSettleOrder).toBeLessThan(connectOrder);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/hooks/useProtocolRfAutoConnect.test.tsx` around lines 464 - 491,
Update the test around useProtocolRfAutoConnect to assert execution order, not
merely invocation. Track the order of awaitNobleBlePrimaryAutoConnectSettled,
awaitNobleBleProtocolSettle, and connectAutomatic, then verify primary settle
occurs first, protocol settle second, and BLE connect last for both tested
platforms.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@reticulum-sidecar/src/stack/live.rs`:
- Around line 3305-3322: Update the reaction-send flow around reaction_target so
the returned payload includes a structured reaction target only when
parse_optional_reply_to_hash successfully returns a valid target. Clear or omit
reaction_target in the content-only fallback, and add a regression test covering
an invalid target_hash to verify both the plain-emoji send and absence of
structured reaction metadata.

In `@src/renderer/components/MqttNetworkPresetSelect.test.tsx`:
- Around line 1-56: Add an accessibility test in the MqttNetworkPresetSelect
test suite that renders the select with its visible label, calls
hydrateAxeThemeColors() without mocking themeColors, runs axe against the
rendered container, and asserts toHaveNoViolations().

In `@src/renderer/lib/connectionPanelStorageMigrations.ts`:
- Line 38: Update preset reconciliation around PRESET_RECONCILE_PRESETS so it
compares and updates only transport fields, excluding authentication fields such
as the manually stored password. Preserve existing manual credentials when
applying meshcoreMqttPresetFields(), and add a test covering a saved waev manual
password that remains unchanged after migration.

In `@src/renderer/lib/meshcoreMqttPresets.ts`:
- Around line 90-100: Update deviceSigningWssFields to explicitly set
tlsInsecure to false, overriding any prior Ripple setting. Add a regression test
that applies a device-signing preset to MQTT settings with tlsInsecure true and
verifies certificate verification is restored.

---

Outside diff comments:
In `@src/renderer/lib/mqttAutoLaunch.ts`:
- Around line 62-76: Align both MQTT call sites with the shared
usesMeshcoreDeviceSigningMqtt predicate: in src/renderer/lib/mqttAutoLaunch.ts
lines 62-76, update shouldAutoLaunchMeshcoreMqttAtStartup to use the stored
preset and settings instead of isLetsMeshSettings alone; in
src/renderer/components/ConnectionPanel.tsx lines 2440-2447, use
usesMeshcoreDeviceSigningMqtt(meshcorePreset, meshcoreMqttSettings) for the
deviation banner. Ensure isLetsMeshSettings covers every host in
DEVICE_SIGNING_HOST_WS_PATHS, and add tests for a Waev preset and Custom
settings targeting mqtt.waev.app.

---

Other comments:
In `@docs/meshcore-meshtastic-parity.md`:
- Line 180: Update the Port cell for the Meshtastic Custom preset in the parity
table from `;` to the em dash placeholder `—`, matching the MeshCore table’s
corresponding Custom entry.

In `@README.md`:
- Line 509: Update the MQTT documentation around the device-signing presets to
state that Custom settings also use device signing when the configured server
matches a known device-signing broker hostname. Preserve the existing
distinction for Ripple and Custom while clarifying that Custom is only
non-device-signing for unmatched hosts.

In `@reticulum-sidecar/src/stack/live.rs`:
- Around line 4761-4764: Update reaction_fields_from_message to require complete
consumption of FIELD_REACTION after decoding: use an owned cursor and verify its
position reaches the raw buffer length, or use read_value_ref with direct map
handling. Return None for valid maps followed by trailing bytes, while
preserving acceptance of complete reaction maps.

In `@src/renderer/components/ConnectionPanel.tsx`:
- Around line 2218-2231: Update the onSelect handler for MqttNetworkPresetSelect
so cancelling either the Ripple or Colorado confirmation triggers a state update
that preserves the current meshcorePreset, allowing React to restore the
controlled select value. Add a test covering cancellation of the Colorado
confirmation and assert the combobox remains set to letsmesh.

In `@src/renderer/components/GamesPanel.tsx`:
- Around line 117-126: Update the celebration flow in the GamesPanel useEffect
and burstConfetti so burstConfetti reports whether a burst actually started,
recording selectedSession.session_id in celebratedWinsRef only after a
successful start. When a second completed local win is selected during an active
burst, queue or retry it after the current burst ends, and add coverage for two
local-win sessions selected during one burst.

In `@src/renderer/runtime/useMeshcoreRuntime.ts`:
- Around line 3479-3490: The setup-abort branch in onAttemptError must clean up
any transport opened by the current attempt before returning 'defer'. Reuse the
existing lateTransport.cleanup path with openedDriverIdentityId (including its
timeout behavior), then preserve the deferred reconnect flag and return value.

---

Nitpick comments:
In `@src/renderer/components/ConnectionPanel.tsx`:
- Around line 713-714: Remove the unused autoConnectCancelRef declaration and
all writes to it in handleConnect, handleCancelConnection, and handleReconnect.
Retain cancelProtocolRfAutoConnect(protocol) as the sole deferred BLE reconnect
cancellation path.

In `@src/renderer/hooks/useProtocolRfAutoConnect.test.tsx`:
- Around line 464-491: Update the test around useProtocolRfAutoConnect to assert
execution order, not merely invocation. Track the order of
awaitNobleBlePrimaryAutoConnectSettled, awaitNobleBleProtocolSettle, and
connectAutomatic, then verify primary settle occurs first, protocol settle
second, and BLE connect last for both tested platforms.

In `@src/renderer/lib/letsMeshConnectionGuards.test.ts`:
- Around line 94-131: Add a test in the validateLetsMeshPresetConnect cases
asserting that an omitted wsPath (undefined) is rejected and the validation
message reflects the expected /ws path, covering stale settings without wsPath.

In `@src/renderer/lib/loraRfReconnectAttempt.test.ts`:
- Around line 293-309: Update the “loraRfReconnectAttempt source contracts” test
to import and use the shared loadRendererLibSource helper when reading
loraRfReconnectAttempt.ts. Remove the duplicated node:fs/node:path imports and
__dirname-based path resolution, while preserving all existing source
assertions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: 6713544c-03f4-4353-a7f4-333626ee5b96

📥 Commits

Reviewing files that changed from the base of the PR and between 948e6bc and a154e35.

⛔ Files ignored due to path filters (18)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml, !**/pnpm-lock.yaml
  • reticulum-sidecar/patches/README.md is excluded by !reticulum-sidecar/patches/**
  • src/renderer/locales/cs/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/de/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/en/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/es/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/fr/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/id/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/it/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/ja/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/ko/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/nl/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/pl/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/pt-BR/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/ru/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/tr/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/uk/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/zh/translation.json is excluded by !src/renderer/locales/**
📒 Files selected for processing (47)
  • README.md
  • docs/key-backup-and-crypto.md
  • docs/letsmesh-mqtt-auth.md
  • docs/meshcore-meshtastic-parity.md
  • docs/reticulum-games-parity.md
  • docs/reticulum-sidecar-ipc.md
  • docs/reticulum.md
  • docs/troubleshooting.md
  • package.json
  • reticulum-sidecar/src/stack/live.rs
  • scripts/update.sh
  • src/renderer/App.tsx
  • src/renderer/components/ConnectionPanel.hostLinkMeter.test.tsx
  • src/renderer/components/ConnectionPanel.test.tsx
  • src/renderer/components/ConnectionPanel.tsx
  • src/renderer/components/GamesPanel.test.tsx
  • src/renderer/components/GamesPanel.tsx
  • src/renderer/components/MqttNetworkPresetSelect.test.tsx
  • src/renderer/components/MqttNetworkPresetSelect.tsx
  • src/renderer/hooks/useProtocolRfAutoConnect.test.tsx
  • src/renderer/lib/confettiBurst.test.ts
  • src/renderer/lib/confettiBurst.ts
  • src/renderer/lib/connectionPanelStorageMigrations.test.ts
  • src/renderer/lib/connectionPanelStorageMigrations.ts
  • src/renderer/lib/letsMeshConnectionGuards.test.ts
  • src/renderer/lib/letsMeshConnectionGuards.ts
  • src/renderer/lib/letsMeshJwt.test.ts
  • src/renderer/lib/letsMeshJwt.ts
  • src/renderer/lib/loraRfReconnectAttempt.test.ts
  • src/renderer/lib/loraRfReconnectAttempt.ts
  • src/renderer/lib/meshcore/meshcorePathUpdatedRuntime.test.ts
  • src/renderer/lib/meshcore/meshcorePathUpdatedRuntime.ts
  • src/renderer/lib/meshcoreMqttPresets.test.ts
  • src/renderer/lib/meshcoreMqttPresets.ts
  • src/renderer/lib/meshcoreMqttTopicPrefix.test.ts
  • src/renderer/lib/meshcoreMqttTopicPrefix.ts
  • src/renderer/lib/mqttAutoLaunch.ts
  • src/renderer/lib/protocolRfAutoConnectGate.ts
  • src/renderer/lib/reticulum/reticulumGamesMetadata.test.ts
  • src/renderer/lib/reticulum/reticulumGamesMetadata.ts
  • src/renderer/lib/sourceContractTestHelpers.ts
  • src/renderer/runtime/loraRfReconnectParity.contract.test.ts
  • src/renderer/runtime/useMeshcoreRuntime.reconnect.test.ts
  • src/renderer/runtime/useMeshcoreRuntime.ts
  • src/renderer/runtime/useMeshtasticRuntime.reconnect-hardening.test.ts
  • src/renderer/runtime/useMeshtasticRuntime.ts
  • vitest.config.mts
💤 Files with no reviewable changes (2)
  • src/renderer/App.tsx
  • src/renderer/components/ConnectionPanel.hostLinkMeter.test.tsx

Comment thread reticulum-sidecar/src/stack/live.rs
Comment thread src/renderer/components/MqttNetworkPresetSelect.test.tsx
Comment thread src/renderer/lib/connectionPanelStorageMigrations.ts
Comment thread src/renderer/lib/meshcoreMqttPresets.ts
rinchen added 2 commits August 8, 2026 07:58
…nd reconnect

Renderer / MeshCore MQTT:
- Startup preset reconcile now rewrites only transport fields, preserving a
  manually stored password/username (connectionPanelStorageMigrations).
- deviceSigningWssFields resets tlsInsecure to false so switching away from
  Ripple restores certificate verification.
- shouldAutoLaunchMeshcoreMqttAtStartup and the deviation banner use the shared
  usesMeshcoreDeviceSigningMqtt predicate (named preset OR device-signing host),
  so Custom settings pointed at a known JWT broker behave consistently.
- ConnectionPanel: cancelling the Ripple/Colorado preset confirm remounts the
  controlled select so it snaps back to the current preset; removed the unused
  autoConnectCancelRef.

Reticulum sidecar (live.rs):
- send_reaction only emits structured reaction_target when the target hash
  parses; the content-only fallback omits it.
- reaction_fields_from_message rejects a FIELD_REACTION with trailing bytes.

Games / reconnect:
- burstConfetti returns whether a burst started; GamesPanel records a win as
  celebrated only on a real start and retries a win queued during an active
  burst.
- useMeshcoreRuntime setup-abort branch cleans up any late-opened transport
  before deferring.

Docs/tests: README/parity Custom clarification; new/updated coverage for each
fix plus axe on MqttNetworkPresetSelect, execution-order assertion in
useProtocolRfAutoConnect, omitted-wsPath guard, and the shared source helper.
An idle LRGP session passes its TTL (tic-tac-toe 24h; chess and
four-in-a-row 7d active, 24h pending) and the protocol rejects any
outbound action, so resign/move/draw failed with a raw
"dispatch_error: session expired" toast while the stored row stayed
"active" — leaving the game stuck with controls that always fail.

Sidecar (games_session prepare_action): on LrgpError::SessionExpired,
persist the session's stored status to "expired" so list_sessions and
future refreshes stop showing a dead game as active, and return the
stable "session_expired" code instead of the raw dispatch_error string.

Renderer (reticulumGamesSession): recognize session_expired (and the
legacy dispatch_error wrapper), flip the local session to expired so the
resign/draw controls disappear and only Delete remains, and show a
friendly, duration-agnostic toast.

Also add an in-game notice under New challenge explaining the per-category
expiry windows so players know unanswered challenges and idle tic-tac-toe
expire after 24h while chess and four-in-a-row allow 7 days idle.

Tests: new Rust regression (age a session past TTL, assert session_expired
+ persisted status) and renderer coverage for the expired flip/toast and
the reason matcher. i18n keys added and translated to all locales.
@rinchen rinchen changed the title MeshCore MQTT broker presets, LoRa reconnect refactor, and Reticulum updates MeshCore MQTT broker presets, LoRa reconnect refactor, and Reticulum games/session fixes Aug 8, 2026
@rinchen
rinchen merged commit a0467b8 into main Aug 8, 2026
19 of 20 checks passed
@rinchen
rinchen deleted the deps branch August 8, 2026 14:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant