Commit Graph

393 Commits

Author SHA1 Message Date
you
ec4ebf2ede fix: re-export insertPacket from db.js (packet-store.js needs it)
Migration subagent removed it from exports but packet-store.js
calls dbModule.insertPacket() on every MQTT ingestion.
2026-03-21 20:46:15 +00:00
you
cbec6b3108 Move hop resolution to client side
Create public/hop-resolver.js that mirrors the server's disambiguateHops()
algorithm (prefix index, forward/backward pass, distance sanity check).

Replace all /api/resolve-hops fetch calls in packets.js with local
HopResolver.resolve() calls. The resolver lazily fetches and caches the
full nodes list via /api/nodes on first use.

The server endpoint is kept as fallback but no longer called by the UI,
eliminating 40+ HTTP requests per session.
2026-03-21 20:39:28 +00:00
you
3a1b0b544b refactor: migrate all SQL queries from packets table to packets_v view (transmissions+observations)
- Create packets_v SQL view joining transmissions+observations to match old packets schema
- Replace all SELECT FROM packets with packets_v in db.js, packet-store.js, server.js
- Update countPackets/countRecentPackets to query observations directly
- Update seed() to use insertTransmission instead of insertPacket
- Remove insertPacket from exports (no longer called)
- Keep packets table schema intact (not dropped yet, pending testing)
2026-03-21 20:38:58 +00:00
you
016c4091db perf: disable SQLite auto-checkpoint, use manual PASSIVE checkpoint
SQLite WAL auto-checkpoint (every 1000 pages/4MB) was causing 200ms+
event loop spikes on a 381MB database. This is synchronous I/O that
blocks the Node.js event loop unpredictably.

Fix: disable auto-checkpoint, run PASSIVE (non-blocking) checkpoint
every 5 minutes. PASSIVE won't stall readers or writers — it only
checkpoints pages that aren't currently in use.
2026-03-21 20:19:22 +00:00
you
0ee8992e09 perf: eliminate synchronous blocking during startup pre-warm
Previous pre-warm called computeAllSubpaths() synchronously (500ms+)
directly in the setTimeout callback, then sequentially warmed 8 more
endpoints. Any browser request arriving during this 1.5s+ window
waited the full duration (user saw 4816ms for a 3-hop resolve-hops).

Fix: ALL pre-warm now goes through self-HTTP-requests which yield the
event loop between each computation. Delayed to 5s so initial page
load requests complete first (they populate cache on-demand).

Removed the sync computeAllSubpaths() call and inline subpath cache
population — the /api/analytics/subpaths endpoint handles this itself.
2026-03-21 20:07:27 +00:00
you
a3d94e74c6 perf: node paths endpoint uses disambiguateHops with prefix index
Replaced inline resolveHopsInternal (allNodes.filter per hop) with
shared disambiguateHops (prefix-indexed). Also uses _parsedPath cache
and per-request disambig cache. /api/nodes/:pubkey/paths was 560ms cold,
should now be much faster.
2026-03-21 19:59:11 +00:00
you
a16bd34a1f perf: revert 200ms gap (made it worse), warm at 100ms instead of 1s
200ms gaps meant clients hit cold caches themselves (worse).
Pre-warm should be fast and immediate — start at 100ms after listen,
setImmediate between endpoints to yield but not delay.
2026-03-21 19:54:10 +00:00
you
e95da27555 perf: 200ms gap between pre-warm requests to drain client queue
setImmediate wasn't enough — each analytics computation blocks for
200-400ms synchronously. Adding a 200ms setTimeout between pre-warm
requests gives pending client requests a window to complete between
the heavy computations.
2026-03-21 19:53:02 +00:00
you
88e4287b3e perf: shared cached node list + cached path JSON parse
- 8 separate 'SELECT * FROM nodes' queries replaced with getCachedNodes()
  (refreshes every 30s, prefix index built once and reused)
- Region-filtered subpaths + master subpaths use _parsedPath cache
- Eliminates repeated SQLite queries + prefix index rebuilds across
  back-to-back analytics endpoint calls
