Commit Graph

139 Commits

Author SHA1 Message Date
you
db9219319d feat: config-driven customization system (Phase 1)
Add GET /api/config/theme endpoint serving branding, theme colors,
node colors, and home page content from config.json with sensible
defaults so unconfigured instances look identical to before.

Client-side (app.js):
- Fetch theme config on page load, before first render
- Override CSS variables from theme.* on document root
- Override ROLE_COLORS/ROLE_STYLE from nodeColors.*
- Replace nav brand text, logo, favicon from branding.*
- Store config in window.SITE_CONFIG for other pages

Home page (home.js):
- Hero title/subtitle from config.home
- Steps and checklist from config.home
- Footer links from config.home.footerLinks
- Chooser welcome text uses configured siteName

Config example updated with all available theme options.

No default appearance changes — all overrides are optional.
2026-03-23 00:37:48 +00:00
you
c267e58418 Fix: sort regional candidates by distance to IATA center
Without sender GPS (channel texts etc), the forward pass had no
anchor and just took candidates[0] — random order. Now regional
candidates are sorted by distKm to observer IATA center before
disambiguation. Closest to region center = default pick.
2026-03-22 23:59:33 +00:00
you
13460cfc93 Fix: move /api/iata-coords route after app initialization
Route was at line 16 before express app was created — caused
'Cannot access app before initialization' crash on startup.
2026-03-22 22:26:15 +00:00
you
935ed6ef85 Show observer IATA regions in packet, node, and live detail views
- packets.js: obsName() now shows IATA code next to observer name, e.g. 'EW-SFC-DR01 (SFO)'
- packets.js: hop conflicts in field table show distance (e.g. '37km')
- nodes.js: both full and sidebar detail views show 'Regions: SJC, OAK, SFO' badges and per-observer IATA
- live.js: node detail panel shows regions in 'Heard By' heading and per-observer IATA
- server.js: /api/nodes/:pubkey/health now returns iata field for each observer
- Bump cache busters
2026-03-22 22:21:01 +00:00
you
70c67d8551 Client-side regional hop filtering (#117)
HopResolver now mirrors server-side layered regional filtering:
- init() accepts observers list + IATA coords
- resolve() accepts observerId, looks up IATA, filters candidates
  by haversine distance (300km radius) to IATA center
- Candidates include regional, filterMethod, distKm fields
- Packet detail view passes observer_id to resolve()

New endpoint: GET /api/iata-coords returns airport coordinates
for client-side use.

Fixes: conflict badges showing "0 conflicts" in packet detail
because client-side resolver had no regional filtering.
2026-03-22 22:18:01 +00:00
you
c1d789b5d7 Regional hop filtering: layered geo + observer approach (#117)
Layer 1 (GPS, bridge-proof): Nodes with lat/lon are checked via
haversine distance to the observer IATA center. Only nodes within
300km are considered regional. Bridged WA nodes appearing in SJC
MQTT feeds are correctly rejected because their GPS coords are
1100km+ from SJC.

Layer 2 (observer-based, fallback): Nodes without GPS fall back to
_advertByObserver index — were they seen by a regional observer?
Less precise but still useful for nodes that never sent ADVERTs
with coordinates.

Layer 3: Global fallback, flagged.

New module: iata-coords.js with 60+ IATA airport coordinates +
haversine distance function.

API response now includes filterMethod (geo/observer/none) and
distKm per conflict candidate.

Tests: 22 unit tests (haversine, boundaries, cross-regional
collision sim, layered fallback, bridge rejection).
2026-03-22 22:09:43 +00:00
you
4cc1ad7b34 Fix #117: Regional filtering for repeater ID resolution
1-byte (and 2-byte) hop IDs match many nodes globally. Previously
resolve-hops picked candidates from anywhere, causing cross-regional
false paths (e.g. Eugene packet showing Vancouver repeaters).

Fix: Use observer IATA to determine packet region. Filter candidates
to nodes seen by observers in the same IATA region via the existing
_advertByObserver index. Fall back to global only if zero regional
candidates exist (flagged as globalFallback).

API changes to /api/resolve-hops response:
- conflicts[]: all candidates with regional flag per hop
- totalGlobal/totalRegional: candidate counts
- globalFallback: true when no regional candidates found
- region: packet IATA region in top-level response

UI changes:
- Conflict count badge (⚠3) instead of bare ⚠
- Tooltip shows regional vs global candidates
- Unreliable hops shown with strikethrough + opacity
- Global fallback hops shown with red dashed underline
2026-03-22 21:38:24 +00:00
you
f523d4f3c4 Audio Lab page (Milestone 1): Packet Jukebox
New #/audio-lab page for understanding and debugging audio sonification.

Server: GET /api/audio-lab/buckets — returns representative packets
bucketed by type (up to 8 per type spanning size range).

Client: Left sidebar with collapsible type sections, right panel with:
- Controls: Play, Loop, Speed (0.25x-4x), BPM, Volume, Voice select
- Packet Data: type, sizes, hops, obs count, hex dump with sampled
  bytes highlighted
- Sound Mapping: computed instrument, scale, filter, volume, voices,
  pan — shows exactly why it sounds the way it does
- Note Sequence: table of sampled bytes → MIDI → freq → duration → gap
- Byte Visualizer: bar chart of payload bytes, sampled ones colored

Enables MeshAudio automatically on first play. Mobile responsive.
2026-03-22 19:19:45 +00:00
you
90bd9e12e5 Fix realistic mode: all WS broadcasts include hash + raw_hex
Secondary broadcast paths (ADVERT, GRP_TXT, TXT_MSG, TRACE, API)
were missing hash field. Without hash, realistic mode's buffer
check (if pkt.hash) failed and packets fell through to
animatePacket individually — causing duplicate feed items and
duplicate sonification.

Also added missing addFeedItem call in animateRealisticPropagation
so the feed shows consolidated entries in realistic mode.
2026-03-22 18:37:53 +00:00
you
f09e338dfa fix: WS broadcast had null packet when observation was deduped
getById() returns null for deduped observations (not stored in byId).
Client filters on m.data.packet being truthy, so all deduped packets
were silently dropped from WS. Fallback to transmission or raw pktData.
2026-03-22 04:38:07 +00:00
you
9171eecc69 Drop legacy packets + paths tables on startup, remove dead code
Migration runs automatically on next startup — drops paths first (FK to
packets), then packets. Removes insertPacket(), insertPath(), all
prepared statements and references to both tables. Server-side type/
observer filtering also removed (client does it in-memory).

Saves ~2M rows (paths) + full packets table worth of disk.
2026-03-22 00:45:27 +00:00
you
3ff29519b4 Fix multi-select filters (AND→OR) and add localStorage persistence
- Server: support comma-separated type filter values (OR logic)
- Server: add observer_id filtering to /api/packets endpoint
- Client: fix type and observer filters to use OR logic for multi-select
- Client: persist observer and type filter selections to localStorage
- Keys: meshcore-observer-filter, meshcore-type-filter
2026-03-22 00:34:38 +00:00
you
ad6a796b35 Disable auto-seeding on empty DB - require --seed flag or SEED_DB=true
Previously db.seed() ran unconditionally on startup and would populate
a fresh database with fake test data. Now seeding only triggers when
explicitly requested via --seed CLI flag or SEED_DB=true env var.

The seed functionality remains available for developers:
  node server.js --seed
  SEED_DB=true node server.js
  node db.js  (direct run still seeds)
2026-03-21 23:00:01 +00:00
you
2894c38435 fix: header row shows first observer's path, not longest path
Removed server-side longest-path override in /api/packets/:id that
replaced the transmission's path_json with the longest observation
path. The header should always reflect the first observer's path.
Individual observation paths are available in the observations array.
2026-03-21 22:58:37 +00:00
you
eca0c9bd61 fix: use packet hash instead of sender_timestamp for channel message dedup
Device clocks on MeshCore nodes are wildly inaccurate (off by hours or
epoch-near values like 4). The channel messages endpoint was using
sender_timestamp as part of the deduplication key, which could cause
messages to fail deduplication or incorrectly collide.

Changed dedupe key from sender:timestamp to sender:hash, which is the
correct unique identifier for a transmission.

Also added TIMESTAMP-AUDIT.md documenting all device timestamp usage.
2026-03-21 21:53:38 +00:00
you
e70dd8b2fa 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
a66cc8f126 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
7a3a3a5ea0 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
cf14701592 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
7e841d89c1 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
795be6996f 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
187a2ac536 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
2c38d3c7d6 perf: yield event loop between pre-warm requests via setImmediate 2026-03-21 19:38:59 +00:00
you
9120985ab1 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
cc55e5733d 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
6b78b3c5e4 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
75af7c3094 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
e087156d90 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
fe552c8a4b 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
3edb02a829 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
7f1c735981 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
5bb17bbe60 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
a892691dd3 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
7d46d96563 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
47fa32f982 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
b3599694c6 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
013e33253d 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
35313c57d4 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
013cbaf5c4 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
49d4841862 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
9664d6089c 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
da475e9c13 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
212990a295 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
ce190886ff fix: normalize packet hash case for deeplink lookups 2026-03-21 05:50:29 +00:00
you
9f53a059c7 fix: regional filters — proper indexed queries + frontend integration
Fixes Kpa-clawbot/meshcore-analyzer#111
2026-03-21 05:48:54 +00:00
you
c6d72e828d 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
d555ea26be feat: add regional filters to all tabs
Fixes Kpa-clawbot/meshcore-analyzer#111
2026-03-21 05:41:02 +00:00
you
bd5171cf95 fix: validate ADVERT data to prevent corrupted node entries
Fixes Kpa-clawbot/meshcore-analyzer#112
2026-03-21 05:34:57 +00:00
you
0ef1eb2595 Make map default center/zoom configurable via config.json
Adds mapDefaults config option with center and zoom properties.
New /api/config/map endpoint serves the defaults. live.js and map.js
fetch the config with fallback to hardcoded Bay Area defaults.

Fixes Kpa-clawbot/meshcore-analyzer#115
2026-03-21 05:29:05 +00:00
you
4bfe1ec363 Fix channel name resolution to use decryption key, not just hash
Channels sharing the same hash prefix but with different keys (e.g. #startrek
and #ai-bot both with hash 2d) now display the correct name by keying on the
actual channel name from decryption rather than just the hash byte.

Fixes Kpa-clawbot/meshcore-analyzer#108
2026-03-21 05:27:02 +00:00