What this release is really about β
v4.6.3 is a follow-up patch focused on how permissions and map visibility work for MQTT sources. v4.6.2 reworked the MQTT ingest pipeline so cross-source dedup and channel decryption finally work; this release fixes the permission and rendering gaps that release exposed.
If you run MeshMonitor with anonymous users or non-admin accounts viewing the map, this release affects you directly.
The big change: MQTT channel permissions live under Virtual Channel Permissions β
In the old slot-indexed permission model, channel access was granted per-source on slots channel_0..channel_7. That works fine for a TCP source where slot 0 always means the same channel β but for an MQTT broker, slot 0 from different senders is a different channel each time. The same channel_0 permission row was effectively a wildcard for whatever channels happened to land there most recently.
Starting in v4.6.3 (with the underlying mechanics from #3108):
- MQTT ingest registers each observed channel by name in the global
channel_databasetable. - Messages, traceroutes, and now node positions are tagged with the channel-database id (encoded as
CHANNEL_DB_OFFSET + id), not the raw slot. - The map filter, the messages filter, and the traceroute / neighbor-info endpoints all read from
channel_database_permissionsfor those rows.
What this means in the UI: when you scope the Users-tab permissions dropdown to an MQTT source, the channel_0..7 grid is now hidden, and you'll see a banner pointing you to the Virtual Channel Permissions section further down the page. Grant View on Map and Read on the relevant channel_database entries (e.g. LongFast, MediumFast) for each user β and crucially for the Anonymous user if you want unauthenticated viewers to see anything.
Why your map went empty after upgrading β
Several users who upgraded to v4.6.2 saw zero nodes on the map for their MQTT sources as the anonymous user (or any non-admin), regardless of what they granted. The cause: MQTT NODEINFO / POSITION ingest wasn't stamping the channel-database id onto the node row. The map filter fell back to checking channel_0:viewOnMap β which the new UI hides β so there was no way to grant access.
v4.6.3 fixes this by stamping node.channel = CHANNEL_DB_OFFSET + dbId at ingest time (#3110). Existing rows with channel=NULL recover as each MQTT node re-broadcasts NODEINFO (typically every few hours under default firmware config). If you don't want to wait, restarting the MQTT broker / bridge container is enough to trigger a fresh round of advertisements on most public brokers.
"Floating lines" β the other half of the fix β
Even with nodes correctly filtered, two endpoints were still leaking enough position data for the frontend to draw line segments between coordinates of nodes the user couldn't see:
/api/sources/:id/traceroutesreturned rows unfiltered, and each row carries aroutePositionsJSON snapshot of every hop's lat/lng at traceroute time. The map drew traceroute segments using that snapshot, even after the matching node markers had been filtered out./api/sources/:id/neighbor-infoenriched each record with positions pulled from the node table without a channel check.
v4.6.3 channel-gates both endpoints (#3110) using the same viewOnMap permission set the nodes endpoint already used. If you have no viewOnMap grant on the channel, you don't see the lines either.
Tapback rendering finally stays put β
If you noticed tapback reactions on the Unified Messages feed flickering β appearing briefly as an emoji pill under the parent message, then turning into a full inline message on the next poll, then back again β this release fixes that (#3105).
Cause: MQTT ingest dropped emoji and replyId from the decoded protobuf, so MQTT-sourced reaction rows hit the DB with both fields null. The unified-merge then picked first-source-wins for these fields under a non-deterministic Promise.all race β sometimes the TCP row (with the correct emoji marker) won, sometimes the MQTT row (with nulls) won. v4.6.3 captures the fields on MQTT ingest, propagates them through server-side decryption, and upgrades the unified-merge to prefer populated values from any source.
MeshCore repeater telemetry, take three β
v4.6.1 added MQTT-side fallbacks for MeshCore repeater telemetry; v4.6.2 added a SendStatusReq + guest-login path for the repeater protocol; and now v4.6.3 fixes the last remaining gap uncovered while debugging #3092: MeshCore contacts learned from the advert stream were never actually written to the meshcore_nodes table (#3107). The remote-telemetry scheduler always read target.advType=NULL, decided every target looked like a Companion, and silently skipped the new repeater paths.
With v4.6.3, advert events mirror to the SQL table immediately, refreshContacts() bulk-backfills on startup, and the per-node telemetry-config route backfills the contact's advType before seeding the retrieval row. The new paths shipped in v4.6.2 finally run for everyone.
Action items after upgrade β
- Open Users β Permissions β scope to each MQTT source and confirm the new banner appears.
- Scroll down to Virtual Channel Permissions and make sure each user (including Anonymous if applicable) has
View on MapandReadgranted on the channel-database rows for the MQTT channels you want them to see. - Reload the map β node markers and traceroute/neighbor lines should match each other (no more floating segments). Allow a few hours for
node.channel=NULLrows from v4.6.2 to backfill on their own, or restart the MQTT container to force fresh adverts. - If you use MeshCore repeaters, re-enable Telemetry Retrieval on a repeater node and check the scheduler logs β the line should say
Requesting telemetry from β¦ (repeater: status + LPP), not(companion: LPP).
Full release notes: CHANGELOG.md.