2026-03-21 19:49:54 +00:00
you
961be4fd80 perf: yield event loop between pre-warm requests via setImmediate 2026-03-21 19:38:59 +00:00
you
95185a381d perf: add observers, nodes, distance to startup pre-warm
These endpoints were missing from the sequential pre-warm,
causing cold cache hits when clients connect before warm completes.
observers was 3s cold, distance was 600ms cold.
2026-03-21 19:36:46 +00:00
you
74fd97f761 Optimize analytics endpoints: prefix maps, cached JSON.parse, reduced scans
- Topology: replace O(N) allNodes.filter with prefix map + hop cache for resolveHop
- Topology: use _parsedPath cached JSON.parse for path_json (3 call sites)
- Topology: build observer map from already-filtered packets instead of second full scan
- Hash-sizes: prefix map for hop resolution instead of allNodes.find per hop
- Hash-sizes: use _parsedPath and _parsedDecoded cached parses
- Channels: use _parsedDecoded cached parse for decoded_json
2026-03-21 19:35:06 +00:00
you
9867b62872 Optimize /api/analytics/distance cold cache performance
- Build prefix map for O(1) hop resolution instead of O(N) linear scan per hop
- Cache resolved hops to avoid re-resolving same hex prefix across packets
- Pre-compute repeater set for O(1) role lookups
- Cache parsed path_json/decoded_json on packet objects (_parsedPath/_parsedDecoded)
2026-03-21 19:22:37 +00:00
you
0fb3553762 perf: precompute hash_size map at startup, update incrementally
The hash_size computation was scanning all 19K+ packets with JSON.parse
on every /api/nodes request, blocking the event loop for hundreds of ms.
Event loop p95 was 236ms, max 1732ms.

Now computed once at startup and updated incrementally on each new packet.
/api/nodes just does a Map.get per node instead of full scan.
2026-03-21 19:00:28 +00:00
you
8811efdb24 fix: hash_size must use newest ADVERT, not oldest
Packets array is sorted newest-first. The previous 'last-wins'
approach (unconditional set) gave the OLDEST packet's hash_size.
Switched to first-wins (!has guard) which correctly uses the
newest ADVERT since we iterate newest-first.

Verified: Kpa Roof Solar has 1-byte ADVERTs (old firmware) and
2-byte ADVERTs (new firmware) interleaved. Newest are 2-byte.
2026-03-21 18:52:06 +00:00
you
e50ce03414 fix: Pass 2 hash_size was overwriting ADVERT-derived values
Pass 1 correctly uses last-wins for ADVERT packets. But Pass 2
(fallback for nodes without ADVERTs) was also unconditionally
overwriting, so a stale 1-byte non-ADVERT packet would clobber
the correct 2-byte value from Pass 1.

Restored the !hashSizeMap.has() guard on Pass 2 only — it should
only fill gaps, never override ADVERT-derived hash_size.
2026-03-21 18:50:50 +00:00
you
ee58e648a6 Fix hash_size using first-seen-wins instead of last-seen-wins
The hashSizeMap was guarded by !hashSizeMap.has(pk), meaning the oldest
ADVERT determined a node's hash_size permanently. If a node upgraded
firmware from 1-byte to 2-byte hash prefix, the stale value persisted.

Remove the guard so newer packets overwrite older ones (last-seen-wins).
2026-03-21 18:43:48 +00:00
you
2170dd7743 security: require API key for POST /api/packets and /api/perf/reset
- New config.apiKey field — when set, POST endpoints require X-Api-Key header
- If apiKey not configured, endpoints remain open (dev/local mode)
- GET endpoints and /api/decode (read-only) remain public
- Closes the packet injection attack surface
2026-03-21 18:40:06 +00:00
you
804c39504c feat: View on Map buttons for distance leaderboard hops and paths
- 🗺️ button on each top hop row → opens map with from/to markers + line
- 🗺️ button on each top path row → opens map with full multi-hop route
- Server now includes fromPk/toPk in topPaths hops for map resolution
- Uses existing drawPacketRoute() via sessionStorage handoff
2026-03-21 17:56:44 +00:00
you
27914fbd62 fix: cap max hop distance at 300km, link to node detail not analytics
- 1000km filter was too generous for LoRa (record ~250km)
- Uhuru kwa watu 📡 ↔ Bay Area hops at 880km were obviously wrong
- Node links in leaderboard now go to #/nodes/:pk (detail) not /analytics
2026-03-21 17:45:51 +00:00
you
110287b9f1 fix: remove dead histogram(null) call crashing distance tab
The subagent left a stray histogram(null, 0, ...) call that fell through
to the legacy path which does Math.min(...null) → Symbol.iterator error.
2026-03-21 17:41:24 +00:00
you
5720d0d948 Add Distance/Range analytics tab
New /api/analytics/distance endpoint that:
- Resolves path hops to nodes with valid GPS coordinates
- Calculates haversine distances between consecutive hops
- Separates stats by link type: R↔R, C↔R, C↔C
- Returns top longest hops, longest paths, category stats, histogram, time series
- Filters out invalid GPS (null, 0/0) and sanity-checks >1000km
- Supports region filtering and caching

