Skip to content

feat(meshcore): channel CRUD + connect-time sync (phase 1/3)#3034

Merged
Yeraze merged 2 commits into
mainfrom
feat/meshcore-channels-phase-1
May 15, 2026
Merged

feat(meshcore): channel CRUD + connect-time sync (phase 1/3)#3034
Yeraze merged 2 commits into
mainfrom
feat/meshcore-channels-phase-1

Conversation

@Yeraze
Copy link
Copy Markdown
Owner

@Yeraze Yeraze commented May 15, 2026

Phase 1 of 3 for MeshCore channels support. See docs/meshcore-channels-plan.md for the full plan.

Summary

Wires meshcore.js's channel API (getChannels / setChannel / deleteChannel) into MeshCoreManager and mirrors the device's channel list into the shared channels table on connect. Phases 2 and 3 (display + Configuration UI) are separate PRs.

What changed

  • meshcoreNativeBackend.ts — three new bridge-shaped commands: get_channels, set_channel, delete_channel. set_channel validates idx range and that the hex-encoded secret decodes to exactly 16 bytes (AES-128).
  • meshcoreManager.ts — public listChannels(), setChannel(idx, name, secretHex), deleteChannel(idx), plus a private syncChannelsFromDevice() that runs after refreshContacts on the Companion connect path. Failure is non-fatal so a half-configured device still connects.
  • channels repositorycleanupInvalidChannels is now source-type aware. The 0–7 slot cap is a Meshtastic-only convention; MeshCore devices report a device-dependent number of channels. Channels owned by a MeshCore source are exempt; Meshtastic-owned and legacy NULL-sourceId rows still get the cap applied.

Why this design

  • Reused the existing channels table, no schema migration. Shared fields (id, name, psk, sourceId) map 1:1 to MeshCore's {channelIdx, name, secret}. Meshtastic-only fields stay NULL for MeshCore rows.
  • AES-128 secret stored as base64 in the existing psk column. The wire/API form stays hex (matches the MeshCore CLI convention).
  • No push events in MeshCore for channel changes, so we sync on connect and after every local write. A "refresh from device" UI affordance can be added later if out-of-band edits become a pain point.

Test plan

  • npx vitest run4943 PASS / 0 FAIL (+17 new tests over main)
  • npm run build — clean
  • tsc --noEmit -p tsconfig.server.json — clean
  • Lint error count unchanged from main (650 pre-existing)
  • Manual: connect a MeshCore Companion device, confirm SELECT * FROM channels WHERE sourceId = '<mc-source>' reflects the device's channel list after connect

New tests

  • meshcoreNativeBackend.test.ts — get_channels mapping, set_channel validation (idx range, 16-byte secret), delete_channel pass-through
  • meshcoreManager.channels.test.ts (new file) — listChannels, setChannel/deleteChannel re-sync, syncChannelsFromDevice shape + scoping + empty-secret handling
  • channels.test.ts (repo) — new "preserves out-of-range channels owned by a MeshCore source" case

Not in this PR (phases 2 & 3)

  • Reading the synced channel list in MeshCoreChannelsView (still hardcoded to one "Public" channel)
  • Configuration UI for create/edit/delete
  • Source-type-aware rendering in ChannelsConfigSection

🤖 Generated with Claude Code

Wires the meshcore.js channel API into MeshCoreManager and mirrors the
device's channel list into the shared `channels` table on connect.

Backend
- meshcoreNativeBackend.ts: bridge-shaped get_channels / set_channel /
  delete_channel commands. set_channel validates idx range and that the
  hex-encoded secret decodes to exactly 16 bytes.
- meshcoreManager.ts: listChannels / setChannel / deleteChannel /
  syncChannelsFromDevice. Sync runs after refreshContacts on the
  Companion connect path; failure is non-fatal. AES-128 secrets are
  stored as base64 in the existing `channels.psk` column; Meshtastic-
  only fields (role, uplinkEnabled, downlinkEnabled, positionPrecision)
  are explicitly null for MeshCore rows.
