CoreScope v3.8.3
Upgrade urgency: High — contains a stored XSS fix. Public dashboards should upgrade immediately.
187 commits since v3.8.2 (47 substantive + 140 auto-generated coverage bumps). Every bullet ends with a commit SHA — git show <sha> to verify.
Highlights
- The live map is buttery now. Packet animations and node pulses moved off SVG-per-frame onto a hardware-accelerated canvas overlay — 60 FPS even when the mesh is firing on all cylinders. (#1490, 914f869; #1521, 75a38f0)
- Dashboards and APIs feel snappier under load.
/api/observersp95 dropped from ~10.8s to sub-second on busy meshes,/api/statsis no longer re-scanning the observations table every 15s for the navbar, and per-observer analytics stopped re-parsing every timestamp three times. (#1483, 13bdee5; #1516, 3850600) - Stored XSS class closed — same class as CVE-2026-45323. Mesh-advertised node names and observer names were rendered to the DOM unescaped on app/nodes/observers/packets/live/analytics/route-view/area-map; a payload like
<img src=x onerror=…>executed. All sinks now escape; follow-up audits closed three more XSS sinks, an unbounded-limitDoS, and several log-injection paths. New CI gate hard-fails PRs that reintroduce either class. (#1537, f15b677; #1539, 53339e0; #1540, 800d61c; #1544, 7b43045; #1543, e4a21fc) - Cross-domain embeds.
?embed=1on/#/mapand/#/channelsstrips the chrome, and a newCORS_ALLOWED_ORIGINSenv unlocks Home Assistant / Grafana panel embeds. (#1500, 367265e) - The map's region filter now actually filters the map. Selecting a region hides non-region nodes by default; a "Show all nodes" toggle preserves the old behavior. (#1501, 28713fa)
- Observers with broken clocks now name themselves. Observers emitting zone-less timestamps get a ⚠️ chip on the observer list and a banner on detail with fix instructions — no more "why is this observer showing 1970?" support tickets. (#1480, 43b93c6)
- Operators with big DBs no longer wait minutes for ingestor startup. Heavy index builds run in the background via a new async-migration runner; the ingestor accepts packets immediately on boot. (#1541, e438451)
- Fresh deploys can decrypt #Public out of the box. Default Public channel key now ships in
channel-rainbow.json. (#897, 451b5e8)
Stored-XSS sink list (for security verification)
HTML-escaped at: app.js global search dropdown (node + channel name); nodes.js table row + Leaflet popups (×2); observers.js table name cell; packets.js observer-name cells (×4) + multi-select checkbox label; live.js node-filter <option> + map tooltip + hop popup; analytics.js topology tooltip + RF-health aria-label; packets.js CHAN hex decode fields; route-view.js hop + union tooltips (×2); area-map.html node popups (×2). Global escapeHtml now covers the full 5-char OWASP set (& < > " '). map.js safeEsc was a no-op identity since #48 — now wired to the real escaper. Backend sanitizeName() deliberately keeps < > " & for lossless storage / meshcore:// deep-links; fix is at the sink per OWASP. Round-2 sweep added: traces.js URL-fragment in popups, observer-detail.js MQTT-meta tooltip, analytics.js RF-health aria-label. Pinned by test-xss-escape-sinks.js and test-anl1-tooltip-render.js with tag-injection and attribute-breakout payloads. (#1537, #1539)
Features
- Hide non-region nodes by default on the live map when a region is selected; "Show all nodes" toggle restores legacy view; state in
localStorage['mc-region-show-all-nodes']. Fixes #1108. (#1501, 28713fa) ?embed=1on/#/mapor/#/channelssuppresses top-nav, bottom-nav, drawer; newcorsAllowedOriginsconfig +CORS_ALLOWED_ORIGINSenv;Access-Control-Allow-Methodstightened toGET, HEAD, OPTIONS. Fixes #1369. (#1500, 367265e)- Customizer: marker stroke color/width/opacity tunable via Colors → Marker Stroke; backed by
--mc-marker-stroke-*CSS vars +markerStrokeconfig block. Fixes #1488. (#1494, ca2c3d6) - Observers: "⚠️ Naive clock" chip on the observer list + banner on detail; backed by
observers_clock_naive_v1migration. Fixes #1478. (#1480, 43b93c6)
Fixes (selected)
- Live page mobile: filter dropdowns no longer clipped by the scrolling toggle bar; settings cog pushed right. Fixes #1529. (#1531, 99cea7b)
- Live nav-pin: 📌 button moved into
.nav-rightso it stops squeezing search/theme/hamburger. Fixes #1526. (0273f15) - Live:
requestAnimationFrame dtclamped to 32ms — no more 8× fast-forward when a backgrounded tab wakes. (#1524, 3e4c456); VCR speed no longer leaks into live mode. Fixes #1346. (#1427, b71b26a) - Live: animations restored to a custom Leaflet pane (z=650) — were painting behind markers since #1334. Fixes #1485. (#1491, 268751f)
- Live canvas follow-up: DPR listener
{once:true}, scratch buffers hoisted, whitespace churn reverted. Fixes #1514. (#1520, bf8bb87) - Channels:
selectChannel()/refreshMessages()no longer stomp WS-pushed messages during the REST replacement window — real production bug. Fixes #1498. (#1513, c9b98cb) - Marker stroke: server defaults restored to v3.7.2 visual after #1494's translucent default looked weak on upgrade. Fixes #1506. (#1507, a7b156d)
- Customizer "🗑️ Reset All" now actually clears everything (CB preset, encrypted-channel toggle, dark-tile pick, marker-stroke vars, per-role writes). Fixes #1496. (#1497, 9bed0e8)
- Packets: collapse chevron on grouped rows no longer reopens a just-closed detail panel. Fixes #1486. (#1492, 7fcb226); HB column uses
getPathLenOffset(route_type)(TRANSPORT path_len is at offset 5). Fixes #1469. (d4280be) - Nodes dark mode:
--card-bgaligned with--surface-2— was washed-out. Fixes #1470. (#1517, 24a840d) - BYOP modal header no longer occludes body content on mobile. Fixes #1487. (#1493, c841dbc)
- Trace tool: validated hash written back via
history.replaceState, so#/tools/trace/<hash>is shareable. Fixes #1522. (#1523, 73ceb47) - Ingestor:
readProcSelfIO()stops stampingtime.Now()beforeos.Open— eliminates phantom rate spikes after transient failures. Fixes #1169. (#1428, 196f1c6) - Ingestor: per-message naive-timestamp warning silenced; observer.last_seen and per-packet rxTime already clamped. (#1479, 0a58aa1)
- Version/commit badge moved from navbar to a Version card on
/#/perf. (#1503, 788a509) - Mobile nav follow-ups (#1471 series): duplicate
.filter-toggle-btnhidden so navbar mirror is the only visible affordance (#1475 f0da38f, #1482 b6e0050); More-sheet mirrors re-injected on each open via click delegate (#1476, 497e419). - Live: "ghost" hops renamed to "inferred". Partial fix for #1505. (#1527, deafe32)
- Live: nav-pin state persisted in
localStorage['live-nav-pinned']. Fixes #1510. (#1515, 878d162)
Upgrade Notes
New config (config.example.json, all opt-in, safe defaults):
corsAllowedOrigins: []— cross-origin allowlist for embed (#1369). Env:CORS_ALLOWED_ORIGINS(comma-separated).markerStroke: { color: "#fff", width: 2, opacity: 1 }— v3.7.2-matching defaults (#1488/#1506).neighborGraph.cacheRecomputeIntervalSeconds: 300(#1483).observersCache.ttlSeconds: 30(#1483).
Auto-applied migrations:
observers_clock_naive_v1—clock_skew_seconds,clock_skew_count_24h,clock_last_naive_atonobservers(#1480).obs_observer_ts_idx_v1— composite(observer_idx, timestamp)index onobservations, runs async via the newRunAsyncMigrationhelper; ingestor accepts packets while it builds (#1483/#1541). State tracked in_async_migrations; idempotent across restarts.
Behavior changes:
- Live map: selecting a region hides non-region nodes by default. Toggle "Show all nodes" once to opt out (persists in localStorage). (#1501)
Access-Control-Allow-Methodsno longer advertisesPOST— cross-origin writes fail preflight by design; same-origin admin writes unaffected. (#1500)- List endpoints silently clamp
limitto 500 (lists) / 200 (analytics, bulk-health). Requests over the cap return the cap. (#1540) - Version badge lives on
/#/perfnow, not the navbar. (#1503)
No breaking config removals or deprecations.
Acknowledgements
- @mxsasha (Sasha Romijn) — responsible disclosure of the stored XSS in #1536.
- @efiten — audit and fix across all unescaped sinks in #1537.
Tagging
git tag -a v3.8.3 e4a21fc9 -m "v3.8.3"