Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,11 @@ if [ -s "$ESLINT_LIST" ]; then
while IFS= read -r f || [ -n "$f" ]; do
set -- "$@" "$f"
if [ "$#" -eq 50 ]; then
pnpm exec eslint --cache --max-warnings 0 -- "$@" || exit 1
pnpm exec eslint --cache --max-warnings 0 --no-warn-ignored -- "$@" || exit 1
set --
fi
done < "$ESLINT_LIST"
[ "$#" -eq 0 ] || pnpm exec eslint --cache --max-warnings 0 -- "$@"
[ "$#" -eq 0 ] || pnpm exec eslint --cache --max-warnings 0 --no-warn-ignored -- "$@"
) || fail 'eslint (staged)'
else
printf 'pre-commit: skip eslint (no staged JS/TS)\n' >&2
Expand Down
10 changes: 5 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,9 @@ See **Renderer: hooks vs runtime vs lib** (layout map above). Legacy `useDevice`

Do **not** remount protocol runtimes in child components. Do **not** compare `protocol === 'meshcore'` for feature gates; use `ProtocolCapabilities` / `useRadioProvider(protocol)`.

Protocol SDK adapters: `src/renderer/lib/protocols/`. Connection lifecycle: `ConnectionDriver`; inbound domain events: `PacketRouter` → stores. **MeshCore post-router side effects:** `lib/ingest/meshcoreIngest.ts` (chat persist, `last_heard`, path-updated), `lib/meshcore/meshcoreLiveContactPersist.ts` (SQLite contact rows), `lib/meshcore/meshcorePubKeyRegistry.ts` (DM/trace pubkeys mirrored into runtime refs). Live UI nodes/messages read `nodeStore` / `messageStore`; `useMeshcoreRuntime` still keeps hook-local `nodesRef` for send/RPC until contact rebuild syncs it. **Favorites:** `setNodeFavorited` patches `meshcoreIdentityIdRef` (fallback `getIdentityIdForProtocol('meshcore')`). **Dedup windows:** cross-transport and channel RF **5 min**; room/tapback **60 s**. Path-updated (129) for existing contacts does not bump SQLite `last_advert` until the next advert (128).
Protocol SDK adapters: `src/renderer/lib/protocols/`. Connection lifecycle: `ConnectionDriver` (`lib/drivers/`); inbound domain events: Protocol → `PacketRouter` → identity stores, then side-effect listeners (ingest already applied). **Meshtastic post-router side effects:** `lib/ingest/meshtasticIngest.ts`, `meshtasticRouterSideEffects.ts` (MQTT uplink / notifications / device_log), `meshtasticNodeSideEffects.ts`, `meshtasticRawPacketSideEffects.ts`, `meshtasticTraceSideEffects.ts`, `meshtasticModulePortSideEffects.ts`, `meshtasticStoreForwardSideEffects.ts`; `meshtasticTransportSideEffects.ts` handles transport-state cleanup, while lifecycle-only SDK attach remains in `meshtasticRuntimeWireEffects.ts` (DeviceStatus / MyNodeInfo / FromRadio / heartbeat / config / remote-admin — not a second packet decode path). **MeshCore post-router side effects:** `lib/ingest/meshcoreIngest.ts` (chat persist, `last_heard`, path-updated), `hooks/meshcore/meshcoreConnSideEffects.ts` + `MeshcoreConnSideEffectsCtx` (`meshcoreConnSideEffectsCtx.ts`) (DM ack 130, waiting drain 131, RF RX 136, CLI, disconnect), `lib/meshcore/meshcoreLiveContactPersist.ts` (SQLite contact rows), `lib/meshcore/meshcorePubKeyRegistry.ts` (DM/trace pubkeys). Live UI nodes/messages read `nodeStore` / `messageStore` via `identityStoreReads` (`getIdentityNode` / `getIdentityChatMessages`); runtimes do not keep hook-local node/message wire mirrors. Transport params / Protocol attach helpers: `meshIdentityBridge`. **Favorites:** `setNodeFavorited` patches `meshcoreIdentityIdRef` (fallback `getIdentityIdForProtocol('meshcore')`). **Dedup windows:** cross-transport and channel RF **5 min**; room/tapback **60 s**. Path-updated (129) for existing contacts does not bump SQLite `last_advert` until the next advert (128).