- channels.ts repository: cleanupInvalidChannels is now source-type-
  aware. The 0–7 slot cap is a Meshtastic-only convention; MeshCore
  devices report a device-dependent number of channels. Channels owned
  by a MeshCore source are exempt; Meshtastic-owned and legacy NULL-
  sourceId rows still get the cap applied.

Tests (+17)
- meshcoreNativeBackend.test.ts: get_channels mapping, set_channel
  validation (idx range, 16-byte secret), delete_channel pass-through.
- meshcoreManager.channels.test.ts (new file): listChannels, setChannel
  + deleteChannel re-sync behaviour, syncChannelsFromDevice upsert
  shape + sourceId scoping + empty-secret handling.
- channels.test.ts (repository): new
  "preserves out-of-range channels owned by a MeshCore source" case.
  Existing tests now also create a sources table for the
  cleanupInvalidChannels JOIN.

No schema migration required (reuses existing channels columns).
This is phase 1 of 3 (see docs/meshcore-channels-plan.md):
  phase 2 = MeshCoreChannelsView reads the synced list,
  phase 3 = Configuration UI for create/edit/delete.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 15, 2026

Claude finished @Yeraze's task —— View job


PR Review: MeshCore Channels Phase 1

Comprehensive review completed. This is a well-implemented Phase 1 that establishes a solid foundation for MeshCore channel management.

Strengths

Architecture & Design

  • Clean separation of concerns: Native backend handles protocol, manager handles business logic, repository handles persistence
  • Source-type awareness: Properly scoped by sourceId and respects MeshCore vs Meshtastic differences
  • Backward compatibility: Reuses existing channels table without schema migration, nulling Meshtastic-only fields
  • Device-first approach: Device is authoritative; local state syncs after every write operation

Implementation Quality

  • Robust validation: Index range (0-255) and secret length (16 bytes) validation with clear error messages
  • Proper error handling: Non-fatal sync failures during connect won't break the connection
  • Consistent patterns: Follows existing manager conventions (Companion-only restrictions, bridge commands)
  • Format conversion: Clean hex ↔ base64 conversion at API boundaries

Test Coverage

  • Comprehensive backend tests: Validates command mapping, error cases, and boundary conditions
  • Manager-level tests: Tests sync logic, re-sync after writes, and source isolation
  • Repository tests: New test ensures MeshCore channels aren't subject to Meshtastic's 8-slot limit

🔍 Security Assessment

Acceptable Risks

  • Cleartext secrets on host link: Documented design decision - secrets are encrypted on mesh, cleartext between MeshMonitor and local MeshCore device
  • Secret re-broadcast: When editing channel names, secrets are re-sent to device (documented behavior)

Good Security Practices

  • Input validation: Proper bounds checking and data validation
  • Source isolation: Channels properly scoped by sourceId
  • Permission model: Will integrate with existing per-source permission system in Phase 3

Performance Considerations

Efficient Design

  • Sync on connect only: Channel enumeration happens once at connection time, not on every poll
  • Minimal overhead: Simple CRUD operations with standard database patterns
  • Bounded operations: Channel enumeration has natural upper limit (device will error when exhausted)

Future Considerations

  • May want a "refresh from device" UI affordance if out-of-band edits become problematic
  • Current design correctly prioritizes simplicity over optimization

🐛 No Significant Issues Found

  • Code quality: Clean, readable, follows established patterns
  • Error handling: Appropriate error propagation and logging
  • Type safety: Proper TypeScript usage throughout
  • Testing: Good coverage of happy path and edge cases

📝 Minor Suggestions

  1. Documentation: The inline comments are excellent - particularly the explanation of why Meshtastic-only fields are nulled
  2. Logging: Good use of debug-level logging for sync operations
  3. Testing: The source-type-aware cleanup test is particularly well-designed

🎯 Phase 1 Acceptance Criteria Met

