Releases: Kpa-clawbot/CoreScope
CoreScope v3.9.2
Upgrade urgency: Recommended. Big batch of operator-felt wins.
Highlights
Accessibility (#1668 — M2/M3/M4/M5 merged; M6 in flight)
- Two-tier CSS palette with semantic tokens (
--palette-*→--color-*/--text-*); operators can now hot-swap theme palettes - WCAG AA color-contrast hits across all 22 audited routes — 443 baseline violations → 0 verified by the new axe CI gate (#1696)
- Typography pass: 14px body / 12px+500 chip floor across the entire chrome (#1679)
- Hash-cell analytics palette, badge contrast, /live VCR controls (#1681)
- Audio-lab BPM + Volume sliders now have aria-labels
MQTT observability
- Per-source MQTT status endpoint + Observers panel (#1682) — connected/disconnected/last-packet/error counters per broker
- Mobile-friendly: cards at ≤640px instead of squished table (#1698)
Cold-load + analytics correctness
- Packet-store cold-load actually loads the database now (#1691). Pre-fix: after restart, only 0.3% of DB rows were loaded into RAM, dashboards looked empty for hours. Root cause: hot-load filtered by
first_seen(immutable per hash) instead of effective recency. Newtransmissions.last_seendenormalized column + writer-side update fixes this end-to-end. - Analytics endpoint returns 503 Retry-After during warm-up (#1688) — operators no longer see post-restart partial slices
- Warm-up banner surfaces backfill state in the UI (#1683)
Firmware 1.16 sync
- Server-side decoder parity for extended ACK payloads (#1695)
- Customizer toggle to hide 1-byte path hops (collide ~8-way at 2k nodes) — #1689
Performance
/packetsinit parallelizes loadObservers + loadPackets (#1693) — first-row time dropped from 25-40s to <5s on slow links- Confidence rating weighted by hash mode (#1687) — 1-byte sightings carry less weight than 6-byte
CI / operability
- OpenAPI completeness gate (#1678 Phase 1)
- Release fast-path: re-tag :edge → :vX.Y.Z when SHA matches (#1680) — most patch releases now skip the 30min full pipeline rerun
- Staging disk monitor + cleanup cron (#1686)
- Slideover E2E flake-gate reduced 20× → 3×, root cause fixed in #1693
Container image
ghcr.io/kpa-clawbot/corescope:v3.9.2 (multi-arch: amd64, arm64)
Verification
- 22 routes × 2 themes × 2 viewports automated a11y gate (axe-core, #1696)
- All 19 PRs ran through the full polish chain (parallel reviewers + Kent Beck TDD gate)
- Cold-load fix verified by
transmissions.last_seentest fixture (#1691)
Operator upgrade (MikroTik)
/container add remote-image=ghcr.io/kpa-clawbot/corescope:v3.9.2 envlist=cs-env mounts=cs-data,cs-caddyfile interface=veth-corescope start-on-boot=yes name=corescope-prod-v392
Acknowledgements
External contributors carry over from v3.9.1.
CoreScope v3.9.1
Upgrade urgency: Recommended — v3.9.1 is what v3.9.0 should have been. v3.9.0's container image never published (Playwright flake gated Docker build). v3.9.1 includes everything in v3.9.0 plus:
- WCAG AA contrast pass — new two-tier CSS palette (raw
--palette-*→ semantic--color-*/--text-*); muted-text family bumped to ≥4.5:1 in both themes (most well above —--text-mutedon dark surface goes 3.5 → 11.58:1). Operator-reported unknown-repeater chip ("dark-blue text on dark-blue background") fixed (2.75:1 → 4.95:1). Closes #1671. Partial fix for #1668. (#1676, f0addfd) - Slideover test stability —
test-slideover-1056-e2e.jswas racing the packets virtual-scroll spacer; tightened selectors, bumped data-row wait to 20s. Cleared the day-of-release CI flakes. Fixes #1662. (#1663+followups, f06359d)
Verification
Test plan: workspace-meshcore/test-plans/v3.9.0-cdp-test-plan.md (93 tests, applies unchanged to v3.9.1).
M1 a11y audit: workspace-meshcore/a11y-audit/reports/violations-summary.md (2,429 BLOCKER → estimated 85% cleared by M2).
Acknowledgements
External contributors from v3.9.0 still apply: @efiten, @EldoonNemar. No new external PRs since v3.9.0.
CoreScope v3.9.0
Upgrade urgency: Medium — fixes the post-restart "relay timelines empty" regression, surfaces silent /api/nodes truncation, and ships operator-controlled per-name hiding.
257 commits since v3.8.3 (72 substantive + 185 auto-generated coverage bumps). Every bullet ends with a commit SHA — git show <sha> to verify.
Highlights
- Your relay timelines survive a restart. Before v3.9.0, every container restart left repeater nodes with empty hop histories until live traffic replayed enough adverts to re-attribute. Now the relay-hop index is rebuilt from
path_jsonduring cold load — per-relay timelines, hop counts, and route stats are intact the moment the server says it's ready. (#1643, 938153d) /api/nodesstops silently truncating at 500 rows. The hard cap was hiding nodes from the map, analytics and packets pages on any mesh of meaningful size — without any warning. Now properly paginated across every consumer, with internal UI requests bypassing the per-page clamp. (#1607, 2610574; #1637, 9002b25; #1589, 7421ead)- Hide your own node from a public dashboard with a prefix rename. New
hiddenNamePrefixesconfig (default["🚫"]) drops matching nodes from/api/nodes*while keeping DB rows for analytics — same convention other MeshCore dashboards already follow, no DB surgery, no permanent loss of history. (#1655, 825b264) - Observer Compare is finally discoverable. The compare page existed before but was a hidden URL trick; now there are three entry points (header CTA, sticky selector strip, observer-table multi-select) leading into a Tufte-grade compare view with state-preserving selection. (#1642, 531bc8a; #1645, c93ae67; #1647, 167af54)
- Per-node Reach. New
/api/nodes/{pubkey}/reach+ UI surfaces directional link quality per neighbor — answers "is my link to X any good in both directions" without staring at a topology graph. (#1627, e2212f5)
What's New
Observer Compare
- Promote observer comparison to first-class: header CTA, sticky selector strip, observer-table multi-select. (#1642, 531bc8a)
- Tufte-grade compare page with themed button vocabulary + state-preserving multi-select across navigations. (#1645, c93ae67)
- Polish: tightened checkboxes, hierarchy, selector strip + mobile fixes. (#1647, 167af54)
- Wire
TableSorton the observers table with numeric/time column types so the sort affordance actually sorts. (#1641, d72ab69)
Reach & Nodes
- Per-node Reach page +
GET /api/nodes/{pubkey}/reach(directional link quality). (#1627, e2212f5) - Paginate
/api/nodesacross map/live/analytics/packets/area-map so the 500-row server cap stops silently truncating UIs. (#1607, 2610574; #1637, 9002b25) - Sortable First Seen column on the Nodes table. (#1587, 7533b3b)
- Firmware
repeat:on|offhint now excludes listener-only observers from the disambiguator. (#1624, a477655) - Link RTC-reset warnings on node detail to the offending packet hashes. (#1590, 1a2b8c4)
Analytics
- Relay Airtime Share endpoint + dumbbell chart. (#1601, 3898688)
- 5-minute rolling-baseline anomaly detection for Write Sources. (#1593, a26a412)
- TRACE packets overlay per-hop SNR on the path graph. (#1622, e9aed64)
- Multi-byte prefix repeaters now show up in the 1-byte hash-usage matrix view. (#1591, 3df8924)
Live & Map
- Fullscreen toggle on the live map + controls collapsed by default. (#1572, d7bd9d5)
- Colorblind simulation overlay (Brettel/Vienot) with reset-to-Wong button. (#1600, 571c960)
- Path symbols legend disclosure on packets. (#1570, 5fd8900)
- OSM / Stamen tile providers with per-provider Leaflet layer control. (#1533, d7cd920)
- Operator-configurable
liveMap.maxNodes(default 2000). (#1577, 1bdb92d)
Config & Operator Surfaces
hiddenNamePrefixes(default["🚫"]) — drop matching nodes from/api/nodes*while preserving DB rows. (#1655, 825b264)- Config-driven disabled-tabs list in the customizer modal. (#1579, 7292d60)
branding.homeUrloverride for embedded deployments. (#1576, 9b36b7c)- Configurable observer-health thresholds. (#1556, 65bd954)
- Detect CDN-fronted deployment + document bypass requirement. (#1564, 63bfa3d)
- Expose
--nav-active-bgas a themeable token. (#1571, 892eb2c)
Performance
- Chunked
Loadwith early HTTP readiness — server accepts requests while heavy load completes in the background. (#1596, bc1822e) - Background subpath + pathHop index builds with ready gates. (#1604, df61660)
- Lazy distance-index build on first request. (#1597, 5629a48)
neighbor_api: foldfirst_seeninto cached map — fixes the #1627 r3 regression. (#1632, 078225a)GOMEMLIMITviaruntime.maxMemoryMBin server + ingestor. (#1595, 1b112f0)- SQLite writer-lock wait/hold instrumentation per component. (#1594, 222bfdf)
Bug Fixes
Ingestor
- Subscribe to MQTT before startup maintenance; buffer until the writer is free. (#1609, 18810b5)
- Decode firmware 1.16.0 extended ACK (5/6-byte payloads). (#1618, 9612f08)
- Write
resolved_pathon new observations (regression from #1289). (#1548, 3feb97f) - Defense-in-depth empty-scope guard in
UpdateNodeDefaultScope. (#1575, cd19285) - Skip
default_scopeupdate whenScopeNameis empty. (#1569, 05af6c6) - Address #1609 follow-up findings — config doc, receipt-time liveness, buffer stop/clamp warn. (#1623, 3d12266)
Reach
- Bust response cache on blacklist change. (#1636, 8295c21)
scanReachRowsDB errors must surface as 500, not 404. (#1635, 43be1bb)- Narrow-viewport CSS — no horizontal scroll, map no longer shrunken. (#1634, 59d6646)
Frontend / UI
- Render analytics Channels group-header sprites as HTML, not escaped text. (#1658, fb6bb08)
- Bump feed-detail-card z-index + make popup draggable. (#1620, f66ff40)
- Theme-track
.vcr-scope-btn.active+.copy-link-btn:hoverbackgrounds. (#1578, 16c7ea4) - Replay handoff no longer freezes the map (suppressLive flag). (#1603, 1f65d78)
- Detach slide-over panel on close — architectural focus-restore fix +
--repeat-each=20CI gate. (#1617, 37a7a92) getTileUrl()now invokes function-typed provider URLs + regression tests. (#1615, dc433e4)- Reliably restore row focus on panel close. (#1602, 1be0aec)
- Gesture hints — edge-drawer mobile-only + row-swipe widening (re-fix). (#1586, 116efe4)
- Honor time-window filter on Route Patterns analytics. (#1592, d6384c3)
- Live corner-cycle button clears drag state. (#1568, 2b45f78)
- Observers aggregate header shows "Last updated" timestamp. (#1563, a7ad2be)
- Mirror
Load'sresolved_pathindexing intoloadChunk. (#1582, 9465949) - Remove dead server-side backfill flag (stuck
backfilling=true). (#1583, f7571a2) - Additional follow-up fixes for #1532. (#1580, 373ee81)
- Document
writeStatsAtomicsymlink-replace semantics + regression test. (#1588, af66943)
API
- Bypass API limit clamps for internal UI requests (revisit of #1540). (#1589, 7421ead)
- Emit
Cache-Control: no-storeon/api/*responses. (#1553, 0c908d2)
Performance
- Chunked
Loadearly-readiness, background index builds, lazy distance index, cachedfirst_seen,GOMEMLIMIThonored, writer-lock instrumentation — see "Performance" under What's New.
Security
- Detect CDN-fronted deployment and document the bypass requirement so rate limits and PoW gates can't be silently routed around. (#1564, 63bfa3d)
Breaking changes
None.
Operator-facing changes / config
New config (config.example.json, all opt-in, safe defaults):
hiddenNamePrefixes: ["🚫"]— node-name prefixes that hide a node from/api/nodes,/api/nodes/search,/api/nodes/{pubkey}. DB rows preserved; analytics history intact. Mirrors the convention used by other MeshCore map dashboards. Opt out with[]. (#1655, 825b264)liveMap.maxNodes: 2000— cap nodes rendered on the live map; raise for big meshes, lower for low-power dashboards. (#1577, 1bdb92d)runtime.maxMemoryMB— setsGOMEMLIMITfor server + ingestor; tune against container limits. (#1595, 1b112f0)- Observer-health thresholds — previously hard-coded, now configurable. (#1556, 65bd954)
branding.homeUrl— override the "Home" link target for embedded deployments. (#1576, 9b36b7c)- Customizer disabled-tabs — config-driven hide list. (#1579, 7292d60)
Behavior changes:
/api/nodespaginates instead of silently truncating at the 500-row cap; internal UI requests bypass the clamp. (#1607, 2610574; #1589, 7421ead)/api/*responses now emitCache-Control: no-store. (#1553, 0c908d2)- Per-node Reach available at
GET /api/nodes/{pubkey}/reach. (#1627, e2212f5) - Hashtag channels from
meshcore-channelscatalogue appear in the channels list without manual config. (#1656, e04c711)
Behind the scenes
- Emoji → Phosphor migration (six PRs). M1 top-nav/mobile-nav/Compare (#1649, 55e4d95) · M2 page headers + table chrome (#1650, 3062745) · M3 detail panes + badges (#1651, b812a98) · M4 map + route overlays (#1652, 2b6809c) · M5 settings + customize (#1653, 1116801) · M6 final sweep + lint gate + carry-forwards (#1654, 89eade6). Tracking: #1648.
- CI: bump go test timeout 10m → 15m (suite grew past 10m post-#1655). (#1661, 0712c5f)
- Test: tighten slideover row selector to avoid the virtual-scroll spacer race. (#1663, 037dc8c)
- Test: subpaths_window tests wait for index readiness after the #1595 chunked load. (#1621, ad41b9b)
- Test: mock
/api/nodes/searchin home-coverage E2E (closes #1313). (#1584, 6a027b0) - Refactor: extract pure helpers into
route-view-utils.js. (#1581, 545013d)
Verification
Test plan: workspace-meshcore/test-plans/v3.9.0-cdp-test-plan.md (93 tests across 16 sections)
Initial run (master pre-#1665, 2026-06-12 00:45 UTC): 56 pass / 22 partial / 5 fail / 14 skipped. Two BLOCKER lint-gate breaches surfaced — .obs-clock-naive-chip (#v384-1.2, 14×
CoreScope v3.8.3
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 byobservers_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"
v3.8.2 — Mobile UX, Customizer, Performance
v3.8.2 — Mobile UX, Customizer, Performance
(Draft for operator review. ~3 weeks of work since v3.8.1, ~25 substantive commits.)
Headliners
- Mobile UX overhaul — packets surface redesigned mobile-first with single-row navbar, semantic-first detail panel, hidden chrome rail; nav controls (Favorites/Search/Customize) finally reachable on phones (#1471 — closes #1415/#1458/#1461/#1467/#1468/#1470).
- Customizer reframe — CB preset is now an end-user OPT-IN, not a force-override. Operator's
config.json nodeColorsare honored by default; per-user customizer override beats the preset (#1446/#1447/#1448/#1449 — fixes longstanding "config not honored" reports). - Dark-tile provider picker — 4 dark map variants pickable in customizer: Carto Dark, Esri Dark Gray, Voyager Inverted, Positron Inverted (#1420/#1430). Honored across all map surfaces including node-detail inset (#1470).
- Observer timestamps fixed — California (UTC-7) observers were perpetually 7h stale because naive timestamps were treated as UTC. Now:
observer.last_seenalways uses ingest time, per-packet rxTime keeps envelope time with symmetric clamp (#1463/#1464/#1465/#1466). - Cold-path cache —
/api/nodesno longer blocks on stale-cache rebuild; serves stale-while-revalidate (#1272/#1436).
Operator-facing changes
UI / UX
- New customizer toggle: Show encrypted channels (default OFF — finally a way to control the encrypted-channel flood without DevTools) (#1454/#1455)
- Traffic share label replaces the misleading "Usefulness" score (renamed everywhere; new
traffic_share_scoreJSON field added alongside the legacyusefulness_scorefor API compat). Tooltips explain both Traffic share and Bridge score with tap-to-toast on mobile (#1456/#1457) - Custom navbar logos no longer squished to 125×36 — proportional aspect preserved for square, tall, wide variants (#1450/#1451)
- Per-role color overrides now actually propagate to marker SVGs (was silently dropped before — #1438/#1439/#1441/#1443)
- "unknown" fake channel no longer accumulates in the channel list (live router was synthesizing it from undecryptable CHAN messages — #1468)
- Hash matrix marks 0x00 and 0xFF as reserved prefixes per MeshCore firmware keygen convention (@halo779 report — #1473/#1474)
- Mobile gesture hints — touch-only gate added (no longer show on desktop), width:fit-content fix for off-screen pills (#1065 chain)
Server / Performance
repeaterEnrichTTLcache cold-path now stale-while-revalidate; first request after expiry no longer blocks (#1272/#1436)- Hourly SQLite WAL checkpoint prevents unbounded WAL growth (#1435)
- Startup warning when
GOMEMLIMIT < 50% of container memory limit— prevents future OOMs (#1264/#1429) - WebSocket MQTT broker support (
ws:///wss://) documented + tested (#902) - Paths-through-node now sorted by recency (LastSeen DESC) with count as tiebreaker — was non-deterministic map iteration order (#1145/#1431)
Bug fixes
- Naive (zone-less) ISO timestamps in MQTT envelopes now symmetrically clamped (was: only future-skew rejected, past-skew passed through verbatim — California observers perpetually stale) (#1463/#1464)
- Observer.last_seen always uses ingest time, never envelope time (architectural cleanup, eliminates whole class of TZ bugs) (#1465/#1466)
- Hop-name mis-resolution on prefix collision — regression test added for #1144 (the production fix shipped earlier; this guards against re-introduction) (#1433)
- Channel-color-picker outside-click E2E flake fixed for real this time (race with
setTimeout(0)listener install — proper fix using rAF×2 + retry) (#1462) - CI: master runs no longer cancelled by subsequent commits (was dropping Deploy Staging silently — #1395/#1426)
- CSS parse-error: stray non-comment text after
#1062block was eating the next CSS rule from CSSOM (gesture-hint .width:fit-content wasn't taking effect for weeks) (#1065/#1453)
Community contributions this cycle
- @efiten — #1145 paths sort, #1264 GOMEMLIMIT warning, #1272 cold-path cache, #902 WS MQTT docs, #1395 CI concurrency fix, WAL checkpoint
- @halo779 — #1473 reserved-prefix report (your in-game testing flagged the firmware convention CoreScope was ignoring)
Thanks 🙏
Known issues / follow-ups
Resolved from v3.8.1's known-issues list:
- ✅ CB preset switching now applies everywhere without reload (#1446 reframe + cascade fix)
- ✅ Customizer (🎨) reachable on mobile portrait via bottom-nav More sheet (#1467, no more rotate-to-landscape workaround)
Still pending from v3.8.1 (carried forward):
- Mobile route view auto-fit can briefly overshoot on iOS Safari URL bar collapse
- Sidebar drag-resize doesn't persist when localStorage is blocked
- "Scan QR" for PSK channel not yet functional
- Analytics → Distance tab still launches the route map via legacy sessionStorage flow (not deep-link)
New in v3.8.2:
- Node-detail card backgrounds are technically dark but visually close to page bg (#1470 deferred — operator's call: ship or revisit)
- Group-header rows in /packets render 200-700px tall in some cases (existing grouping behavior, not new this cycle)
Upgrade notes
- Frontend cache-bust automatic via
__BUST__token - Backend image:
ghcr.io/kpa-clawbot/corescope:v3.8.2(will tag on release) - No config migration required
- New
traffic_share_scorefield appears in/api/nodesand/api/nodes/{pk}responses alongside existingusefulness_score— no breaking change
v3.8.1 — Route, Refined
CoreScope v3.8.1 — Route, Refined
Released 2026-05-27. Previous: v3.7.2 (2026-05-06). Supersedes v3.8.0 (released earlier today, before this perf fix landed).
⚡ Patch in v3.8.1: /api/nodes TTL fix
Community contribution from @efiten (#1425). The repeaterEnrichTTL was 15 seconds but the background recomputer runs every 5 minutes — so the cache was stale for 4m45s out of every 5min. On dense hop-graph deployments that meant 18s /api/nodes cold-miss responses. TTL is now derived as 2 * recomputer interval so the cache stays warm between ticks. Measured 18.6s → ~0.5s on the contributor's prod mesh (analyzer.on8ar.eu).
If you already deployed v3.8.0, just upgrade to v3.8.1 — same image semantics, drop-in.
Three weeks, 135 merged PRs. The headline is the Route Viewer redesign — packets and the paths they travel, finally rendered as one coherent story. Underneath: startup-window improvements for large DBs, a near-complete mobile chrome overhaul.
🗺️ NEW: Route Viewer
The new route viewer (route-view.js + route-view.css) puts packet context, observer paths, and resolved hops in one sidebar — and reflows cleanly to a bottom sheet on mobile.
- Packet context block in the sidebar header — per-type fact list (ADVERT name/role/sig/pubkey, DM src→dst with encryption state, GRP_TXT channel + decrypted preview, TRACE official-vs-observed hops, PATH src→dst with payload-source chips). Merges
pkt.decoded_json+obs.decoded_jsonand falls back to byte-levelraw_hexparsing for encrypted DMs. - Multi-path picker — chip per unique observer-path (
<count>/<total>+ hex hop string). Click to isolate; "All" renders an edge-deduplicated UNION view (each unique edge once, stroke weight = observer count). - Deep-link URLs —
#/map?packet=<hash>&obs=<id>. Bookmarkable, shareable, single source of truth. sessionStorage flow removed. - Hop resolution priority chain — server
resolved_path→ sharedHopResolver(observer-IATA-aware, same as packets page) → raw prefix. Kills a whole class of "route view named hops differently than packet detail" bugs. - Markers — uniform 22 px filled circle with seq number inside, hollow endpoint ring for SRC/DST, double concentric ring on SRC=DST loops, spider-fan for sub-14 px collisions debounced to
zoomend. - Colorblind-preset live colors —
routeRampper preset (viridis / plasma / pure-luminance) written to--mc-rt-ramp-0..4CSS vars and hot-recoloured oncb-preset-changed/theme-changed. - Desktop chrome — drag-to-resize sidebar persisted to localStorage, Material/Drive-style collapse chevron centred on the right edge.
- Mobile bottom sheet — anchored above bottom-nav + safe-area inset, thin summary line when collapsed (
TYPE · N hops · X km · M obs), expands to ~75 vh. All three legacy mobile detail panels closed on route entry.
Closes: #1418, #1419, #1422 · PR #1423
Related map work that landed this cycle: role-aware marker shapes + outline rings (#1334), WCAG 2.2 AA pass on cluster bubbles + role pills + multi-byte labels (#1357), thinner always-on marker outline (#1347).
⚡ Startup
- Hot startup window — new
packetStore.hotStartupHoursconfig loads only N hours of packet history synchronously and fills the rest in background daily chunks. Large DBs (multi-GB) no longer block the HTTP listener on a full-retention load before serving traffic (#1187). - Ingestor now owns the neighbor-graph and all schema migrations; the server is read-only. Removes a class of startup races where two processes raced the same migration (#1286, #1289).
🔬 NEW: Protocol decoder coverage
The MeshCore decoder now handles every documented payload type, with the legend updated end-to-end so operators see real names instead of Type 6.
- GRP_DATA + MULTIPART wire-format decoded (#1280) — group-data sensor payloads + multipart fragmentation are no longer opaque blobs.
- CONTROL flags +
advertRolefix — accurate role attribution on ADVERTs from edge cases that were previously misclassified (#1280). payloadTypeNamestable centralised, exposed at the API + UI; new TransportCodes / Feat1 / Feat2 enums; RAW_CUSTOM type plumbed through; sensor-payload docs published (#1291).
🆕 NEW: Operator features
- Repeater "usefulness score" (bridge axis) — second of four planned ranking dimensions for how much a repeater is structurally necessary to mesh connectivity (#1275, #672 axis 2/4).
- Area-based visual node filter — define GPS polygons in config; attribute and filter packets by transmitter location across nodes/packets/map/live/analytics (#839).
- Scoped vs unscoped transport-route statistics — per-region HMAC slicing of route stats; mirrors the existing
hashChannelspattern (#915). - Sortable Scope column on the Nodes list (#1195).
- Observer IATA badges on packets, with new filter grammar to slice by observer location (#1189).
- Multi-byte hash capability is now persisted across restart with O(1) per-key lookup; operator-friendly capability badge surfaces it in the UI (#1324).
- Material Design dark mode — properly polished pass (closes #893, #1389). Primary surface set, contrast, button + chip + form-control restyle.
- Colorblind presets in the theme customizer — Wong / Deuteranopia-tuned / Plasma / Achromat ramps, applied to roles + multi-byte tiers + (in this release) the new route viewer's sequence ramp (#1378, #1408, #1414, #1407).
- Mobile gesture system — swipe rows for actions, swipe tabs to switch sections, slide-over dismiss (#1185); edge-swipe to open the nav drawer (#1184); first-visit hints so the gestures are discoverable (#1186).
- New CoreScope logo + Aldrich webfont (#1137, #1138).
🐛 Bug Fixes
Protocol / Ingest
- MQTT decode no longer panics on malformed path length — bounds check prevents
slice [218:15]panic (#1214). - MQTT half-open TCP recovery — watchdog forces paho reconnect on stall (#1336); per-attempt logging surfaces silent reconnect-loop death (#1216).
- Ingest-time vs observer time — ingestor stamps server
now()for packet/observation storage; reverted earlier envelope-timestamp path that was driving counters wrong (#1233, #1372). - Hop disambiguator quality — source-diversity confidence weighting in tier-1 resolver (#1235), hop-context + observation-count tiebreak (#1198), 6 deferred quality items (#1200).
- Geo-implausible edges rejected at neighbor-graph build time (#1230).
- Clock-skew robustness — RTC-reset outliers excluded from hash median + recent-bad count (#1288).
- Canonical
resolved_patheverywhere —/api/nodes/{pk}/paths(#1282) and paths-through resolver (#1353) now use the persisted resolved path instead of naive prefix matching; fixes wrong-node attribution. - Structural pubkey attribution via new
from_pubkeycolumn (#1152). - dbschema startup race fixed by giving optional-column migrations a canonical source (#1322).
UI / Channels / Packets
- Channels page — chat-app redesign restores prod row layout + detail view (#1376); shows latest message time, not first-seen (#1368); drops ghost "unknown" bucket for encrypted-no-key packets (#1377); stops force-enabling "show encrypted" on every init (#1410); mobile UX overhaul (#1227).
- Packets page — orders by ingest id rather than rxTime so fresh activity is actually visible (#1349); observer IATA shown on packets with new filter grammar (#1189).
- Scopes tab — fixed JSON.parse by dropping duplicate
/apiprefix in scope-stats fetch path (#1379). - Live region filter no longer wipes the feed — correctly parses
{observers:[...]}response shape (#1140). - Prefix Tool Network Overview shows configured-hash-size counts, not math-only slices (#1271).
- Customizer —
nodeColorsstopped force-overridingROLE_COLORSso CB presets actually propagate (#1408, #1414). - Node detail full-page renders error state on 404 instead of silent blank (#1158); restored WCAG AA contrast on "Paths Through" links in dark mode (#1159); section reorder puts Recent Packets above Paths (#1160); dropped orphan separators from "Heard By" rows (#1161).
🎨 UI / UX Polish
- Material Design dark-mode polish (#1389, closes #893) — primary surface set, contrast pass, button + chip + form-control restyle.
- Customizer — live preview pane mirroring map / nodes / packets (#1408 deps), per-preset WCAG validator on save (#1407 chain).
- Multi-byte hash UI — capability badge with operator-friendly tooltip (#1324), config-driven counts everywhere (#1271).
- Nodes page — neighbor-aware reference-node selector (#1389 chain),
geo/role/hash-sizefilter URL params survive reloads. - Live page — reconnect indicator + last-event-ago badge.
- Theme — operator-customizable accent + role colors via single JSON file, shared across map / nodes / packets / live / channels.
🔧 Operator & Infra
- New config keys (see Upgrade notes for full list):
packetStore.hotStartupHours,compression.gzip,compression.websocket,areas,hashRegions. - Compression — opt-in HTTP gzip + WebSocket permessage-deflate, both default-off (#934).
- Area-based filters — define GPS polygons in config; surfaces an
Area:selector across analytics / nodes / packets / map / live (#839). - Scoped transport-route stats —
hashRegionsHMAC mirrors thehashChannelspattern, lets operators slice transport-route metrics by region (#915). /api/perf/io— exposescancelledWriteBytesPerSecfrom/proc/self/ioplus ingestor side (#1167).- Ingestor/server architectural split — neighbor-graph + schema migrations are now ingestor-owned (#1286, #1289); see Upgrade notes.
/#/perfpage — parallel health fetch + sort + auto...
v3.8.0 — Route, Refined (superseded by v3.8.1)
⚠️ Superseded by v3.8.1 — released the same day with a community-contributed/api/nodesTTL fix (#1425, @efiten). Use v3.8.1 instead.
CoreScope v3.8.0 — Route, Refined
Released 2026-05-27. Previous: v3.7.2 (2026-05-06).
Three weeks, 135 merged PRs. The headline is the Route Viewer redesign — packets and the paths they travel, finally rendered as one coherent story. Underneath: startup-window improvements for large DBs, a near-complete mobile chrome overhaul.
🗺️ NEW: Route Viewer
The new route viewer (route-view.js + route-view.css) puts packet context, observer paths, and resolved hops in one sidebar — and reflows cleanly to a bottom sheet on mobile.
- Packet context block in the sidebar header — per-type fact list (ADVERT name/role/sig/pubkey, DM src→dst with encryption state, GRP_TXT channel + decrypted preview, TRACE official-vs-observed hops, PATH src→dst with payload-source chips). Merges
pkt.decoded_json+obs.decoded_jsonand falls back to byte-levelraw_hexparsing for encrypted DMs. - Multi-path picker — chip per unique observer-path (
<count>/<total>+ hex hop string). Click to isolate; "All" renders an edge-deduplicated UNION view (each unique edge once, stroke weight = observer count). - Deep-link URLs —
#/map?packet=<hash>&obs=<id>. Bookmarkable, shareable, single source of truth. sessionStorage flow removed. - Hop resolution priority chain — server
resolved_path→ sharedHopResolver(observer-IATA-aware, same as packets page) → raw prefix. Kills a whole class of "route view named hops differently than packet detail" bugs. - Markers — uniform 22 px filled circle with seq number inside, hollow endpoint ring for SRC/DST, double concentric ring on SRC=DST loops, spider-fan for sub-14 px collisions debounced to
zoomend. - Colorblind-preset live colors —
routeRampper preset (viridis / plasma / pure-luminance) written to--mc-rt-ramp-0..4CSS vars and hot-recoloured oncb-preset-changed/theme-changed. - Desktop chrome — drag-to-resize sidebar persisted to localStorage, Material/Drive-style collapse chevron centred on the right edge.
- Mobile bottom sheet — anchored above bottom-nav + safe-area inset, thin summary line when collapsed (
TYPE · N hops · X km · M obs), expands to ~75 vh. All three legacy mobile detail panels closed on route entry.
Closes: #1418, #1419, #1422 · PR #1423
Related map work that landed this cycle: role-aware marker shapes + outline rings (#1334), WCAG 2.2 AA pass on cluster bubbles + role pills + multi-byte labels (#1357), thinner always-on marker outline (#1347).
⚡ Startup
- Hot startup window — new
packetStore.hotStartupHoursconfig loads only N hours of packet history synchronously and fills the rest in background daily chunks. Large DBs (multi-GB) no longer block the HTTP listener on a full-retention load before serving traffic (#1187). - Ingestor now owns the neighbor-graph and all schema migrations; the server is read-only. Removes a class of startup races where two processes raced the same migration (#1286, #1289).
🔬 NEW: Protocol decoder coverage
The MeshCore decoder now handles every documented payload type, with the legend updated end-to-end so operators see real names instead of Type 6.
- GRP_DATA + MULTIPART wire-format decoded (#1280) — group-data sensor payloads + multipart fragmentation are no longer opaque blobs.
- CONTROL flags +
advertRolefix — accurate role attribution on ADVERTs from edge cases that were previously misclassified (#1280). payloadTypeNamestable centralised, exposed at the API + UI; new TransportCodes / Feat1 / Feat2 enums; RAW_CUSTOM type plumbed through; sensor-payload docs published (#1291).
🆕 NEW: Operator features
- Repeater "usefulness score" (bridge axis) — second of four planned ranking dimensions for how much a repeater is structurally necessary to mesh connectivity (#1275, #672 axis 2/4).
- Area-based visual node filter — define GPS polygons in config; attribute and filter packets by transmitter location across nodes/packets/map/live/analytics (#839).
- Scoped vs unscoped transport-route statistics — per-region HMAC slicing of route stats; mirrors the existing
hashChannelspattern (#915). - Sortable Scope column on the Nodes list (#1195).
- Observer IATA badges on packets, with new filter grammar to slice by observer location (#1189).
- Multi-byte hash capability is now persisted across restart with O(1) per-key lookup; operator-friendly capability badge surfaces it in the UI (#1324).
- Material Design dark mode — properly polished pass (closes #893, #1389). Primary surface set, contrast, button + chip + form-control restyle.
- Colorblind presets in the theme customizer — Wong / Deuteranopia-tuned / Plasma / Achromat ramps, applied to roles + multi-byte tiers + (in this release) the new route viewer's sequence ramp (#1378, #1408, #1414, #1407).
- Mobile gesture system — swipe rows for actions, swipe tabs to switch sections, slide-over dismiss (#1185); edge-swipe to open the nav drawer (#1184); first-visit hints so the gestures are discoverable (#1186).
- New CoreScope logo + Aldrich webfont (#1137, #1138).
🐛 Bug Fixes
Protocol / Ingest
- MQTT decode no longer panics on malformed path length — bounds check prevents
slice [218:15]panic (#1214). - MQTT half-open TCP recovery — watchdog forces paho reconnect on stall (#1336); per-attempt logging surfaces silent reconnect-loop death (#1216).
- Ingest-time vs observer time — ingestor stamps server
now()for packet/observation storage; reverted earlier envelope-timestamp path that was driving counters wrong (#1233, #1372). - Hop disambiguator quality — source-diversity confidence weighting in tier-1 resolver (#1235), hop-context + observation-count tiebreak (#1198), 6 deferred quality items (#1200).
- Geo-implausible edges rejected at neighbor-graph build time (#1230).
- Clock-skew robustness — RTC-reset outliers excluded from hash median + recent-bad count (#1288).
- Canonical
resolved_patheverywhere —/api/nodes/{pk}/paths(#1282) and paths-through resolver (#1353) now use the persisted resolved path instead of naive prefix matching; fixes wrong-node attribution. - Structural pubkey attribution via new
from_pubkeycolumn (#1152). - dbschema startup race fixed by giving optional-column migrations a canonical source (#1322).
UI / Channels / Packets
- Channels page — chat-app redesign restores prod row layout + detail view (#1376); shows latest message time, not first-seen (#1368); drops ghost "unknown" bucket for encrypted-no-key packets (#1377); stops force-enabling "show encrypted" on every init (#1410); mobile UX overhaul (#1227).
- Packets page — orders by ingest id rather than rxTime so fresh activity is actually visible (#1349); observer IATA shown on packets with new filter grammar (#1189).
- Scopes tab — fixed JSON.parse by dropping duplicate
/apiprefix in scope-stats fetch path (#1379). - Live region filter no longer wipes the feed — correctly parses
{observers:[...]}response shape (#1140). - Prefix Tool Network Overview shows configured-hash-size counts, not math-only slices (#1271).
- Customizer —
nodeColorsstopped force-overridingROLE_COLORSso CB presets actually propagate (#1408, #1414). - Node detail full-page renders error state on 404 instead of silent blank (#1158); restored WCAG AA contrast on "Paths Through" links in dark mode (#1159); section reorder puts Recent Packets above Paths (#1160); dropped orphan separators from "Heard By" rows (#1161).
🎨 UI / UX Polish
- Material Design dark-mode polish (#1389, closes #893) — primary surface set, contrast pass, button + chip + form-control restyle.
- Customizer — live preview pane mirroring map / nodes / packets (#1408 deps), per-preset WCAG validator on save (#1407 chain).
- Multi-byte hash UI — capability badge with operator-friendly tooltip (#1324), config-driven counts everywhere (#1271).
- Nodes page — neighbor-aware reference-node selector (#1389 chain),
geo/role/hash-sizefilter URL params survive reloads. - Live page — reconnect indicator + last-event-ago badge.
- Theme — operator-customizable accent + role colors via single JSON file, shared across map / nodes / packets / live / channels.
🔧 Operator & Infra
- New config keys (see Upgrade notes for full list):
packetStore.hotStartupHours,compression.gzip,compression.websocket,areas,hashRegions. - Compression — opt-in HTTP gzip + WebSocket permessage-deflate, both default-off (#934).
- Area-based filters — define GPS polygons in config; surfaces an
Area:selector across analytics / nodes / packets / map / live (#839). - Scoped transport-route stats —
hashRegionsHMAC mirrors thehashChannelspattern, lets operators slice transport-route metrics by region (#915). /api/perf/io— exposescancelledWriteBytesPerSecfrom/proc/self/ioplus ingestor side (#1167).- Ingestor/server architectural split — neighbor-graph + schema migrations are now ingestor-owned (#1286, #1289); see Upgrade notes.
/#/perfpage — parallel health fetch + sort + auto-pause refresh when tab is hidden (#1261).
🧪 Testing & dev
- Playwright E2E suite stabilized — flake fixes that unblocked master (#1330, #1310, #1317).
- 122 new assertions for the route viewer covering hop resolution priority, raw_hex byte extraction, edge weight scales (incl. boundary fix), spider-fan collision grouping + loop double-ring, channel hash → name resolution, and CB-preset CSS var integration.
- CI workflow updated to run the new route-view tests on every PR....
v3.7.2
v3.7.2 — hotfix: ingestor backfill infinite-loop fix (#1119, #1121)
v3.7.1
Identical to v3.7.0 + live PSK decrypt fix (#1031). This is the tagged release with the Docker image on GHCR.
docker pull ghcr.io/kpa-clawbot/corescope:v3.7.1
See v3.7.0 release notes for full changelog.
v3.7.0 — Operator's Toolkit
CoreScope v3.7.0 — "Operator's Toolkit"
50 commits · 30 issues closed · 95 files changed · +7,710 / -293 LOC
Channel Decryption
Rebuilt from scratch. Pure-JS AES-128-ECB + SHA-256/HMAC — works on HTTP and HTTPS. PSK channels: add by key, name them, delete them, see decrypt count. Live WebSocket auto-decrypt with unread badges. Channel hash-byte dedup fixed.
Analytics
Selectable timeframes (1h/24h/7d/30d). Region attribution by repeater home, not observer. "All" filter fixed. Topology dedup (no more triple-counted repeaters). Timestamp format applied to chart axes.
New Surfaces
/#/roles— role distribution + per-role clock-skew/api/backup— API-key gated SQLite export via VACUUM INTO/api/healthz— readiness probe
Packets & Filters
Transport route type filter (transport == true, T_FLOOD, T_DIRECT). Clear filters button. Scroll position preserved. Null hash/timestamp packets cleaned. GRP_DATA type 6 support.
Map & Nodes
Short pubkey-prefix URLs (📡 Copy short URL). Multi-byte hash support indicator on markers. Channel color picker fixed.
MQTT & Ingestor
Async path_json backfill (no more startup blocks). Per-source region field. MQTT startup resilience (unreachable sources don't block). Configurable connect timeout. Observer IATA whitelist + blacklist. Per-hop TRACE SNR extraction in ingestor.
Observer & Node
Flood/direct filter on comparison page. TRACE per-hop SNR in byte breakdown. Clock skew evidence UI. Separate last-status vs last-packet timestamps. Soft-deleted observers excluded. Export/import includes favorites.
Infra
CORS allowlist. RW SQLite connection cache. GeoFilter Builder draft save/load/download. False-positive path exclusion.
Upgrade: New config fields in config.example.json: observerIATAWhitelist, observerBlacklist, per-source region/connectTimeoutSec, CORS allowedOrigins. One-time DB migration runs async on startup.
⚠️ Docker image not available for this tag (CI trigger issue). Use v3.7.1 for the container image:ghcr.io/kpa-clawbot/corescope:v3.7.1