mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-05-12 09:54:43 +00:00
eaeb65b426a7ac80e7d8e2c32aaa4b5ca0ba1521
107 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
36ee71d17e |
feat(#1085): fold Roles page into Analytics tab (#1088)
Red commit:
|
||
|
|
8b924cd217 |
feat(ui): encode view & filter state in URL hash (#749) (#1072)
## Summary Encodes view + filter state in the URL hash so deep links restore the exact page state (issue #749). ## Changes New shared helper `public/url-state.js` exposing `URLState`: - `parseSort('col:asc')` → `{column, direction}` (defaults to `desc`) - `serializeSort('col', 'desc')` → `'col'` (omits default direction) - `parseHash('#/nodes/abc?tab=x')` → `{route: 'nodes/abc', params: {tab:'x'}}` - `buildHash(route, params)` and `updateHashParams(updates, currentHash)` for round-tripping while preserving subpaths. Wired into: - **packets.js** — sort column/direction now in `#/packets?sort=col[:asc]`, restored on init (overrides localStorage). Subpath `#/packets/<hash>` preserved. - **nodes.js** — sort encoded as `#/nodes?sort=col[:asc]`, restored on init. Subpath `#/nodes/<pubkey>` preserved. - **analytics.js** — both selected tab (`tab=topology`) AND time-window picker value (`window=7d`) now round-trip via URL. Subview keys used by rf-health (`range/observer/from/to`) cleared when switching tabs to keep URLs clean. Existing deep links (`#/nodes/<pubkey>`, `#/packets/<hash>`, `?filter=…`, `?node=…`, `?observer=…`, `?channel=…`, `?timeWindow=…`, `?region=…`) all keep working — additive change only. ## Tests TDD red→green: - Red: `5e1482e` (stub throws "not implemented"; 18/18 tests fail on assertions) - Green: `512940e` (helper implemented; 18/18 pass) Wired `test-url-state.js` into `test-all.sh`. Fixes #749 --------- Co-authored-by: clawbot <clawbot@users.noreply.github.com> |
||
|
|
f9cd43f06f |
fix(analytics): integrate channels list with PSK decrypt UX + add link from Channels page (#1042)
## What Integrates the Analytics → Channels section with the PSK decrypt UX (PRs #1021–#1040). Replaces nonsense `chNNN` placeholders with useful display names and groups the table the same way the Channels sidebar does. ## Before - Encrypted channels showed raw `ch185`, `ch64`, `ch?` placeholders. - Locally-decrypted PSK channels (with stored keys + labels) were not surfaced — every encrypted row looked identical and useless. - Single flat list, sorted by last activity by default. ## After - **My Channels** 🔑 — any analytics row whose hash byte matches a stored PSK key (via `ChannelDecrypt.getStoredKeys()` + `computeChannelHash`). Display name uses the user's label if set, otherwise the key name. - **Network** 📻 — known cleartext channels (server-provided names) and rainbow-table-decoded encrypted channels. - **Encrypted** 🔒 — unknown encrypted, rendered as `🔒 Encrypted (0xNN)` instead of `chNNN`. - Within each group: messages descending (most active first). - New `📊 Channel Analytics →` link in the Channels page sidebar header → `#/analytics`. ## How - Pure `decorateAnalyticsChannels(channels, hashByteToKeyName, labels)` — testable in isolation, sets `displayName` + `group` per row. - `buildHashKeyMap()` — async helper that resolves stored PSK keys to their channel hash bytes via `computeChannelHash`. Used at render time; first paint uses an empty map (best-effort) and re-renders once keys resolve. Graceful fallback when `ChannelDecrypt` is missing or there are no stored keys. - `channelTbodyHtml` gains an `opts.grouped` flag — opt-in so the existing flat sort still works for any other caller. - The analytics API endpoint is **unchanged** — this is purely frontend rendering. ## Tests `test-analytics-channels-integration.js` — 19 assertions covering decoration, grouping, sort order, and the channels-page link. Added to `test-all.sh`. Red commit: `5081b12` (12 assertion failures + stub). Green commit: `6be16d9` (all 19 pass). --------- Co-authored-by: bot <bot@corescope.local> Co-authored-by: meshcore-bot <bot@meshcore.local> |
||
|
|
a56ee5c4fe |
feat(analytics): selectable timeframes via ?window/?from/?to (#842) (#1018)
## Summary Selectable analytics timeframes (#842). Adds backend support for `?window=1h|24h|7d|30d` and `?from=&to=` on the three main analytics endpoints (`/api/analytics/rf`, `/api/analytics/topology`, `/api/analytics/channels`), and a time-window picker in the Analytics page UI that drives them. Default behavior with no query params is unchanged. ## TDD trail - Red: `bbab04d` — adds `TimeWindow` + `ParseTimeWindow` stub and tests; tests fail on assertions because the stub returns the zero window. - Green: `75d27f9` — implements `ParseTimeWindow`, threads `TimeWindow` through `compute*` loops + caches, wires HTTP handlers, adds frontend picker + E2E. ## Backend changes - `cmd/server/time_window.go` — full `ParseTimeWindow` (`?window=` aliases + `?from=/&to=` RFC3339 absolute range; invalid input → zero window for backwards compatibility). - `cmd/server/store.go` — new `GetAnalytics{RF,Topology,Channels}WithWindow` wrappers; `compute*` loops skip transmissions whose `FirstSeen` (or per-obs `Timestamp` for the region+observer slice) falls outside the window. Cache key composes `region|window` so different windows do not poison each other. - `cmd/server/routes.go` — handlers call `ParseTimeWindow(r)` and dispatch to the `*WithWindow` methods. ## Frontend changes - `public/analytics.js` — new `<select id="analyticsTimeWindow">` rendered under the region filter (All / 1h / 24h / 7d / 30d). Selecting an option triggers `loadAnalytics()` which appends `&window=…` to every analytics fetch. ## Tests - `cmd/server/time_window_test.go` — covers all aliases, absolute range, no-params backwards compatibility, `Includes()` bounds, and `CacheKey()` distinctness. - `cmd/server/topology_dedup_test.go`, `cmd/server/channel_analytics_test.go` — updated callers to pass `TimeWindow{}`. ## E2E (rule 18) `test-e2e-playwright.js:592-611` — opens `/#/analytics`, asserts the picker is rendered with a `24h` option, then asserts that selecting `24h` triggers a network request to `/api/analytics/rf?…window=24h`. ## Backwards compatibility No params → zero `TimeWindow` → original code paths (no filter, region-only cache key). Verified by `TestParseTimeWindow_NoParams_BackwardsCompatible` and by the existing analytics tests still passing unchanged on `_wt-fix-842`. Fixes #842 --------- Co-authored-by: you <you@example.com> Co-authored-by: corescope-bot <bot@corescope> |
||
|
|
736b09697d |
fix(analytics): apply customizer timestamp format to chart axes (closes #756) (#981)
## Summary Fixes #756 — the customizer timestamp format setting (ISO/ISO+ms/locale) and timezone (UTC/local) were not applied to chart X-axis labels, tooltips, or certain inline timestamps in the analytics pages. ## Changes ### `public/app.js` - Added `formatChartAxisLabel(date, shortForm)` — a shared helper that reads the customizer's `timestampFormat` and `timestampTimezone` preferences and formats dates for chart axes accordingly. `shortForm=true` returns time-only (for intra-day charts), `shortForm=false` returns date+time (for multi-day ranges). ### `public/analytics.js` - `rfXAxisLabels()`: now calls `formatChartAxisLabel()` instead of hardcoded `toLocaleTimeString()` - `rfTooltipCircles()`: tooltip timestamps now use `formatAbsoluteTimestamp()` instead of raw ISO - Subpath detail first/last seen: now uses `formatAbsoluteTimestamp()` - Neighbor graph last_seen: now uses `formatAbsoluteTimestamp()` ### `public/node-analytics.js` - Packet timeline chart labels: now use `formatChartAxisLabel()` (respects short vs long form based on time range) - SNR over time chart labels: now use `formatChartAxisLabel()` ## Behavior by setting | Setting | Chart axis (short) | Chart axis (long) | |---------|-------------------|-------------------| | ISO | `14:30` | `05-03 14:30` | | ISO+ms | `14:30:05` | `05-03 14:30:05` | | Locale | `2:30 PM` | `May 3, 2:30 PM` | All respect the UTC/local timezone toggle. ## Testing - Server builds cleanly (`go build`) - Served `app.js` contains `formatChartAxisLabel` (verified via curl) - Graceful fallback: all callsites check `typeof formatChartAxisLabel === 'function'` before calling, preserving backward compat if script load order changes --------- Co-authored-by: you <you@example.com> |
||
|
|
ea78581eea |
fix(#858): packets/hour chart — bars rendering + x-axis label decimation (#865)
Two bugs in the Overview tab Packets/Hour chart: 1. **Bars not rendering**: `barW` went negative when `data.length` was large (e.g. 720 hours for 30-day range), producing zero-width invisible bars. Fix: `Math.max(1, ...)` floor on bar width. 2. **X-axis labels overlapping**: Every single hour label was emitted (`02h03h04h...`). Fix: decimate labels based on time range — every 6h for ≤24h, every 12h for ≤72h, every 24h beyond. Shows `MM-DD` on midnight boundaries for multi-day ranges. **Scope**: Only touches the Overview tab `Packets / Hour` section and the shared `barChart` floor (one-line change). No modifications to Topology, Channels, Distance, or other tabs. Fixes #858 Co-authored-by: you <you@example.com> |
||
|
|
b5372d6f73 |
fix(#859): remove opacity gradient from Per-Observer Reachability rows (#863)
Fixes #859 ## What The "Per-Observer Reachability" and "Best Path to Each Node" sections in the Topology tab had inline `opacity` styles on each `.reach-ring` row that decreased with hop count (`1 - hops * 0.06`, floored at 0.3). This made text progressively darker/unreadable toward the bottom. ## Fix Removed the inline `opacity:${opacity}` style from both `renderPerObserverReach()` and `renderBestPath()`. The rows now render at full opacity with text colors governed by CSS variables as intended. ## Changed - `public/analytics.js`: removed opacity computation and inline style in two functions (4 lines removed, 2 added) ## Scope Only touches Per-Observer Reachability and Best Path rendering. No changes to Overview, Channels, or shared helpers. Co-authored-by: you <you@example.com> |
||
|
|
5afed0951b |
fix(#860): cap channel timeline chart to top 8 by volume (#864)
## What & Why The "Messages / Hour by Channel" chart on `/#/analytics` Channels tab rendered all channels in both the SVG and legend, causing legend overflow when 20+ channels are present. ## Fix - Sort channels by total message volume (descending) - Render only the top 8 in the chart and legend - Show "+N more" in the legend when channels are truncated - `maxCount` for Y-axis scaling is computed from visible channels only, so the chart uses its full vertical range Single-file change: `public/analytics.js` — only `renderChannelTimeline()` modified. No shared helpers touched. Fixes #860 Co-authored-by: you <you@example.com> |
||
|
|
441409203e |
feat(#845): bimodal_clock severity — surface flaky-RTC nodes instead of hiding as 'No Clock' (#850)
## Problem Nodes with flaky RTC (firmware emitting interleaved good and nonsense timestamps) were classified as `no_clock` because the broken samples poisoned the recent median. Operators lost visibility into these nodes — they showed "No Clock" even though ~60% of their adverts had valid timestamps. Observed on staging: a node with 31K samples where recent adverts interleave good skew (-6.8s, -13.6s) with firmware nonsense (-56M, -60M seconds). Under the old logic, median of the mixed window → `no_clock`. ## Solution New `bimodal_clock` severity tier that surfaces flaky-RTC nodes with their real (good-sample) skew value. ### Classification order (first match wins) | Severity | Good Fraction | Description | |----------|--------------|-------------| | `no_clock` | < 10% | Essentially no real clock | | `bimodal_clock` | 10–80% (and bad > 0) | Mixed good/bad — flaky RTC | | `ok`/`warn`/`critical`/`absurd` | ≥ 80% | Normal classification | "Good" = `|skew| <= 1 hour`; "bad" = likely uninitialized RTC nonsense. When `bimodal_clock`, `recentMedianSkewSec` is computed from **good samples only**, so the dashboard shows the real working-clock value (e.g. -7s) instead of the broken median. ### Backend changes - New constant `BimodalSkewThresholdSec = 3600` - New severity `bimodal_clock` in classification logic - New API fields: `goodFraction`, `recentBadSampleCount`, `recentSampleCount` ### Frontend changes - Amber `Bimodal` badge with tooltip showing bad-sample percentage - Bimodal nodes render skew value like ok/warn/severe (not the "No Clock" path) - Warning line below sparkline: "⚠️ X of last Y adverts had nonsense timestamps (likely RTC reset)" ### Tests - 3 new Go unit tests: bimodal (60% good → bimodal_clock), all-bad (→ no_clock), 90%-good (→ ok) - 1 new frontend test: bimodal badge rendering with tooltip - Existing `TestReporterScenario_789` passes unchanged Builds on #789 (recent-window severity). Closes #845 --------- Co-authored-by: you <you@example.com> |
||
|
|
a371d35bfd |
feat(#847): dedupe Top Longest Hops by pair + add obs count and SNR cues (#848)
## Problem The "Top 20 Longest Hops" RF analytics card shows the same repeater pair filling most slots because the query sorts raw hop records by distance with no pair deduplication. A single long link observed 12+ times dominates the leaderboard. ## Fix Dedupe by unordered `(pk1, pk2)` pair. Per pair, keep the max-distance record and compute reliability metrics: | Column | Description | |--------|-------------| | **Obs** | Total observations of this link | | **Best SNR** | Maximum SNR seen (dB) | | **Median SNR** | Median SNR across all observations (dB) | Tooltip on each row shows the timestamp of the best observation. ### Before | # | From | To | Distance | Type | SNR | Packet | |---|------|----|----------|------|-----|--------| | 1 | NodeX | NodeY | 200 mi | R↔R | 5 dB | abc… | | 2 | NodeX | NodeY | 199 mi | R↔R | 6 dB | def… | | 3 | NodeX | NodeY | 198 mi | R↔R | 4 dB | ghi… | ### After | # | From | To | Distance | Type | Obs | Best SNR | Median SNR | Packet | |---|------|----|----------|------|-----|----------|------------|--------| | 1 | NodeX | NodeY | 200 mi | R↔R | 12 | 8.0 dB | 5.2 dB | abc… | | 2 | NodeA | NodeB | 150 mi | C↔R | 3 | 6.5 dB | 6.5 dB | jkl… | ## Changes - **`cmd/server/store.go`**: Group `filteredHops` by unordered pair key, accumulate obs count / best SNR / median SNR per group, sort by max distance, take top 20 - **`cmd/server/types.go`**: Update `DistanceHop` struct — replace `SNR` with `BestSnr`, `MedianSnr`, add `ObsCount` - **`public/analytics.js`**: Replace single SNR column with Obs, Best SNR, Median SNR; add row tooltip with best observation timestamp - **`cmd/server/store_tophops_test.go`**: 3 unit tests — basic dedupe, reverse-pair merge, nil SNR edge case ## Test Coverage - `TestDedupeTopHopsByPair`: 5 records on pair (A,B) + 1 on (C,D) → 2 results, correct obsCount/dist/bestSnr/medianSnr - `TestDedupeTopHopsReversePairMerges`: (B,A) and (A,B) merge into one entry - `TestDedupeTopHopsNilSNR`: all-nil SNR records → bestSnr and medianSnr both nil - Existing `TestAnalyticsRFEndpoint` and `TestAnalyticsRFWithRegion` still pass Closes #847 --------- Co-authored-by: you <you@example.com> |
||
|
|
a0fddb50aa |
fix(#789): severity from recent samples; Theil-Sen drift with outlier rejection (#828)
Closes #789. ## The two bugs 1. **Severity from stale median.** `classifySkew(absMedian)` used the all-time `MedianSkewSec` over every advert ever recorded for the node. A repeater that was off for hours and then GPS-corrected stayed pinned to `absurd` because hundreds of historical bad samples poisoned the median. Reporter's case: `medianSkewSec: -59,063,561.8` while `lastSkewSec: -0.8` — current health was perfect, dashboard said catastrophic. 2. **Drift from a single correction jump.** Drift used OLS over every `(ts, skew)` pair, with no outlier rejection. A single GPS-correction event (skew jumps millions of seconds in ~30s) dominated the regression and produced `+1,793,549.9 s/day` — physically nonsense; the existing `maxReasonableDriftPerDay` cap then zeroed it (better than absurd, but still useless). ## The two fixes 1. **Recent-window severity.** New field `recentMedianSkewSec` = median over the last `N=5` samples or last `1h`, whichever is narrower (more current view). Severity now derives from `abs(recentMedianSkewSec)`. `MeanSkewSec`, `MedianSkewSec`, `LastSkewSec` are preserved unchanged so the frontend, fleet view, and any external consumers continue to work. 2. **Theil-Sen drift with outlier filter.** Drift now uses the Theil-Sen estimator (median of all pairwise slopes — textbook robust regression, ~29% breakdown point) on a series pre-filtered to drop samples whose skew jumps more than `maxPlausibleSkewJumpSec = 60s` from the previous accepted point. Real µC drift is fractions of a second per advert; clock corrections fall well outside. Capped at `theilSenMaxPoints = 200` (most-recent) so O(n²) stays bounded for chatty nodes. ## What stays the same - Epoch-0 / out-of-range advert filter (PR #769). - `minDriftSamples = 5` floor. - `maxReasonableDriftPerDay = 86400` hard backstop. - API shape: only additions (`recentMedianSkewSec`); no fields removed or renamed. ## Tests All in `cmd/server/clock_skew_test.go`: - `TestSeverityUsesRecentNotMedian` — 100 bad samples (-60s) + 5 good (-1s) → severity = `ok`, historical median still huge. - `TestDriftRejectsCorrectionJump` — 30 min of clean linear drift + one 1000s jump → drift small (~12 s/day). - `TestTheilSenMatchesOLSWhenClean` — clean linear data, Theil-Sen within ~1% of OLS. - `TestReporterScenario_789` — exact reproducer: 1662 samples, 1657 @ -683 days then 5 @ -1s → severity `ok`, `recentMedianSkewSec ≈ 0`, drift bounded; legacy `medianSkewSec` preserved as historical context. `go test ./... -count=1` (cmd/server) and `node test-frontend-helpers.js` both pass. --------- Co-authored-by: clawbot <bot@corescope.local> Co-authored-by: you <you@example.com> |
||
|
|
a9a18ff051 |
fix: neighbor graph slider persists to localStorage, default 0.7 (#776)
## Summary The neighbor graph min score slider didn't persist its value to localStorage, resetting to 0.10 on every page load. This was a poor default for most use cases. ## Changes - **Default changed from 0.10 to 0.70** — more useful starting point that filters out low-confidence edges - **localStorage persistence** — slider value saved on change, restored on page load - **3 new tests** in `test-frontend-helpers.js` verifying default value, load behavior, and save behavior ## Testing - `node test-frontend-helpers.js` — 547 passed, 0 failed - `node test-packet-filter.js` — 62 passed, 0 failed - `node test-aging.js` — 29 passed, 0 failed --------- Co-authored-by: you <you@example.com> |
||
|
|
ceea136e97 |
feat: observer graph representation (M1+M2) (#774)
## Summary Fixes #753 — Milestones M1 and M2: Observer nodes in the neighbor graph are now correctly labeled, colored, and filterable. ### M1: Label + color observers **Backend** (`cmd/server/neighbor_api.go`): - `buildNodeInfoMap()` now queries the `observers` table after building from `nodes` - Observer-only pubkeys (not already in the map as repeaters etc.) get `role: "observer"` and their name from the observers table - Observer-repeaters keep their repeater role (not overwritten) **Frontend**: - CSS variable `--role-observer: #8b5cf6` added to `:root` - `ROLE_COLORS.observer` was already defined in `roles.js` ### M2: Observer filter checkbox (default unchecked) **Frontend** (`public/analytics.js`): - Observer checkbox added to the role filter section, **unchecked by default** - Observers create hub-and-spoke patterns (one observer can have 100+ edges) that drown out the actual repeater topology — hiding them by default keeps the graph clean - Fixed `applyNGFilters()` which previously always showed observers regardless of checkbox state ### Tests - Backend: `TestBuildNodeInfoMap_ObserverEnrichment` — verifies observer-only pubkeys get name+role from observers table, and observer-repeaters keep their repeater role - All existing Go tests pass - All frontend helper tests pass (544/544) --------- Co-authored-by: you <you@example.com> |
||
|
|
ba7cd0fba7 |
fix: clock skew sanity checks — filter epoch-0, cap drift, min samples (#769)
Nodes with dead RTCs show -690d skew and -3 billion s/day drift. Fix: 1. **No Clock severity**: |skew| > 365d → `no_clock`, skip drift 2. **Drift cap**: |drift| > 86400 s/day → nil (physically impossible) 3. **Min samples**: < 5 samples → no drift regression 4. **Frontend**: 'No Clock' badge, '–' for unreliable drift Fixes the crazy stats on the Clock Health fleet view. --------- Co-authored-by: you <you@example.com> |
||
|
|
6a648dea11 |
fix: multi-byte adopters — all node types, role column, advert precedence (#754) (#767)
## Fix: Multi-Byte Adopters Table — Three Bugs (#754) ### Bug 1: Companions in "Unknown" `computeMultiByteCapability()` was repeater-only. Extended to classify **all node types** (companions, rooms, sensors). A companion advertising with 2-byte hash is now correctly "Confirmed". ### Bug 2: No Role Column Added a **Role** column to the merged Multi-Byte Hash Adopters table, color-coded using `ROLE_COLORS` from `roles.js`. Users can now distinguish repeaters from companions without clicking through to node detail. ### Bug 3: Data Source Disagreement When adopter data (from `computeAnalyticsHashSizes`) shows `hashSize >= 2` but capability only found path evidence ("Suspected"), the advert-based adopter data now takes precedence → "Confirmed". The adopter hash sizes are passed into `computeMultiByteCapability()` as an additional confirmed evidence source. ### Changes - `cmd/server/store.go`: Extended capability to all node types, accept adopter hash sizes, prioritize advert evidence - `public/analytics.js`: Added Role column with color-coded badges - `cmd/server/multibyte_capability_test.go`: 3 new tests (companion confirmed, role populated, adopter precedence) ### Tests - All 10 multi-byte capability tests pass - All 544 frontend helper tests pass - All 62 packet filter tests pass - All 29 aging tests pass --------- Co-authored-by: you <you@example.com> |
||
|
|
29157742eb |
feat: show collision details in Hash Usage Matrix for all hash sizes (#758)
## Summary Shows which prefixes are colliding in the Hash Usage Matrix, making the "PREFIX COLLISIONS: N" count actionable. Fixes #757 ## Changes ### Frontend (`public/analytics.js`) - **Clickable collision count**: When collisions > 0, the stat card is clickable and scrolls to the collision details section. Shows a `▼` indicator. - **3-byte collision table**: The collision risk section and `renderCollisionsFromServer` now render for all hash sizes including 3-byte (was previously hidden/skipped for 3-byte). - **Helpful hint**: 3-byte panel now says "See collision details below" when collisions exist. ### Backend (`cmd/server/collision_details_test.go`) - Test that collision details include correct prefix and node name/pubkey pairs - Test that collision details are empty when no collisions exist ### Frontend Tests (`test-frontend-helpers.js`) - Test clickable stat card renders `onclick` and `cursor:pointer` when collisions > 0 - Test non-clickable card when collisions = 0 - Test collision table renders correct node links (`#/nodes/{pubkey}`) - Test no-collision message renders correctly ## What was already there The backend already returned full collision details (prefix, nodes with pubkeys/names/coords, distance classification) in the `hash-collisions` API. The frontend already had `renderCollisionsFromServer` rendering a rich table with node links. The gap was: 1. The 3-byte tab hid the collision risk section entirely 2. No visual affordance to navigate from the stat count to the details ## Perf justification No new computation — collision data was already computed and returned by the API. The only change is rendering it for 3-byte (same as 1-byte/2-byte). The collision list is already limited by the backend sort+slice pattern. --------- Co-authored-by: you <you@example.com> |
||
|
|
3bdf72b4cf |
feat: clock skew UI — node badges, detail sparkline, fleet analytics (#690 M2+M3) (#752)
## Summary Frontend visualizations for clock skew detection. Implements #690 M2 and M3. Does NOT close #690 — M4+M5 remain. ### M2: Node badges + detail sparkline - Severity badges (⏰ green/yellow/orange/red) on node list next to each node - Node detail: Clock Skew section with current value, severity, drift rate - Inline SVG sparkline showing skew history, color-coded by severity zones ### M3: Fleet analytics view - 'Clock Health' section on Analytics page - Sortable table: Name | Skew | Severity | Drift | Last Advert - Filter buttons by severity (OK/Warning/Critical/Absurd) - Summary stats: X nodes OK, Y warning, Z critical - Color-coded rows ### Changes - `public/nodes.js` — badge rendering + detail section - `public/analytics.js` — fleet clock health view - `public/roles.js` — severity color helpers - `public/style.css` — badge + sparkline + fleet table styles - `cmd/server/clock_skew.go` — added fleet summary endpoint - `cmd/server/routes.go` — wired fleet endpoint - `test-frontend-helpers.js` — 11 new tests --------- Co-authored-by: you <you@example.com> |
||
|
|
45623672d9 |
fix: integrate multi-byte capability into adopters table, fix filter buttons (#712) (#713)
## Summary Fixes #712 — Multi-byte capability filter buttons broken + needs integration with Hash Adopters. ### Changes **M1: Fix filter buttons breaking after first click** - Root cause: `section.replaceWith(newSection)` replaced the entire DOM node, but the event listener was attached to the old node. After replacement, clicks went unhandled. - Fix: Instead of replacing the whole section, only swap the table content inside a stable `#mbAdoptersTableWrap` div. The event listener on `#mbAdoptersSection` persists across filter changes. - Button active state is now toggled via `classList.toggle` instead of full DOM rebuild. **M2: Better button labels** - Changed from icon-only (`✅ 76`) to descriptive labels: `✅ Confirmed (76)`, `⚠️ Suspected (81)`, `❓ Unknown (223)` **M3: Integrate with Multi-Byte Hash Adopters** - Merged capability status into the existing adopters table as a new "Status" column - Removed the separate "Repeater Multi-Byte Capability" section - Filter buttons now apply to the integrated table - Nodes without capability data default to ❓ Unknown - Capability data is looked up by pubkey from the existing `multiByteCapability` API response (no backend changes needed) ### Performance - No new API calls — capability data already exists in the hash sizes response - Filter toggle is O(n) where n = number of adopter nodes (typically <500) - Event delegation on stable parent — no listener re-attachment needed ### Tests - Updated existing `renderMultiByteCapability` tests for new label format - Added 5 new tests for `renderMultiByteAdopters`: empty state, status integration, text labels with counts, unknown default, Status column presence - All 507 frontend tests pass, all Go tests pass Co-authored-by: you <you@example.com> |
||
|
|
ef8bce5002 |
feat: repeater multi-byte capability inference table (#706)
## Summary Adds a new "Repeater Multi-Byte Capability" section to the Hash Stats analytics tab that classifies each repeater's ability to handle multi-byte hash prefixes (firmware >= v1.14). Fixes #689 ## What Changed ### Backend (`cmd/server/store.go`) - New `computeMultiByteCapability()` method that infers capability for each repeater using two evidence sources: - **Confirmed** (100% reliable): node has advertised with `hash_size >= 2`, leveraging existing `computeNodeHashSizeInfo()` data - **Suspected** (<100%): node's prefix appears as a hop in packets with multi-byte path headers, using the `byPathHop` index. Prefix collisions mean this isn't definitive. - **Unknown**: no multi-byte evidence — could be pre-1.14 or 1.14+ with default settings - Extended `/api/analytics/hash-sizes` response with `multiByteCapability` array ### Frontend (`public/analytics.js`) - New `renderMultiByteCapability()` function on the Hash Stats tab - Color-coded table: green confirmed, yellow suspected, gray unknown - Filter buttons to show all/confirmed/suspected/unknown - Column sorting by name, role, status, evidence, max hash size, last seen - Clickable rows link to node detail pages ### Tests (`cmd/server/multibyte_capability_test.go`) - `TestMultiByteCapability_Confirmed`: advert with hash_size=2 → confirmed - `TestMultiByteCapability_Suspected`: path appearance only → suspected - `TestMultiByteCapability_Unknown`: 1-byte advert only → unknown - `TestMultiByteCapability_PrefixCollision`: two nodes sharing prefix, one confirmed via advert, other correctly marked suspected (not confirmed) ## Performance - `computeMultiByteCapability()` runs once per cache cycle (15s TTL via hash-sizes cache) - Leverages existing `GetNodeHashSizeInfo()` cache (also 15s TTL) — no redundant advert scanning - Path hop scan is O(repeaters × prefix lengths) lookups in the `byPathHop` map, with early break on first match per prefix - Only computed for global (non-regional) requests to avoid unnecessary work --------- Co-authored-by: you <you@example.com> |
||
|
|
e0e9aaa324 |
feat: noise floor column chart with color-coded thresholds (#659)
## Noise Floor: Line Chart → Color-Coded Column Chart Implements M3a from the [RF Health Dashboard spec](https://github.com/Kpa-clawbot/CoreScope/issues/600#issuecomment-2784399622) — replacing the noise floor line chart with discrete color-coded columns. ### What changed **`public/analytics.js`** — replaced `rfNFLineChart()` with `rfNFColumnChart()`: - **Color-coded bars by threshold**: green (`< -100 dBm`), yellow (`-100 to -85 dBm`), red (`≥ -85 dBm`) - **Instant hover tooltips**: exact dBm value + UTC timestamp via native SVG `<title>` — no delay - **Column highlighting on hover**: CSS `:hover` with opacity change + border stroke - **Inline legend**: green/yellow/red threshold key in chart header - **Removed reference lines**: the `-100 warning` and `-85 critical` dashed lines are eliminated — threshold info is now encoded directly in bar color (data-ink ratio improvement) - **No gap detection**: column charts render discrete bars — each data point is an independent observation, so line-chart-style gap detection doesn't apply. Every sample gets a bar. - **Reboot markers**: vertical dashed lines with "reboot" labels at reboot timestamps (shared `rfRebootMarkers` helper, same as other RF charts) - **Division-by-zero guard**: constant values or single data points use a ±5 dBm window so bars render with visible height - **Sparklines unchanged**: fleet overview sparklines remain as polylines (correct at 140×24px scale) ### Why columns instead of lines A polyline connecting discrete 5-minute noise floor samples creates false visual continuity — it implies interpolation between measurements that doesn't exist. When readings jump between -115 and -95 irregularly, the line becomes a jagged mess. Column bars encode each sample as a discrete, independent observation: one bar = one measurement. ### Testing - 12 unit tests in `test-frontend-helpers.js` covering: SVG output, threshold color coding, tooltips, empty/single/constant data, legend rendering, reboot markers, shared time axis - All existing tests pass (packet-filter: 62, aging: 29, frontend-helpers: 490) ### No backend changes Pure frontend change — ~150 lines in `analytics.js`. Fixes #600 --------- Co-authored-by: you <you@example.com> |
||
|
|
bd54707987 |
feat: distance unit preference — km, mi, or auto (#621) (#646)
## Summary - **`app.js`**: `getDistanceUnit()`, `formatDistance(km)`, `formatDistanceRound(km)` helpers. Auto mode uses `navigator.language` — miles for `en-US`, `en-GB`, `my`, `lr`; km everywhere else. - **`customize-v2.js`**: Distance Unit preference (km / mi / auto) in Display Settings panel. Stored in `localStorage['meshcore-distance-unit']` via the existing apply pipeline. Override dot and reset work. Display tab badge counts it. - **`nodes.js`**: Neighbor table distance cell uses `formatDistance()`. - **`analytics.js`**: All rendered km values use `formatDistance()` or `formatDistanceRound()`. Column headers (`km`/`mi`) respond to the active unit. Collision classification thresholds (Local < 50 km / Regional 50–200 km / Distant > 200 km) also adapt. Default is `auto` — no change for existing users unless their locale maps to miles. ## Test plan - [x] `node test-frontend-helpers.js` — 456 passed, 0 failed (10 new formatDistance tests) - [ ] Set unit to **mi** in customize → Neighbors table shows `7.6 mi` instead of `12.3 km` - [ ] Analytics → Distance tab → stat cards, leaderboard, and column headers all show miles - [ ] Collision tool → Local/Regional/Distant thresholds show `31 mi` / `124 mi` - [ ] Route patterns popup shows miles per hop and total - [ ] Reset override dot → unit returns to auto Closes #621 🤖 Generated with [Claude Code](https://claude.ai/claude-code) --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: you <you@example.com> |
||
|
|
37be3dcd1f |
fix: Prefix Tool text consistency — use 'repeaters' everywhere (#642) (#645)
## Summary Fixes remaining text inconsistencies in the Prefix Tool after #643 added the repeater filter. The Torvalds review on #643 flagged: 1. **Must-fix (already addressed in #643):** "About these numbers" text — fixed 2. **Out-of-scope:** Empty state says "No nodes" should say "No repeaters" This PR fixes ALL remaining "nodes" references in the Prefix Tool to say "repeaters": - Empty state: "No nodes in the network yet" → "No repeaters in the network yet" - Stat card label: "Total nodes" → "Total repeaters" - Region note link: "Check all nodes →" → "Check all repeaters →" - Recommendation text: "With N nodes" → "With N repeaters" Verified: zero occurrences of stale "all nodes", "Total nodes", or "No nodes" remain in the Prefix Tool section. Closes #642 Co-authored-by: you <you@example.com> |
||
|
|
dc079064f5 |
fix: clarify Hash Issues vs Prefix Tool collision data discrepancy (#643)
## Summary Hash Issues and Prefix Tool tabs showed different collision counts because the Prefix Tool was including all node types (companions, rooms, sensors) while Hash Issues correctly filtered to repeaters only. **Only repeaters matter for prefix collisions** — they're the nodes that relay packets using hash-based addressing. Non-repeater collisions are harmless noise. ## Changes 1. **Filtered Prefix Tool to repeaters only** — matches Hash Issues' scope 2. **Updated explanatory text** — both tabs now clearly state they cover repeaters 3. **Added cross-reference links** between the two tabs 4. **Added hash_size badges** in Prefix Tool results Both tabs should now agree on collision counts for each byte size. ## Review Status - ✅ Self-review - ✅ Torvalds review — caught stale 'regardless of role' text, fixed - ✅ All tests pass Fixes #642 --------- Co-authored-by: you <you@example.com> |
||
|
|
43098a0705 |
refactor: DRY hash matrix rendering in analytics.js (#419) (#640)
## Summary Fixes #419 — DRY violation in `renderHashMatrixFromServer` in analytics.js. The 1-byte and 2-byte branches shared ~80% identical HTML structure (stat cards, matrix grid, detail panel, legend, tooltip init, click handlers). This refactor extracts four shared helpers: ### New helpers | Helper | Purpose | |--------|---------| | `classifyHashCell(count, isConfirmed, isPossible)` | Unified cell classification → `{cls, bg}` | | `hashCellTd(hex, cellSize, cls, bg, count, tipHtml, fontWeight)` | Shared `<td>` element generation | | `hashTooltipHtml(hexLabel, statusText, nodesHtml)` | Tooltip HTML assembly | | `renderHashMatrixPanel(el, statCards, cellFn, detailWidth, legend, clickFn)` | Full matrix assembly pipeline | ### What changed - Both branches now call `renderHashMatrixPanel()` with branch-specific callbacks for cell rendering and detail click handling - Cell classification logic (empty → taken → possible → collision with heat scaling) is unified in `classifyHashCell()` - Tooltip and `<td>` generation consolidated — no more duplicated inline template strings - Zero behavioral changes — all existing rendering, tooltips, and click interactions are preserved ### Tests All existing tests pass (445 frontend helpers, 62 packet filter, 29 aging). Co-authored-by: you <you@example.com> |
||
|
|
e046a6f632 |
fix: mobile accessibility — touch targets, ARIA, small viewport support (#630) (#633)
## Summary Fixes critical and major mobile accessibility items from #630, focused on small phone viewports (320px–375px). ### Critical fixes 1. **Touch targets ≥ 44px** — All interactive elements (filter buttons, tab buttons, search inputs, nav buttons, region pills, dropdowns) get `min-height: 44px; min-width: 44px` via `@media (pointer: coarse)` — desktop/mouse users are unaffected. 2. **ARIA live regions** — Added `aria-live="polite"` to: packet list (`#pktLeft`), node list (`#nodesLeft`), analytics content (`#analyticsContent`), live feed (`#liveFeed` with `role="log"`). Screen readers now announce dynamic content updates. 3. **Color-only status indicators** — Status dots in live view marked `aria-hidden="true"` (text labels like "Online"/"Degraded"/"Offline" already present alongside). 4. **Detail panel on mobile** — Side panel (`panel-right`) renders as a full-screen fixed overlay on ≤640px. Close button (✕) added to nodes detail panel. Escape key closes both nodes and packets detail panels. ### Major fixes 5. **Analytics tabs overflow** — Tabs switch to `flex-wrap: nowrap; overflow-x: auto` on ≤640px, preventing overflow on 320px screens. 6. **Table horizontal scroll** — Added `.table-scroll-wrap` class and `min-width: 480px` on `.data-table` at ≤640px for horizontal scrolling when columns don't fit. 7. **SPA focus management** — On every page navigation, focus moves to first heading (`h1`/`h2`/`h3`) or falls back to `#app`. Uses `requestAnimationFrame` for correct DOM timing. ### Bonus - Analytics tabs get `role="tablist"` + `aria-label` for screen reader semantics. ### Known follow-ups (not blocking) - Individual tab buttons should get `role="tab"` + `aria-selected` + `aria-controls` for complete ARIA tab pattern. - `sr-status-label` and `table-scroll-wrap` CSS classes are defined but not yet used in JS — ready for future use when status text labels and table wrappers are wired up. Closes #630 Co-authored-by: you <you@example.com> |
||
|
|
f7000992ca |
fix(rf-health): auto-scale airtime Y-axis + hover tooltips (#600) (#623)
## Summary Addresses user feedback on #600 — two improvements to RF Health detail panel charts: ### 1. Auto-scale airtime Y-axis Previously fixed 0-100% which made low-activity nodes unreadable (e.g. 0.1% TX barely visible). Now auto-scales to the actual data range with 20% headroom (minimum 1%), matching how the noise floor chart already works. ### 2. Hover tooltips on all chart data points Invisible SVG `<circle>` elements with native `<title>` tooltips on every data point across all 4 charts: - **Noise floor**: `NF: -112.3 dBm` + UTC timestamp - **Airtime**: `TX: 2.1%` or `RX: 8.3%` + UTC timestamp - **Error rate**: `Err: 0.05%` + UTC timestamp - **Battery**: `Batt: 3.85V` + UTC timestamp Uses native browser SVG tooltips — zero dependencies, accessible, no JS event handlers. ### Design rationale (Tufte) - Auto-scaling increases data-ink ratio by eliminating wasted vertical space - Tooltips provide detail-on-demand without cluttering the chart with labels on every point ### Spec update Added M2 feedback improvements section to `docs/specs/rf-health-dashboard.md`. --------- Co-authored-by: you <you@example.com> |
||
|
|
596ccf2322 |
fix(rf-health): offset TX/RX airtime labels when overlapping
When TX and RX values are within 12px, TX label shifts up and RX shifts down to avoid rendering on top of each other. |
||
|
|
232770a858 |
feat(rf-health): M2 — airtime, error rate, battery charts with delta computation (#605)
## M2: Airtime + Channel Quality + Battery Charts Implements M2 of #600 — server-side delta computation and three new charts in the RF Health detail view. ### Backend Changes **Delta computation** for cumulative counters (`tx_air_secs`, `rx_air_secs`, `recv_errors`): - Computes per-interval deltas between consecutive samples - **Reboot handling:** detects counter reset (current < previous), skips that delta, records reboot timestamp - **Gap handling:** if time between samples > 2× interval, inserts null (no interpolation) - Returns `tx_airtime_pct` and `rx_airtime_pct` as percentages (delta_secs / interval_secs × 100) - Returns `recv_error_rate` as delta_errors / (delta_recv + delta_errors) × 100 **`resolution` query param** on `/api/observers/{id}/metrics`: - `5m` (default) — raw samples - `1h` — hourly aggregates (GROUP BY hour with AVG/MAX) - `1d` — daily aggregates **Schema additions:** - `packets_sent` and `packets_recv` columns added to `observer_metrics` (migration) - Ingestor parses these fields from MQTT stats messages **API response** now includes: - `tx_airtime_pct`, `rx_airtime_pct`, `recv_error_rate` (computed deltas) - `reboots` array with timestamps of detected reboots - `is_reboot_sample` flag on affected samples ### Frontend Changes Three new charts in the RF Health detail view, stacked vertically below noise floor: 1. **Airtime chart** — TX (red) + RX (blue) as separate SVG lines, Y-axis 0-100%, direct labels at endpoints 2. **Error Rate chart** — `recv_error_rate` line, shown only when data exists 3. **Battery chart** — voltage line with 3.3V low reference, shown only when battery_mv > 0 All charts: - Share X-axis and time range (aligned vertically) - Reboot markers as vertical hairlines spanning all charts - Direct labels on data (no legends) - Resolution auto-selected: `1h` for 7d/30d ranges - Charts hidden when no data exists ### Tests - `TestComputeDeltas`: normal deltas, reboot detection, gap detection - `TestGetObserverMetricsResolution`: 5m/1h/1d downsampling verification - Updated `TestGetObserverMetrics` for new API signature --------- Co-authored-by: you <you@example.com> |
||
|
|
747aea37b7 |
fix(rf-health): add region filter support to metrics summary
Frontend passes RegionFilter query string to summary API. Backend filters results by observer IATA region. Added iata field to MetricsSummaryRow. |
||
|
|
968c104e14 |
feat(rf-health): show observer detail in side panel instead of page bottom
- Change RF Health detail view from bottom-of-page to a right-sliding side panel - Grid stays visible and stable when detail is open (no layout shift) - Click another observer updates panel in place; close button (×) dismisses - On mobile (<640px): panel stacks below grid at full width - Filter out observers with insufficient data (<2 sparkline points) from grid entirely - Follows the same split-layout pattern used by the nodes page |
||
|
|
6f35d4d417 |
feat: RF Health Dashboard M1 — observer metrics + small multiples grid (#604)
## RF Health Dashboard — M1: Observer Metrics Storage, API & Small Multiples Grid Implements M1 of #600. ### What this does Adds a complete RF health monitoring pipeline: MQTT stats ingestion → SQLite storage → REST API → interactive dashboard with small multiples grid. ### Backend Changes **Ingestor (`cmd/ingestor/`)** - New `observer_metrics` table via migration system (`_migrations` pattern) - Parse `tx_air_secs`, `rx_air_secs`, `recv_errors` from MQTT status messages (same pattern as existing `noise_floor` and `battery_mv`) - `INSERT OR REPLACE` with timestamps rounded to nearest 5-min interval boundary (using ingestor wall clock, not observer timestamps) - Missing fields stored as NULLs — partial data is always better than no data - Configurable retention pruning: `retention.metricsDays` (default 30), runs on startup + every 24h **Server (`cmd/server/`)** - `GET /api/observers/{id}/metrics?since=...&until=...` — per-observer time-series data - `GET /api/observers/metrics/summary?window=24h` — fleet summary with current NF, avg/max NF, sample count - `parseWindowDuration()` supports `1h`, `24h`, `3d`, `7d`, `30d` etc. - Server-side metrics retention pruning (same config, staggered 2min after packet prune) ### Frontend Changes **RF Health tab (`public/analytics.js`, `public/style.css`)** - Small multiples grid showing all observers simultaneously — anomalies pop out visually - Per-observer cell: name, current NF value, battery voltage, sparkline, avg/max stats - NF status coloring: warning (amber) at ≥-100 dBm, critical (red) at ≥-85 dBm — text color only, no background fills - Click any cell → expanded detail view with full noise floor line chart - Reference lines with direct text labels (`-100 warning`, `-85 critical`) — not color bands - Min/max points labeled directly on the chart - Time range selector: preset buttons (1h/3h/6h/12h/24h/3d/7d/30d) + custom from/to datetime picker - Deep linking: `#/analytics?tab=rf-health&observer=...&range=...` - All charts use SVG, matching existing analytics.js patterns - Responsive: 3-4 columns on desktop, 1 on mobile ### Design Decisions (from spec) - Labels directly on data, not in legends - Reference lines with text labels, not color bands - Small multiples grid, not card+accordion (Tufte: instant visual fleet comparison) - Ingestor wall clock for all timestamps (observer clocks may drift) ### Tests Added **Ingestor tests:** - `TestRoundToInterval` — 5 cases for rounding to 5-min boundaries - `TestInsertMetrics` — basic insertion with all fields - `TestInsertMetricsIdempotent` — INSERT OR REPLACE deduplication - `TestInsertMetricsNullFields` — partial data with NULLs - `TestPruneOldMetrics` — retention pruning - `TestExtractObserverMetaNewFields` — parsing tx_air_secs, rx_air_secs, recv_errors **Server tests:** - `TestGetObserverMetrics` — time-series query with since/until filters, NULL handling - `TestGetMetricsSummary` — fleet summary aggregation - `TestObserverMetricsAPIEndpoints` — DB query verification - `TestMetricsAPIEndpoints` — HTTP endpoint response shape - `TestParseWindowDuration` — duration parsing for h/d formats ### Test Results ``` cd cmd/ingestor && go test ./... → PASS (26s) cd cmd/server && go test ./... → PASS (5s) ``` ### What's NOT in this PR (deferred to M2+) - Server-side delta computation for cumulative counters - Airtime charts (TX/RX percentage lines) - Channel quality chart (recv_error_rate) - Battery voltage chart - Reboot detection and chart annotations - Resolution downsampling (1h, 1d aggregates) - Pattern detection / automated diagnosis --------- Co-authored-by: you <you@example.com> |
||
|
|
1fbdd1c3d3 |
feat: Prefix Tool tab on Analytics page (#347) (#599)
## Summary - Adds a new **Prefix Tool** tab to the Analytics page (alongside Hash Stats / Hash Issues) - **Network Overview**: per-tier collision stats (1/2/3-byte) and a network-size-based recommendation — collapsible, folded by default - **Prefix Checker**: accepts a 1/2/3-byte hex prefix or full public key; shows colliding nodes at each tier with severity badges (✅ / ⚠️ / 🔴); clicking a node navigates to its detail page - **Prefix Generator**: picks a random collision-free prefix at the chosen hash size; links to [meshcore-web-keygen](https://agessaman.github.io/meshcore-web-keygen/) with the prefix pre-filled - **Hash Issues tab**: adds a "🔎 Check a prefix →" shortcut in the nav - **Deep-link support**: `#/analytics?tab=prefix-tool&prefix=A3F1` pre-fills and runs the checker; `?generate=2` pre-selects and runs the generator - **No new API endpoints** — 100% client-side using the existing `/nodes` list ## Verification Live on staging: **https://staging.on8ar.eu/#/analytics?tab=prefix-tool** ## Test plan - [x] Network Overview card is collapsed by default; expands on click; stats are correct - [x] Prefix Checker: 2-char input shows 1-byte results; 4-char shows 2-byte; 6-char shows 3-byte; 64-char pubkey shows all three tiers - [x] Prefix Checker: invalid hex shows error; odd-length input shows error - [x] Prefix Generator: Generate picks an unused prefix; "Try another" cycles; keygen link opens with prefix pre-filled - [x] Deep link `?prefix=A3F1` pre-fills checker and scrolls to it - [x] Deep link `?generate=2` pre-selects 2-byte and runs generator - [x] Hash Issues tab shows "🔎 Check a prefix →" in the nav - [x] FAQ link at bottom of generator opens correct MeshCore docs anchor 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
790a713ba9 |
perf: combine 4 subpath API calls into single bulk endpoint (#587)
## Summary
Consolidates the 4 parallel `/api/analytics/subpaths` calls in the Route
Patterns tab into a single `/api/analytics/subpaths-bulk` endpoint,
eliminating 3 redundant server-side scans of the subpath index on cache
miss.
## Changes
### Backend (`cmd/server/routes.go`, `cmd/server/store.go`)
- New `GET
/api/analytics/subpaths-bulk?groups=2-2:50,3-3:30,4-4:20,5-8:15`
endpoint
- Groups format: `minLen-maxLen:limit` comma-separated
- `GetAnalyticsSubpathsBulk()` iterates `spIndex` once, bucketing
entries into per-group accumulators by hop length
- Hop name resolution is done once per raw hop and shared across groups
- Results are cached per-group for compatibility with existing
single-key cache lookups
- Region-filtered queries fall back to individual
`GetAnalyticsSubpaths()` calls (region filtering requires
per-transmission observer checks)
### Frontend (`public/analytics.js`)
- `renderSubpaths()` now makes 1 API call instead of 4
- Response shape: `{ results: [{ subpaths, totalPaths }, ...] }` —
destructured into the same `[d2, d3, d4, d5]` variables
### Tests (`cmd/server/routes_test.go`)
- `TestAnalyticsSubpathsBulk`: validates 3-group response shape, missing
params error, invalid format error
## Performance
- **Before:** 4 API calls → 4 scans of `spIndex` + 4× hop resolution on
cache miss
- **After:** 1 API call → 1 scan of `spIndex` + 1× hop resolution
(shared cache)
- Cache miss cost reduced by ~75% for this tab
- No change on cache hit (individual group caching still works)
Fixes #398
Co-authored-by: you <you@example.com>
|
||
|
|
87ac61748c |
perf(analytics): compute network status client-side, eliminate redundant API call (#583)
## Summary Reduces the analytics nodes tab from 3 parallel API calls to 2 by computing network status (active/degraded/silent counts) client-side instead of fetching from `/nodes/network-status`. ## What Changed **`public/analytics.js` — `renderNodesTab()`:** - Removed the `/nodes/network-status` API call from the `Promise.all` batch - Added client-side computation of active/degraded/silent counts using the shared `getHealthThresholds()` function from `roles.js` - Uses `nodesResp.total` and `nodesResp.counts` (already returned by `/nodes` endpoint) for total node count and role breakdown ## Why This Works The `/nodes` response already includes: - `total` — count of all matching nodes (server-computed across full DB) - `counts` — role counts across all nodes (from `GetAllRoleCounts()`) - Per-node `last_seen`/`last_heard` timestamps The `getHealthThresholds()` function in `roles.js` provides the same degraded/silent thresholds used server-side, so client-side status computation produces equivalent results for the loaded node set. ## Performance - **Before:** 3 parallel API calls (`/nodes`, `/nodes/bulk-health`, `/nodes/network-status`) - **After:** 2 parallel API calls (`/nodes`, `/nodes/bulk-health`) - Network status computation is O(n) over the 200 loaded nodes — negligible client-side cost - The `/nodes/network-status` endpoint scanned ALL nodes in the DB on every call; this eliminates that server-side work entirely ## Testing - All frontend helper tests pass (445/445) - All packet filter tests pass (62/62) - All aging tests pass (29/29) - All Go backend tests pass Fixes #392 --------- Co-authored-by: you <you@example.com> |
||
|
|
aac038abb9 |
fix: filter inconsistent hash sizes by role and add 7-day time window (#567)
## Summary Fixes #566 — The "Inconsistent Hash Sizes" list on the Analytics page included all node types and had no time window, causing false positives. ## Changes ### 1. Role filter on inconsistent nodes (`cmd/server/store.go`) Added role filter to the `inconsistentNodes` loop in `computeHashCollisions()` so only repeaters and room servers are included. Companions are excluded since they were never affected by the firmware bug. This matches the existing role filter on collision bucketing from #441. ```go // Before: if cn.HashSizeInconsistent { // After: if cn.HashSizeInconsistent && (cn.Role == "repeater" || cn.Role == "room_server") { ``` ### 2. 7-day time window on hash size computation (`cmd/server/store.go`) Added a 7-day recency cutoff to `computeNodeHashSizeInfo()`. Adverts older than 7 days are now skipped, preventing legitimate historical config changes (e.g., testing different byte sizes) from creating permanent false positives. ### 3. Frontend description text (`public/analytics.js`) Updated the description to reflect the filtered scope: now says "Repeaters and room servers" instead of "Nodes", mentions the 7-day window, and notes that companions are excluded. ## Tests - `TestInconsistentNodesExcludesCompanions` — verifies companions are excluded while repeaters and room servers are included - `TestHashSizeInfoTimeWindow` — verifies adverts older than 7 days are excluded from hash size computation - Updated existing hash size tests to use recent timestamps (compatible with the new time window) - All existing tests pass: `cmd/server` ✅, `cmd/ingestor` ✅ ## Perf justification The time window filter adds a single string comparison per advert in the scan loop — O(n) with a tiny constant. No impact on hot paths. --------- Co-authored-by: you <you@example.com> |
||
|
|
0e1beac52f |
fix: neighbor affinity graph empty results + performance + accessibility (#523) (#524)
## Summary Fixes the neighbor affinity graph returning empty results despite abundant ADVERT data in the store. **Root cause:** `extractFromNode()` in `neighbor_graph.go` only checked for `"from_node"` and `"from"` fields in the decoded JSON, but real ADVERT packets store the originator public key as `"pubKey"`. This meant `fromNode` was always empty, so: - Zero-hop edges (originator↔observer) were never created - Originator↔path[0] edges were never created - Only observer↔path[last] edges could be created (and only for non-empty paths) **Fix:** Check `"pubKey"` first in `extractFromNode()`, then fall through to `"from_node"` and `"from"` for other packet types. ## Bugs Fixed | Bug | Issue | Fix | |-----|-------|-----| | Empty graph results | #522 | `extractFromNode()` now reads `pubKey` field from ADVERTs | | 3-4s response time | #523 comment | Graph was rebuilding correctly with 60s TTL cache — the slow response was due to iterating all packets finding zero matches. With edges now being found, the cache works as designed. | | Incomplete visualization | #523 comment | Downstream of bug 1+2 — fixed by fixing the builder | | Accessibility | #523 comment | Added text-based neighbor list, dynamic aria-label, keyboard focus CSS, dashed lines for ambiguous edges, confidence symbols | ## Changes - **`cmd/server/neighbor_graph.go`** — Fixed `extractFromNode()` to check `pubKey` field (real ADVERT format) - **`cmd/server/neighbor_graph_test.go`** — Added 2 new tests: `TestBuildNeighborGraph_AdvertPubKeyField` (real ADVERT format) and `TestBuildNeighborGraph_OneByteHashPrefixes` (1-byte prefix collision scenario) - **`public/analytics.js`** — Added accessible text-based neighbor list, dynamic aria-label, dashed line pattern for ambiguous edges - **`public/style.css`** — Added `:focus-visible` keyboard focus indicator for canvas ## Testing All Go tests pass (`go test ./... -count=1`). New tests verify the fix prevents regression. Fixes #523, Fixes #522 --------- Co-authored-by: you <you@example.com> |
||
|
|
58f791266d |
feat: affinity debugging tools (#482) — milestone 6 (#521)
## Summary Milestone 6 of #482: Observability & Debugging tools for the neighbor affinity system. These tools exist because someone will need them at 3 AM when "Show Neighbors is showing the wrong node for C0DE" and they have 5 minutes to diagnose it. ## Changes ### 1. Debug API — `GET /api/debug/affinity` - Full graph state dump: all edges with weights, observation counts, last-seen timestamps - Per-prefix resolution log with disambiguation reasoning (Jaccard scores, ratios, thresholds) - Query params: `?prefix=C0DE` filter to specific prefix, `?node=<pubkey>` for specific node's edges - Protected by API key (same auth as `/api/admin/prune`) - Response includes: edge count, node count, cache age, last rebuild time ### 2. Debug Overlay on Map - Toggle-able checkbox "🔍 Affinity Debug" in map controls - Draws lines between nodes showing affinity edges with color coding: - Green = high confidence (score ≥ 0.6) - Yellow = medium (0.3–0.6) - Red = ambiguous (< 0.3) - Line thickness proportional to weight, dashed for ambiguous - Unresolved prefixes shown as ❓ markers - Click edge → popup with observation count, last seen, score, observers - Hidden behind `debugAffinity` config flag or `localStorage.setItem('meshcore-affinity-debug', 'true')` ### 3. Per-Node Debug Panel - Expandable "🔍 Affinity Debug" section in node detail page (collapsed by default) - Shows: neighbor edges table with scores, prefix resolutions with reasoning trace - Candidates table with Jaccard scores, highlighting the chosen candidate - Graph-level stats summary ### 4. Server-Side Structured Logging - Integrated into `disambiguate()` — logs every resolution decision during graph build - Format: `[affinity] resolve C0DE: c0dedad4 score=47 Jaccard=0.82 vs c0dedad9 score=3 Jaccard=0.11 → neighbor_affinity (ratio 15.7×)` - Logs ambiguous decisions: `scores too close (12 vs 9, ratio 1.3×) → ambiguous` - Gated by `debugAffinity` config flag ### 5. Dashboard Stats Widget - Added to analytics overview tab when debug mode is enabled - Metrics: total edges/nodes, resolved/ambiguous counts (%), avg confidence, cold-start coverage, cache age, last rebuild ## Files Changed - `cmd/server/neighbor_debug.go` — new: debug API handler, resolution builder, cold-start coverage - `cmd/server/neighbor_debug_test.go` — new: 7 tests for debug API - `cmd/server/neighbor_graph.go` — added structured logging to disambiguate(), `logFn` field, `BuildFromStoreWithLog` - `cmd/server/neighbor_api.go` — pass debug flag through `BuildFromStoreWithLog` - `cmd/server/config.go` — added `DebugAffinity` config field - `cmd/server/routes.go` — registered `/api/debug/affinity` route, exposed `debugAffinity` in client config - `cmd/server/types.go` — added `DebugAffinity` to `ClientConfigResponse` - `public/map.js` — affinity debug overlay layer with edge visualization - `public/nodes.js` — per-node affinity debug panel - `public/analytics.js` — dashboard stats widget - `test-e2e-playwright.js` — 3 Playwright tests for debug UI ## Tests - ✅ 7 Go unit tests (API shape, prefix/node filters, auth, structured logging, cold-start coverage) - ✅ 3 Playwright E2E tests (overlay checkbox, toggle without crash, panel expansion) - ✅ All existing tests pass (`go test ./cmd/server/... -count=1`) Part of #482 --------- Co-authored-by: you <you@example.com> |
||
|
|
15634362c9 |
feat: neighbor graph visualization in analytics (#482) — milestone 7 (#513)
## Summary Adds a **Neighbor Graph** tab to the Analytics page — an interactive force-directed graph visualization of the mesh network's neighbor affinity data. Part of #482 (Milestone 7 — Analytics Graph Visualization) ## What's New ### Neighbor Graph Tab - New "Neighbor Graph" tab in the analytics tab bar - Force-directed graph layout using HTML5 Canvas (vanilla JS, no external libs) - Nodes rendered as circles, colored by role using existing `ROLE_COLORS` - Edges as lines with thickness proportional to affinity score - Ambiguous edges highlighted in yellow ### Interactions - **Click node** → navigates to node detail page (`#/nodes/{pubkey}`) - **Hover node** → tooltip showing name, role, neighbor count - **Drag nodes** → rearrange layout interactively - **Mouse wheel** → zoom in/out (towards cursor position) - **Drag background** → pan the view ### Filters - **Role checkboxes** — toggle repeater, companion, room, sensor visibility - **Minimum score slider** — filter out weak edges (0.00–1.00) - **Confidence filter** — show all / high confidence only / hide ambiguous ### Stats Summary Displays above the graph: total nodes, total edges, average score, resolved %, ambiguous count ### Data Source Uses `GET /api/analytics/neighbor-graph` endpoint from M2, with region filtering via the shared RegionFilter component. ## Performance - Canvas-based rendering (not SVG) for performance with large graphs - Force simulation uses `requestAnimationFrame` with cooling/dampening — stops iterating when layout stabilizes - O(n²) repulsion is acceptable for typical mesh sizes (~500 nodes); for larger meshes, a Barnes-Hut approximation could be added later - Animation frame is properly cleaned up on page destroy ## Tests - Updated tab count assertion (≥10 tabs) - New Playwright test: tab loads, canvas renders, stats shown (≥3 stat cards) - New Playwright test: filter changes update stats ## Files Changed - `public/analytics.js` — new tab + full graph visualization implementation - `test-e2e-playwright.js` — 2 new tests + updated assertion --------- Co-authored-by: you <you@example.com> |
||
|
|
96d0bbe487 |
fix: replace Euclidean distance with haversine in analytics hop distances (#478)
## Summary Fixes #433 — Replace the inaccurate Euclidean distance approximation in `analytics.js` hop distances with proper haversine calculation, matching the server-side computation introduced in PR #415. ## Problem PR #415 moved collision analysis server-side and switched from the frontend's Euclidean approximation (`dLat×111, dLon×85`) to proper haversine. However, the **hop distance** calculation in `analytics.js` (subpath detail panel) still used the old Euclidean formula. This caused: - **Inconsistent distances** between hop distances and collision distances - **Significant errors at high latitudes** — e.g., Oslo→Stockholm: Euclidean gives ~627km, haversine gives ~415km (51% error) - The `dLon×85` constant assumes ~40° latitude; at 60° latitude the real scale factor is ~55.5km/degree, not 85 ## Changes | File | Change | |------|--------| | `public/analytics.js` | Replace `dLat*111, dLon*85` Euclidean with `HopResolver.haversineKm()` (with inline fallback) | | `public/hop-resolver.js` | Export `haversineKm` in the public API for reuse | | `test-frontend-helpers.js` | Add 4 tests: export check, zero distance, SF→LA accuracy, Euclidean vs haversine divergence | | `cmd/server/helpers_test.go` | Add `TestHaversineKm`: zero, SF→LA, symmetry, Oslo→Stockholm accuracy | | `public/index.html` | Cache buster bump | ## Performance No performance impact — `haversineKm` replaces an inline arithmetic expression with another inline arithmetic expression of identical O(1) complexity. Only called per hop pair in the subpath detail panel (typically <10 hops). ## Testing - `node test-frontend-helpers.js` — 248 passed, 0 failed - `go test -run TestHaversineKm` — PASS Co-authored-by: you <you@example.com> |
||
|
|
6712da7d7c |
fix: add region filtering to hash-collisions endpoint (#477)
## Summary The `/api/analytics/hash-collisions` endpoint always returned global results, ignoring the active region filter. Every other analytics endpoint (RF, topology, hash-sizes, channels, distance, subpaths) respected the `?region=` query parameter — this was the only one that didn't. Fixes #438 ## Changes ### Backend (`cmd/server/`) - **routes.go**: Extract `region` query param and pass to `GetAnalyticsHashCollisions(region)` - **store.go**: - `collisionCache` changed from `*cachedResult` → `map[string]*cachedResult` (keyed by region, `""` = global) — consistent with `rfCache`, `topoCache`, etc. - `GetAnalyticsHashCollisions(region)` and `computeHashCollisions(region)` now accept a region parameter - When region is specified, resolves regional observers, scans packets for nodes seen by those observers, and filters the node list before computing collisions - Cache invalidation updated to clear the map (not set to nil) ### Frontend (`public/`) - **analytics.js**: The hash-collisions fetch was missing `+ sep` (the region query string). All other fetches in the same `Promise.all` block had it — this was simply overlooked in PR #415. - **index.html**: Cache busters bumped ### Tests (`cmd/server/routes_test.go`) - `TestHashCollisionsRegionParamIgnored` → renamed to `TestHashCollisionsRegionParam` with updated comments reflecting that region is now accepted (with no configured regional observers, results match global — which the test verifies) ## Performance No new hot-path work. Region filtering adds one scan of `s.packets` (same as every other region-filtered analytics endpoint) only when `?region=` is provided. Results are cached per-region with the existing 60s TTL. Without `?region=`, behavior is unchanged. Co-authored-by: you <you@example.com> |
||
|
|
01ca843309 |
perf: move collision analysis to server-side endpoint (fixes #386) (#415)
## Summary Moves the hash collision analysis from the frontend to a new server-side endpoint, eliminating a major performance bottleneck on the analytics collision tab. Fixes #386 ## Problem The collision tab was: 1. **Downloading all nodes** (`/nodes?limit=2000`) — ~500KB+ of data 2. **Running O(n²) pairwise distance calculations** on the browser main thread (~2M comparisons with 2000 nodes) 3. **Building prefix maps client-side** (`buildOneBytePrefixMap`, `buildTwoBytePrefixInfo`, `buildCollisionHops`) iterating all nodes multiple times ## Solution ### New endpoint: `GET /api/analytics/hash-collisions` Returns pre-computed collision analysis with: - `inconsistent_nodes` — nodes with varying hash sizes - `by_size` — per-byte-size (1, 2, 3) collision data: - `stats` — node counts, space usage, collision counts - `collisions` — pre-computed collisions with pairwise distances and classifications (local/regional/distant/incomplete) - `one_byte_cells` — 256-cell prefix map for 1-byte matrix rendering - `two_byte_cells` — first-byte-grouped data for 2-byte matrix rendering ### Caching Uses the existing `cachedResult` pattern with a new `collisionCache` map. Invalidated on `hasNewTransmissions` (same trigger as the hash-sizes cache) and on eviction. ### Frontend changes - `renderCollisionTab` now accepts pre-fetched `collisionData` from the parallel API load - New `renderHashMatrixFromServer` and `renderCollisionsFromServer` functions consume server-computed data directly - No more `/nodes?limit=2000` fetch from the collision tab - Old client-side functions (`buildOneBytePrefixMap`, etc.) preserved for test helper exports ## Test results - `go test ./...` (server): ✅ pass - `go test ./...` (ingestor): ✅ pass - `test-packet-filter.js`: ✅ 62 passed - `test-aging.js`: ✅ 29 passed - `test-frontend-helpers.js`: ✅ 227 passed ## Performance impact | Metric | Before | After | |--------|--------|-------| | Data transferred | ~500KB (all nodes) | ~50KB (collision data only) | | Client computation | O(n²) distance calc | None (server-cached) | | Main thread blocking | Yes (2000 nodes × pairwise) | No | | Server caching | N/A | 15s TTL, invalidated on new transmissions | --------- Co-authored-by: you <you@example.com> Co-authored-by: Kpa-clawbot <kpabap+clawdbot@gmail.com> |
||
|
|
faca80e626 |
feat: add multi-byte hash usage matrix with stats and improved tooltips (#269)
- Add 1/2/3-byte selector to Hash Issues analytics page - 1-byte and 2-byte modes show 16×16 matrix with stat cards (nodes tracked, using N-byte ID, prefix space used, prefix collisions) - 3-byte mode shows summary stat cards instead of unrenderable grid - Fix "Nodes tracked" to always show total node count across all modes - Use CSS variable colours for matrix cells (light/dark mode compatible) - Replace native title tooltips with custom styled popovers - Hide collision risk card when 3-byte mode is selected - Fix double-tooltip bug on mode switch via _matrixTipInit guard - Fix tooltip persisting outside matrix grid on mouseleave https://dev.ve7kod.ca/#/analytics Hash Issues --------- Co-authored-by: Jesse <your@email.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
8c63200679 |
feat: hash size distribution by repeaters (Go server) (#264)
## Summary Adds `distributionByRepeaters` to the `/api/analytics/hash-sizes` endpoint in the **Go server**. ### Problem PR #263 implemented this feature in the deprecated Node.js server (server.js). All backend changes should go in the Go server at `cmd/server/`. ### Solution - For each hash size (1, 2, 3), count how many unique repeaters (nodes) advertise packets with that hash size - Uses the existing `byNode` map already computed in `computeAnalyticsHashSizes()` - Added to both the live response and the empty/fallback response in routes.go - Frontend changes from PR #263 (`public/analytics.js`) already render this field — no frontend changes needed ### Response shape ```json { "distributionByRepeaters": { "1": 42, "2": 7, "3": 2 }, ...existing fields... } ``` ### Testing - All Go server tests pass - Replaces PR #263 (which modified the wrong server) Closes #263 --------- Co-authored-by: you <you@example.com> |
||
|
|
71ec5e6fca |
rename: MeshCore Analyzer → CoreScope (frontend + .squad)
Phase 1 of the CoreScope rename — frontend display strings and squad agent metadata only. index.html: - <title>, og:title, twitter:title → CoreScope - Brand text span → CoreScope - og:image/twitter:image URLs → corescope repo (placeholder) - Cache busters bumped public/*.js headers (19 files): - All file header comments updated public/*.css headers: - style.css, home.css updated JavaScript strings: - app.js: GitHub URL → corescope - home.js: 3 fallback siteName references - customize.js: default siteName + heroTitle Tests: - test-e2e-playwright.js: title assertion → corescope - test-frontend-helpers.js: GitHub URL constant - benchmark.js: header string - test-all.sh: header string .squad: - team.md, casting/history.json - All 7 agent charters + 5 history files NOT renamed (intentional): - localStorage keys (meshcore-*) - CSS classes (.meshcore-marker) - Window globals (_meshcore*) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
f04f1b8e77 |
fix: accessibility — chart labels, table scope, form labels (#210, #211, #212)
#210: Add role="img" aria-label to 9 Chart.js canvases in node-analytics.js and observer-detail.js with descriptive labels. #211: Add scope="col" to all <th> elements across analytics.js, audio-lab.js, compare.js, node-analytics.js, nodes.js, observer-detail.js, observers.js, and packets.js (40+ headers). #212: Add aria-label to packet filter input and time window select in packets.js. Add for/id associations to all customize.js inputs: branding, theme colors, node/type colors, heatmap sliders, onboarding fields, and export controls. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
d9523f23a0 |
fix: harden node detail rendering with Number() casts and Array.isArray guards, fixes #190
Add defensive type safety to node detail page rendering: - Wrap all .toFixed() calls with Number() to handle string values from Go backend - Use Array.isArray() for hash_sizes_seen instead of || [] fallback - Apply same fixes to both full-screen and side-panel views - Add 9 new tests for renderHashInconsistencyWarning and renderNodeBadges with hash_size_inconsistent data (including non-array edge cases) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
47ee63ed55 |
fix: #191 #192 #193 #194 — repeater-only collision matrix, expand=observations, store-based node health, goRuntime in perf
#191: Hash collision matrix now filters to role=repeater only (routing-relevant) #192: expand=observations in /api/packets now returns full observation details (txToMap includes observations, stripped by default) #193: /api/nodes/:pubkey/health uses in-memory PacketStore when available instead of slow SQL queries #194: goRuntime (heapMB, sysMB, numGoroutine, numGC, gcPauseMs) restored in /api/perf response Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
d3347f9d99 |
fix(analytics): channels table perf + sortable columns (#166, #167)
Performance (#166): - renderChannelTimeline: replace O(n²) data.find() with O(1) lookup map - renderChannelTimeline: precompute maxCount once instead of per-point - renderChannels: pre-build sub-section HTML before single innerHTML write Sortable columns (#167): - All 6 channel table columns are now sortable (click header) - Default sort: last activity descending (latest message first) - Sort preference persists to localStorage (meshcore-channel-sort) - Toggles asc/desc on re-click; smart default direction per column type - Uses existing .sortable/.sort-active CSS patterns on .analytics-table Tests: 23 new tests for sortChannels, loadChannelSort, saveChannelSort, channelTheadHtml, channelTbodyHtml (134 total frontend tests, 0 failures) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
c524efc74d |
fix: section links are real deep-linkable URLs, not javascript:void
TOC: #/analytics?tab=collisions§ion=inconsistentHashSection etc. Back-to-top: #/analytics?tab=collisions (scrolls to top of tab) All copyable, shareable, bookmarkable. |
||
|
|
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. |