Skip to content

fix(messaging): expose isUnmessagable/isLicensed so the DM UI hides for unmessagable nodes (#3755)#3766

Merged
Yeraze merged 1 commit into
mainfrom
fix/unmessagable-not-exposed-in-nodes-api
Jun 26, 2026
Merged

fix(messaging): expose isUnmessagable/isLicensed so the DM UI hides for unmessagable nodes (#3755)#3766
Yeraze merged 1 commit into
mainfrom
fix/unmessagable-not-exposed-in-nodes-api

Conversation

@Yeraze

@Yeraze Yeraze commented Jun 26, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes #3755. The DM UI did not hide for Meshtastic nodes that have the unmessagable bit set — the DM button still appeared in the Nodes tab and the message composer stayed writable, despite the node's intent to not receive direct messages.

Root cause

is_unmessagable / is_licensed are stored on the nodes table (migration 101, #3684), but mapDbNodeToDeviceInfo() — which builds the node object the API returns — never copied them onto the client object (it handles isFavorite, isIgnored, etc., but not these two). So every node reached the frontend with isUnmessagable === undefined, and the two UI guards fail open:

  • NodesTab.tsx — DM button shown when !node.isUnmessagable → always shown
  • MessagesTab.tsx — composer set read-only when isUnmessagable === true → never

Only the local node worked, because its flag flows through a separate owner/identity endpoint, not the node list.

Fix

Pass isUnmessagable and isLicensed through mapDbNodeToDeviceInfo(), mirroring the existing isIgnored handling.

Testing

  • New meshtasticManager.unmessagable.test.ts — 3 cases: flag 1 → exposed true, 0 → exposed false, null (legacy rows) → omitted.
  • Verified live against the dev DB: /api/nodes now returns isUnmessagable: true for the 36 nodes that carry the bit (previously null for all).
  • Full Vitest suite green; tsc clean.

🤖 Generated with Claude Code

…or unmessagable nodes (#3755)

mapDbNodeToDeviceInfo dropped the is_unmessagable / is_licensed columns (added
in migration 101) when building the client node object, so every node reached
the frontend with isUnmessagable === undefined. The DM guards in NodesTab
(hide the DM button) and MessagesTab (lock the composer) therefore failed open
for remote Meshtastic nodes — only the local node, which flows through a
separate owner/identity path, worked. Pass both flags through the mapper.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011JEaCGwY9Wz8jeV4e22GW4
@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown

Claude finished @Yeraze's task —— View job


PR Review

Tasks:

  • Read changed files
  • Review implementation in meshtasticManager.ts
  • Review test coverage in meshtasticManager.unmessagable.test.ts
  • Check against existing patterns in codebase
  • Post final review

Summary

This is a clean, minimal fix for a genuine security-adjacent bug (DM UI failing open). The approach is correct and consistent with the existing codebase patterns. A few observations:

Implementation (src/server/meshtasticManager.ts)

✅ Correct pattern. The new null/undefined guard + Boolean() coerce exactly mirrors isIgnored, isFavorite, and favoriteLocked above it — no pattern deviation.

✅ No over-engineering. The fix is exactly the two missing field assignments, nothing else.

Minor observation (non-blocking): The schema in src/db/schema/nodes.ts defines these columns with .default(false) on all three backends. That means getAllNodes should never actually return null for these on a post-migration 101 DB — only legacy rows inserted before the migration could be null. The null guard is still correct and defensive, but worth noting the code comment says "if they exist" which is slightly misleading for post-migration rows that will always have a value. Not worth changing.

Minor observation (non-blocking): MessagesTab.tsx:effectiveReadOnly uses isUnmessagable === true (strict equality), so undefined correctly keeps it writable. But NodesTab.tsx uses !node.isUnmessagable — this means undefined also passes (falsy), hiding the DM button only when true. The fix is correct here since without this PR, isUnmessagable was always undefined, and now nodes with isUnmessagable: true in the DB will correctly get true propagated. This is fine as-is.

Tests (src/server/meshtasticManager.unmessagable.test.ts)

✅ Good coverage. Three cases cover the essential surface:

  1. 1 (SQLite truthy integer) → true
  2. 0false
  3. nullundefined (legacy row regression guard)

✅ Correct mock strategy. Using vi.hoisted for getAllNodes and mocking tcpTransport.js to prevent socket creation is the right approach for this manager's test harness.

One gap worth noting (non-blocking): There's no test asserting that isLicensed is independently omitted when null while isUnmessagable is set (or vice versa). The two flags are always tested together. Not a blocking issue since they're symmetric, but a mixed-state test would add robustness.

One gap worth noting (non-blocking): No test validates the UI-side guards directly (the NodesTab DM button hiding and MessagesTab composer locking). These guards were pre-existing behavior that already worked for the local node — the PR description acknowledges this. Adding UI-level tests is out of scope here.

Overall

Approve. The fix is correct, minimal, and well-tested. The root cause analysis in the PR description is accurate. No blocking issues found.

@Yeraze

Yeraze commented Jun 26, 2026

Copy link
Copy Markdown
Owner Author

Thanks! Agreed on the if they exist note — the null guard only matters for legacy pre-migration-101 rows; post-migration rows always have the .default(false) value. Leaving the guard (and comment) as-is per your call, since it's harmless defensiveness. No code changes.

@Yeraze Yeraze merged commit b71a1fa into main Jun 26, 2026
19 checks passed
@Yeraze Yeraze deleted the fix/unmessagable-not-exposed-in-nodes-api branch June 26, 2026 01:07
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.

[BUG] MeshCore: You should not be able to send messages to repeaters

1 participant