Skip to content

feat: MeshCom LoRa mesh integration#936

Open
ceotjoe wants to merge 34 commits intoaccius:Stagingfrom
ceotjoe:feature/meshcom-v2
Open

feat: MeshCom LoRa mesh integration#936
ceotjoe wants to merge 34 commits intoaccius:Stagingfrom
ceotjoe:feature/meshcom-v2

Conversation

@ceotjoe
Copy link
Copy Markdown
Collaborator

@ceotjoe ceotjoe commented May 1, 2026

Summary

  • Full MeshCom LoRa mesh integration — receives UDP JSON packets from the meshcom-udp rig-bridge plugin and displays nodes on the map, messages in a chat-style tab, and telemetry/weather data in the node list
  • Dual data path — works in both local/direct mode (rig-bridge SSE → browser) and cloud relay mode (rig-bridge → OHC server → SSE fan-out → browser), with 30 s HTTP polling as a safety net for initial load and packet recovery
  • Real-time updates via SSE — MeshCom packets are fanned out over the existing relay/stream SSE using the same { type: 'plugin', event: 'meshcom' } envelope as WSJTX decodes; RigContext already forwarded these as rig-plugin-data window events
  • Per-session data isolation — server stores nodes/messages per relay session ID so multiple browser users on the same OHC server see their own data
  • Click-to-reply — tap any message to reveal inline reply buttons (broadcast, group, direct); reply target populates the To field automatically
  • Free-text To field — replaced the fixed dropdown with <input type="text" list="…"> + <datalist> so any callsign or group can be entered freely while known nodes are still offered as suggestions
  • Full i18n — all 16 language files updated with MeshCom panel strings
  • CSS variables throughout — no hardcoded colours; all theming via CSS custom properties
  • Time-based message expiry — messages older than 8 h are pruned server-side
  • Cloud relay session refactor — session ID moved from shared server config to per-browser localStorage; RigContext migrates old server-config sessions automatically on first load; ohc-relay-configured flag distinguishes explicit cloud relay setup from auto-generated data-isolation IDs
  • rig-bridge: meshcom plugin bus events bridged to /stream SSE (was the only event type missing); meshcom-udp plugin documented in README

Test plan

  • Enable meshcom-udp plugin in rig-bridge config; confirm nodes appear on map and messages in Messages tab within one beacon interval
  • Verify local/direct mode: rig-bridge reachable from browser, no cloud relay — panel populates via SSE without polling
  • Verify cloud relay mode: panel populates via relay/stream SSE fan-out; status shows active
  • Click a message → reply buttons appear; clicking one pre-fills To field and focuses message input
  • Type a free callsign in the To field; confirm it is accepted and sent; confirm suggestions appear for known nodes
  • Disconnect cloud relay → reconnect → session migrates automatically (no re-click of Connect Cloud Relay needed)
  • Confirm all 16 language files render MeshCom strings without fallback to English

Checklist

  • App loads without console errors
  • Tested in Dark, Light, and Retro themes
  • Responsive at different screen sizes (desktop + mobile)
  • If touching server.js: caches have TTLs and size caps (we serve 2,000+ concurrent users)
  • If adding an API route: includes caching and error handling
  • If adding a panel: wired into Modern, Classic, and Dockable layouts
  • No hardcoded colors — uses CSS variables (var(--accent-cyan), etc.)
  • No .bak, .old, console.log debug lines, or test scripts included

ceotjoe and others added 24 commits April 27, 2026 10:17
Adds full MeshCom UDP JSON integration following the same pattern as
APRS-TNC and Meshtastic: rig-bridge plugin → plugin bus → cloud-relay
→ OHC server routes → React panel + map markers.

rig-bridge:
- meshcom-udp.js: new plugin binding UDP port 1799, deduplicates packets
  by hw_id+msg_id (mesh rebroadcast), emits normalised 'meshcom' bus
  events (subtype pos/msg/telem) — no direct HTTP, no serverUrl config
- cloud-relay.js: subscribes to 'meshcom' bus events, batches into
  meshcomPackets[], includes in relay/state push alongside APRS
- server.js (rig-bridge): forwards meshcomPackets[] from relay/state
  to /api/meshcom/local/{pos|msg|telem} endpoints
- Plugin UI: MeshCom UDP listed with configurable bind port and address