✅ Backend channel CRUD methods implemented
✅ Connect-time sync from device to database
✅ Source-type-aware repository cleanup
✅ Comprehensive test coverage
✅ Clean build and type checking
✅ No UI changes (proper phase separation)

Final Recommendation: ✅ APPROVE

This PR successfully delivers Phase 1 with high code quality, proper testing, and no significant issues. The architecture sets up Phases 2 and 3 well. Ready to merge.


Job Run

@github-actions
Copy link
Copy Markdown

System Test Results

MeshMonitor System Test Results

Test Run: 2026-05-15 18:15:20 EDT

Test Summary

Test Suite Result
Configuration Import ✅ PASSED
Quick Start Test ✅ PASSED
Security Test ✅ PASSED
V1 API Test ✅ PASSED
Reverse Proxy Test ✅ PASSED
Reverse Proxy + OIDC ✅ PASSED
Virtual Node CLI Test ✅ PASSED
Backup & Restore Test ✅ PASSED
Database Migration Test ✅ PASSED
DB Backing Consistency ✅ PASSED
API Exercise (3 DBs) ✅ PASSED

✅ Overall Result: PASSED

All deployment configurations are working correctly!

Test Details

Configuration Import:

  • Tests configuration import and device reboot cycle
  • Verifies channel roles, PSKs, and LoRa configuration
  • Note: Channel name verification skipped due to architectural limitation

Quick Start Test:

  • Zero-config deployment (no SESSION_SECRET or COOKIE_SECURE required)
  • HTTP access without HSTS
  • Auto-generated admin user with default credentials
  • Session cookies work over HTTP
  • Meshtastic node connection and message exchange verified

Security Test:

  • Verifies Node IP address hidden from anonymous users in API responses
  • Verifies MQTT configuration hidden from anonymous users
  • Verifies Node IP address visible to authenticated users
  • Verifies MQTT configuration visible to authenticated users
  • Verifies protected endpoints require authentication

V1 API Test:

  • Tests v1 REST API endpoints with Bearer token authentication
  • Verifies Bearer token requests bypass CSRF protection
  • Verifies POST/PUT/DELETE work without CSRF token when using Bearer auth
  • Verifies session-based requests still require CSRF token

Reverse Proxy Test:

  • Production deployment with COOKIE_SECURE=true
  • HTTPS-ready configuration
  • Trust proxy enabled for reverse proxy compatibility
  • CORS configured for HTTPS domain
  • Meshtastic node connection and message exchange verified

Reverse Proxy + OIDC Test:

  • OIDC authentication integration
  • Mock OIDC provider health checks
  • Authorization flow and session creation
  • Hybrid mode (OIDC + local auth)
  • Meshtastic node connection verified

Virtual Node CLI Test:

  • Virtual Node Server enabled on TCP port 4404
  • Meshtastic Python client successfully connects
  • Node data download and synchronization verified
  • Test message sent on gauntlet channel (index 3)
  • Message delivery confirmed via Web UI API
  • Virtual Node Server connection logging verified

Backup & Restore Test:

  • System backup created from running dev container
  • New container spun up with RESTORE_FROM_BACKUP env var
  • Data integrity verified (node count, message count, settings)
  • Restore event logged in audit log
  • Dev container unaffected by restore test

Database Migration Test:

  • SQLite to PostgreSQL migration verified
  • SQLite to MySQL migration verified
  • Data integrity confirmed for both target databases
  • Row counts match between source and target

DB Backing Consistency Test:

  • SQLite, PostgreSQL, and MySQL backends tested with same device
  • Node counts within ±10 across all three backends
  • Favorite counts identical across all backends
  • Key station verified as favorite on all backends

CI on PR #3034 failed the new "preserves out-of-range channels owned by
a MeshCore source" test on the PostgreSQL backend:

  error: column "sourceid" of relation "channels" does not exist

PostgreSQL folds unquoted identifiers to lowercase; the schema defines
the column as "sourceId" (quoted), so the raw INSERT also needs the
quotes. SQLite is identifier-case-insensitive and the original test
worked there. MySQL's default sql_mode treats "..." as a string literal
rather than a delimited identifier, so the column reference must stay
bare for that backend.

