- Fix#148: channels endpoint returned null for msgLengths when no
decrypted messages exist. Initialize msgLengths as make([]int, 0)
in store path and guard channels slice in DB fallback path.
- Fix#149: nodes endpoint always returned hash_size=null and
hash_size_inconsistent=false. Add GetNodeHashSizeInfo() to
PacketStore that scans advert packets to compute per-node hash
size, flip-flop detection, and sizes_seen. Enrich nodes in both
handleNodes and handleNodeDetail with computed hash data.
fixes#148, fixes#149
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Go's /api/health was missing the eventLoop object that Node.js provides.
The perf.js frontend reads health.eventLoop.p95Ms which crashed with
'Cannot read properties of undefined' when served by the Go server.
Adds eventLoop field using GC pause data from runtime.MemStats.PauseNs
(last 256 pauses) to compute p50Ms, p95Ms, p99Ms, currentLagMs, maxLagMs
— matching the Node.js response shape exactly.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Go analytics topology endpoint was counting every unique hop string
from packet paths (including unresolved 1-byte hex prefixes) as a unique
node, inflating the count from ~540 to 6502. Now resolves each hop via
the prefix map and deduplicates by public key, matching the Node.js
behavior.
fixes#146
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- /api/perf: add goRuntime (heap, GC, goroutines, CPU), packetStore
stats (totalLoaded, observations, index sizes, estimatedMB),
sqlite stats (dbSizeMB, walSizeMB, row counts), real RF cache
hit/miss tracking, and endpoint sorting by total time spent
- /api/health: add memory.heapMB, goRuntime (goroutines, gcPauses,
numCPU), real packetStore packet count and estimatedMB, real
cache stats from RF cache; remove hardcoded-zero eventLoop
- store.go: add cacheHits/cacheMisses tracking in GetAnalyticsRF,
GetPerfStoreStats() and GetCacheStats() methods
- db.go: add path field to DB struct, GetDBSizeStats() for file
sizes and row counts
- Tests: verify new fields in health/perf endpoints, add
TestGetDBSizeStats, wire up PacketStore in test server setup
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implement all analytics endpoints from in-memory PacketStore instead of
returning stubs/empty data. Each handler now matches the Node.js response
shape field-by-field.
Endpoints fixed:
- /api/analytics/topology (#135): full hop distribution, top repeaters,
top pairs, hops-vs-SNR, per-observer reachability, cross-observer
comparison, best path analysis
- /api/analytics/distance (#137): haversine distance computation,
category stats (R↔R, C↔R, C↔C), distance histogram, top hops/paths,
distance over time
- /api/analytics/hash-sizes (#136): hash size distribution from raw_hex
path byte parsing, hourly breakdown, top hops, multi-byte node tracking
- /api/analytics/hash-issues (#138): hash-sizes data now populated so
frontend collision tab can compute inconsistent sizes and collision risk
- /api/analytics/route-patterns (#134): subpaths and subpath-detail now
compute from in-memory store with hop resolution
- /api/nodes/bulk-health (#140): switched from N per-node SQL queries to
in-memory PacketStore lookups with observer stats
- /api/channels (#142): response shape already correct via GetChannels;
analytics/channels now returns topSenders, channelTimeline, msgLengths
- /api/analytics/channels: full channel analytics with sender tracking,
timeline, and message length distribution
All handlers fall back to DB/stubs when store is nil (test compat).
All 42+ existing Go tests pass. go vet clean.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Style .version-badge anchor elements to use --nav-text-muted color
instead of browser-default blue. Adds hover state using --nav-text.
Works with both light and dark themes via existing CSS variables.
fixes#139
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Go server and ingestor tests now run with -coverprofile
- Coverage percentages parsed and printed in CI output
- Badge JSON files generated (.badges/go-server-coverage.json,
.badges/go-ingestor-coverage.json) matching existing format
- Badges uploaded as artifacts from go-build job, downloaded
in test job, and published alongside existing Node.js badges
- Coverage summary table added to GitHub Step Summary
fixes#141
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Commit hash is now an <a> linking to GitHub commit (full hash in URL, 7-char display)
- Version tag only shown on prod (port 80/443 or no port), linked to GitHub release
- Staging (non-standard port) shows commit + engine only, no version noise
- Detect prod vs staging via location.port
- Updated tests: 16 cases covering prod/staging/links/edge cases
- Bumped cache busters
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add formatVersionBadge() that renders version, short commit hash, and
engine as a single badge in the nav stats area. Format: v2.6.0 · abc1234 [go].
Skips commit when 'unknown' or missing. Truncates commit to 7 chars.
Replaces the standalone engine badge call in updateNavStats().
8 unit tests cover all edge cases (missing fields, v-prefix dedup,
unknown commit, truncation).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Previous approach invalidated cache on every ingest (every 1s with live
mesh data). Now uses TTL-only expiry (15s). Separate cache mutex avoids
data race with main store RWMutex.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Node.js: reads version from package.json, commit from .git-commit file
or git rev-parse --short HEAD at runtime, with unknown fallback.
Go: uses -ldflags build-time variables (Version, Commit) with fallback
to .git-commit file and git command at runtime.
Dockerfile: copies .git-commit if present (CI bakes it before build).
Dockerfile.go: passes APP_VERSION and GIT_COMMIT as build args to ldflags.
deploy.yml: writes GITHUB_SHA to .git-commit before docker build steps.
docker-compose.yml: passes build args to Go staging build.
Tests updated to verify version and commit fields in both endpoints.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Cache the computed RF analytics result for 15 seconds.
1.2M observation scan takes ~140ms; cached response <1ms.
Cache invalidated when new packets are ingested.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Build and deploy the Go staging container (port 82) after Node staging
is healthy. Uses continue-on-error so Go staging failures don't block
the Node.js deploy. Health-checks the Go container for up to 60s and
verifies /api/stats returns the engine field.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move per-transmission work (hash indexing, type resolution, packet sizes)
outside the per-observation loop. Cache SNR dereference, pre-resolve type
name once per transmission. Reduces redundant map lookups from 1.2M to 52K.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Both backends now return an 'engine' field ('node' or 'go') in
/api/stats and /api/health responses so the frontend can display
which backend is running.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Show [go] or [node] badge in the nav stats bar when /api/stats
returns an engine field. Gracefully hidden when field is absent.
- Add formatEngineBadge() to app.js (top-level, testable)
- Add .engine-badge CSS class using CSS variables
- Add 5 unit tests in test-frontend-helpers.js
- Bump cache busters
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add byPayloadType index to PacketStore for O(1) type-5 lookups
- Channels scan reduced from 52K to ~17K packets (3x fewer iterations)
- Use struct-based JSON decoding (avoids map[string]interface{} allocations)
- Pre-allocate snrVals/rssiVals/scatterAll with capacity hints for 1.2M obs
- Remove second-pass time.Parse loop (1.2M calls) in RF analytics
Track min/max timestamps as strings during first pass instead
- Index also populated during IngestNewFromDB for new packets
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace SQLite-backed handlers for /api/channels, /api/channels/:hash/messages,
/api/analytics/rf, and /api/analytics/channels with in-memory PacketStore queries.
Before (SQLite via packets_v VIEW on 1.2M rows):
/api/channels 7.2s
/api/channels/:hash/msgs 8.2s
/api/analytics/rf 4.2s
After (in-memory scan of ~50K transmissions):
Target: all under 100ms
Three new PacketStore methods:
- GetChannels(region) — filters payload_type 5 + decoded type CHAN
- GetChannelMessages(hash, limit, offset) — deduplicates by sender+hash
- GetAnalyticsRF(region) — full RF stats with histograms, scatter, per-type SNR
All handlers fall back to DB queries when store is nil (test compat).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add detail-collapsed class to split-layout initial HTML so the empty
right panel is hidden before any packet is selected. The class is
already removed when a packet row is clicked and re-added when the
close button is pressed.
Add 3 tests verifying the detail pane starts collapsed and that
open/close toggling is wired correctly.
Bump cache busters.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Streams transmissions + observations from SQLite at startup into
5 indexed in-memory structures. QueryPackets and QueryGroupedPackets
now serve from RAM (<10ms) instead of hitting SQLite (2.3s).
- store.go: PacketStore with byHash, byTxID, byObsID, byObserver, byNode indexes
- main.go: create + load store at startup
- routes.go: dispatch to store for packet/stats endpoints
- websocket.go: poller ingests new transmissions into store
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add go-build job to deploy.yml that builds and tests cmd/server and cmd/ingestor
- Go job gates the Node.js test job and deploy job
- Re-enable frontend coverage detection (was hardcoded to false)
- Remove stale temp files from repo root (recover-delta.sh, merge.sh, replacements.txt, reps.txt)
- Add temp scripts and Go build artifacts to .gitignore
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Change staging-go HTTP port from 81 to 82 (via STAGING_GO_HTTP_PORT)
to avoid conflict with CI's Node.js staging on port 81
- Change staging-go MQTT port from 1884 to 1885 (via STAGING_GO_MQTT_PORT)
to avoid conflict with Node.js staging MQTT on port 1884
- Add MQTT_BROKER=mqtt://localhost:1883 env var so Go ingestor connects
to its own internal mosquitto instead of unreachable prod external IP
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add #/compare page that lets users select two observers and compare
which packets each sees. Fetches last 24h of packets per observer,
computes set diff client-side using O(n) Set lookups. Shows summary
cards (both/only-A/only-B), stacked bar, type breakdown, and tabbed
detail tables. URL is shareable via ?a=ID1&b=ID2 query params.
- New file: public/compare.js (comparePacketSets + page module)
- Added compare button to observers page header
- 11 new tests for comparePacketSets (87 total frontend tests)
- Cache busters bumped
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fixes#131
The Nodes tab required a full page reload to see newly advertised nodes
because loadNodes() cached the node list in _allNodes and never
re-fetched it on WebSocket updates.
Changes:
- WS handler now filters for ADVERT packets only (payload_type 4 or
payloadTypeName ADVERT), instead of triggering on every packet type
- Uses 5-second debounce to avoid excessive API calls during bursts
- Resets _allNodes cache and invalidates API cache before re-fetching
- loadNodes(refreshOnly) parameter: when true, updates table rows and
counts without rebuilding the entire panel (preserves scroll position,
selected node, tabs, filters, and event listeners)
- Extracted isAdvertMessage() as testable helper with window._nodesIsAdvertMessage hook
- 13 new tests (76 total frontend helpers)
- Cache busters bumped
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fixes#130 — Nodes loaded from the database (API) are now dimmed with
reduced opacity when stale, matching the static map behavior, instead of
being completely removed by pruneStaleNodes(). WS-only (dynamically
added) nodes are still pruned to prevent memory leaks.
Changes:
- loadNodes() marks API-loaded nodes with _fromAPI flag
- pruneStaleNodes() dims _fromAPI nodes (fillOpacity 0.25) vs removing
- Active nodes restore full opacity when refreshed
- 3 new tests for dim/restore/WS-only behavior (63 total passing)
- Cache busters bumped
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When multiple nodes share the same hash prefix (e.g. 1CC4 and 1C82 both
starting with 1C under 1-byte hash_size), updatePathSeenTimestamps() was
non-deterministically picking the first DB match, keeping dead nodes alive
on the map. Now resolveUniquePrefixMatch() only resolves prefixes that
match exactly one node. Ambiguous prefixes are cached in a negative-cache
set to avoid repeated DB queries.
- Extract resolveUniquePrefixMatch() used by both autoLearnHopNodes and
updatePathSeenTimestamps (DRY)
- Add ambiguousHopPrefixes negative cache (Set)
- LIMIT 2 in the uniqueness query to detect collisions efficiently
- 3 new regression tests: ambiguous prefix, unique prefix, 1-byte
collision scenario (204 -> 207 tests)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Paho MQTT client uses tcp:// and ssl:// schemes, not mqtt:// and mqtts://.
Also properly configure TLS for mqtts connections with InsecureSkipVerify
when rejectUnauthorized is false.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When a CHANNEL_MSG (GRP_TXT) can't be decrypted, the decoder now includes:
- channelHashHex: zero-padded uppercase hex string of the channel hash byte
- decryptionStatus: 'decrypted', 'no_key', or 'decryption_failed'
Frontend changes:
- Packet list preview shows '🔒 Ch 0xXX (no key)' or '(decryption failed)'
- Detail pane hex breakdown shows channel hash with status label
- Detail pane message area shows channel hash info for undecrypted packets
6 new decoder tests (58 total): channelHashHex formatting, decryptionStatus
for no keys, empty keys, bad keys, and short encrypted data.
Fixes#123
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The frontend sends ISO timestamps to filter by observation time.
Go was filtering by transmission first_seen which missed packets
with recent observations but old first_seen. Now converts ISO to
unix epoch and queries the observations table directly.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
autoLearnHopNodes was creating stub 'repeater' entries in the nodes table
for every unresolved hop prefix. With hash_size=1, this generated thousands
of phantom nodes (6,638 fake repeaters on a ~300-node mesh).
Root cause fix:
- autoLearnHopNodes no longer calls db.upsertNode() for unresolved hops
- Hop prefixes are still cached to avoid repeated DB lookups
- Unresolved hops display as raw hex via hop-resolver (no behavior change)
Cleanup:
- Added db.removePhantomNodes() — deletes nodes with public_key <= 16 chars
(real MeshCore pubkeys are 64 hex chars / 32 bytes)
- Called at server startup to purge existing phantoms
Tests: 14 new assertions in test-db.js (109 total, all passing)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>