Commit Graph

357 Commits

Author SHA1 Message Date
Kpa-clawbot 744702ccf6 feat(perf): show Go Runtime stats instead of Event Loop on Go backend
When engine=go, the perf page now renders Go-specific runtime stats
(goroutines, GC collections, GC pause times, heap breakdown, CPUs)
instead of the misleading Node.js Event Loop metrics. Falls back to
the existing Node UI when engine is not 'go' or goRuntime data is
missing. Includes color-coded GC pause thresholds.

fixes #153

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-27 11:49:10 -07:00
Kpa-clawbot a6f713a09c chore: bump cache busters to trigger CI deploy 2026-03-27 10:40:18 -07:00
Kpa-clawbot 9ca7777851 fix: version-badge link contrast in nav stats bar
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>
2026-03-27 10:14:50 -07:00
Kpa-clawbot b92e71fa0e refine version badge: clickable links, version only on prod
- 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>
2026-03-27 09:54:53 -07:00
Kpa-clawbot a7a280801a feat: display version and commit hash in stats bar
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>
2026-03-27 09:52:13 -07:00
Kpa-clawbot e47b5f85ed feat: display backend engine badge in stats bar
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>
2026-03-27 09:31:32 -07:00
Kpa-clawbot bad023ccad fix: hide packet detail pane on fresh page load
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>
2026-03-27 09:10:51 -07:00
Kpa-clawbot 99c23f8b59 feat: add observer packet comparison page (fixes #129)
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>
2026-03-27 08:11:30 -07:00
Kpa-clawbot 55db2bef27 fix: auto-update Nodes tab when ADVERT packets arrive via WebSocket
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>
2026-03-27 08:08:43 -07:00
Kpa-clawbot 65a7f055de fix: dim stale nodes on live map instead of removing them
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>
2026-03-27 08:02:09 -07:00
Kpa-clawbot 14b7e56403 Show channel hash and decryption status for undecrypted GRP_TXT packets
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>
2026-03-27 07:54:27 -07:00
Kpa-clawbot c386f119b0 fix: prune stale nodes from Live page counter (fixes #133)
nodeMarkers map in live.js grew unbounded because ADVERT-injected
nodes were never removed. Added time-based pruning using health
thresholds from roles.js (24h for companions/sensors, 72h for
repeaters/rooms). Prune interval runs every 60 seconds.

- Track _liveSeen timestamp on each nodeData entry
- Update timestamp on every ADVERT (new or existing node)
- pruneStaleNodes() removes nodes exceeding silentMs threshold
- 5 new tests verifying pruning logic and threshold behavior
- Cache busters bumped

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-27 01:47:42 -07:00
Kpa-clawbot 48328e2cb3 fix: fully collapse detail pane on dismiss — table expands to full width
Closes #125

When the ✕ close button (or Escape) is pressed, the detail pane now
fully hides via display:none (CSS class 'detail-collapsed' on the
split-layout container) so the packets table expands to 100% width.
Clicking a packet row removes the class and restores the detail pane.

Previously the pane only cleared its content but kept its 420px width,
leaving a blank placeholder that wasted ~40% of screen space.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-27 00:04:06 -07:00
Kpa-clawbot 4cfdd85063 fix: resolve 4 issues + optimize E2E test performance
Issues fixed:
- #127: Firefox copy URL - shared copyToClipboard() with execCommand fallback
- #125: Dismiss packet detail pane - close button with keyboard support
- #124: Customize window scrollbar - flex layout fix for overflow
- #122: Last Activity stale times - use last_heard || last_seen

Test improvements:
- E2E perf: replace 19 networkidle waits, cut navigations 14->7, remove 11 sleeps
- 8 new unit tests for copyToClipboard helper (47->55 in test-frontend-helpers)
- 1 new E2E test for packet pane dismiss

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-26 12:41:25 -07:00
you e340949253 feat: optimize observations table — 478MB → 141MB
Schema v3 migration:
- Replace observer_id TEXT (64-char hex) with observer_idx INTEGER FK
- Drop redundant hash, observer_name, created_at columns
- Store timestamp as epoch integer instead of ISO string
- In-memory dedup Set replaces expensive unique index lookups
- Auto-migration on startup with timestamped backup (never overwrites)
- Detects already-migrated DBs via pragma user_version + column inspection

Fixes:
- disambiguateHops: restore 'known' field dropped during refactor (fba5649)
- Skip MQTT connections when NODE_ENV=test
- e2e test: encodeURIComponent for # channel hashes in URLs
- VACUUM + TRUNCATE checkpoint after migration (not just VACUUM)
- Daily TRUNCATE checkpoint at 2:00 AM UTC to reclaim WAL space

Observability:
- SQLite stats in /api/perf (DB size, WAL size, freelist, row counts, busy pages)
- Rendered in perf dashboard with color-coded thresholds

Tests: 839 pass (89 db + 30 migration + 70 helpers + 200 routes + 34 packet-store + 52 decoder + 255 decoder-spec + 62 filter + 47 e2e)
2026-03-25 22:33:39 +00:00
you 63f2f7c995 refactor: unify live page packet rendering into renderPacketTree()
Major refactor of live.js data flow:

- Replaced animatePacket() and animateRealisticPropagation() with
  single renderPacketTree(packets, isReplay) function
- All paths use the same function: WS arrival, VCR replay, DB load,
  feed card replay button
- VCR fetches use expand=observations to get full observation data
- expandToBufferEntries() extracts per-observer paths from observations
- startReplay() pre-aggregates VCR buffer by hash before playback
- Feed dedup accumulates observation packets for full tree replay
- Longest path shown in feed (scans all observations, not just first)
- Replay button uses full observation set for starburst animation

Server changes:
- WS broadcast includes path_json per observation
- packet-store insert() uses longest path for display (was earliest)

DB changes:
- Removed seed() function and synthetic test data

Not pushed to prod — local testing only.
2026-03-25 06:02:48 +00:00
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 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 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 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 0ac7f63035 Fix: localStorage preferences take priority over server config
app.js was fetching /api/config/theme and overwriting ROLE_COLORS,
ROLE_STYLE, branding AFTER customize.js had already restored them
from localStorage. Now skips server overrides for any section
where user has local preferences.

Also added branding restore from localStorage on DOMContentLoaded.
2026-03-23 03:58:01 +00:00
you cb68b3e828 Fix: restore branding (site name, logo, favicon) from localStorage on load 2026-03-23 03:54:19 +00:00
you 403b9c8d71 Fix: nav bar text fully customizable via --nav-text (Basic)
Added --nav-text and --nav-text-muted CSS variables. All nav
selectors (.top-nav, .nav-brand, .nav-link, .nav-btn, .nav-stats)
use these instead of --text/--text-muted. Nav Text is in Basic
settings. Nav Muted Text in Advanced.

This is separate from page text because nav sits on a dark
background — page text color would be unreadable on the nav.
2026-03-23 03:49:12 +00:00
you cdd8bf43f5 Fix: load customize.js right after roles.js, BEFORE app/map
customize.js was loading last — saved colors restored AFTER the
map already created markers with default colors. Now loads right
after roles.js, before app.js. ROLE_STYLE colors are updated
before any page renders.
2026-03-23 03:41:29 +00:00
you 1cb3baf4ab fix: replace all hardcoded colors with CSS variables
- Move --status-green/yellow/red from home.css to style.css :root (light+dark)
- Replace hardcoded status colors in style.css (.tl-snr, .health-dot, .byop-err,
  .badge-hash-*, .fav-star.on, .spark-fill) with CSS variable references
- Replace hardcoded colors in live.css (VCR mode, stat pills, fdc-link, playhead)
- Replace --primary/--bg-secondary/--text-primary/--text-secondary dead vars with
  canonical --accent/--input-bg/--text/--text-muted in style.css, map.js, live.js,
  traces.js, packets.js
- Fix nodes.js legend colors to use ROLE_COLORS globals instead of hardcoded hex
- Replace hardcoded hex in home.js (SNR), perf.js (indicators), map.js (accuracy
  circles) with CSS variable references via getComputedStyle or var()
- Add --detail-bg to customizer (THEME_CSS_MAP, DEFAULTS, ADVANCED_KEYS, labels)
- Move font/mono out of ADVANCED_KEYS into separate Fonts section in customizer
- Remove debug console.log lines from customize.js
- Bump cache busters in index.html
2026-03-23 03:29:38 +00:00
you 0f086748f4 Fix: restore saved colors IMMEDIATELY on script load, not DOMContentLoaded
customize.js loads after roles.js but before app.js triggers
navigate(). Restoring colors in the IIFE body (not DOMContentLoaded)
ensures ROLE_STYLE/ROLE_COLORS/TYPE_COLORS are updated BEFORE
the map or any page creates markers.
2026-03-23 03:25:52 +00:00
you 7f1e6a3959 Debug: log autoSave and restore for node/type colors 2026-03-23 03:06:41 +00:00
you 06868b9e60 Fix: nav bar uses --text and --text-muted, not separate nav-text
Changed nav brand, links, buttons from hardcoded #fff/#cbd5e1 to
var(--text) and var(--text-muted). Setting primary text color
now changes nav text too. Removed unnecessary --nav-text variable.
2026-03-23 03:05:04 +00:00
you 318a70c7d0 Nav text uses --nav-text variable, customizable in Advanced
Defaults to white. Admin can change it for light nav backgrounds.
Nav bar brand, links, buttons all use var(--nav-text, #fff).
2026-03-23 02:59:22 +00:00
you 8e990f61d4 Fix: initState merges localStorage → export includes saved changes
State now loads: DEFAULTS → server config → localStorage.
Admin saves locally, comes back later, opens customizer —
sees their saved values, export includes everything.
2026-03-23 02:56:59 +00:00
you a69b828f22 Fix: auto-save all customizations to localStorage on every change
Every color pick, text edit, step change auto-saves (debounced
500ms). No manual save needed. Also fixed syncBadgeColors on
restore and removed stray closing brace.
2026-03-23 02:56:03 +00:00