Branches the column reference by backend.dbType. SQLite + PG + MySQL
now all pass the test locally; verified PG against a fresh postgres:16
container.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 15, 2026

Claude finished @Yeraze's task —— View job


PR Review: MeshCore Channels Phase 1

Comprehensive review completed. This is a well-implemented Phase 1 that establishes a solid foundation for MeshCore channel management.

Strengths

Architecture & Design

  • Clean separation of concerns: Native backend handles protocol, manager handles business logic, repository handles persistence
  • Source-type awareness: Properly scoped by sourceId and respects MeshCore vs Meshtastic differences
  • Backward compatibility: Reuses existing channels table without schema migration, nulling Meshtastic-only fields
  • Device-first approach: Device is authoritative; local state syncs after every write operation

Implementation Quality

  • Robust validation: Index range (0-255) and secret length (16 bytes) validation with clear error messages
  • Proper error handling: Non-fatal sync failures during connect won't break the connection
  • Consistent patterns: Follows existing manager conventions (Companion-only restrictions, bridge commands)
  • Format conversion: Clean hex ↔ base64 conversion at API boundaries

Test Coverage

  • Comprehensive backend tests: Validates command mapping, error cases, and boundary conditions
  • Manager-level tests: Tests sync logic, re-sync after writes, and source isolation
  • Repository tests: New test ensures MeshCore channels aren't subject to Meshtastic's 8-slot limit

🔍 Security Assessment

Acceptable Risks

  • Cleartext secrets on host link: Documented design decision - secrets are encrypted on mesh, cleartext between MeshMonitor and local MeshCore device
  • Secret re-broadcast: When editing channel names, secrets are re-sent to device (documented behavior)

Good Security Practices

  • Input validation: Proper bounds checking and data validation
  • Source isolation: Channels properly scoped by sourceId
  • Permission model: Will integrate with existing per-source permission system in Phase 3

Performance Considerations

Efficient Design

  • Sync on connect only: Channel enumeration happens once at connection time, not on every poll
  • Minimal overhead: Simple CRUD operations with standard database patterns
  • Bounded operations: Channel enumeration has natural upper limit (device will error when exhausted)

Future Considerations

  • May want a "refresh from device" UI affordance if out-of-band edits become problematic
  • Current design correctly prioritizes simplicity over optimization

🐛 No Significant Issues Found

  • Code quality: Clean, readable, follows established patterns
  • Error handling: Appropriate error propagation and logging
  • Type safety: Proper TypeScript usage throughout
  • Testing: Good coverage of happy path and edge cases

📝 Minor Suggestions

  1. Documentation: The inline comments are excellent - particularly the explanation of why Meshtastic-only fields are nulled
  2. Logging: Good use of debug-level logging for sync operations
  3. Testing: The source-type-aware cleanup test is particularly well-designed

🎯 Phase 1 Acceptance Criteria Met

✅ Backend channel CRUD methods implemented
✅ Connect-time sync from device to database
✅ Source-type-aware repository cleanup
✅ Comprehensive test coverage
✅ Clean build and type checking
✅ No UI changes (proper phase separation)

Final Recommendation: ✅ APPROVE

This PR successfully delivers Phase 1 with high code quality, proper testing, and no significant issues. The architecture sets up Phases 2 and 3 well. Ready to merge.


Job Run

@github-actions
Copy link
Copy Markdown

System Test Results

MeshMonitor System Test Results

Test Run: 2026-05-15 19:13:30 EDT

Test Summary

Test Suite Result
Configuration Import ✅ PASSED
Quick Start Test ✅ PASSED
Security Test ✅ PASSED
V1 API Test ✅ PASSED
Reverse Proxy Test ✅ PASSED
Reverse Proxy + OIDC ✅ PASSED
Virtual Node CLI Test ✅ PASSED
Backup & Restore Test ✅ PASSED
Database Migration Test ✅ PASSED
DB Backing Consistency ✅ PASSED
API Exercise (3 DBs) ✅ PASSED

