Frontend Architecture
The MeshDash frontend is a Single-Page Application (SPA) served from /static/. There is no JavaScript framework — it uses vanilla JS modules loaded via <script> tags, a shared global state object, and a custom view-loader system.
Directory Layout
static/
├── css/
│ └── style.css ← all component styles, CSS variables, layout
├── js/
│ ├── app.js ← core: state, SSE, loadView(), fetch patch, init
│ ├── ui.js ← shared utilities: toast, modals, inspector, topbar
│ ├── webserial.js ← Web Serial bridge (browser USB)
│ ├── overview.js ← overview view module
│ ├── map.js ← map view module
│ ├── dmes.js ← direct messages module
│ ├── channels.js ← channels module
│ ├── analytics.js ← analytics module
│ ├── compare.js ← node comparison module
│ ├── shark.js ← MeshShark packet analyser
│ ├── traceroute.js ← traceroute module
│ ├── iot.js ← web telemetry module
│ ├── tasks.js ← task scheduler module
│ ├── autoreply.js ← auto-reply module
│ ├── plugins.js ← plugin manager module
│ ├── node_config.js ← node configuration module
│ └── terminal.js ← C2 command terminal
└── views/
├── overview.html ← HTML template for the overview view
├── map.html
├── dmes.html
├── channels.html
├── analytics.html
├── compare.html
├── shark.html
├── traceroute.html
├── iot.html
├── tasks.html
├── autoreply.html
├── plugins.html
├── node_config.html
└── settings.html
How Views Load
The <main id="content"> element is the mount point. When a sidebar nav item is clicked it calls loadView('overview') (or whichever view name):
- Aborts any in-flight fetch from the previous view using
AbortController GET /static/views/{viewName}.html— fetches the view HTML template- Injects the HTML into
#contentas<div class="view-wrapper">...</div> - Calls the view's init function via a dispatch table (e.g.
window.C2CommsApp.init()) - On mobile, closes the sidebar
Views are lazy-loaded — the JS module for a view is only loaded from the server the first time that view is opened. Subsequent visits reuse the already-loaded module from window.
Global State: window.meshState
All live data lives in window.meshState, shared across every view:
window.meshState = {
connectionStatus: 'Initializing', // string from SSE
nodes: {}, // dict: node_id → node object (live, updated by SSE)
packets: [], // recent packets from /api/packets (historical load)
stats: {}, // session stats from SSE 'stats' events
currentView: null, // name of the currently active view
sessionStart: Date.now(),
local_node_id: null,
dmUnread: {}, // node_id → unread count for Direct Messages
channelUnread: {} // channel_index → unread count for Channels
}
Any JS module can read window.meshState.nodes directly. It is always the current live state — no stale copies.
SSE — Real-Time Event Stream
On startup, app.js opens an EventSource to /sse (or /sse/{slot_id} for non-primary slots). The SSE manager (_sse) handles:
- Exponential backoff reconnection (1 s → 16 s max)
- A 90-second dead-stream watchdog — forces reconnect if no event received
- Tab visibility change handler — reconnects if stream is dead when tab becomes visible
On each SSE event, app.js updates window.meshState and then calls the active view's update function (if it exists). The dispatch is:
| SSE Event | meshState update | UI side-effect |
|---|---|---|
nodes | Merges node list into meshState.nodes | View refreshes node grid |
node_update | Merges single node | Updates node card in-place |
packet | Prepends to meshState.packets | MeshShark / overview live feed |
stats | Updates meshState.stats | Overview KPI strip updates |
connection_status | Updates meshState.connectionStatus | Topbar CORE/RF dots, status text |
message_status_update | — | DM/Channels message bubble status icon |
local_node_info | Updates meshState.local_node_id | Topbar node ID display |
system_update | — | Toast notification |
traceroute_result | — | Traceroute result panel renders |
activity | — | TX/RX LED indicators in topbar flash |
plugin_update | — | Plugins grid updates card status |
ping | — | Resets 90-second dead-stream watchdog |
Active Slot (Multi-Radio)
The frontend tracks which radio slot is being viewed via window._activeSlotId (default: 'node_0'). Switching slots (from the Settings view) calls:
window._sseSetSlot('node_1');
This updates CONFIG.ssePath to /sse/node_1 and reconnects the SSE stream. All API calls that need to be slot-scoped use the _slotAppend(url) helper which appends ?slot_id=node_1.
Global Utility Functions
escapeHtml(text)fmtTime(ts)HH:MM:SS string.fmtTimeAgo(ts)"2m ago", "3h ago").fmtUptime(sec)"2d 4h" / "1h 30m" / "45m".getMeshVal(node, ...keys)deviceMetrics, user, position, etc.).triggerToast(msg, type)'ok' 'warn' 'err' 'acc'.escapeHtml, openInspector, closeInspectorwindow in ui.js, accessible from any view module.Web Serial Fetch Patch
When the Web Serial bridge is active, app.js wraps window.fetch. Any POST /api/messages call is silently redirected to WebSerialBridge.sendText() instead of the server. The caller receives a synthetic Response object so existing code never knows the difference. See Web Serial Connection for full details.
Startup Sequence
DOMContentLoadedfires- Fetch
/api/statusto detectpublic_modeand hide logout if needed - Init
C2Terminal(bottom terminal bar) - Init Web Serial topbar button if feature is enabled
- Load plugin nav menu from
/api/system/plugins/menuand inject into sidebar - Fetch historical packets from
/api/packets/historyto pre-populate MeshShark - Start SSE connection
- Call
loadView('overview')— renders initial view - Start periodic timers: diagnostics clock (1 s), system poll (30 s), SSE watchdog (15 s)
- Acquire Wake Lock to prevent CPU/network sleep during active monitoring
CSS Design System
All styling is in /static/css/style.css. It uses CSS custom properties (variables) for the entire colour palette:
--bg to --bg4#020912 → #12243f--acc#00c8f5 — primary interactive colour--ok#00f090 — success, online, connected--warn#ffa800 — warnings, pending states--err#ff3050 — errors, disconnected, danger--pur#b060ff — secondary accent, beta features--txt, --txt2, --txt3--mono'Share Tech Mono', monospace — all IDs, codes, values--sans'Exo 2', sans-serif — labels, names, UI textThe background uses a CRT scanline overlay (body::before with a repeating linear gradient) for the characteristic terminal aesthetic. All interactive elements use var(--acc) for their active/hover state with a cyan box-shadow glow.
Lazy Loading
View JS modules are loaded on-demand the first time a view is opened. window._lazyLoadView(viewName, callback) checks if the module is already loaded (the global like window.C2CommsApp exists), and if not, dynamically creates a <script> tag to load /static/js/{viewName}.js. Once loaded the callback (which calls the init function) fires. Subsequent visits to the same view skip the script load entirely.
The overview module is always pre-loaded (not lazy) since it is the default landing view.
AbortController Per View
Each call to loadView() creates a new AbortController and immediately aborts any in-flight controller from the previous navigation. This prevents a slow view load from a previous navigation completing after you have already navigated away and overwriting the newly loaded view's content.
Wake Lock
On startup, MeshDash requests a screen wake lock via the navigator.wakeLock API. This prevents the device's CPU from entering low-power sleep states that could drop the network connection and disconnect the SSE stream. The wake lock is re-acquired whenever the tab becomes visible again (browsers release it automatically when the tab is hidden).