**Identity-scoped UI stores:** `identityStore`, `nodeStore`, `messageStore`, `connectionStore` — nodes/messages keyed by `identityId`. **MQTT status bridge:** `mirrorMqttStatusToConnection` copies main-process `mqtt.onStatus` IPC into `connectionStore.mqttStatus` from legacy runtime handlers until MQTT moves fully into `ConnectionDriver`. **SQLite → UI:** `lib/hydrateIdentityStoresFromDb.ts` (coordinator: `identityHydrationCoordinator.ts`; Meshtastic node map: `meshtasticDbCacheHydration.ts`; message cap: `meshtasticMessageLoadLimit.ts`); manual refresh via `hooks/useDbRefresh.ts`. Runtimes may still merge DB rows into hook-local refs on connect; identity-scoped Zustand hydration is the canonical UI path ([#375]). **MeshCore contacts DB:** `meshcore_contacts.last_advert` is Unix **seconds**; age prune uses `src/shared/meshcoreContactAgeCutoff.ts` (do not compare in ms).
**Identity-scoped UI stores:** `identityStore`, `nodeStore`, `messageStore`, `connectionStore` — nodes/messages keyed by `identityId`. **MQTT status bridge:** `mirrorMqttStatusToConnection` copies main-process `mqtt.onStatus` IPC into `connectionStore.mqttStatus` from runtime handlers until MQTT moves fully into `ConnectionDriver`. **SQLite → UI:** `lib/hydrateIdentityStoresFromDb.ts` (coordinator: `identityHydrationCoordinator.ts`; Meshtastic node map: `meshtasticDbCacheHydration.ts`; message cap: `meshtasticMessageLoadLimit.ts`); manual refresh via `hooks/useDbRefresh.ts`. Identity-scoped Zustand hydration is the canonical UI path ([#375]). **MeshCore contacts DB:** `meshcore_contacts.last_advert` is Unix **seconds**; age prune uses `src/shared/meshcoreContactAgeCutoff.ts` (do not compare in ms).

### Protocol entry points

Expand Down Expand Up @@ -229,7 +229,7 @@ Do **not** reintroduce Meshtastic-only startup gates, child-before-parent init,

### Meshtastic channel URLs & Store & Forward

- **Config apply (Radio / Modules / Security):** Firmware `setConfig` / `setModuleConfig` replace full protobuf structs. UI must merge cached device slices with form edits via `meshtasticConfigApply.ts` (`mergeMeshtasticConfigApplyValue`, `buildMeshtasticModuleApplyValue`); slices live in `deviceStore.meshtasticConfigSlices` and `moduleConfigs` (PacketRouter + `meshtasticLegacyWireSubscriptions`). Module-specific validation: `meshtasticMqttModuleApply.ts`, `meshtasticSerialModuleApply.ts`. Apply failures surface `clientNotification` text within 8s (`meshtasticClientNotification.ts` → `formatMeshtasticModuleApplyError`); inline status via `ConfigApplyNotice.tsx`. Forms re-sync after reboot via `useSyncFormFromConfig`.
- **Config apply (Radio / Modules / Security):** Firmware `setConfig` / `setModuleConfig` replace full protobuf structs. UI must merge cached device slices with form edits via `meshtasticConfigApply.ts` (`mergeMeshtasticConfigApplyValue`, `buildMeshtasticModuleApplyValue`); slices live in `deviceStore.meshtasticConfigSlices` and `moduleConfigs` (PacketRouter + runtime lifecycle wire effects). Module-specific validation: `meshtasticMqttModuleApply.ts`, `meshtasticSerialModuleApply.ts`. Apply failures surface `clientNotification` text within 8s (`meshtasticClientNotification.ts` → `formatMeshtasticModuleApplyError`); inline status via `ConfigApplyNotice.tsx`. Forms re-sync after reboot via `useSyncFormFromConfig`.
- **Administration tab:** `AdminPanel.tsx` — device commands and Danger Zone (reboot, shutdown, factory reset, NodeDB reset, OTA/DFU); shared `ConfirmModal.tsx` with Radio/Modules destructive flows. Local-only OTA/DFU disabled when **Configure node** targets a remote node.
- **Remote admin module snapshot:** `meshtasticRemoteAdminModuleFetches.ts` — canonical list/count of `ModuleConfig` reads during remote snapshot (`REMOTE_ADMIN_MODULE_CONFIG_FETCHES`).
- **Channel URLs:** `src/shared/meshtasticUrlEncoder.ts` (parse/generate), `src/shared/meshtasticChannelApply.ts` (replace vs add-only apply); Radio panel UI; Meshtastic-only.
Expand Down Expand Up @@ -264,9 +264,9 @@ Panels: `src/renderer/components/`. New tabs: `lazyTabPanels.ts` / `lazyAppPanel
- **Components:** `ChatPanel.tsx` (channel/DM UI) + shared `ChatComposer.tsx` (drafts, mentions, chunking, spellcheck, emoji; also used by `RoomsPanel.tsx`). Scroll-at-bottom helper: `chatScrollUtils.ts` (`getDistFromChatBottom`).
- **Payload / links:** `ChatPayloadText.tsx` — mention highlighting, search marks, URL linkification; link previews via `chat:fetchLinkPreview` (`src/main/fetchLinkPreview.ts`, blocks hostname and **resolved** private/loopback addresses via DNS pin + undici `Agent`, 10s fetch timeout, 3s DNS lookup timeout, 64 KiB HTML cap, LRU preview/image caches, single-flight dedup). Reply quotes: `replyPreview.ts`.
- **Storage helpers:** `src/renderer/lib/chatPanelProtocolStorage.ts` — drafts (`mesh-client:drafts:<protocol>`), open DM tabs, last-read, per-view mute (`mesh-client:mutedViews:<protocol>`), starred (`mesh-client:starred:<protocol>`, cap 200).
- **Notifications:** `src/renderer/lib/chatNotifications.ts` — `playMessageNotification(type)` via Web Audio: `channel` = single 880 Hz pulse (150 ms); `dm` / `reply` = dual pulse (587.33 Hz then 783.99 Hz, 50 ms each, 35 ms gap). Resumes suspended `AudioContext` when the window is hidden/minimized. Type selection in `chatUnreadCounts.ts` (`resolveChatNotificationType`, `pickAudibleNotificationType`; batch priority reply > dm > channel). **ChatPanel** plays when the user is on Chat but reading another view; **App** plays for other panels / backgrounded window (avoids double beep). Meshtastic hidden-window desktop notifications are visual-only (`silent: true` in `meshtasticLegacyWireSubscriptions.ts`); typed Web Audio from App owns sound. Global mute `mesh-client:notifMuted`; per-view mute in `mutedViews`. Main-process **tray** icon shows unread when chat or MeshCore Rooms traffic arrives while backgrounded (`src/main/index.ts` `buildTrayIcon`).
- **Notifications:** `src/renderer/lib/chatNotifications.ts` — `playMessageNotification(type)` via Web Audio: `channel` = single 880 Hz pulse (150 ms); `dm` / `reply` = dual pulse (587.33 Hz then 783.99 Hz, 50 ms each, 35 ms gap). Resumes suspended `AudioContext` when the window is hidden/minimized. Type selection in `chatUnreadCounts.ts` (`resolveChatNotificationType`, `pickAudibleNotificationType`; batch priority reply > dm > channel). **ChatPanel** plays when the user is on Chat but reading another view; **App** plays for other panels / backgrounded window (avoids double beep). Meshtastic hidden-window desktop notifications are visual-only (`silent: true` in `meshtasticRouterSideEffects.ts`); typed Web Audio from App owns sound. Global mute `mesh-client:notifMuted`; per-view mute in `mutedViews`. Main-process **tray** icon shows unread when chat or MeshCore Rooms traffic arrives while backgrounded (`src/main/index.ts` `buildTrayIcon`).
- **Meshtastic dedup:** `meshtasticMessageDedup.ts` — merges delayed RF/MQTT duplicates (10-minute content window) in `useMeshtasticRuntime` ingest.
- **Hop badges:** `MessageRecord.rxHops` / `viaStoreForward` round-trip via `storeRecordAdapters.ts` and `meshtasticDbCacheHydration.ts` (`hopCount` on `MessageRecord` bridges to `rxHops` for Meshtastic PacketRouter rows); Chat hop pills read `ChatMessage.rxHops`. **MeshCore (primary):** companion `pathLen` on events 7/8 → `meshcoreCompanionRxPathLenToHopCount` → `DomainEvent.payload.hopCount` (`MeshCoreProtocol`, `meshcoreDirectMessageDecode`); waiting-message drain also sets `rxHops` from `pathLen`. **MeshCore (fallback):** raw-log correlation via `resolveMeshcoreIngestRxHops` / `MESHCORE_CHAT_CORRELATE_WINDOW_MS` (3000ms); `rawPacketsRef` synced inside event-136 `setRawPackets` updater for same-tick ingest (`meshcoreLegacyConnEvents`). **Meshtastic:** `MeshtasticProtocol` uses `meshtasticComputedRfHopsAway` — omit hops for `viaMqtt` and `hopStart === 0`; else `hopStart - hopLimit` when `hopStart > 0 && hopLimit <= hopStart`.
- **Hop badges:** `MessageRecord.rxHops` / `viaStoreForward` round-trip via `storeRecordAdapters.ts` and `meshtasticDbCacheHydration.ts` (`hopCount` on `MessageRecord` bridges to `rxHops` for Meshtastic PacketRouter rows); Chat hop pills read `ChatMessage.rxHops`. **MeshCore (primary):** companion `pathLen` on events 7/8 → `meshcoreCompanionRxPathLenToHopCount` → `DomainEvent.payload.hopCount` (`MeshCoreProtocol`, `meshcoreDirectMessageDecode`); waiting-message drain also sets `rxHops` from `pathLen`. **MeshCore (fallback):** raw-log correlation via `resolveMeshcoreIngestRxHops` / `MESHCORE_CHAT_CORRELATE_WINDOW_MS` (3000ms); `rawPacketsRef` synced inside event-136 `setRawPackets` updater for same-tick ingest (`meshcoreConnSideEffects`). **Meshtastic:** `MeshtasticProtocol` uses `meshtasticComputedRfHopsAway` — omit hops for `viaMqtt` and `hopStart === 0`; else `hopStart - hopLimit` when `hopStart > 0 && hopLimit <= hopStart`.
- **MeshCore dedup:** `meshcoreStoreDedup.ts` — RF/MQTT merge, companion TX echo, tapback self-echo, room BBS paths in `useMeshcoreRuntime` ingest.
- **Reactions / tapbacks:** `reactions.ts` (Meshtastic protobuf) + `meshcoreChannelText.ts` (MeshCore default outbound keyless `@[Name] emoji` / `@[Name] body`; opt-in **MeshCore Open compatibility** in App enables keyed replies, `r:` reactions, and `g:` GIF send via `meshcoreOpenWireCompatEnabled` in `appSettingsStorage.ts`; inbound keyed/sec↔ms parent match via `meshcoreMessageMatchesReplyKey`; inbound emoji-only replies promoted via `meshcorePromoteEmojiOnlyReplyToTapback`; inbound Open `r:HASH:INDEX` in `meshcoreOpenReaction.ts`); `ChatPanel.tsx` attaches tapbacks via `replyId` + runtime/panel `sendReaction`.
- **Mention segments:** `src/renderer/lib/chatMentionSegments.ts` — parse/build `@[Name]` tokens; `MentionAutocomplete.tsx` renders the dropdown.
Expand Down
2 changes: 2 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ Sanitize user-controlled strings before logs and IPC per [AGENTS.md](AGENTS.md).

**Renderer layers:** `runtime/` (single-mount protocol runtimes), `hooks/` (facades and store selectors), `lib/` (drivers, sessions, types), `stores/` (identity-scoped UI: `identityStore`, `nodeStore`, `messageStore`, `connectionStore`; Reticulum also uses session-global `reticulumIdentityStore` for sidecar identity status). Prefer `useProtocolFacade(protocol)` in App for new wiring. Hook/runtime boundaries: [AGENTS.md](AGENTS.md#renderer-hook-architecture-multi-protocol) ([#375](https://github.com/Colorado-Mesh/mesh-client/issues/375), [#377](https://github.com/Colorado-Mesh/mesh-client/issues/377)).

**Drivers / identity bridge:** `lib/drivers/ConnectionDriver.ts` owns RF/MQTT session lifecycle and dispatches Protocol events into `lib/drivers/PacketRouter.ts` (store ingest first, then side-effect listeners). `PacketRouter` invokes generic listeners before event-type listeners, in registration order within each group; attach persistence/ingest before dependent UI side effects. `lib/meshIdentityBridge.ts` builds transport params and attaches Meshtastic Protocol ingress; `lib/identityStoreReads.ts` is the canonical read path for identity-scoped nodes/messages (`getIdentityNode` / `getIdentityChatMessages`).

### Protocols

- **Meshtastic:** `runtime/useMeshtasticRuntime.ts`, `lib/protocols/MeshtasticProtocol.ts`, `lib/connection.ts` (`createConnection`).
Expand Down
2 changes: 1 addition & 1 deletion docs/development-environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,7 @@ By default all three Vitest projects run in **parallel** (`groupOrder: 0`). On m

CI runs coverage in three parallel jobs (`renderer-ui`, `renderer-logic`, `main`) and merges blob reports via `pnpm run test:coverage:merge` (see [`.github/workflows/tests.yaml`](../.github/workflows/tests.yaml)).

Monolithic protocol runtimes (`useMeshtasticRuntime`, `useMeshcoreRuntime`) also use **source contract tests** (read `.ts` files and assert wiring strings) where full `renderHook` integration would require heavy BLE/MQTT mocking; see `*.reconnect*.test.ts` beside those runtimes.
Monolithic protocol runtimes (`useMeshtasticRuntime`, `useMeshcoreRuntime`) also use **source contract tests** (read `.ts` files and assert wiring strings) where full `renderHook` integration would require heavy BLE/MQTT mocking; see `*.reconnect*.test.ts` beside those runtimes. Another example: [`meshtasticRuntimeWireEffects.diagnostics.contract.test.ts`](../src/renderer/lib/meshtastic/meshtasticRuntimeWireEffects.diagnostics.contract.test.ts) asserts LocalStats / RF hop-SNR still call `processNodeUpdate` from `meshtasticNodeSideEffects` / `meshtasticRawPacketSideEffects`.

#### Browser dev without Electron

Expand Down
2 changes: 1 addition & 1 deletion docs/diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -683,7 +683,7 @@ Meshtastic short/full header rules (unchanged):

#### Connected-node telemetry ingest (Meshtastic)

`meshtasticLegacyWireSubscriptions` calls `processNodeUpdate` on **LocalStats** telemetry and on RF mesh packets that update `hops_away`/SNR so `cuHistory`, hop history, and incremental RF analysis stay live (previously only device-metrics / NodeInfo paths fed the store).
`meshtasticNodeSideEffects` feeds **LocalStats** into `diagnosticsStore.processNodeUpdate` (CU history / connected-node RF analysis). `meshtasticRawPacketSideEffects` feeds RF mesh packets that update `hops_away`/SNR so hop history and incremental RF analysis stay live. (Previously only device-metrics / NodeInfo paths — and briefly `meshtasticRuntimeWireEffects` — fed the store.) Guarded by `meshtasticRuntimeWireEffects.diagnostics.contract.test.ts`.

#### Proximity classification (`classifyProximity()`)

Expand Down
2 changes: 1 addition & 1 deletion docs/meshcore-meshtastic-parity.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ Room servers (`hw_model === 'Room'`, contact type 3) are BBS nodes on the mesh.

## MeshCore: identity-scoped UI stores

Live Chat and Nodes read **identity-scoped** `nodeStore` / `messageStore` (keyed by `identityId`). Hook-local refs in `useMeshcoreRuntime` (`nodesRef`, pubkeys) remain for send/RPC until contact rebuild syncs. Hydration: [`hydrateIdentityStoresFromDb.ts`](../src/renderer/lib/hydrateIdentityStoresFromDb.ts). **Chat-driven `last_heard`** (`meshcoreIngest`, `ensureMeshcoreChatSenderInNodeStore`) updates node freshness on text traffic, not only adverts.
Live Chat and Nodes read **identity-scoped** `nodeStore` / `messageStore` (keyed by `identityId`) via `identityStoreReads` (`getIdentityNode` / `getIdentityChatMessages`). DM/trace pubkeys live in `meshcorePubKeyRegistry` (mirrored into runtime maps for send/RPC). Hydration: [`hydrateIdentityStoresFromDb.ts`](../src/renderer/lib/hydrateIdentityStoresFromDb.ts). **Chat-driven `last_heard`** (`meshcoreIngest`, `ensureMeshcoreChatSenderInNodeStore`) updates node freshness on text traffic, not only adverts.

## MeshCore: Rooms scroll UX

Expand Down
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export default tseslint.config(
'**/build/**',
'**/out/**',
'**/node_modules/**',
'.cache/**',
'src/renderer/lib/nomad/vendor/**',
'eslint.config.mjs',
'**/*.d.ts',
Expand Down
8 changes: 4 additions & 4 deletions scripts/check-log-panel-filter.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
*
* Files checked:
* Meshtastic main: noble-ble-manager.ts, mqtt-manager.ts
* Meshtastic renderer: useMeshtasticRuntime.ts, meshtasticLegacyWireSubscriptions.ts
* Meshtastic renderer: useMeshtasticRuntime.ts, meshtasticRuntimeWireEffects.ts
* MeshCore main: meshcore-mqtt-adapter.ts
* MeshCore renderer: useMeshcoreRuntime.ts, meshcoreLegacyConnEvents.ts
* MeshCore renderer: useMeshcoreRuntime.ts, meshcoreConnSideEffects.ts
*
* Filter source: src/renderer/components/LogPanel.tsx
*
Expand All @@ -36,12 +36,12 @@ const DEVICE_FILES = {
path.join(ROOT, 'src', 'main', 'noble-ble-manager.ts'),
path.join(ROOT, 'src', 'main', 'mqtt-manager.ts'),
path.join(ROOT, 'src', 'renderer', 'runtime', 'useMeshtasticRuntime.ts'),
path.join(ROOT, 'src', 'renderer', 'lib', 'meshtastic', 'meshtasticLegacyWireSubscriptions.ts'),
path.join(ROOT, 'src', 'renderer', 'lib', 'meshtastic', 'meshtasticRuntimeWireEffects.ts'),
],
meshcore: [
path.join(ROOT, 'src', 'main', 'meshcore-mqtt-adapter.ts'),
path.join(ROOT, 'src', 'renderer', 'runtime', 'useMeshcoreRuntime.ts'),
path.join(ROOT, 'src', 'renderer', 'hooks', 'meshcore', 'meshcoreLegacyConnEvents.ts'),
path.join(ROOT, 'src', 'renderer', 'hooks', 'meshcore', 'meshcoreConnSideEffects.ts'),
],
reticulum: [
path.join(ROOT, 'src', 'main', 'reticulum-sidecar-manager.ts'),
Expand Down
Loading