efitenandClaude Opus 5.5 3960fadd96 fix(pages): drop late responses that belong to an earlier page visit (#2218, #2230, #2200) (#2248)
Fixes #2218, fixes #2230, fixes #2200.

Node analytics, the account page and analytics await their data, then
write into the shared `#app` container. A response that lands after the
user has navigated away replaces the new page. In #2218's reproduction,
the Nodes list, with 200 rows, became an Analytics heading.

## The shared part

`public/app.js` keeps one view generation, `viewGeneration()`:
- `navigate()` bumps it before the old page's `destroy()`, so every page
init counts as a new visit, including a switch to another node of the
same page type.
- A page captures the value at init, and after each `await` drops the
result when the value has moved on.

`.eslintrc.json` declares it as a global.

## Per page

**#2218, `node-analytics.js`**
- `loadAnalytics` and `loadBatteryChart` check the generation on both
success and error. A page-local `loadSeq` makes a time-range click a new
load on the same visit, so an older range can't land over a newer one.
- `NodeHopAnalytics.load` already checked `container.isConnected`, so it
is unchanged.

**#2230, `account.js`**
- The render after `CSAuth.ready()` runs only while the generation is
unchanged.
- `destroy()` was empty. It now clears `shown.app` and `shown.userId`.

**#2200, `analytics.js`**
- A page-local `_loadSeq` sits next to the generation. Results and
errors of an older window/region selection or an earlier visit are
dropped, so a late failure of an old load no longer shows an error over
newer results.
- The bulk load renders only the six tabs drawn from its data. A tab
that loads its own data (Prefix Tool, Nodes, Roles, ...) is no longer
rebuilt or replaced by the bulk result: a deep-linked one renders at
init, and a window, region or area change re-renders it directly.
- `renderTab` guards the six tabs drawn from the bulk data (overview,
rf, topology, channels, hashsizes, collisions). Until the data arrives
they show "Loading analytics…", or the current load's error. Overview no
longer dereferences an undefined `rf`.
- Three related fixes in the same lines:
  - The error message is now escaped with `esc()`; it was inserted raw.
- `destroy()` removes the RegionFilter and AreaFilter listeners. They
piled up on every visit and called `loadAnalytics` from other pages.
  - `destroy()` resets the stored error.

## Tests

| File | Assertions | Covers |
|---|---|---|
| `tests/unit/test-issue-2218-node-analytics-stale-render.js` | 12 |
Loads the real router: the generation rises on every init, including
another node. Late success and error after leaving, a stale visit of the
same node, an older range landing last, late battery success and error.
The current response, battery and error still render |
| `tests/unit/test-issue-2230-account-stale-render.js` | 6 | Leaving
before ready (login view and "not enabled"), re-entering before ready,
the current render, an auth change after `destroy()` |
| `tests/unit/test-issue-2200-analytics-stale-load.js` | 19 | An older
selection landing last, a late rejection of an older selection, leave
and re-enter (success and error), the current error rendered escaped and
kept on a tab click, Overview and RF clicked before the data |
| `tests/e2e/test-issue-2218-node-analytics-stale-e2e.js` | 4 steps |
Holds `/api/nodes/*/analytics` with `page.route`, returns to `#/nodes`,
then releases the response as a 200 and as a 500. The 200 rows stay and
no Analytics heading appears; a normal visit still renders |

How I checked them:
- With the production files reverted and the tests kept, 7 of 12, 4 of 6
and 7 of 12 unit assertions fail. With the fix, all pass.
- The E2E fails on the unfixed frontend with "Nodes rows 200 before
release, 0 after".
- Single mutations, each reverted:
- removing the success check in `loadAnalytics` (analytics) fails 2
assertions;
  - removing the check after `CSAuth.ready()` fails 3.

Other test changes:
- `test-analytics-theme-refresh-before-data.js` and
`test-user-management-ui.js` get a `viewGeneration: () => 0` stub.
- The new E2E is registered in `scripts/non-unit-tests.json` and
`deploy.yml` (shard 2, next to the #748 suite). The unit tests are in
`test-all.sh`.
- The broad suite's "Node side panel Details link navigates" test
selected an anchor that now opens Analytics, as #2218 notes. It now
clicks `#nodesRight .node-detail-btn`.

Results:
- `sh test-all.sh` passes, and `npx eslint public/*.js` reports 0
errors.
- E2E on a fixture seeded as in CI:

  | Suite | Result |
  |---|---|
  | `test-e2e-playwright.js` | 148 passed, 3 skipped |
  | `test-user-management-e2e.js` | 28/28 |
  | the new #2218 suite | 4/4 |
  | `test-issue-748-node-analytics-coverage-e2e.js` | 2/2 |
  | `test-analytics-fluid-charts.js` | 8/8 |
  | `test-issue-1657-analytics-channels-group-sprites-e2e.js` | 5/5 |

## Not done

- Two tabs that each fetch their own data can still overwrite each other
after a fast tab switch between them. #2200 does not ask for that.
- `nodes.js` `navigateToNode` calls its own `init` without the router,
so it does not bump the generation. None of the three pages fixed here
depends on that.
- I placed the E2E in shard 2 without measuring suite times, which the
`deploy.yml` comment asks for.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 10:39:15 +02:00
2026-04-05 06:36:03 +00:00
2026-03-20 05:38:23 +00:00

CoreScope

Go Server Coverage Go Ingestor Coverage E2E Tests Frontend Coverage Deploy

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 its packet and analytics API from an in-memory packet store with 7 indexes (hash, txID, obsID, observer, node, path hop, payload type). 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
GOMEMLIMIT (memory-constrained hosts) set to ≥1.5× working set (e.g. 1536 MiB on a 2 GB Pi for a ~1 GB store). Lower values trigger a GC death-spiral. Configure via the GOMEMLIMIT env var or runtime.maxMemoryMB in config.json; env wins. Applies to both server and ingestor. See #1010.

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 8× speed.

Live VCR playback — watch packets flow across the Bay Area mesh

📦 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.

Packets view

🗺️ Network Overview

At-a-glance mesh stats — node counts, packet volume, observer coverage.

Network overview

📊 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.

Node analytics

💬 Channel Chat

Decoded group messages with sender names, @mentions, timestamps — like reading a Discord channel for your mesh.

Channels

📱 Mobile Ready

Full experience on your phone — proper touch controls, iOS safe area support, and a compact VCR bar.

Live view on iOS

And More

  • 17 Analytics Tabs — RF, topology, channels, hash stats, distance, route patterns, scopes, 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
  • Scope Audit: per repeater, which declared regions show no forwarding and which scopes it forwards without declaring them
  • 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

Optional Features

Off by default, enabled in config.json:

  • User Accounts: registration with email activation, settings sync, an admin area, hashtag channel proposals, mail notifications for watched nodes, and a personal data export. The dashboard stays public. See docs/user-guide/accounts.md
  • Mobile RX Coverage: crowdsourced coverage from phones connected to a companion radio, drawn as hex bins on the RX Coverage page and on each node's Reach page. See docs/client-rx-coverage.md

Quick Start

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
DB_PATH Override SQLite database path
GOMEMLIMIT Go soft memory limit, wins over runtime.maxMemoryMB

The HTTP port is set with port in config.json or the server's -port flag.

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

  1. Flash an observer node with MESH_PACKET_LOGGING=1 build flag
  2. Connect via USB to a host running meshcoretomqtt
  3. Configure meshcoretomqtt with your IATA region code and MQTT broker address
  4. Packets appear on topic meshcore/{IATA}/{PUBKEY}/packets

To decode a single raw hex packet without storing it, use POST /api/decode.

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        # Server tests
│   └── ingestor/            # Go MQTT ingestor
│       ├── main.go          # MQTT subscription + packet processing
│       ├── decoder.go       # Packet decoder (shared logic)
│       ├── db.go            # SQLite write path
│       └── *_test.go        # Ingestor tests
├── 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         # 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
├── tests/                   # Node.js test suite: unit/ (test-all.sh) and e2e/ (Playwright)
├── test-all.sh              # Runs every suite in tests/unit
└── tools/                   # Generators, E2E tests, utilities

For Developers

Building

make build        # all four binaries for your machine, into dist/
make build-server # just one
make crossbuild   # static linux/amd64 + linux/arm64 binaries

The SQLite driver is mattn/go-sqlite3, which is cgo, so a plain GOOS=linux go build from a Mac will not work: cross-compiling needs a C compiler that can target the other platform. make crossbuild uses zig as that compiler (install it and it just works) and links statically against musl, so the result is one self-contained file that runs on Alpine or scratch. The container build does the same thing — see Dockerfile.

Test Suite

Go tests cover the backend, Node.js unit tests in tests/unit/ cover the frontend logic, and Playwright tests in tests/e2e/ validate the UI in a browser.

# Go backend tests
cd cmd/server && go test ./... -v
cd cmd/ingestor && go test ./... -v

# Or across all 16 modules at once
make test

# Node.js frontend + integration tests
npm test

# Playwright E2E (requires running server on localhost:3000)
node tests/e2e/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.on8ar.eu: all read 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

GPL-3.0-or-later

S
Description
No description provided
Readme GPL-3.0
137 MiB
0 Stars 1 Watchers 0 Forks
Languages
JavaScript 48.1%
Go 46.5%
CSS 2.9%
Shell 1.6%
HTML 0.6%
Other 0.2%