Commit Graph

526 Commits

Author SHA1 Message Date
you
fba5649979 refactor: consolidate hop disambiguation — remove 3 duplicate implementations
- server.js disambiguateHops() now delegates to server-helpers.js
  (was a full copy of the same algorithm, ~70 lines removed)
- live.js resolveHopPositions() now delegates to shared HopResolver
  (was a standalone reimplementation, ~50 lines removed)
- HopResolver.init() called when live page loads/updates node data
- Net -106 lines, same behavior, single source of truth

All unit tests pass (241). E2E 13/16 (3 pre-existing Chromium crashes).
2026-03-24 22:19:16 +00:00
you
459d51f5a5 fix: re-run decollision on zoom regardless of hash labels
zoomend handler was gated on filters.hashLabels — decollision only
re-ran on zoom when hash labels were enabled. Now always re-renders
markers on zoom so pixel offsets stay correct at every zoom level.
2026-03-24 20:57:45 +00:00
you
863ee604be fix: re-run marker decollision on map resize
Added map.on('resize') handler that re-renders markers, recalculating
pixel-based decollision offsets for the new container size. Previously
only zoomend triggered re-render — resize left stale offsets.

Added E2E test verifying markers survive a viewport resize.
2026-03-24 20:55:39 +00:00
you
305da30b88 fix: run map.invalidateSize before marker decollision on every render
On SPA navigation, the map container may not have its final dimensions
when markers render, causing latLngToLayerPoint to return incorrect
pixel coordinates for decollision. This resulted in overlapping markers
that only resolved on a full page refresh.

Fix: call map.invalidateSize() at the start of every renderMarkers()
call, ensuring correct container dimensions before deconfliction runs.
2026-03-24 20:24:26 +00:00
you
d6ea3dd9fd fix: cast snr/rssi to Number before toFixed() — fixes crash on string values
Observer detail, home health timeline, and traces all called
.toFixed() on snr/rssi values that may be strings from the DB.
Wrapping in Number() matches what live.js already does.
2026-03-24 20:17:41 +00:00
you
14ff1821d6 fix: hash-based packet deduplication in Live feed
Root cause: addFeedItem had no dedup logic — each WS message created
a new feed entry regardless of hash. Dedup only worked when the
'Realistic propagation' toggle was ON (which buffers by hash before
calling animateRealisticPropagation). Default mode called animatePacket
directly for every observation, producing duplicate feed entries.

Fix: Added feedHashMap (hash -> {element, count, pkt, addedAt}) that
tracks recent feed items by packet hash. When a packet with a known
hash arrives within 30s, the existing feed item is updated in-place:
- Observation count badge incremented
- Item flashed and moved to top of feed
- No duplicate DOM element created

Also adds data-hash attribute to feed items for testability.

Tests: 5 new Playwright tests in test-live-dedup.js covering:
- Same hash different observers → single entry
- Different hashes → separate entries
- 5 rapid sequential duplicates → single entry with count 5
- Same hash same observer → still deduplicates
- Packets without hash → not deduplicated
2026-03-24 19:35:28 +00:00
you
1bdf41a631 fix: separate heatmap opacity controls for Map and Live pages
- Live page showHeatMap() now reads meshcore-live-heatmap-opacity from
  localStorage and applies it to the canvas element (was hardcoded 0.3)
- Customizer now has two clearly labeled sliders:
  🗺️ Nodes Map — controls the static map page heatmap
  📡 Live Map — controls the live page heatmap
- Each uses its own localStorage key (meshcore-heatmap-opacity vs
  meshcore-live-heatmap-opacity)
- Added E2E tests for live opacity persistence and dual slider existence
- 13/15 E2E tests pass locally (2 fail due to ARM chromium OOM after
  heavy live page tests — CI on x64 will handle them)

Closes #119 properly this time.
2026-03-24 19:25:28 +00:00
you
52d52af6ec fix: force-enable heat toggle when matrix mode is off
Recover from stale localStorage state where heat checkbox stayed
disabled even after matrix/ghosts mode was turned off. Explicitly
sets ht.disabled = false in the else branch of matrix init.