✅ Overall Result: PASSED

All deployment configurations are working correctly!

Test Details

Configuration Import:

  • Tests configuration import and device reboot cycle
  • Verifies channel roles, PSKs, and LoRa configuration
  • Note: Channel name verification skipped due to architectural limitation

Quick Start Test:

  • Zero-config deployment (no SESSION_SECRET or COOKIE_SECURE required)
  • HTTP access without HSTS
  • Auto-generated admin user with default credentials
  • Session cookies work over HTTP
  • Meshtastic node connection and message exchange verified

Security Test:

  • Verifies Node IP address hidden from anonymous users in API responses
  • Verifies MQTT configuration hidden from anonymous users
  • Verifies Node IP address visible to authenticated users
  • Verifies MQTT configuration visible to authenticated users
  • Verifies protected endpoints require authentication

V1 API Test:

  • Tests v1 REST API endpoints with Bearer token authentication
  • Verifies Bearer token requests bypass CSRF protection
  • Verifies POST/PUT/DELETE work without CSRF token when using Bearer auth
  • Verifies session-based requests still require CSRF token

Reverse Proxy Test:

  • Production deployment with COOKIE_SECURE=true
  • HTTPS-ready configuration
  • Trust proxy enabled for reverse proxy compatibility
  • CORS configured for HTTPS domain
  • Meshtastic node connection and message exchange verified

Reverse Proxy + OIDC Test:

  • OIDC authentication integration
  • Mock OIDC provider health checks
  • Authorization flow and session creation
  • Hybrid mode (OIDC + local auth)
  • Meshtastic node connection verified

Virtual Node CLI Test:

  • Virtual Node Server enabled on TCP port 4404
  • Meshtastic Python client successfully connects
  • Node data download and synchronization verified
  • Test message sent on gauntlet channel (index 3)
  • Message delivery confirmed via Web UI API
  • Virtual Node Server connection logging verified

Backup & Restore Test:

  • System backup created from running dev container
  • New container spun up with RESTORE_FROM_BACKUP env var
  • Data integrity verified (node count, message count, settings)
  • Restore event logged in audit log
  • Dev container unaffected by restore test

Database Migration Test:

  • SQLite to PostgreSQL migration verified
  • SQLite to MySQL migration verified
  • Data integrity confirmed for both target databases
  • Row counts match between source and target

DB Backing Consistency Test:

  • SQLite, PostgreSQL, and MySQL backends tested with same device
  • Node counts within ±10 across all three backends
  • Favorite counts identical across all backends
  • Key station verified as favorite on all backends

@Yeraze Yeraze merged commit d397005 into main May 15, 2026
20 checks passed
Yeraze added a commit that referenced this pull request May 16, 2026
Bumps the version across all five canonical files (package.json,
package-lock.json, helm/meshmonitor/Chart.yaml,
desktop/src-tauri/tauri.conf.json, desktop/package.json) for the
4.5.2 release.

Headline: MeshCore channel support is the major feature in this
release.

- Phase 1 (#3034) — MeshCoreManager exposes listChannels / setChannel
  / deleteChannel and mirrors the device's channel list into the
  shared `channels` table on connect.
- Phase 2 (#3038) — MeshCoreChannelsView renders one tab per channel
  from the synced list; the send path is channel-idx aware.
- Phase 3 (#3039) — Configuration UI adds, edits, regenerates the
  secret of, and deletes channels on the device; PUT/DELETE
  /api/channels/:id route writes through to MeshCore via the correct
  registry.
- Polish (#3040) — empty-slot filter (the MAX_CHANNELS=40 leak),
  DB reconcile for stale empty rows, DM-view filters out channel
  pseudo-pubkeys and the local node from the peer sidebar.

Also includes the per-node position-precision fix (#3033) from this
release window.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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