Skip to content

fix(config): persist & surface local node isUnmessagable/isLicensed (#3684)#3688

Merged
Yeraze merged 1 commit into
mainfrom
fix/local-node-unmessageable-3684
Jun 23, 2026
Merged

fix(config): persist & surface local node isUnmessagable/isLicensed (#3684)#3688
Yeraze merged 1 commit into
mainfrom
fix/local-node-unmessageable-3684

Conversation

@Yeraze

@Yeraze Yeraze commented Jun 23, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #3684 (and resolves the duplicate #3683). Fixes Bug 1: the Configuration tab's Unmessageable checkbox always rendered unchecked for the local node, because the flag was never ingested or persisted and /admin/get-owner's local branch hardcoded it to false. (Bug 2 — a map indicator for unmessageable nodes — is intentionally deferred as a separate enhancement.)

The Meshtastic User protobuf carries is_unmessagable (optional bool) and is_licensed (bool), which decode to isUnmessagable / isLicensed on nodeInfo.user. These were dropped on ingest, so the frontend (already wired to config.localNodeInfo.isUnmessagable) had nothing to show.

Changes

  • Schema (src/db/schema/nodes.ts): new isUnmessagable / isLicensed boolean columns across SQLite / PostgreSQL / MySQL (default false).
  • Migration 101 (src/server/migrations/101_add_node_unmessagable.ts): idempotent across all three backends (SQLite try/catch duplicate-column, PG IF NOT EXISTS, MySQL information_schema). Registered in src/db/migrations.ts; count test bumped 100→101. (Note: the CLAUDE.md brief said "next is 100," but origin/main already shipped migration 100 meshcore_channel_scope, so this is 101.)
  • Repository (src/db/repositories/nodes.ts): read/write both columns on the update (preserves prior value via ?? existing), insert, and upsert paths.
  • Ingest (src/server/meshtasticManager.ts): store nodeInfo.user.isUnmessagable/isLicensed ?? false; extend the localNodeInfo type and populate it from the local NodeInfo (outside the name-change guard, since the flag can change without a name change).
  • get-owner (src/server/server.ts): the local-node branch now returns the persisted DB values (guarded for an absent nodeNum) instead of hardcoded false. The remote branch already did this.
  • Types: added the two fields to DbNode (src/db/types.ts), the service-facing DbNode (src/services/database.ts), and DeviceInfo (src/types/device.ts).

Testing

  • New round-trip test in src/db/repositories/nodes.test.ts (default false on insert, stored true round-trips, update path) plus extended PG/MySQL DDL.
  • Migration count/last-name test updated.
  • Full Vitest suite: 14680 passed, 0 failed. tsc --noEmit: 0 errors in any touched file.

Frontend

No frontend change needed — ConfigurationTab.tsx already reads config.localNodeInfo.isUnmessagable, and getCurrentConfig() returns localNodeInfo directly, so the now-populated flags surface to the checkbox.

🤖 Generated with Claude Code

…3684)

The Config tab "Unmessageable" checkbox always rendered unchecked for the
local node because User.is_unmessagable / is_licensed were never ingested
or persisted — the /admin/get-owner local branch hardcoded both to false.

Bug 1 (data plumbing) end to end:
- Schema: add isUnmessagable/isLicensed boolean columns to the nodes table
  for all three backends (SQLite 0/1, PG/MySQL boolean).
- Migration 101 (add_node_unmessagable): idempotent ALTER for SQLite/PG/MySQL,
  both columns default false/0. Registered + count test bumped 100->101.
- Repository: upsertNode now reads/writes both columns on the insert, upsert,
  and update paths; getNode already selects all columns so they round-trip.
  DbNode types (src/db/types.ts, src/services/database.ts, src/types/device.ts)
  gain the two fields.
- Ingest: meshtasticManager NodeInfo handler stores
  nodeData.isUnmessagable/isLicensed (?? false) from nodeInfo.user.
- localNodeInfo: the in-memory local node cache now carries both flags and is
  updated from the local NodeInfo, so getCurrentConfig().localNodeInfo (read by
  ConfigurationTab) reflects the real setting.
- get-owner: the local-node branch returns the persisted DB values instead of
  hardcoded false, guarded for when nodeNum is absent.
- Tests: nodes repository round-trip test for both flags (insert default,
  stored true, update path); DDL for PG/MySQL test backends extended.

Bug 2 (map indicator) is intentionally deferred.

Full vitest suite: 14680 passed, 0 failed. tsc: no new errors in touched files.

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

github-actions Bot commented Jun 23, 2026

Copy link
Copy Markdown

Claude finished @Yeraze's task —— View job


PR Review: fix(config): persist & surface local node isUnmessagable/isLicensed (#3684)

  • Read changed files
  • Review schema changes
  • Review migration
  • Review repository layer
  • Review ingest (meshtasticManager.ts)
  • Review server.ts get-owner fix
  • Review test coverage
  • Review types/service layer

Summary

This is a well-structured, minimal-footprint fix that follows existing codebase patterns closely. The root cause diagnosis is correct: the local-node branch of /admin/get-owner was hardcoding the flags to false, and the ingest path never persisted them. The fix addresses both sides properly.

Observations

Schema & Migration (src/db/schema/nodes.ts, 101_add_node_unmessagable.ts)

Schema changes are consistent across all three backends (SQLite INTEGER mode:'boolean', PostgreSQL BOOLEAN, MySQL BOOLEAN) with DEFAULT false/0. The migration is idempotent in all three dialects: SQLite try/catch on duplicate column, PostgreSQL IF NOT EXISTS, MySQL information_schema check. This follows the established pattern correctly.

Repository (src/db/repositories/nodes.ts)

Fields are correctly added to all three paths (update, insert, upsert conflict-set). The update path uses ?? existingNode.isUnmessagable which correctly preserves the prior value when the incoming nodeData doesn't include the field. Insert/upsert defaults to false.

Minor observation: upsertNodeSqlite (the legacy SQLite-sync path at line 1720) does NOT set isUnmessagable/isLicensed on update or insert. Since the callers of this legacy sync path appear to be the backup-restore import path (importNodeSqlite) and the shim facade, this could mean imported nodes lose these flags. It's a pre-existing pattern for this sync path (e.g. hideFromMap has the same gap), so this may be acceptable, but worth noting for completeness.

Ingest (src/server/meshtasticManager.ts lines 7929-7930, 8132-8134)

Both the initial NodeInfo processing path and the local-node update path now populate the flags. The PR correctly moves the localNodeInfo update outside the nameChanged guard (isLocked check) so the flag can update without requiring a name change — this is the right call since the User proto's capability flags are independent of the identity fields.

get-owner (src/server/server.ts)

The local-node branch now queries the DB for the persisted flags instead of hardcoding false. The guard for an absent nodeNum (if (localNodeInfo.nodeNum)) is correct — the local node row may not exist in the DB yet at the time of the request, and the fallback to false is a safe default.

Types (src/db/types.ts, src/services/database.ts, src/types/device.ts)

All three type definitions are updated consistently. No breaking changes to existing fields.

Test coverage (src/db/repositories/nodes.test.ts, src/db/migrations.test.ts)

The repository test covers the three key scenarios for the new columns: default-false on insert, stored-true round-trip, and update path. The migration count test is properly bumped to 101. PG and MySQL DDL strings in the test include the new columns.

Issues to Consider

  1. upsertNodeSqlite gap (minor): As noted above, the legacy SQLite sync upsert path (used by backup/import) doesn't propagate isUnmessagable/isLicensed. If a backup is exported and re-imported, these flags will be reset to false. This is consistent with how hideFromMap was handled previously, so it may be a known/accepted limitation, but worth a follow-up issue if these flags need to survive backup/restore.

  2. localNodeInfo in-memory vs. DB for getCurrentConfig(): The getCurrentConfig() method returns this.localNodeInfo for the in-memory snapshot. With this PR, localNodeInfo.isUnmessagable/isLicensed is set from NodeInfo during ingest, but the get-owner endpoint reads from the DB. There are now two code paths serving the same UI element — the Config tab uses get-owner (reads DB), but if getCurrentConfig() is used elsewhere to surface these flags, it would read localNodeInfo (in-memory). The in-memory and DB values should stay in sync as long as every ingest writes to both, which appears to be the case here. Just worth being aware of.

  3. No frontend change needed claim: The PR states no frontend change is needed because ConfigurationTab.tsx already reads config.localNodeInfo.isUnmessagable. That's accurate only if localNodeInfo in getCurrentConfig() includes these fields. Looking at the code, getCurrentConfig() returns this.localNodeInfo which now has these fields. The get-owner endpoint is what the Config tab actually calls for the checkbox. This is correct as-implemented.

Verdict

LGTM. The fix is correct, well-scoped, and follows codebase conventions throughout. The upsertNodeSqlite gap is a minor pre-existing pattern (not introduced by this PR) and doesn't affect the primary bug fix. Tests are adequate for the changes made.

@Yeraze Yeraze merged commit 453436a into main Jun 23, 2026
19 checks passed
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.

Config tab: Unmessageable checkbox always shows unchecked for local node; map has no unmessageable indicator

1 participant