Skip to content

feat(meshcore): virtual node — connect the MeshCore app to a node through MeshMonitor over WiFi (#3535)#3540

Merged
Yeraze merged 7 commits into
mainfrom
feat/3535-meshcore-virtual-node-phase0
Jun 18, 2026
Merged

feat(meshcore): virtual node — connect the MeshCore app to a node through MeshMonitor over WiFi (#3535)#3540
Yeraze merged 7 commits into
mainfrom
feat/3535-meshcore-virtual-node-phase0

Conversation

@Yeraze

@Yeraze Yeraze commented Jun 18, 2026

Copy link
Copy Markdown
Owner

Summary

Implements a MeshCore virtual node (#3535): MeshMonitor exposes a MeshCore node it already manages as a TCP companion endpoint, so the MeshCore mobile app can connect to it over WiFi and use the node as if it were local — see contacts/channels, and send/receive messages — when the phone has no direct BLE/USB range.

This is the device end of the MeshCore companion protocol. Since MeshMonitor already holds the node's single companion slot (via meshcore.js as a client), the server synthesizes reads from mirrored state and forwards writes to the real node — structurally parallel to the existing Meshtastic virtualNodeServer.ts, with MeshCore's binary frame format instead of protobuf.

Verified end-to-end against the real meshcore-flutter app over WiFi: connect → identity → 94 contacts + channels → channel send → auto-responder reply received back in the app.

What's included (Phases 0–2)

  • Phase 0 — handshake. AppStart→SelfInfo, DeviceQuery→DeviceInfo, GetDeviceTime→CurrTime, SetDeviceTime→Ok.
  • Phase 1 — read mirror. GetContacts (real contacts), GetChannel→ChannelInfo, GetBatteryVoltage, SetFloodScope→Ok (no-op), and live incoming messages via the MsgWaiting push → SyncNextMessageContactMsgRecv/ChannelMsgRecv.
  • Phase 2 — sends. SendChannelTxtMsg/SendTxtMsg forwarded to the node; DM key-prefixes resolved to full contact keys.
  • UI. A "Virtual Node" enable + listening-port control on the MeshCore source form (DashboardPage); config persisted in sources.config.virtualNode and parsed in meshcoreRegistry. The sourceRoutes validation gate now allows meshcore (not just meshtastic_tcp).
  • Lifecycle. meshcoreManager starts the server after the local node is known and stops it on disconnect.

Files

  • New: meshcoreCompanionCodec.ts (pure protocol: frame parse/build, command decode, response/push encoders), meshcoreVirtualNodeServer.ts (per-source TCP server), + tests for both, + sourceRoutes.virtualNode.test.ts.
  • Touched: meshcoreManager.ts, meshcoreRegistry.ts, routes/sourceRoutes.ts, pages/DashboardPage.tsx, docker-compose.dev.yml (exposes dev port 5000).
  • Design: docs/internal/dev-notes/MESHCORE_VIRTUAL_NODE_DESIGN.md.

Testing

  • Every wire encoder is round-tripped through meshcore.js's own decoders — a deviceless fidelity guarantee that our byte layout matches what the app expects.
  • The server is covered by loopback-socket integration tests (handshake, contacts, channels, battery, send forwarding, live RX, echo suppression).
  • Full Vitest suite green (0 failures, 2308 suites); tsc clean.

Two protocol subtleties found in live testing (documented + regression-tested)

  1. Channel send acks Ok(0), DM send acks Sent(6) — replying Sent to a channel send hangs the app's send promise.
  2. The channel-N marker is in toPublicKey for sent messages but fromPublicKey for received — incoming channel replies must be detected in both fields or they get mis-encoded as a DM and dropped.

Not included (Phase 3 — follow-up)

  • Admin-gated config writes (SetRadioParams, SetAdvertName, …).
  • DM delivery receipts: the node returns a real expectedAckCrc that sendMessage currently logs but discards; relaying it would let the SendConfirmed push light up the app's delivered tick. DMs currently show sent but not delivered.

Notes

  • TCP/WiFi only (no BLE — MeshMonitor is typically headless). Default VN port 5000, distinct from the node's own 4403 companion port.
  • Per-source scoped; reads scope by sourceId.

🤖 Generated with Claude Code

Yeraze and others added 6 commits June 18, 2026 10:53
…3535)

Expose a MeshCore node that MeshMonitor already manages as a virtual node the
MeshCore mobile app can connect to over WiFi/TCP. Phase 0 brings the app to
"connected, identity shown, empty mailbox"; reads are synthesized from the
manager's local-node state and nothing is forwarded to the real node yet
(Phase 2). Structure mirrors the proven Meshtastic virtualNodeServer.ts.

- meshcoreCompanionCodec.ts: the device end of the companion frame protocol —
  frame parse/build (0x3c app→node / 0x3e node→app, u16 LE length), command
  decode, and response encoders (SelfInfo, CurrTime, DeviceInfo, ContactsStart/
  EndOfContacts, NoMoreMessages, Ok, Err) derived by inverting meshcore.js's
  decoders. Pure protocol + unit conversions, no DB/IO.
- meshcoreVirtualNodeServer.ts: per-source TCP server. AppStart→SelfInfo (from
  getLocalNode), GetDeviceTime→CurrTime, DeviceQuery→DeviceInfo, GetContacts→
  empty, SyncNextMessage→NoMoreMessages, unsupported→Err(UnsupportedCmd).
  Per-client framing buffers, audit logging, inactivity cleanup.
- meshcoreManager.ts: virtualNode config field; start after local-node refresh,
  stop on disconnect; isVirtualNodeServerRunning().
- meshcoreRegistry.ts: parse sources.config.virtualNode into runtime config
  (default port 5000, distinct from the node's 4403 companion port).
- DashboardPage.tsx: "Virtual Node" fieldset in the MeshCore source form
  (enable toggle + listening port) with save-path validation + plumbing.

Tests: codec encoders round-trip through meshcore.js's OWN decoders (deviceless
fidelity check); server handshake driven over a real loopback socket. Full
Vitest suite green (0 failures). Remaining exit criterion — real app over WiFi —
needs hardware. Design: docs/internal/dev-notes/MESHCORE_VIRTUAL_NODE_DESIGN.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ive RX (#3535)

Observed the real meshcore-flutter command stream against the Phase 0 server:
DeviceQuery → AppStart → GetBatteryVoltage → SetDeviceTime → GetContacts →
GetChannel → SyncNextMessage → SetFloodScope(54). Phase 0 returned empty/Err
for the data-bearing commands, so the app showed an empty node list, no
channels, and gave up. Phase 1 answers them from MeshMonitor's mirrored state.

Codec: add encoders (round-tripped through meshcore.js's own decoders) for
Contact(3), ChannelInfo(18), BatteryVoltage(12), ContactMsgRecv(7),
ChannelMsgRecv(8), and the MsgWaiting(0x83) push; decode GetChannel's index;
add hexToBytes / toEpochSeconds helpers and the full CommandCodes table.

Server:
- GetContacts → ContactsStart · N×Contact · EndOfContacts from getContacts()
  (out-path hop bytes + OUT_PATH_UNKNOWN handling, name, position, timestamps).
- GetChannel(idx) → ChannelInfo from the synced channels table (base64 psk →
  16-byte secret), Err(NotFound) past the last slot so the app stops iterating.
- GetBatteryVoltage → BatteryVoltage from the local node telemetry.
- SetFloodScope → Ok (read-only no-op) so the app doesn't treat it as fatal.
- Live RX: subscribe to the manager's 'message' event; enqueue per-client and
  push MsgWaiting, drained via SyncNextMessage as ContactMsgRecv/ChannelMsgRecv.
  Self-originated messages are filtered so sends aren't echoed back. Per-client
  queue seeds empty (no history replay → no dupes on reconnect).
- Per-command logging (◀ cmd) retained for field debugging.

Also: allow virtualNode config on meshcore sources (sourceRoutes gate previously
meshtastic_tcp only) + regression test; expose port 5000 in docker-compose.dev.

Sends (SendTxtMsg/SendChannelTxtMsg → real node) remain Phase 2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…#3535)

The app's send (SendChannelTxtMsg, and SendTxtMsg for DMs) hit the Phase-1
Err default. Phase 2 forwards them to the physical node via the manager.

Codec: decode SendTxtMsg ([txtType][attempt][senderTs:u32][pubKeyPrefix:6][text])
and SendChannelTxtMsg ([txtType][channelIdx][senderTs:u32][text]); add
encodeSent(result, expectedAckCrc, estTimeout) — round-tripped through
meshcore.js's decoder.

Server:
- SendChannelTxtMsg → manager.sendMessage(text, undefined, channelIdx) → Sent(0).
- SendTxtMsg → resolve the 6-byte key prefix against getContacts() to the full
  contact key → manager.sendMessage(text, fullKey) → Sent(0); Err(NotFound) if
  the prefix matches no known contact.
- Node-side failure / exception → Err(BadState). Messaging is not admin-gated.

expectedAckCrc is 0 for now (no synchronous CRC from the proxied send); the
SendConfirmed(0x82) delivery push is a later refinement. Config writes remain
Phase 3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…them sent (#3535)

Testing surfaced channel messages forwarding to the node but never showing as
sent in the app. Root cause: the companion protocol uses different ack
responses per send type — meshcore.js's sendChannelTextMessage awaits Ok(0)
(fire-and-forget broadcast) while sendTextMessage (DM) awaits Sent(6) (carries
the ack CRC). Phase 2 replied Sent(6) to channel sends, so the app's send
promise stayed pending forever.

Channel send → Ok(0); DM send keeps Sent(6). Test updated accordingly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Incoming replies (e.g. an auto-responder's "🤖 Copy") reached the VN source but
never showed in the app. Root cause: the manager tags channel messages with the
synthetic `channel-N` marker in toPublicKey for messages we SENT but in
fromPublicKey for messages we RECEIVED (toPublicKey is unset on RX). The VN only
checked toPublicKey, so incoming channel messages were mis-encoded as a
ContactMsgRecv with a garbage key prefix (hexToBytes('channel-1')) and the app
dropped them.

- encodeIncomingMessage: detect the channel marker in fromPublicKey OR
  toPublicKey, so received channel messages encode as ChannelMsgRecv.
- handleIncomingMessage: also skip our own channel transmissions heard back
  over the air (fromName === local node name) so the app — which shows channel
  sends optimistically — doesn't get a duplicate.

Tests: incoming ChannelMsgRecv delivery + own-name echo suppression.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…3535)

Drop the high-frequency per-command (◀ cmd) and per-read (contacts/channel)
traces to debug; keep lifecycle + send-forward logs at info. Update the design
doc to mark Phases 0-2 implemented/verified and record the two protocol
subtleties found in testing (channel→Ok vs DM→Sent; channel-N marker field by
direction).

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

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown

Claude finished @Yeraze's task —— View job


PR Review: MeshCore Virtual Node (Phases 0–2)

Tasks:

  • Gather context and understand the PR
  • Review meshcoreCompanionCodec.ts (protocol layer)
  • Review meshcoreVirtualNodeServer.ts (TCP server)
  • Review test files
  • Review modified files (manager, registry, routes, UI)
  • Compile and post final review

Overall, this is a high-quality implementation with clear documentation, strong test coverage, and good separation of concerns. The codec/server split is clean and mirrors the existing Meshtastic VN architecture. Here are my findings:

Bugs / Potential Issues

1. Double import of databaseService in meshcoreVirtualNodeServer.ts

src/server/meshcoreVirtualNodeServer.ts lines 4 and 6 both import databaseService from the same module:

import databaseService from '../services/database.js';         // line 4
import databaseServiceDefault from '../services/database.js';  // line 6

Line 4 is used only for auditLogAsync; line 6 is used as the ChannelsDb default. These should be one import. The extra binding doesn't cause a runtime bug but is dead code.

2. encodeDeviceInfo uses writeInt8 for firmware version instead of writeUInt8

src/server/meshcoreCompanionCodec.ts:296:

head.writeInt8(info.firmwareVer | 0, 1);

If firmwareVer is above 127 (e.g. version 128+), this will silently produce a negative wire value. Should be writeUInt8 or a & 0xff clamp, consistent with how txPower / maxTxPower are handled as & 0xff at lines 261–262.

3. handleGetChannel matches on ch.id rather than channel index

src/server/meshcoreVirtualNodeServer.ts:395:

const row = channels.find((ch) => ch.id === channelIdx);

If the DB auto-increments id (e.g. row IDs are 1, 2, 3…), this will fail to find channel index 0 and always return Err(NotFound). The app requests slots by zero-based index; the DB id is likely a surrogate key. This should match on a channelIndex / slot field rather than id. This is a functional bug that would prevent channel delivery to the app after initial handshake.

4. encodeOk returns the wrong response for SendChannelTxtMsg

The PR description correctly notes that channel sends should reply Ok(0), not Sent(6), and the code does this. But the test comment at line 291 says:

"The app's sendChannelTextMessage awaits Ok(0), not Sent(6)"

This is correct, but worth highlighting that if formVnAllowAdmin is always hardcoded to false at DashboardPage.tsx:515 (allowAdminCommands: false) while the UI checkbox (formVnAllowAdmin) is populated on edit (line 381), the admin checkbox has no effect for MeshCore sources — a minor UX inconsistency compared to Meshtastic sources (line 538 which does pass formVnAllowAdmin).

5. Race: handleClientDisconnect may be called after client is already removed

src/server/meshcoreVirtualNodeServer.ts:237-241:

socket.on('close', () => this.handleClientDisconnect(clientId));
socket.on('error', (error) => {
  logger.error();
  this.handleClientDisconnect(clientId);
});

An error event is followed by a close event on a socket. handleClientDisconnect already guards with if (!client) return (line 247) so this is harmless, but the audit log will fire for close even though the client was already removed by error. This could produce duplicate audit entries in edge cases.

6. cleanupInactiveClients calls handleClientDisconnect after socket.destroy()

src/server/meshcoreVirtualNodeServer.ts:589-592:

client.socket.destroy();
this.handleClientDisconnect(clientId);

socket.destroy() will emit a close event asynchronously, which will trigger the registered close handler and call handleClientDisconnect a second time. The guard prevents double-cleanup, but the audit log may fire twice. Consider removing the explicit handleClientDisconnect call here and relying solely on the close event, or explicitly calling socket.removeAllListeners() first.

Design / Code Quality

7. pubKeyHexToBytes silently truncates odd-length hex

src/server/meshcoreCompanionCodec.ts:496:

Buffer.from(clean.length % 2 === 0 ? clean : clean.slice(0, -1), 'hex').copy(out, 0, 0, 32);

Stripping the last nibble for an odd-length key is lossy and silent. The typical input is a 64-char hex string; odd-length would only happen from malformed data. A debug log or zero-fill for the trailing byte would make bad-key cases easier to diagnose.

8. TestClient.expectFrames is potentially racy

src/server/meshcoreVirtualNodeServer.test.ts:131-133:

expectFrames(n: number): Promise<Buffer[]> {
  return Promise.all(Array.from({ length: n }, () => new Promise<Buffer>((resolve) => this.waiters.push(resolve))));
}

If frames arrive before expectFrames is called, onData will try to call this.waiters.shift() on an empty array (shift returns undefined and the frames are silently dropped). The tests work because commands are sent after setting up the promise array, but it's a subtle ordering dependency. Documenting this or using a queue approach would be more robust.

9. No test for allowAdminCommands=true forwarding (Phase 3 gap)

The allowAdminCommands option is wired through but currently always returns Err(UnsupportedCmd) for admin commands — that's expected. A test asserting that an admin command is rejected (not silently no-op'd) when allowAdminCommands=false would protect Phase 3 from accidentally enabling admin forwarding without the gate check. Currently only SetFloodScope (which is explicitly whitelisted to Ok) has a test.

10. Channel echo suppression uses fromName equality (fragile)

src/server/meshcoreVirtualNodeServer.ts:500:

if (this.isChannelMessage(msg) && local?.name && msg.fromName === local.name) return;

Node names are user-configurable strings. If two nodes on the same channel share a name, one node's messages will be incorrectly suppressed. The PR acknowledges this is the only available signal since channel packets carry no sender key, and it's documented in the design doc. A comment here pointing to the design doc limitation would help future maintainers.

Security

11. No rate-limiting on client connections

The server accepts unlimited TCP connections (only bounded by OS limits). A malicious client on the local network could exhaust file descriptors by opening thousands of connections. A configurable maxClients (defaulting to something small like 5) would be a low-cost hardening step. The Meshtastic virtualNodeServer.ts could be checked to see if it has the same gap.

12. auditLogAsync logs client IP directly from socket.remoteAddress

socket.remoteAddress is safe (it's the kernel-supplied socket address, not user input), but JSON.stringify({ clientId, sourceId, ip: socket.remoteAddress || 'unknown' }) includes the raw IP in the audit detail field. This is consistent with the existing Meshtastic VN behavior, so it's fine — just noting it for privacy awareness.

Test Coverage

Test coverage is good overall. Specific gaps:

  • No test for the buffer-overflow eviction path (client.buffer.length > MAX_FRAME_BYTES * 4).
  • No test for cleanupInactiveClients (the 5-minute inactivity eviction timer).
  • No test for the stop() path with connected clients (verifying they're cleanly destroyed).
  • The MeshCore formVnAllowAdmin / allowAdminCommands: false hardcode for MeshCore sources (DashboardPage.tsx:515) has no coverage.

Minor

  • src/server/meshcoreVirtualNodeServer.ts:161: emoji in a logger.info call is fine for dev visibility but may look odd in structured JSON log outputs if a log shipper is in use.
  • SEND_EST_TIMEOUT_MS = 8000 (line 428) is undocumented. A brief comment explaining where this value comes from (firmware default? app default?) would help.

Summary: The dual-import on line 6 and the ch.id === channelIdx channel-lookup bug (item 3) are the only issues likely to cause observable failures. The writeInt8 for firmware version (item 2), the allowAdminCommands hardcode for MeshCore (item 4), and the cleanup race (item 6) are lower-priority but worth fixing before Phase 3 lands. Everything else is polish.

The skill stopped at green CI. Add a review-feedback phase: gather automated
(Claude Code Review bot) + human review comments, triage each concern (fix /
apply / reply-why-not / escalate), close the loop on threads, and re-verify CI
after any fix. Updates the report template (Review Feedback + Open Items) and
rules (green CI isn't the finish line; never silently ignore a concern).
Both the commands/ and skills/ copies kept in sync.

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

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown

Claude finished @Yeraze's task —— View job


PR Review: MeshCore Virtual Node (Phases 0–2)

Tasks:

  • Gather context and understand the PR
  • Review meshcoreCompanionCodec.ts (protocol layer)
  • Review meshcoreVirtualNodeServer.ts (TCP server)
  • Review test files
  • Review modified files (manager, registry, routes, UI)
  • Compile and post final review

Overall, this is a well-structured implementation with clean separation between the codec and server layers, strong round-trip test coverage, and thoughtful documentation of protocol subtleties. The architecture mirrors the existing Meshtastic VN cleanly. Below are my findings ordered by severity.

Bugs / Issues That Can Cause Failures

1. handleGetChannel matches on ch.id rather than channel index (meshcoreVirtualNodeServer.ts:395)

const row = channels.find((ch) => ch.id === channelIdx);

getAllChannels() returns DB rows where id is a surrogate auto-increment key. Channel index 0 will return Err(NotFound) unless the first row's auto-increment ID happens to be 0. The app iterates channels by zero-based index — this means channel delivery is broken after the handshake for most deployments. The lookup should match on a channelIndex/slot column instead of id. The test at line 71 sets { id: 0, ... } which papers over the bug (id=0 matches channelIdx=0) but doesn't reflect production row IDs.

2. Duplicate databaseService import (meshcoreVirtualNodeServer.ts:4,6)

import databaseService from '../services/database.js';         // line 4
import databaseServiceDefault from '../services/database.js';  // line 6

Line 4 (databaseService) is used for auditLogAsync; line 6 (databaseServiceDefault) is cast to ChannelsDb in the constructor. These should be a single import.

3. encodeDeviceInfo uses writeInt8 instead of writeUInt8 (meshcoreCompanionCodec.ts:296)

head.writeInt8(info.firmwareVer | 0, 1);

If firmwareVer is 128+, this silently encodes a negative signed value. Use writeUInt8 or clamp with & 0xff, consistent with how txPower/maxTxPower are handled at lines 261–262.

Design / Code Quality

4. allowAdminCommands hardcoded to false for MeshCore sources (DashboardPage.tsx:515)

cfg.virtualNode = { enabled: true, port: vnPort, allowAdminCommands: false };

The form state formVnAllowAdmin is correctly populated on edit (line 381) and passed through for Meshtastic sources (line 538), but MeshCore sources always persist false regardless of the checkbox. The checkbox becomes a no-op for MeshCore. This is documented as a Phase 3 gap, but the discrepancy between the populated UI state and the persisted value may confuse operators. Consider hiding the checkbox for MeshCore in the UI until Phase 3 is ready, or adding a comment near the checkbox.

5. Duplicate-disconnect race in cleanupInactiveClients (meshcoreVirtualNodeServer.ts:589–592)

client.socket.destroy();
this.handleClientDisconnect(clientId);

socket.destroy() will emit a close event asynchronously, which triggers the registered close handler and calls handleClientDisconnect a second time. The if (!client) return guard prevents double-removal, but the audit log entry will fire twice. Consider removing the explicit handleClientDisconnect call here and relying solely on the close event.

6. Similarly, errorclose event sequence fires handleClientDisconnect twice (meshcoreVirtualNodeServer.ts:237–241)

When a socket error occurs, Node.js emits error then close on the same socket. Both handlers call handleClientDisconnect. The guard prevents a crash, but the disconnect audit log fires twice. This is consistent with the existing Meshtastic VN behavior, so it's low-priority, but worth noting.

7. Echo suppression uses node name equality (meshcoreVirtualNodeServer.ts:500)

if (this.isChannelMessage(msg) && local?.name && msg.fromName === local.name) return;

Two nodes with the same name on the same channel will have one's messages silently suppressed. The design doc acknowledges this is the only available signal — a brief inline comment pointing to this limitation would help future maintainers.

8. pubKeyHexToBytes silently truncates odd-length hex (meshcoreCompanionCodec.ts:496)

Buffer.from(clean.length % 2 === 0 ? clean : clean.slice(0, -1), 'hex').copy(out, 0, 0, 32);

Stripping the last nibble for malformed input is silent and lossy. A debug log here would make bad-key cases easier to diagnose in the field.

9. SEND_EST_TIMEOUT_MS = 8000 is undocumented (meshcoreVirtualNodeServer.ts:428)

There's no comment explaining where 8000 ms comes from (firmware default? app expectation?). A brief comment would help.

Security

10. No limit on concurrent client connections

The server accepts unlimited TCP connections. A client on the local network could exhaust file descriptors by opening many connections. A small configurable maxClients limit (e.g. 5) would be a low-cost hardening step, especially since this is a companion-app slot (only one phone typically connects at a time). The existing Meshtastic VN could be checked to see if it has the same gap.

Test Coverage Gaps

  • ch.id === channelIdx bug: the CHANNELS_DB stub sets id: 0 which matches channelIdx: 0 by coincidence; this masks the production bug (item 1 above).
  • No test for the buffer-overflow eviction path (buffer.length > MAX_FRAME_BYTES * 4).
  • No test for cleanupInactiveClients (the 5-minute inactivity eviction timer).
  • No test for stop() with connected clients (verifying clients are cleanly destroyed).
  • The allowAdminCommands: false hardcode for MeshCore in DashboardPage.tsx has no coverage.
  • TestClient.expectFrames is ordering-sensitive: if frames arrive before expectFrames is called, the frame is dropped silently (shift on empty array returns undefined). This is fine for the current tests since commands are sent after registering waiters, but it's fragile for future tests.

Minor

  • meshcoreVirtualNodeServer.ts:161: emoji in a logger.info call is fine for dev visibility but may look odd in structured JSON log shippers.
  • meshcoreCompanionCodec.test.ts:117: the firmware_build_date assertion uses a snake_case field name — worth verifying this matches the actual meshcore.js event shape so the test doesn't pass vacuously.
  • The makeManager utility at test line 76–81 has a subtle quirk: 'getLocalNode' in overrides checks object-own keys, so a prototype method wouldn't be detected. This is fine for the current usage but could surprise future test authors.

Summary: The channel-lookup bug (item 1) and the duplicate import (item 2) are the highest-priority fixes. The writeInt8 issue (item 3) and the allowAdminCommands hardcode (item 4) are lower priority but should be addressed before Phase 3. Everything else is polish or hardening. The round-trip codec tests are a particularly strong quality signal.
| Branch: feat/3535-meshcore-virtual-node-phase0

@Yeraze Yeraze merged commit 5513b42 into main Jun 18, 2026
16 checks passed
@Yeraze Yeraze deleted the feat/3535-meshcore-virtual-node-phase0 branch June 18, 2026 18:16
This was referenced Jun 18, 2026
Yeraze added a commit that referenced this pull request Jun 19, 2026
Finalize the 4.11.0 release (from 4.11.0-rc2) and bring documentation up to
date with everything new since 4.10.

Version bump across all five files: package.json, package-lock.json,
helm/meshmonitor/Chart.yaml, desktop/src-tauri/tauri.conf.json,
desktop/package.json.

Documentation:
- CHANGELOG: finalize the Unreleased section as [4.11.0], adding the
  previously-missing entries — headline MeshCore virtual node (#3540),
  MeshCore path-by-repeater-name (#3550), MeshCore map icons + filtering
  (#3563), per-node Hide from Map (#3565), telemetry time-range selector
  (#3530), newer AirQualityMetrics fields (#3517), UI/map unification
  (#3561), plus the bug-fix and documentation changes since 4.10.4.
- virtual-node.md: document the new MeshCore Virtual Node alongside the
  Meshtastic one (default port 5000, per-source enablement, admin-command
  safety, MeshCore app setup, troubleshooting) + a two-variants intro note.
- configuration/index.md, docs/index.md, README.md: mention the MeshCore
  Virtual Node in the Virtual Node feature blurbs.
- CLAUDE.md: refresh stale version header (4.10.0 -> 4.11.0) and migration
  count (84 -> 92, latest 092_add_hide_from_map_to_nodes).

Co-Authored-By: Claude Opus 4.8 (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