New Distance tab in analytics UI with:
- Summary cards (total hops, paths, avg/max distance)
- Link type breakdown table
- Distance histogram
- Average distance over time sparkline
- Top 20 longest hops leaderboard
- Top 10 longest multi-hop paths table
2026-03-21 17:33:33 +00:00
you
ca4aa72574 fix: region filter nodes by ADVERT observers, not data packets
The previous approach matched nodes via data packet hashes seen by
regional observers — but mesh packets propagate everywhere, so nearly
every node matched every region (550/558).

New approach: _advertByObserver index tracks which observers saw each
node's ADVERT packets. ADVERTs are local broadcasts that indicate
physical presence, so they're the correct signal for geographic filtering.

Also fixes role counts to reflect filtered results, not global totals.
2026-03-21 08:31:55 +00:00
you
63a525ecc1 fix: stack overflow in /analytics/rf — replace Math.min/max spread
Math.min(...arr) and Math.max(...arr) blow the call stack when arr
has tens of thousands of elements. Replaced with simple for-loop
arrMin/arrMax helpers.
2026-03-21 08:26:34 +00:00
you
d2dbb1d8e6 fix: region dropdown matches btn-icon style, says 'All Regions'
- border-radius 6px (rectangle) instead of 16px (pill)
- Same padding/font-size as btn-icon
- Removed redundant 'Region:' label from dropdown mode
- Default label: 'All Regions' instead of 'All'
2026-03-21 08:24:36 +00:00
you
5f06d967d3 fix: btn-icon contrast — add color: var(--text)
BYOP button text was nearly invisible on dark background due to
missing explicit color on .btn-icon class.
2026-03-21 08:23:15 +00:00
you
ace705ef8e fix: use dropdown for region filter on packets page
Pills look out of place in the dense toolbar. Added { dropdown: true }
option to RegionFilter.init() to force dropdown mode regardless of
region count.
2026-03-21 08:22:12 +00:00
you
9345dbf50f fix: bump nodes.js cache buster so region filter loads
RegionFilter was added to nodes page but cache buster wasn't bumped,
so browsers served the old cached nodes.js without the filter.
2026-03-21 08:09:33 +00:00
you
debc873b26 fix: BYOP button accessibility compliance
- Add aria-label and aria-haspopup='dialog' to BYOP trigger button
- Add aria-label to close button and textarea
- Add role='status' and aria-live='polite' to result container
- Add role='alert' to error messages for screen reader announcement
- Fix textarea focus style: visible outline instead of outline:none
- Update cache busters
2026-03-21 08:06:37 +00:00
you
4ffeb8204e fix: analytics RF stats respect region filter
- Separate region filtering from SNR filtering in /api/analytics/rf
- totalAllPackets now shows regional observation count (was global)
- Add totalTransmissions (unique hashes in regional set)
- Payload types and packet sizes use all regional data, not just SNR-filtered
- Signal stats (SNR, RSSI, scatter) use SNR-filtered subset
- Handle empty SNR/RSSI arrays gracefully (no Infinity/-Infinity)
2026-03-21 08:01:30 +00:00
you
39f5b40322 packets: replace single-select region dropdown with shared RegionFilter component
- Fixes empty region dropdown (was populated before async regionMap loaded)
- Now uses multi-select RegionFilter component consistent with other pages
- Loads regions from /api/observers with proper async handling
- Supports multi-region filtering
2026-03-21 08:00:44 +00:00
you
d2480f15c2 fix: null safety for analytics stats when region has no signal data
rf.snr.min/max/avg etc can be null when a region filter excludes all
packets with signal data. Added sf() helper for safe toFixed.
2026-03-21 07:43:14 +00:00
you
9cbe275828 fix: network-status missing ? in region query string
/nodes/network-status + &region=X was producing an invalid URL.
Now correctly uses ? as separator when it's the first param.
2026-03-21 07:42:07 +00:00
you
c2acf40951 fix: revert broken SQL region filtering for nodes — use in-memory index
The subagent used a non-existent column (sender_key) in the SQL join.
Reverted to the same byObserver + _nodeHashIndex approach used by
bulk-health and network-status endpoints.
2026-03-21 07:15:22 +00:00
you
80b6e1cac1 fix: use SQL for region filtering on nodes page
The previous approach used pktStore._nodeHashIndex which only tracks
nodes appearing as sender/dest in decoded packet JSON. Most nodes only
send ADVERTs, so they had no entries in _nodeHashIndex and were filtered
out when a region was selected (showing 0 results).