13/13 E2E tests pass locally.
2026-03-24 19:17:17 +00:00
you
16eb7ef07d fix: persist live page heat checkbox + add E2E test
Live page liveHeatToggle now saves to localStorage (meshcore-live-heatmap).
Map page was already fixed but live page was missed.
Added E2E test that verifies persistence across reload.

13/13 E2E tests pass locally.
2026-03-24 17:57:17 +00:00
you
325fdbe50e fix: heatmap opacity flash on new packet arrival
When new data arrived, toggleHeatmap() destroyed and recreated the
heat layer, causing a brief flash at full opacity before the CSS
opacity was applied via setTimeout. Now reuses the existing layer
via setLatLngs() for data updates, and hooks the 'add' event for
immediate opacity on first creation. No more flash.

All 12 E2E tests pass locally.
2026-03-24 17:53:29 +00:00
you
014b30936d fix: heatmap opacity slider affects entire layer, not just blue minimum
The previous implementation used L.heatLayer's minOpacity which only
controlled the opacity of the coolest (blue) gradient stops. Now sets
CSS opacity on the canvas element directly, affecting all gradient
colors uniformly. Closes #119 properly.
2026-03-24 17:26:22 +00:00
you
b63f42ac75 feat: add heatmap opacity slider in customization UI (closes #119) 2026-03-24 16:07:26 +00:00
you
3111d755a2 fix: persist heat checkbox across page reload (closes #120) 2026-03-24 16:07:26 +00:00
you
8efdff420a fix: packet row expand crash — 'child' not defined, should be 'c'
renderPath(childPath, child.observer_id) referenced undefined variable
'child' instead of loop variable 'c'. Crashed the entire render loop
when expanding a grouped packet row.
2026-03-24 14:27:00 +00:00
you
efd7d811ca fix: encrypted payload field sizes match firmware source (Mesh.cpp)
Per firmware: PAYLOAD_VER_1 uses dest(1) + src(1) + MAC(2), not 6+6+4.
Confirmed from Mesh.cpp lines 129-130: uint8_t dest_hash = payload[i++]
and MeshCore.h: CIPHER_MAC_SIZE = 2.

Changed: decodeEncryptedPayload (REQ/RESPONSE/TXT_MSG), decodeAck,
decodeAnonReq (dest 1B + pubkey 32B + MAC 2B), decodePath (1+1+2).
Updated test min-length assertions.
2026-03-24 01:32:58 +00:00
you
5b496a8235 feat: add missing payload types from firmware spec
Added GRP_DATA (0x06), MULTIPART (0x0A), CONTROL (0x0B), RAW_CUSTOM (0x0F)
to decoder.js, app.js display names, and packet-filter.js.
Source: firmware/src/Packet.h PAYLOAD_TYPE definitions.
2026-03-24 01:23:12 +00:00
you
3016493089 fix: use last_heard||last_seen for status in nodes table and map
renderRows() in nodes.js and three places in map.js were using only
n.last_seen to compute active/stale status, ignoring the more recent
n.last_heard from in-memory packets. This caused nodes that were recently
heard but had an old DB last_seen to incorrectly show as stale.

Also adds 29 unit tests for the aging system (getNodeStatus,
getStatusInfo, getStatusTooltip, threshold values).
2026-03-23 23:32:01 +00:00
you
685d48c62c fix: remove duplicate map link from hex breakdown longitude row
Keep the 📍map link in the Location metadata row (goes to app map).
Remove the redundant 📍 Map pill in the hex breakdown (went to Google Maps).
One link, one style.
2026-03-23 23:27:08 +00:00
you
656c8b8a07 fix: remove all /resolve-hops server API calls from packets page
Was making N API calls per observer for ambiguous hops on every page load,
plus another per packet detail view. All hop resolution now uses the
client-side HopResolver which already handles ambiguous prefixes.
Eliminates the main perf regression.
2026-03-23 23:23:08 +00:00
you
4677bff52e fix: remove 200 packet cap from WebSocket live update handler
Was slicing to 200 packets after every live update, truncating the
initial 32K+ packet list. Now keeps all packets.
2026-03-23 23:11:47 +00:00
you
aa181ce8d4 fix: ast.field not node.field in alias resolver, 37 tests passing
ReferenceError: node is not defined — was using wrong variable name.
Verified with 37 tests covering: firmware type names, aliases, route,
numeric ops, string ops, payload dot notation, hops, size, observations,
AND/OR/NOT, parentheses, and error handling.
2026-03-23 23:08:22 +00:00
you
7f948d3d63 fix: packet filter uses firmware type names (GRP_TXT, TXT_MSG, REQ, etc.)
Was using display names like 'Channel Msg' which aren't standard.
Now resolves to firmware names: GRP_TXT, TXT_MSG, REQ, ADVERT, etc.
Also accepts aliases: 'channel', 'dm', 'Channel Msg' all map to the
correct firmware name for convenience.
2026-03-23 23:02:37 +00:00
you
1254aa904a M3: Add tooltips to status labels explaining active/stale thresholds
- Add getStatusTooltip() helper with role-aware explanations
- Tooltips on status labels in: node badges, status explanation, detail table
- Tooltips on map legend active/stale counts per role
- Native title attributes (long-press on mobile)
- Bump cache busters
2026-03-23 22:51:11 +00:00
you
3094b96e07 feat: Packet Filter Language M1 — Wireshark-style filter engine + UI
Add a filter language for the packets page. Users can type expressions like:
  type == Advert && snr > 5
  payload.name contains "Gilroy"
  hops > 2 || route == FLOOD

Architecture: Lexer → Parser → AST → Evaluator(packet) → boolean

- packet-filter.js: standalone IIFE exposing window.PacketFilter
  - Supports: ==, !=, >, <, >=, <=, contains, starts_with, ends_with
  - Logic: &&, ||, !, parentheses
  - Fields: type, route, hash, snr, rssi, hops, observer, size, payload.*
  - Case-insensitive string comparisons, null-safe
  - Self-tests included (node packet-filter.js)
- packets.js: filter input with 300ms debounce, error display, match count
- style.css: filter input states (focus, error, active)
- index.html: script tag added before packets.js
2026-03-23 22:48:59 +00:00
you
418e1a761a Node Aging M2: status filters + localStorage persistence
- Nodes page: Add Active/Stale/All pill button filter
- Nodes page: Expand Last Heard dropdown (Any,1h,2h,6h,12h,24h,48h,3d,7d,14d,30d)
- Map page: Add Active/Stale/All status filter (hides markers, not just fades)
- Map legend: Show active/stale counts per role (e.g. '420 active, 42 stale')
- localStorage persistence for all filters:
  - meshcore-nodes-status-filter
  - meshcore-nodes-last-heard
  - meshcore-map-status-filter
- Bump cache busters
2026-03-23 22:37:52 +00:00
you
bb409a2e00 fix: nodes list shows actual last heard time, not just last advert
Server now computes last_heard from in-memory packet store (all traffic
types) and includes it in /api/nodes response. Client prefers last_heard
over DB last_seen for display, sort, filter, and status calculation.

Fixes inconsistency where list showed '5d ago' but side pane showed
'26m ago' for the same node.
2026-03-23 20:32:56 +00:00
you
5b64541985 refactor: extract shared node status/badge helpers, add status explanation to side pane
- Create getStatusInfo(), renderNodeBadges(), renderStatusExplanation(),
  renderHashInconsistencyWarning() shared helpers
- Side pane (renderDetail) now uses shared helpers and shows status explanation
  (was previously missing)
- Full page (loadFullNode) uses same shared helpers
- Both views now render identical status info
- Bump cache buster for nodes.js
2026-03-23 20:23:02 +00:00
you
091355c427 fix: fetch all nodes (up to 5000), filter client-side
Was fetching only 200 nodes with server-side filtering — missed nodes.
Now fetches full list once, caches it, filters by role/search/lastHeard
in the browser. Region change invalidates cache.
2026-03-23 20:13:44 +00:00
you
a9f0739392 fix: stale markers 70% opacity, 90% grayscale + dimmed
35% was too faint. Now subtle but obvious — visible but clearly
desaturated compared to active nodes.
2026-03-23 20:11:31 +00:00
you
06126bb571 fix: bump cache buster — roles.js was stale, getNodeStatus not defined
Browser cached old roles.js (without getNodeStatus) but loaded new
nodes.js (which calls it). Bumped all cache busters to force reload.
2026-03-23 19:57:36 +00:00
you
5c487204e7 feat: node aging M1 — visual aging on map + list
Two-state node freshness: Active vs Stale

- roles.js: add getNodeStatus(role, lastSeenMs) helper returning 'active'/'stale'
  - Repeaters/Rooms: stale after 72h
  - Companions/Sensors: stale after 24h
  - Backward compat: getHealthThresholds() with degradedMs/silentMs still works

- map.js: stale markers get .marker-stale CSS class (opacity 0.35, grayscale 70%)
  - Applied to both SVG shape markers and hash label markers
  - makeMarkerIcon() and makeRepeaterLabelIcon() accept isStale parameter

- nodes.js: visual aging in table, side pane, and full detail
  - Table: Last Seen column colored green (active) or muted (stale)
  - Side pane: status shows 🟢 Active or  Stale (was 🟢/🟡/🔴)
  - Full detail: Status row with role-appropriate explanation
    - Stale repeaters: 'not heard for Xd — repeaters typically advertise every 12-24h'
    - Stale companions: 'companions only advertise when user initiates'
  - Fixed lastHeard fallback to n.last_seen when health API has no stats

- style.css: .marker-stale, .last-seen-active, .last-seen-stale classes
2026-03-23 19:56:22 +00:00
you
ca951fc260 feat: make all nodes table columns sortable with click headers
- All 5 columns (Name, Public Key, Role, Last Seen, Adverts) are now
  sortable by clicking the column header
- Click toggles between ascending/descending sort
- Visual indicator (▲/▼) shows current sort column and direction
- Sort preference persisted to localStorage (meshcore-nodes-sort)
- Removed old Sort dropdown since headers replace it
- Client-side sorting on already-fetched data
- Default: Last Seen descending (most recent first)
2026-03-23 19:52:39 +00:00
you
7d62103129 Fix App Flags display to show type enum + add map link to location rows
- App Flags now shows human-readable type (Companion/Repeater/Room Server/Sensor)
  instead of confusing individual flag names like 'chat, repeater'
- Boolean flags (location, name) shown separately after type: 'Room Server + location, name'
- Added Google Maps link on longitude row using existing detail-map-link style
2026-03-23 19:48:13 +00:00
you
ebcdb994ef fix: map markers use role color always, not gray when hash_size is missing
Nodes without hash_size (older instances, no adverts seen) were showing
as gray #888 instead of their role color. Now always uses ROLE_STYLE color.
2026-03-23 19:21:30 +00:00
you
c524efc74d fix: section links are real deep-linkable URLs, not javascript:void
TOC: #/analytics?tab=collisions&section=inconsistentHashSection etc.
Back-to-top: #/analytics?tab=collisions (scrolls to top of tab)
All copyable, shareable, bookmarkable.
2026-03-23 18:51:02 +00:00
you
01688093af feat: Hash Issues page — section nav links at top, back-to-top on each section
TOC at top: Inconsistent Sizes | Hash Matrix | Collision Risk
Each section header has '↑ top' link on the right.
Smooth scroll navigation.
2026-03-23 18:48:05 +00:00
you
ef1e7751ea feat: deep-linkable sections within analytics tabs
Sections: inconsistentHashSection, hashMatrixSection, collisionRiskSection
Use ?tab=collisions&section=inconsistentHashSection to jump directly.
Scrolls after tab render completes (400ms delay for async content).
2026-03-23 18:47:18 +00:00
you
cd6f5dda86 feat: deep-linkable analytics tabs — #/analytics?tab=collisions
Parses ?tab= from hash URL and activates that tab on load.
e.g. #/analytics?tab=collisions → Hash Issues tab
2026-03-23 18:44:45 +00:00
you
2e54e51ec5 feat: deep-linkable sections on node detail page
Added ids: node-stats, node-observers, fullPathsSection, node-packets.
Use ?section=<id> to scroll to any section on load.
e.g. #/nodes/<pubkey>?section=node-packets
Variable hash size badge and analytics links updated to use ?section=.
2026-03-23 17:00:10 +00:00
you
790e96331d fix: recent packets always sorted newest-first
Server was returning oldest-first despite .reverse() — sort client-side
to guarantee descending time order on both detail page and side pane.
2026-03-23 16:58:09 +00:00
you
cc9a98bc69 fix: inconsistent hash table — proper contrast, row stripes, colored size badges
- Removed yellow text and redundant Status column
- Sizes Seen now uses colored badges (orange 1B, pale green 2B, bright green 3B)
- Row striping, card border/radius, accent-colored node links
- Current hash in mono with muted byte count
2026-03-23 16:52:59 +00:00
you
508520980c fix: hash size badge colors — 1B orange, 2B pale green, 3B bright green
1-byte is worst (most collisions), 3-byte is best (least collisions).
Colors now reflect quality: orange → pale green → bright green.
2026-03-23 16:50:12 +00:00
you
e7cdff9669 fix: link to actual firmware bug commit and release
- Bug: github.com/meshcore-dev/MeshCore/commit/fcfdc5f
  'automatic adverts not using configured multibyte path setting'
- Fix: github.com/meshcore-dev/MeshCore/releases/tag/repeater-v1.14.1
- Both links on node detail page banner and analytics Hash Issues tab
2026-03-23 16:49:33 +00:00
you
47734f0b02 feat: Hash Issues tab — shows inconsistent hash size nodes above collisions
- Renamed 'Hash Collisions' tab to 'Hash Issues'
- New section at top: 'Inconsistent Hash Sizes' table listing all nodes
  that have sent adverts with varying hash sizes
- Each node links to its detail page with ?highlight=hashsize for
  per-advert hash size breakdown
- Shows current hash prefix, all sizes seen, and affected count
- Green checkmark when no inconsistencies detected
- Existing collision grid and risk table unchanged below
2026-03-23 16:48:09 +00:00
you
09dcae274f feat: variable hash size badge links to detail page, shows per-advert hash sizes
- Badge is now a link to the detail page with ?highlight=hashsize
- Detail page auto-scrolls to Recent Packets section
- Each advert shows its hash size badge (yellow if different from current)
- Detail page shows always-visible explanation banner (not hidden)
- Side pane badge links to detail page too
2026-03-23 16:46:24 +00:00
you
80ae87380d fix: rename 'hash mismatch' to 'variable hash size' 2026-03-23 16:44:07 +00:00
you
0dae1ba3b7 fix: hash mismatch badge is clickable — expands explanation inline
Badge shows cursor:help and clicking toggles a yellow-bordered info box
explaining the issue and suggesting firmware update. Stats row just shows
'⚠️ varies' with tooltip. Much less jarring than a dead yellow badge.
2026-03-23 16:43:42 +00:00
you
211eb37fb2 fix: QR overlay sizing — override node-qr class margin/width, 56px square
node-map-qr-overlay also has node-qr class which was adding margin-top
and setting max-width to 100px. Override with !important and reset margins.
2026-03-23 16:41:59 +00:00
you
c706a39bbd fix: QR overlay — white 50% backing, transparent white spots, black modules
White semi-transparent square behind QR so black modules pop.
White rects in SVG already set to transparent by JS.
Same white backing in dark mode too (QR needs light bg to scan).
2026-03-23 16:39:50 +00:00
you
f7a4ad2b73 feat: detect and flag inconsistent hash sizes across adverts
Tracks all hash_size values seen per node. If a node has sent adverts
with different hash sizes, flags it as hash_size_inconsistent with a
yellow ⚠️ badge on both side pane and detail page. Tooltip mentions
likely firmware bug (pre-1.14.1). Stats row shows all sizes seen.
2026-03-23 16:39:02 +00:00