feat(notifications): surface device ClientNotifications + firmware 2.8 favorite/ignore cap (#3548)#3578
Conversation
…ware 2.8 favorite/ignore cap (#3548) MeshMonitor decoded FromRadio.ClientNotification (mesh.proto field 16) and dropped it. Surface these device-originated warnings/errors as toasts, and handle the firmware 2.8 protected-node-cap refusal for Set Favorite / Ignore. Backend: - clientNotificationPolicy.ts (pure/testable): suppression patterns (recurring power-save "sleeping for N interval" INFO + key-verification variants + empty messages), the 2.8 protected-node-cap refusal parser, level->severity mapping, and a per-source ToastThrottle that dedupes identical messages within a window. - meshtasticProtobufService.ts: add the clientNotification dispatch branch (was falling through to the generic catch-all). - dataEventEmitter.ts: client-notification event type + emitClientNotification(). - meshtasticManager.ts: handleClientNotification() reverts the optimistic favorite/ignore flag and re-broadcasts the node when the device refuses at the protected-node cap, then applies the suppression/throttle policy and emits the toast event. Source-scoped throughout. Frontend: - DeviceNotificationToaster.tsx: listens for client-notification inside ToastProvider, maps level->severity, shows the toast. Wired into App.tsx. - WS forwarding + per-source room filtering needed no changes. Scope: the 2.8 NodeDB warm-tier restructure and the on-disk snr_q4 field do NOT affect the over-the-air wire MeshMonitor reads. OTA NodeInfo.snr stays a float in dB; no protobuf/decode change. A regression test guards this. The cap-refusal warning is only emitted by firmware for the locally-connected node (from == 0). Tests (20 new, full suite green): policy unit tests, manager handler tests (reconciliation/suppression/throttle, per-source), and protobuf dispatch + SNR float guard. Docs: FAQ (node count + blocked-node 2.8 behavior; device-notification toasts), IgnoredNodesSection inline help, CHANGELOG, and the support plan dev-note. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011JEaCGwY9Wz8jeV4e22GW4
|
Claude finished @Yeraze's task —— View job PR Review: feat(notifications): surface device ClientNotifications + firmware 2.8 favorite/ignore capTasks:
Overall this is a well-architected, well-tested feature. The separation of concerns between the pure policy module, the manager handler, and the frontend toaster is clean. Below are findings by category. Code Quality & Best Practices1. Duplicated
The frontend can't import from the backend, so some duplication is expected, but the frontend version uses 2. Guard In 3. Debug log in hot path
Potential Bugs4. The firmware format string is 5. Toast emitted even on cap-refusal revert failure In
If the DB revert fails, the toast is still emitted — the user sees "can't favorite…" but MeshMonitor's state may be inconsistent. This is arguably the right behavior (surface the issue), but worth a comment since the current code silently diverges from the optimistic state on DB error. 6. Missing test: DB revert failure path The test suite ( Performance7. The map cleanup at 8. The throttle key is Security9. Log injection via device-controlled
logger.info(`🔔 [${this.sourceId}] Device notification (level ${data.level}): ${message}`);And logger.info(`[DataEventEmitter] Client notification (level ${data.level}): ${data.message}`);The 10. Toast message length is not bounded
Test CoverageThe test coverage is solid overall:
Missing test cases (minor):
SummaryThis is a clean, well-designed feature with good test coverage and clear documentation of the design constraints. The main actionable items are:
|
…gaps (#3578) Addresses Claude Code Review findings on PR #3578: - Sanitize device-controlled message before logging/forwarding: strip control chars (log-injection defense) and bound length to 500 (#9, #10). - Widen protected-cap regex to {1,8} hex digits so a short node id still reconciles (#4). - Exclude clientNotification from the unknown-FromRadio debug JSON.stringify dump (#3). - Drop the redundant `&& this.sourceId` guard (always set) and comment why the toast still fires when the DB revert fails (#2, #5). - Frontend: name the level magic numbers and note they mirror the backend NOTIFICATION_LEVEL (#1). - Tests: sanitizer (control chars/whitespace/truncation/empty), short-hex-id parse, and the DB-revert-failure path (#6). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011JEaCGwY9Wz8jeV4e22GW4
|
Thanks for the thorough review — triaged all findings. Addressed in e1c9894: Fixed
Considered, leaving as-is
Full Vitest suite green (6973 passed) after the changes. |
…ion-surfacing # Conflicts: # CHANGELOG.md
|
Claude finished @Yeraze's task —— View job PR Review: feat(notifications): surface device ClientNotifications + firmware 2.8 favorite/ignore capTasks:
This is a well-structured follow-up review on commit Code Quality & Best Practices1.
logger.info(`[DataEventEmitter] Client notification (level ${data.level}): ${data.message}`);
2. In const msg = (n.message ?? '').trim();This double-trim is harmless but slightly redundant. Since 3.
Potential Bugs4.
5. Missing The debug dump guard at Performance6.
7. Toast duration hardcoded at 8000ms
Security8. Sanitized message forwarded correctly The sanitized One minor gap: Test CoverageWell covered overall:
Minor gaps (non-blocking):
SummaryThe previous 10 findings were all addressed correctly in
The feature is well-implemented, well-tested, and ready to merge pending the multi-source room scoping check. |
|
Thanks for the follow-up. The three remaining items are non-blocking; closing the loop:
CI is green on the merged head ( |
Summary
Closes the actionable part of #3548 (Meshtastic firmware 2.8 NodeDB warm-tier tracking).
MeshMonitor already decoded
FromRadio.ClientNotification(mesh.proto field 16) and then dropped it. This PR surfaces those device-originated messages as toasts, and handles the one genuinely new firmware-2.8 behavior: a Set Favorite / Ignore can be refused when the device's protected-node list is full.Why this is the only code change for #3548
A deep dive on firmware PR meshtastic/firmware#10705 showed the 2.8 NodeDB restructure is almost entirely on-disk / internal-RAM and does not change the over-the-air wire MeshMonitor reads over TCP:
snr_q4inNodeInfoLiteNodeInfo.snrstaysfloat)set_favorite()refuses at capClientNotification)Changes
Backend
clientNotificationPolicy.ts(new, pure/testable) — suppression patterns (recurring power-save "sleeping for N interval" INFO, key-verification handshake variants, empty messages), the 2.8 protected-node-cap refusal parser, level→severity mapping, and a per-sourceToastThrottlethat dedupes identical messages within a window (so a per-packet duty-cycle warning toasts once).meshtasticProtobufService.ts— newclientNotificationdispatch branch (previously fell through to the generic catch-all).dataEventEmitter.ts—client-notificationevent type +emitClientNotification().meshtasticManager.ts—handleClientNotification()reverts the optimistic favorite/ignore flag + re-broadcasts the node when the device refuses at the cap, then applies the suppression/throttle policy and emits the toast. Source-scoped.Frontend
DeviceNotificationToaster.tsx(new) — listens forclient-notificationinsideToastProvider, maps level→severity, shows the toast. Wired intoApp.tsx. WS forwarding + per-source room filtering needed no changes.Docs / UX
faq.md— 2.8 node-count + blocked-node behavior; device-notification toasts.IgnoredNodesSection.tsx— inline "blocked, not deleted" clarification.CHANGELOG.md+docs/internal/dev-notes/MT28_NODEDB_SUPPORT_PLAN.md.Compatibility with firmware 2.7.x
ClientNotificationis not new in 2.8 — 2.7.x already emits several. The throttle/suppression policy (keyed on message text, since the proto has no subsystem field and hardcodeslevel=WARNING) keeps the recurring ones (duty-cycle, power-save) from spamming users while still surfacing valuable one-shot warnings (duplicate-key, config errors). The cap-refusal reconciliation is inert on 2.7.x — that message string is never emitted, so zero false reverts.Testing
20 new tests; full Vitest suite green (6967 passed, 0 failed).
clientNotificationPolicy.test.ts— suppression, cap-refusal parsing (incl. ignoring theverifyverb and all 2.7.x strings), throttle dedupe.meshtasticManager.clientNotification.test.ts— handler reverts the correct flag scoped to itssourceId, surfaces normal warnings, suppresses/dedupes.meshtasticProtobufService.test.ts—ClientNotificationdispatch + OTANodeInfo.snrstays a float (snr_q4 regression guard).Known limitation
The cap-refusal warning is only emitted by firmware for the locally-connected node (
from == 0); remote-admin favorites get no notification. Confirming the local admin path resolves tofrom == 0needs a live 2.8 dev build (no 2.8 hardware available here).🤖 Generated with Claude Code
https://claude.ai/code/session_011JEaCGwY9Wz8jeV4e22GW4