Now uses a direct SQL join between observations and transmissions to find
all sender_keys observed by regional observers, which correctly includes
ADVERT-only nodes.
2026-03-21 07:10:56 +00:00
you
0ac7687313 Fix region filtering in Route Patterns, Nodes, and Network Status tabs
- Add RegionFilter.regionQueryString() to all API calls in renderSubpaths and renderNodesTab
- Add region filtering to /api/analytics/subpaths (filter packets by regional observer hashes)
- Add region filtering to /api/nodes/bulk-health (filter nodes by regional presence)
- Add region filtering to /api/nodes/network-status (filter node counts by region)
- Add region param to nodes lookup in hash collision tab
- Update cache keys to include region param for proper cache separation
2026-03-21 07:10:38 +00:00
you
207d7a87b9 fix: remove unnecessary region filter from packet trace screen 2026-03-21 07:10:09 +00:00
you
09e3a7b4d1 Fix region filter resetting analytics tab to overview
Track current active tab in _currentTab variable so that
loadAnalytics() re-renders the current tab instead of always
resetting to 'overview' when region filter changes.
2026-03-21 07:00:29 +00:00
you
55c45c964b region filter: add label, ARIA, dropdown mode for >4 regions
- Add 'Region:' label before filter controls
- ARIA roles: group with aria-label, checkbox roles on pills, aria-checked
- When >4 regions: render multi-select dropdown with checkboxes
  - Trigger shows summary (All / selected names / N selected)
  - All option at top, click-outside closes
- Pill bar mode unchanged for ≤4 regions (just added label + ARIA)
2026-03-21 06:57:48 +00:00
you
86c6665c6a chore: remove bare channel names from rainbow table — only #channels are valid 2026-03-21 06:48:36 +00:00
you
94854c8d40 channels: only show decrypted messages, hide encrypted garbage
- Filter on decoded.type === 'CHAN' (successful decryption) only
- Skip GRP_TXT packets (failed decryption) entirely
- Channel key = decoded channel name instead of hash byte
- Remove channelHashNames lookup, encrypted field, isCollision logic
- Remove encrypted UI badges/indicators from frontend
- Channels with 0 decrypted messages no longer appear
2026-03-21 06:45:45 +00:00
you
472635ba69 Include channel-rainbow.json in Docker image 2026-03-21 06:38:02 +00:00
you
7ac051d2a9 Add rainbow table of pre-computed channel keys for common MeshCore channels
- channel-rainbow.json: 592 pre-computed SHA256-derived keys for common
  channel names (cities, topics, ham radio, emergency, etc.)
- server.js: Load rainbow table at startup as lowest-priority key source
- config.example.json: Add #LongFast to hashChannels list

Key derivation verified against MeshCore source: SHA256('#name')[:16bytes].
Rainbow table boosted decryption from ~48% to ~88% in testing.
2026-03-21 06:35:14 +00:00
you
1a7145fc46 Use hashChannels for derived keys, keep only hardcoded public key in channelKeys
Channel keys for #test, #sf, #wardrive, #yo, #bot, #queer, #bookclub, #shtf
are all SHA256(channelName).slice(0,32) — no need to hardcode them. Move to
hashChannels array for auto-derivation. Only the MeshCore default public key
(8b3387e9c5cdea6ac9e5edbaa115cd72) needs explicit specification since it's
not derived from its channel name.
2026-03-21 06:22:05 +00:00
you
8ad2348a16 fix: simplify channel key scheme + add CHAN packet detail renderer
- Remove composite key scheme (ch_/unk_ prefixes) that broke URL routing
  due to # in channel names. Use plain numeric channelHash as key instead.
- All packets with same hash byte go in one bucket; name is set from
  first successful decryption.
- Add packet detail renderer for decoded CHAN type showing channel name,
  sender, and sender timestamp.
- Update cache buster for packets.js.
2026-03-21 06:11:01 +00:00
you
6402d291c1 fix: normalize packet hash case for deeplink lookups 2026-03-21 05:50:29 +00:00
you
f303b7a4b9 fix: regional filters — proper indexed queries + frontend integration
Fixes Kpa-clawbot/meshcore-analyzer#111
2026-03-21 05:48:54 +00:00
you
378adc03c9 fix: restore channel message decryption — correct hash matching in API
The /api/channels endpoint was returning simple numeric hash (e.g. '45') while
/api/channels/:hash/messages was using composite keys (e.g. 'ch_#LongFast',
'unk_45') internally. This mismatch meant no channel ever matched, so all
messages appeared encrypted.

Fix: return the composite key as the hash field from /api/channels so the
frontend passes the correct identifier. Also add encodeURIComponent() to
channel API calls in the frontend since composite keys can contain '#'.
2026-03-21 05:47:51 +00:00
you
cd0a225bc7 fix: rewrite favorites filter — correct packet matching + feed list filtering 2026-03-21 05:43:52 +00:00
you
b236b41568 feat: add regional filters to all tabs
Fixes Kpa-clawbot/meshcore-analyzer#111
2026-03-21 05:41:02 +00:00