server:
- server/routes/meshcom.js: new route — in-memory node store with ETag
  (304 on no change), ?since= incremental messages, /api/meshcom/nodes,
  /api/meshcom/messages, /api/meshcom/send, /api/meshcom/status
- Altitude converted ft→m at ingest (MeshCom sends GPS feet)
- Firmware normalised from both encodings: local node sends "4.35"+"p",
  LoRa-relayed sends integer 35+"p"; both → "4.35p" via normalizeFirmware()

frontend:
- MeshComPanel.jsx: three-tab panel (Nodes / Messages / Info) with
  battery bar, weather/telemetry row, age indicator, send-to form
- WorldMap.jsx: hexagonal SVG marker, popup with weather + firmware,
  lat/lon 0 guard uses Number.isFinite (not falsy check)
- useMeshCom.js: 30s poll (LoRa cadence), ETag on nodes, ?since= on
  messages — minimises traffic on idle mesh
- primaryCall() helper in callsign.js: strips relay path from src field
  ("OE1XYZ,OE2ABC" → "OE1XYZ") for panel and map display
- Wired into App.jsx, DockableApp.jsx, useMapLayers (showMeshCom toggle)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Two root causes identified:

1. /api/meshcom/status made an outbound HTTP call to rig-bridge
   (AbortSignal.timeout(2000)), holding a browser connection open for up
   to 2s on every poll cycle when rig-bridge was not running. With
   HTTP/1.1's 6-connection-per-host limit, three concurrent MeshCom
   fetches (nodes + messages + status) could starve DX cluster, rig
   control and all other panels.

   Fix: status endpoint is now fully synchronous. Connectivity is derived
   from lastIngestTime (stamped whenever a pos/msg/telem packet arrives)
   — no outbound call ever made. Response time < 1ms.

2. useMeshCom fired all three fetches via Promise.all with no timeouts,
   meaning a single slow response blocked the entire refresh cycle and
   the "Loading…" spinner never cleared.

   Fix:
   - loading initialised to false — panel renders immediately with empty
     state, never blocks perceived page readiness
   - Each fetch now carries AbortSignal.timeout(5000) so a non-responsive
     server cannot hold a connection open beyond 5s
   - Fetches fire independently (not Promise.all) — a slow nodes response
     cannot delay message or status updates
   - refresh() is no longer async/awaited — fire-and-forget per fetch

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Adds 32 meshcomPanel.* translation keys covering every user-visible
string in the panel and map popup, translated into all supported
languages: de, fr, es, it, pt, nl, ru, ja, zh, ko, ca, sl, ms, th, ka.

MeshComPanel.jsx:
- Added useTranslation to all four sub-components (NodesTab, MessagesTab,
  InfoTab, MeshComPanel)
- Replaced every hardcoded English string with t() calls
- TABS array refactored to TAB_IDS ('nodes'|'messages'|'info') with
  labels resolved via t() at render time — active-tab comparisons
  now use stable IDs rather than translated strings
- Interpolated keys: nodeCount {{count}}, infoStats {{nodes}}/{{messages}},
  sendGroup {{n}}, mapToggleHide/Show

WorldMap.jsx:
- Map popup battery, altitude, age and firmware labels use
  t('meshcomPanel.mapPopup*') with {{age}} interpolation
- t added to useEffect dependency array

Technical terms intentionally left untranslated: MeshCom, LoRa,
OE1KBC, ICSSW, UDP, RSSI, SNR, FW, code snippet content in <pre>.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Icon (panel header + map marker):
- Replaced the placeholder teal hexagon with the official MeshCom logo
  shape: central filled circle + 6 outer open circles + hexagonal ring
  of spokes, in brand crimson #8B1A2A
- Panel SVG uses var(--bg-panel) fill on outer nodes so they correctly
  mask spoke lines on both dark and light themes
- Map marker wraps the same design in a semi-transparent white disc for
  visibility on any map tile; aged nodes (>30 min) render in grey
- iconSize changed 20×22 → 24×24, iconAnchor centred (12,12) instead
  of bottom-pinned, matching the circular shape

Map toggle button:
- Replaced the bespoke teal ON/OFF button with the same style used by
  DXClusterPanel: rgba background, JetBrains Mono, IconMap icon prefix,
  coloured border — crimson when active, grey when inactive

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Clicking any received message selects it (crimson left border +
subtle background highlight). Two contextual reply buttons appear
inline beneath the message text:

  ↩ Group N / ↩ Broadcast  — replies to the same dst as the original
  ↩ CALLSIGN               — direct reply to the sender's callsign
                             (hidden when both targets would be identical)

