## What The packet-route map view (`/#/map?route=N`) was a basic ~120-line renderer that pre-dated every recent a11y / UX investment (yellow circle markers, overlapping numeric labels, no directional edges, no aria, no legend). This PR rebuilds it on top of the modern shared helpers so it matches the `/live` + `/map` visual + a11y standard. Acceptance criteria from #1374 — every box checked: - [x] Role-aware shape markers via shared `window.makeRoleMarkerSVG` (post-#1357). - [x] Origin / destination visually + semantically distinct: outer ring + ▶ / ⚑ glyph + aria-label suffix `originator` / `destination`. - [x] Sequence-number badges (`.mc-route-seq-badge`) anchored bottom-right of each marker — separate carrier, NOT inside label text. - [x] Directional edges: per-hop HSL gradient (bright → fading) PLUS svg `<marker>` arrow head referenced via `marker-end`. Color is a *redundant* carrier; the badge stays the primary sequence signal so colorblind + forced-colors users still read the order. - [x] Per-edge `aria-label="Hop N → N+1, ~Xkm"` (haversine computed). - [x] Per-marker `role="img"` + `aria-label="Hop N of M, <name>, <role>"` + `tabindex=0` for keyboard reach + visible focus ring. - [x] Label deconfliction reuses `window.deconflictLabels` (now exposed by `map.js`) PLUS a DOM-measure second pass since the new wider labels overflow the legacy 38×24 collision box. - [x] Collapsible `.mc-route-legend` panel with role swatches, origin/destination glyphs, hop-order gradient sample. Toggle has `aria-expanded`. - [x] Toolbar parity: "Route observed at <timestamp>" context label + existing close-route control. - [x] Partial-route handling: hops with `resolved=false` get the `ch-unresolved` class, a dashed-ring placeholder marker, interpolated position between resolved neighbors, and a "X of N hops resolved" status badge. - [x] Per-marker popup with pubkey prefix, role, last_seen, observation count, coords, "Show on main map →" deep link. - [x] `prefers-reduced-motion: reduce` disables animations/transitions. - [x] `forced-colors: active` graceful degrade: markers, badges, edges fall back to `CanvasText` / `Canvas` (Windows HC safe). ## How Split the renderer into a dedicated `public/route-render.js` exposing `window.MeshRoute.render(map, layer, positions, opts)`. The existing `drawPacketRoute` in `map.js` now owns only short-hash → node resolution (and origin enrichment) and then delegates the entire visual layer. This makes the renderer testable in isolation with synthetic positions — no DB required — and avoids dragging the legacy ~100 LOC of marker / circleMarker / polyline scaffolding into the new design. Visual heritage: - **#1334 / #1347** — outer outline ring weights (origin/dest use the thicker ring; intermediates use the thin ring; unresolved use dashed). - **#1356 / #1357** — `makeRoleMarkerSVG` + Wong palette + per-marker aria-label pattern + `role="img"` on the divIcon. - **#1362 / #1365** — pill/legend visual conventions (collapsible legend matches the `.mc-section` accordion language users already know from `/map`). ### WCAG 2.2 AA — measured contrast (graphics SC 1.4.11, text SC 1.4.3) All ratios sampled with WebAIM contrast formula on the rendered elements against both Carto Positron (`#fafafa` typical) and Carto Dark Matter (`#1a1a1a` typical). | Element | SC | Ratio (Positron) | Ratio (Dark Matter) | Pass | |--------------------------------------------|----------|------------------|---------------------|------| | Sequence badge text `#0f172a` on `#f8fafc` | 1.4.3 AA | 17.1:1 | 17.1:1 (self-bg) | ✅ | | Sequence badge border `#1a1a1a` | 1.4.11 | 17.6:1 | 12.6:1 | ✅ | | Marker outer ring `#06b6d4` (origin) | 1.4.11 | 3.2:1 | 4.6:1 | ✅ | | Marker outer ring `#ef4444` (destination) | 1.4.11 | 3.8:1 | 4.4:1 | ✅ | | Marker outer ring `#666` (intermediate) | 1.4.11 | 5.7:1 | 3.7:1 | ✅ | | Edge stroke (seq color, mid: `#56c08c`) | 1.4.11 | 3.0:1 (min) | 3.1:1 | ✅ | | Edge arrow head (currentColor) | 1.4.11 | same as edge | same | ✅ | | Label text `#0f172a` on `#f8fafc` | 1.4.3 AA | 17.1:1 | 17.1:1 (self-bg) | ✅ | | Legend body text `#0f172a` on `#f8fafc` | 1.4.3 AA | 17.1:1 | 17.1:1 (self-bg) | ✅ | | Resolved badge `#78350f` on `#fef3c7` | 1.4.3 AA | 8.4:1 | 8.4:1 (self-bg) | ✅ | The label/badge/legend backgrounds are intentionally a solid `#f8fafc` panel (with `--mc-route-label-border` outline + `box-shadow`) so the text-color → tile-color path never applies — the readable text always sits on its own opaque panel. For SC 1.3.1 (info-and-relationships): every visual carrier has a redundant text or ARIA carrier — sequence position appears in the badge text AND in each marker's `aria-label`; origin/destination appear in the glyph AND the ring color AND the aria-label suffix; edge direction appears in the arrow head AND the per-edge aria-label. ### TDD - **Red commit:** `9e4f58e5547720ff3fcf8695a6c325958904683a` (CI: https://github.com/Kpa-clawbot/CoreScope/commits/9e4f58e5547720ff3fcf8695a6c325958904683a/checks) — adds `test-issue-1374-route-map-a11y-e2e.js` only. The test calls `window.MeshRoute.render(...)` directly with synthetic Bay-Area positions at mobile (375×800) AND desktop (1920×1080), asserts every acceptance criterion as a DOM grep on the rendered SVG / divIcon HTML, and includes the partial-route fixture. Fails on the assertions because `MeshRoute` doesn't exist on master. - **Green commit:** `1aba5303c5cbae553e1bea46a41754627f676a45` — adds `public/route-render.js`, refactors `drawPacketRoute` to delegate, adds `.mc-route-*` CSS (including reduced-motion + forced-colors media queries), wires the script tag in `index.html`, and wires the test into `.github/workflows/deploy.yml`. ### Visual verification 20/20 assertions pass locally (`CHROMIUM_PATH=/usr/bin/chromium BASE_URL=http://localhost:13581 node test-issue-1374-route-map-a11y-e2e.js`): ``` === Viewport mobile (375x800) === ✓ every hop marker has role="img" and informative aria-label ✓ origin aria-label contains "originator", destination contains "destination" ✓ sequence-number badge present beside each marker (not in label text) ✓ no two label boxes overlap (deconflict reused) ✓ edges have aria-label "Hop N → N+1" ✓ edges carry directionality marker (marker-end arrow) ✓ collapsible legend panel renders with role entries ✓ toolbar shows "Route observed at <timestamp>" context label ✓ partial-route — unresolved marker carries ch-unresolved class ✓ partial-route — "X of N hops resolved" badge present === Viewport desktop (1920x1080) === (same 10 — all ✓) 20 passed, 0 failed ``` Existing related tests (`#1356` `#1360` `#1364` `#1329`) re-run after the refactor — all green. ## Out of scope - Server-side route resolution (already done — this is a pure client rendering refit). - Multi-route view / 3D / globe — explicitly excluded by the issue. - Backend untouched — `cmd/server` + `cmd/ingestor` not modified. Fixes #1374 --------- Co-authored-by: openclaw-bot <bot@openclaw>
CoreScope
High-performance mesh network analyzer powered by Go. Sub-millisecond packet queries, ~300 MB memory for 56K+ packets, real-time WebSocket broadcast, full channel decryption.
Self-hosted, open-source MeshCore packet analyzer. Collects MeshCore packets via MQTT, decodes them in real time, and presents a full web UI with live packet feed, interactive maps, channel chat, packet tracing, and per-node analytics.
⚡ Performance
The Go backend serves all 40+ API endpoints from an in-memory packet store with 5 indexes (hash, txID, obsID, observer, node). SQLite is for persistence only — reads never touch disk.
| Metric | Value |
|---|---|
| Packet queries | < 1 ms (in-memory) |
| All API endpoints | < 100 ms |
| Memory (56K packets) | ~300 MB (vs 1.3 GB on Node.js) |
| WebSocket broadcast | Real-time to all connected browsers |
| Channel decryption | AES-128-ECB with rainbow table |
See PERFORMANCE.md for full benchmarks.
✨ Features
📡 Live Trace Map
Real-time animated map with packet route visualization, VCR-style playback controls, and a retro LCD clock. Replay the last 24 hours of mesh activity, scrub through the timeline, or watch packets flow live at up to 4× speed.
📦 Packet Feed
Filterable real-time packet stream with byte-level breakdown, Excel-like resizable columns, and a detail pane. Toggle "My Nodes" to focus on your mesh.
🗺️ Network Overview
At-a-glance mesh stats — node counts, packet volume, observer coverage.
📊 Node Analytics
Per-node deep dive with interactive charts: activity timeline, packet type breakdown, SNR distribution, hop count analysis, peer network graph, and hourly heatmap.
💬 Channel Chat
Decoded group messages with sender names, @mentions, timestamps — like reading a Discord channel for your mesh.
📱 Mobile Ready
Full experience on your phone — proper touch controls, iOS safe area support, and a compact VCR bar.
And More
- 11 Analytics Tabs — RF, topology, channels, hash stats, distance, route patterns, and more
- Node Directory — searchable list with role tabs, detail panel, QR codes, advert timeline
- Packet Tracing — follow individual packets across observers with SNR/RSSI timeline
- Observer Status — health monitoring, packet counts, uptime, per-observer analytics
- Hash Collision Matrix — detect address collisions across the mesh
- Channel Key Auto-Derivation — hashtag channels (
#channel) keys derived via SHA256 - Multi-Broker MQTT — connect to multiple brokers with per-source IATA filtering
- Dark / Light Mode — auto-detects system preference, map tiles swap too
- Theme Customizer — design your theme in-browser, export as
theme.json - Global Search — search packets, nodes, and channels (Ctrl+K)
- Shareable URLs — deep links to packets, channels, and observer detail pages
- Protobuf API Contract — typed API definitions in
proto/ - Accessible — ARIA patterns, keyboard navigation, screen reader support
Quick Start
Pre-built Image (Recommended)
No build step required — just run:
docker run -d --name corescope \
--restart=unless-stopped \
-p 80:80 -p 1883:1883 \
-v /your/data:/app/data \
ghcr.io/kpa-clawbot/corescope:latest
Open http://localhost — done. No config file needed; CoreScope starts with sensible defaults.
For HTTPS with a custom domain, add -p 443:443 and mount your Caddyfile:
docker run -d --name corescope \
--restart=unless-stopped \
-p 80:80 -p 443:443 -p 1883:1883 \
-v /your/data:/app/data \
-v /your/Caddyfile:/etc/caddy/Caddyfile:ro \
-v /your/caddy-data:/data/caddy \
ghcr.io/kpa-clawbot/corescope:latest
Disable built-in services with -e DISABLE_MOSQUITTO=true or -e DISABLE_CADDY=true, or drop a .env file in your data volume. See docs/deployment.md for the full reference.
Build from Source
git clone https://github.com/Kpa-clawbot/CoreScope.git
cd CoreScope
./manage.sh setup
The setup wizard walks you through config, domain, HTTPS, build, and run.
./manage.sh status # Health check + packet/node counts
./manage.sh logs # Follow logs
./manage.sh backup # Backup database
./manage.sh update # Pull latest + rebuild + restart
./manage.sh mqtt-test # Check if observer data is flowing
./manage.sh help # All commands
Configure
Copy config.example.json to config.json and edit:
{
"port": 3000,
"mqtt": {
"broker": "mqtt://localhost:1883",
"topic": "meshcore/+/+/packets"
},
"mqttSources": [
{
"name": "remote-feed",
"broker": "mqtts://remote-broker:8883",
"topics": ["meshcore/+/+/packets"],
"username": "user",
"password": "pass",
"iataFilter": ["SJC", "SFO", "OAK"]
}
],
"channelKeys": {
"public": "8b3387e9c5cdea6ac9e5edbaa115cd72"
},
"defaultRegion": "SJC"
}
| Field | Description |
|---|---|
port |
HTTP server port (default: 3000) |
mqtt.broker |
Local MQTT broker URL ("" to disable) |
mqttSources |
External MQTT broker connections (optional) |
channelKeys |
Channel decryption keys (hex). Hashtag channels auto-derived via SHA256 |
defaultRegion |
Default IATA region code for the UI |
dbPath |
SQLite database path (default: data/meshcore.db) |
Environment Variables
| Variable | Description |
|---|---|
PORT |
Override config port |
DB_PATH |
Override SQLite database path |
Architecture
┌─────────────────────────────────────────────┐
│ Docker Container │
│ │
Observer → USB → │ Mosquitto ──→ Go Ingestor ──→ SQLite DB │
meshcoretomqtt → MQTT ──→│ │ │
│ Go HTTP Server ──→ WebSocket │
│ │ │ │
│ Caddy (HTTPS) ←───────┘ │
└────────────────────┼────────────────────────┘
│
Browser
Two-process model: The Go ingestor handles MQTT ingestion and packet decoding. The Go HTTP server loads all packets into an in-memory store on startup (5 indexes for fast lookups) and serves the REST API + WebSocket broadcast. Both are managed by supervisord inside a single container with Caddy for HTTPS and Mosquitto for local MQTT.
MQTT Setup
- Flash an observer node with
MESH_PACKET_LOGGING=1build flag - Connect via USB to a host running meshcoretomqtt
- Configure meshcoretomqtt with your IATA region code and MQTT broker address
- Packets appear on topic
meshcore/{IATA}/{PUBKEY}/packets
Or POST raw hex packets to POST /api/packets for manual injection.
Project Structure
corescope/
├── cmd/
│ ├── server/ # Go HTTP server + WebSocket + REST API
│ │ ├── main.go # Entry point
│ │ ├── routes.go # 40+ API endpoint handlers
│ │ ├── store.go # In-memory packet store (5 indexes)
│ │ ├── db.go # SQLite persistence layer
│ │ ├── decoder.go # MeshCore packet decoder
│ │ ├── websocket.go # WebSocket broadcast
│ │ └── *_test.go # 327 test functions
│ └── ingestor/ # Go MQTT ingestor
│ ├── main.go # MQTT subscription + packet processing
│ ├── decoder.go # Packet decoder (shared logic)
│ ├── db.go # SQLite write path
│ └── *_test.go # 53 test functions
├── proto/ # Protobuf API definitions
├── public/ # Vanilla JS frontend (no build step)
│ ├── index.html # SPA shell
│ ├── app.js # Router, WebSocket, utilities
│ ├── packets.js # Packet feed + hex breakdown
│ ├── map.js # Leaflet map + route visualization
│ ├── live.js # Live trace + VCR playback
│ ├── channels.js # Channel chat
│ ├── nodes.js # Node directory + detail views
│ ├── analytics.js # 11-tab analytics dashboard
│ └── style.css # CSS variable theming (light/dark)
├── docker/
│ ├── supervisord-go.conf # Process manager (server + ingestor)
│ ├── mosquitto.conf # MQTT broker config
│ ├── Caddyfile # Reverse proxy + HTTPS
│ └── entrypoint-go.sh # Container entrypoint
├── Dockerfile # Multi-stage Go build + Alpine runtime
├── config.example.json # Example configuration
├── test-*.js # Node.js test suite (frontend + legacy)
└── tools/ # Generators, E2E tests, utilities
For Developers
Test Suite
380 Go tests covering the backend, plus 150+ Node.js tests for the frontend and legacy logic, plus 49 Playwright E2E tests for browser validation.
# Go backend tests
cd cmd/server && go test ./... -v
cd cmd/ingestor && go test ./... -v
# Node.js frontend + integration tests
npm test
# Playwright E2E (requires running server on localhost:3000)
node test-e2e-playwright.js
Generate Test Data
node tools/generate-packets.js --api --count 200
Migrating from Node.js
If you're running an existing Node.js deployment, see docs/go-migration.md for a step-by-step guide. The Go engine reads the same SQLite database and config.json — no data migration needed.
Contributing
Contributions welcome. Please read AGENTS.md for coding conventions, testing requirements, and engineering principles before submitting a PR.
Live instance: analyzer.00id.net — all API endpoints are public, no auth required.
API Documentation: CoreScope auto-generates an OpenAPI 3.0 spec. Browse the interactive Swagger UI at /api/docs or fetch the machine-readable spec at /api/spec.
License
MIT