The second button only appears when the group/broadcast target differs
from the sender, avoiding a duplicate option.

"Replying to CALLSIGN" context strip appears above the send form while
a reply is active, with an × to cancel. Cancelling resets the To:
field back to Broadcast (*).

Clicking a reply button pre-selects the To: dropdown and focuses
the message input immediately via inputRef.

If the sender is not a known node, their callsign is dynamically added
to the To: dropdown via dropdownCalls so the select value is always
valid regardless of node list state.

On successful send the reply context is automatically cleared.

i18n: 5 new meshcomPanel.reply* keys added to all 17 language files
(en, de, fr, es, it, pt, nl, ru, ja, zh, ko, ca, sl, ms, th, ka).

Colour scheme: message sender callsign and Send button updated from
teal to brand crimson #8B1A2A for consistency with the new icon.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
MeshCom and APRS callsigns include numeric SSID suffixes (-12, -99, etc.)
that are not part of the licensed callsign and cause QRZ.com lookups to
fail. extractBaseCall() now strips -\d+ before processing slash-portable
notation, so OE1XYZ-12 resolves to OE1XYZ on QRZ.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces hardcoded #8B1A2A (crimson) on the Messages-tab source
callsign with var(--accent-cyan) from the theme palette, which has
adequate contrast on both dark and light themes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Every hardcoded hex colour in MeshComPanel is now a theme-aware CSS
custom property:

  #ef4444 → var(--accent-red)      (errors, battery low)
  #f59e0b → var(--accent-amber)    (battery mid)
  #22c55e → var(--accent-green)    (battery full, connected dot)
  #6b7280 / accius#666 / accius#888 → var(--text-muted)   (inactive dots, borders)
  #2dd4bf → var(--accent-cyan)     (node age border, active tab)
  #8B1A2A / #c0394e → var(--accent-red)  (selected border, reply btns, send btn)
  rgba(139,26,42,…) tints → var(--bg-tertiary) / var(--bg-secondary)
  rgba(100,100,100,…) → var(--bg-secondary)
  #fff on send button → var(--title-bar-text)

SVG logo strokes/fills kept as brand-identity crimson (#8B1A2A).
Active-tab label kept as #000 — no CSS variable covers "always dark
on a cyan background" across all themes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Messages are now pruned from the in-memory buffer in the same 60-second
cleanup interval that already handles node expiry:

  - MESHCOM_MESSAGE_MAX_AGE_HOURS env var (default 8)
  - Messages are stored in arrival order; cleanup walks from the front
    and splices any entries older than the cutoff in one call (O(n) scan,
    O(k) splice where k = expired count — typically 0 during normal ops)
  - Ring-buffer cap (200 entries) is retained as a safety net

Node expiry is unchanged (MESHCOM_NODE_MAX_AGE_MINUTES, default 60 min).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a full MeshCom section to the rig-bridge README covering:
- How the plugin fits into the data pipeline (node → UDP → rig-bridge
  → cloud relay → OHC server → panel/map)
- MeshCom firmware configuration (--extudp, broadcast IP)
- Rig Bridge setup via the Plugins UI and manual JSON config
- Full config reference table for all six fields
- Packet types (pos / msg / telem) and processing details
- Deduplication behaviour (hw_id + msg_id, 60 s TTL)
- OHC data retention table with env var overrides
- Troubleshooting table for common issues
- Entry in Plugin Manager summary table
- meshcom-udp.js added to project structure tree

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
rig-bridge/plugins/meshcom-udp.js
- Merge normalizeLat/normalizeLon into single normalizeCoord helper
- Hoist isDuplicate + bus guard above type switch (was repeated 3×)
- Count packetsRx only after successful JSON parse; capture msg.toString() once; log non-JSON frames in verbose mode
- socket 'error' handler now cleans up dedupTimer and socket instead of
  just setting running=false; logs if socket.close() itself throws
- disconnect() empty catch{} → error log with port-still-in-use hint
- Warn at connect() time when plugin bus is absent
- msgId: json.msg_id || null → ?? null (CLAUDE.md: || is for strings/booleans)
- Update JSDoc: document actual relay path and wire→bus telem field renames

server/routes/rig-bridge.js
- Replace silent .catch(()=>{}) on MeshCom ingest forward with logging
  .then(r=>logWarn if !r.ok).catch(e=>logWarn) so failures are visible

server/routes/meshcom.js
- CONFIG.rigControl?.port || 5555 → ?? 5555 (port 0 is valid per CLAUDE.md)
- Store setInterval ref as cleanupTimer; call .unref() so it doesn't
  prevent process exit
- Extract parseOrNull(v) helper; remove inline parseFloat repetition
- Array.from(weather.values()) instead of spread in weather endpoint
- /api/meshcom/send catch distinguishes TimeoutError from ECONNREFUSED
  with actionable user-facing messages; logs via logWarn
- "ring buffer" → "bounded FIFO" in header comment
- Improve "Merge weather" comment for clarity

src/hooks/useMeshCom.js
- export const → export function
- fetchStatus catch now logs non-abort/timeout errors (was swallowed)
- sendMessage wraps apiFetch in try/catch to translate network/timeout
  errors into user-friendly messages before they surface in the UI
- Correct header comment (loading is always false, not "cleared on mount")

src/components/MeshComPanel.jsx
- Compute primaryCall(node.call) once per row; use in three places
- Remove dead isDirect variable
- Map toggle background ternary simplified (both branches were identical)
- Extract replyButtonStyle constant (was duplicated inline)
- Extract replyLabel(target) helper to replace nested ternary

src/components/CallsignLink.jsx
- Remove unused useEffect import

rig-bridge/README.md
- Expand architecture diagram to show full ingest path
  (relay/state → meshcom/local/{subtype} → store → GET endpoints)
- telem row documents wire-name → bus-name renames
  (temp→tempC, pressure→pressureHpa, co2→co2ppm)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously all MeshCom data (nodes, messages, weather) was stored in
three module-level singletons, meaning every user on a shared server
saw every other user's mesh traffic merged into one pool.

server/routes/meshcom.js
- Replace the three global Maps/array with a per-session `sessions` Map
  (sessionId → { nodes, messages, weather, lastIngestTime, lastAccessTime })
- getOrCreateSession() — used on write paths (ingest); creates session on
  first packet from that rig-bridge relay connection
- getSessionIfExists() — used on read paths; never silently creates an
  empty session (which would prevent TTL expiry)
- Cleanup timer now runs two levels: session-level expiry (evict entire
  session after SESSION_TTL_MS = 90 min idle, configurable via
  MESHCOM_SESSION_TTL_MINUTES) then data-level expiry within each live
  session (same node/message TTLs as before)
- All ingest endpoints (pos/msg/telem) require sessionId in the body;
  return 400 if absent
- All GET endpoints (nodes/messages/weather/status) require ?session=<id>;
  return empty payload (not 400) when session is unknown so the browser
  panel degrades gracefully before the first relay push arrives

server/routes/rig-bridge.js
- Inject sessionId into the MeshCom ingest body when forwarding packets
  from the cloud relay push, using the sessionId already extracted from
  the relay request headers/body

src/hooks/useMeshCom.js
- Add getSessionId() — generates an 8-char alphanumeric ID, persisted in
  localStorage under 'ohc-meshcom-session'; rejects legacy UUIDs (same
  pattern as useWSJTX to avoid Bitdefender false positives)
- Append ?session=<id> to all GET requests (nodes, messages, status)
- Pass session in POST /api/meshcom/send body
- Return sessionId from the hook (available to callers if needed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The relay ingest uses the session ID from the x-relay-session header
(= 'ohc-wsjtx-session' in localStorage, the relay session established
when the user configured rig-bridge). useMeshCom was generating a
separate 'ohc-meshcom-session' key, so the server stored data under
the relay session but the browser polled under a different ID and
received nothing.

Fix: getSessionId() now reads from 'ohc-wsjtx-session', the shared
relay session key. All relay-delivered data (WSJTX, APRS, MeshCom)
flows through the same relay session, so all hooks must poll with the
same session ID that rig-bridge sends in x-relay-session.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously useWSJTX had its own inline getSessionId() reading from
'ohc-wsjtx-session', and useMeshCom was patched to read from the same
WSJTX key as a workaround. Both data types flow through the same cloud
relay plugin under the same session ID, so the session utility should
be shared and named after what it actually is.

src/utils/relaySession.js (new)
- getRelaySessionId() — single source of truth for the relay session ID
- Canonical localStorage key: 'ohc-relay-session'
- On first load, migrates any existing 'ohc-wsjtx-session' value so
  existing rig-bridge configurations keep working without reconfiguration
- Validates ID format (8–12 char alphanumeric) to reject legacy UUIDs

src/hooks/useWSJTX.js
- Remove inline getSessionId() / localStorage logic
- Import and use getRelaySessionId() from the shared util

src/hooks/useMeshCom.js
- Remove workaround comment and inline key reference
- Import and use getRelaySessionId() from the shared util

Any future hook consuming relay-delivered data should import
getRelaySessionId() from src/utils/relaySession.js.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
relaySession.js — isValidId() previously rejected IDs longer than 12
characters. The /api/rig-bridge/relay/configure endpoint generates IDs
via crypto.randomBytes(8).toString('hex'), which produces a 16-character
lowercase hex string. Because 16 > 12 the validation silently failed,
causing getRelaySessionId() to generate a fresh random ID that never
matched the rig-bridge session — all ingest data was stored under the
16-char ID while the browser polled with the new 8-char ID, so the
MeshCom panel always showed empty data.

Upper bound widened to 32 (accommodates 8-char client-generated IDs,
16-char relay/configure IDs, and any future formats up to 32 chars).
Comment updated to document both generator variants.

meshcom.js — demote the three ingest log lines from logDebug to logInfo
so packet arrivals are visible without LOG_LEVEL=debug, making it easier
to confirm data is reaching the server when troubleshooting.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a logInfo line in the relay/state handler that fires whenever the
server receives MeshCom packets from the rig-bridge cloud relay push.
Previously the entire ingest path was silent on success, making it
impossible to tell from the server log whether packets were arriving at
all, even with LOG_LEVEL=debug.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…licked

relay/configure generates a fresh 16-char session ID and the user pastes
it into the rig-bridge config. Previously that ID was only saved to the
OHC server config (config.rigControl.cloudRelaySession); the browser's
getRelaySessionId() reads from localStorage and was unaware of it, so
all relay-consuming hooks (WSJTX, MeshCom, APRS) kept polling with
whatever random ID they had before — never matching the rig-bridge.

Fix: add setRelaySessionId() to relaySession.js and call it from the
SettingsPanel button handler immediately after receiving the session from
relay/configure. The ID is now written to localStorage ('ohc-relay-session')
so every hook picks it up on their next render without requiring a page
reload or manual localStorage edit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The session ID belongs to the browser, not the server. Storing it in
server config makes no sense in a multi-user environment — every user
clicking "Connect Cloud Relay" would overwrite a shared field.

- Read initial UI state from localStorage['ohc-relay-session'] instead
  of config.rigControl.cloudRelaySession
- Remove cloudRelaySession from the onSave payload entirely
- Disconnect button now removes the localStorage key so relay-consuming
  hooks generate a fresh ID on next load
- The "Active — session..." display still works; it reads the same
  localStorage value that getRelaySessionId() uses

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mirrors the WSJTX data path:
  meshcom-udp bus event → cloud-relay POST /relay/state
  → server fans { type:'plugin', event:'meshcom' } to relayStreamClients
  → RigContext EventSource → window 'rig-plugin-data' CustomEvent
  → useMeshCom SSE handler → immediate setNodes / setMessages

Polling stays active as a safety net for initial load and packet
recovery; lastMessageTsRef is kept in sync by both paths so the
?since= incremental parameter remains accurate.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The cloud relay onmessage handler only dispatched 'state' type messages.
'plugin' and 'plugin-init' messages (WSJTX decodes, MeshCom packets, APRS)
were silently dropped instead of being forwarded as rig-plugin-data window
events. Individual hooks (useMeshCom, useWsjtx) listen for those window
events and never received anything via the cloud relay SSE path.

Adds the same plugin dispatch branch that the local/direct SSE path already
had, making the two paths consistent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nfig

Root cause of "plugin offline":
  RigContext read cloudRelaySession from rigConfig (server config), but
  we removed it from the save payload in a previous commit. So rigConfig
  always had an empty session → isCloudRelay = false → local-direct mode
  → relay/stream SSE never opened → relayStreamClients had no entries for
  the session → SSE fan-out wrote to nobody → MeshCom status showed offline.

Fix:
  - relaySession.js: add ohc-relay-configured flag + isRelayConfigured(),
    setRelayConfigured(), clearRelaySession() helpers. The flag is set only
    when the user explicitly clicks "Connect Cloud Relay", preventing an
    auto-generated data-isolation session from enabling cloud relay mode.
  - RigContext: read cloudRelaySession from localStorage via isRelayConfigured()
    + localStorage['ohc-relay-session'] instead of rigConfig.cloudRelaySession.
    Includes _migrateCloudRelaySession() which silently copies old server-config
    sessions to localStorage on first render so users don't need to re-connect.
  - SettingsPanel: cloudRelaySession useState falls back to server config for
    migration; Connect calls setRelayConfigured(true); Disconnect calls
    clearRelaySession() (clears both session and configured flag).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three bugs in the no-cloud-relay path:

1. rig-bridge.js: meshcom bus events were never bridged to the /stream SSE.
   decode, status, qso, aprs etc all had pluginBus.on() handlers that call
   broadcast() — meshcom had none. Added the missing handler so browsers in
   local/direct mode receive { type:'plugin', event:'meshcom' } over the
   same SSE connection used for WSJTX decodes.

2. useMeshCom: polling fetches would overwrite SSE-populated state with empty
   server responses (the OHC server has no ingest data in local mode).
   - fetchNodes: skip setNodes if server returns empty AND SSE is live
   - fetchStatus: skip entirely while SSE is fresh (last event < 25 min ago)
   - SSE handler: set connected=true immediately on packet arrival instead
     of waiting for the next 30 s poll cycle

3. server/routes/meshcom.js: ACTIVE_WINDOW_MS was 5 min. LoRa beacons can
   be 15+ min apart, so sparse networks showed "offline" between beacons in
   cloud relay mode. Raised to 30 min.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In local mode the OHC server proxies the send to rig-bridge via
CONFIG.rigControl.host:port — but when the server is in the cloud and
rig-bridge is on the local network this fails with a network error that
surfaces as "MeshCom UDP plugin not available".

Fix: mirror the pattern used by freq/mode/PTT commands in RigContext.
In local/direct mode the browser POSTs directly to rig-bridge's
/api/meshcom-udp/send endpoint instead of going through the server proxy.

- RigContext: expose rigUrl and isCloudRelay in context value
- useMeshCom: accept rigBridgeUrl option; when set, fetch() directly to
  rig-bridge instead of apiFetch() to /api/meshcom/send
- MeshComPanel: read rigUrl + isCloudRelay from useRig(), pass
  rigBridgeUrl={isCloudRelay ? null : rigUrl} to useMeshCom

Cloud relay mode is unchanged — still proxies via the OHC server.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ggestions

The <select> only allowed choosing from known nodes or fixed groups.
Replaced with <input type="text" list="meshcom-to-suggestions"> so the
user can type any callsign or group number freely while still getting
autocomplete suggestions for:
  * — broadcast
  0–5 — mesh groups
  known node callsigns (from live node list)
  reply-to sender (when a message is selected)

Input auto-uppercases on every keystroke so callsigns arrive correctly
formatted. The send handler already had `toField || '*'` as a guard for
an empty field. Reply buttons still call setToField() directly — unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ceotjoe ceotjoe marked this pull request as draft May 1, 2026 10:17
@ceotjoe ceotjoe linked an issue May 1, 2026 that may be closed by this pull request
ceotjoe and others added 4 commits May 1, 2026 12:27
Matches the badge shown for APRS TNC in the rig-bridge config UI.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add MeshCom UDP _(Beta)_ to the table of contents (was missing entirely)
- Renumber TOC entries 9-12 to accommodate the new entry
- Add _(Beta)_ suffix to both MeshCom UDP Plugin section headings
- Fix stale troubleshooting row: MeshCom now works in local/direct mode
  too, not only via Cloud Relay

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Group 1 — before merge:
- relaySession.js: add console.warn to all 5 empty catch blocks so
  localStorage failures are visible (privacy browsers, quota exceeded)
- meshcom-udp.js: guard json.src in handlePacket — prevents phantom
  'UNDEFINED' nodes from malformed UDP datagrams
- meshcom.js: validate ?since= query param with Number.isFinite to
  prevent NaN breaking incremental message fetch
- meshcom.js: validate env-var parseInt with parseEnvMinutes helper —
  NaN would silently disable node/session expiry (memory leak)
- MeshComPanel.jsx: fix group reply regex — /^[0-5]$/ instead of
  lexicographic string comparison that misclassified multi-char callsigns
- meshcom.js: fix wrong function name in comment (getSessionId →
  getRelaySessionId); demote message body from logInfo to logDebug

Group 2 — before merge:
- cloud-relay.js: implement relayWsjtx/relayAprs/relayMeshCom config
  flags (documented but previously unread); all default to true
- cloud-relay.js: restore pendingAprs and pendingMeshCom on push
  failure — only pendingDecodes was previously restored
- useMeshCom.js: add msgId dedup to polling merge path to match the
  SSE handler and prevent duplicate messages

Group 3 — before draft promotion:
- meshcom-udp.js: always warn on non-JSON datagrams (was gated behind
  verbose flag, hiding firmware format changes in production)
- meshcom-udp.js: add packetsTxErrors counter to getStatus() so
  silent async UDP TX failures are observable
- rig-bridge.js: log SSE client eviction on both fan-out paths
- MeshComPanel.jsx + WorldMap.jsx: compute node ageMin at render time
  from node.timestamp so SSE-delivered nodes age correctly in
  local/direct mode where polling returns no server-side ageMin
- useMeshCom.js: move SSE_STALE_MS to module level alongside
  POLL_INTERVAL and FETCH_TIMEOUT_MS

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ceotjoe and others added 5 commits May 1, 2026 23:51
- themes.css: add --bg-row-alt to all four themes (dark/light/legacy/retro)
  so alternating node/message row backgrounds are visible on light and
  correctly tinted on legacy (phosphor green) and retro
- MeshComPanel.jsx: use var(--bg-row-alt) for node list zebra stripe
  instead of rgba(255,255,255,0.03) which was invisible on light theme
- MeshComPanel.jsx: fix send button text color — var(--title-bar-text)
  is only defined in the retro theme, making the button text invisible
  in dark/light/legacy when active; replace with #fff (white on red)
- WorldMap.jsx: replace hardcoded #2dd4bf/accius#888/#aaa in the MeshCom
  node map popup with var(--accent-cyan)/var(--text-muted) so the
  popup callsign and age text adapt correctly to every theme

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add className="panel" (padding:0) to the outer MeshCom container so it
inherits var(--bg-panel), border, border-radius, and backdrop-filter
from the shared .panel class — matching DX Cluster and other panels.

Add className="meshcom-header" to the header bar and add a CSS rule in
main.css that gives it the retro Windows title-bar gradient without the
-10px margin compensation needed by 10px-padded panels (MeshCom uses
padding:0 so the header is already flush to the edges).

Result per theme:
  dark   — semi-transparent dark panel, amber border, blur backdrop
  light  — semi-transparent white panel, blue border, blur backdrop
  legacy — near-black panel, green border
  retro  — Windows 95 grey 3D bevel panel, blue gradient title bar

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The retro theme's button rule (color:#000 !important) and inline
var(--text-primary) (#000) were overriding the title bar's white color.
Adding a wildcard child selector with higher specificity [0,3,1] vs the
button rule's [0,1,1] ensures spans, the node count, and the map toggle
button all render white against the blue gradient title bar.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous wildcard rule forced all children white. A more specific
button selector [0,3,2] overrides it to keep button text black, matching
the retro theme's raised 3D button appearance.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The wildcard * rule was hitting SVG icon elements inside the button.
Extend the button black-text rule to also cover button descendants so
the IconMap SVG renders black alongside the button label text.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ceotjoe
Copy link
Copy Markdown
Collaborator Author

ceotjoe commented May 2, 2026

Please only merge after #943! Needs to be adjusted slightly then.

Conflicts resolved in rig-bridge/package.json and rig-bridge/rig-bridge.js:
upstream bumped to 2.1.3 (patch), feature branch is at 2.2.0 (minor
for MeshCom integration) — keeping 2.2.0 as it supersedes the patch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ceotjoe ceotjoe marked this pull request as ready for review May 7, 2026 16:18
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.

[FEATURE] MeshCom integration

1 participant