diff --git a/NODE-ANALYTICS-PLAN.md b/NODE-ANALYTICS-PLAN.md new file mode 100644 index 00000000..3f965e2d --- /dev/null +++ b/NODE-ANALYTICS-PLAN.md @@ -0,0 +1,251 @@ +# Node Analytics Page β Implementation Plan + +## Overview +A dedicated per-node analytics page (`#/nodes/:pubkey/analytics`) showing charts, breakdowns, and computed stats. Linked from node sidebar and full-screen detail views. + +## Route & Navigation +- **Hash route:** `#/nodes/:pubkey/analytics` +- **Entry points:** + - Sidebar detail: "π Analytics" button next to "π Copy URL" + - Full-screen detail: same button placement + - Direct URL (shareable) +- **Back navigation:** "β Back to node" link returns to `#/nodes/:pubkey` + +## API Endpoint + +### `GET /api/nodes/:pubkey/analytics?days=7` + +Returns all data needed for the page in a single request. Server computes aggregations in SQLite for efficiency. + +```json +{ + "node": { "public_key": "...", "name": "...", "role": "..." }, + "timeRange": { "from": "ISO", "to": "ISO", "days": 7 }, + "activityTimeline": [ + { "bucket": "2026-03-19T10:00:00Z", "count": 5 } + ], + "snrTrend": [ + { "timestamp": "ISO", "snr": 11.5, "rssi": -44, "observer_id": "...", "observer_name": "..." } + ], + "packetTypeBreakdown": [ + { "payload_type": 4, "label": "Advert", "count": 120 }, + { "payload_type": 5, "label": "Channel Msg", "count": 45 } + ], + "observerCoverage": [ + { "observer_id": "...", "observer_name": "...", "packetCount": 200, "avgSnr": 8.5, "avgRssi": -60, "firstSeen": "ISO", "lastSeen": "ISO" } + ], + "hopDistribution": [ + { "hops": 1, "count": 150 }, + { "hops": 2, "count": 30 } + ], + "peerInteractions": [ + { "peer_key": "...", "peer_name": "...", "messageCount": 15, "lastContact": "ISO" } + ], + "computedStats": { + "availabilityPct": 92.5, + "longestSilenceMs": 14400000, + "longestSilenceStart": "ISO", + "signalGrade": "B+", + "snrMean": 8.2, + "snrStdDev": 3.1, + "relayPct": 22.5, + "totalPackets": 450, + "uniqueObservers": 3, + "uniquePeers": 8, + "avgPacketsPerDay": 64.3 + }, + "uptimeHeatmap": [ + { "dayOfWeek": 0, "hour": 14, "count": 12 } + ] +} +``` + +### Server Implementation (`server.js`) + +Add route handler at `/api/nodes/:pubkey/analytics`. All queries use the same LIKE-based matching as existing `getNodeHealth()`. Key queries: + +1. **activityTimeline** β `SELECT strftime('%Y-%m-%dT%H:00:00Z', timestamp) as bucket, COUNT(*) as count FROM packets WHERE ... AND timestamp > ? GROUP BY bucket ORDER BY bucket` +2. **snrTrend** β `SELECT timestamp, snr, rssi, observer_id, observer_name FROM packets WHERE ... AND snr IS NOT NULL ORDER BY timestamp` (raw points, chart.js handles rendering) +3. **packetTypeBreakdown** β `SELECT payload_type, COUNT(*) as count FROM packets WHERE ... GROUP BY payload_type` +4. **observerCoverage** β `SELECT observer_id, observer_name, COUNT(*), AVG(snr), AVG(rssi), MIN(timestamp), MAX(timestamp) FROM packets WHERE ... GROUP BY observer_id ORDER BY COUNT(*) DESC` +5. **hopDistribution** β Parse `path_json` in JS, count hop lengths +6. **peerInteractions** β Parse `decoded_json`, extract sender/recipient pubkeys and names, aggregate +7. **uptimeHeatmap** β `SELECT strftime('%w', timestamp) as dow, strftime('%H', timestamp) as hour, COUNT(*) FROM packets WHERE ... GROUP BY dow, hour` +8. **computedStats** β Derived from above data: + - `availabilityPct`: count distinct hours with packets / total hours in range Γ 100 + - `longestSilenceMs`: iterate timestamps, find max gap + - `signalGrade`: A (snr>15, stddev<2), B (snr>8), C (snr>3), D (snr<=3) + - `relayPct`: packets with hop count > 1 / total with path data Γ 100 + +Add a helper function `getNodeAnalytics(pubkey, days)` in `db.js` to keep it organized. + +## Frontend + +### New File: `public/node-analytics.js` + +IIFE pattern matching existing pages. Registers with the router for `#/nodes/:pubkey/analytics`. + +### Layout + +``` +βββββββββββββββββββββββββββββββββββββββββββββββββββ +β β Back to SomeNodeName β +β β +β βββββββββββββββ βββββββββββββββ ββββββββββββββ β +β β Availabilityβ β Signal Gradeβ β Packets/Dayβ β +β β 92.5% β β B+ β β 64.3 β β +β βββββββββββββββ βββββββββββββββ ββββββββββββββ β +β βββββββββββββββ βββββββββββββββ ββββββββββββββ β +β β Observers β β Relay % β β Longest β β +β β 3 β β 22.5% β β Silence 4h β β +β βββββββββββββββ βββββββββββββββ ββββββββββββββ β +β β +β βββββββββββββββββββββββββββββββββββββββββββββββ β +β β Activity Timeline (bar chart, hourly) β β +β βββββββββββββββββββββββββββββββββββββββββββββββ β +β β +β ββββββββββββββββββββββββ ββββββββββββββββββββββ β +β β SNR Trend (line) β β Packet Types (pie) β β +β ββββββββββββββββββββββββ ββββββββββββββββββββββ β +β β +β ββββββββββββββββββββββββ ββββββββββββββββββββββ β +β β Observer Coverage β β Hop Distribution β β +β β (horizontal bar) β β (bar chart) β β +β ββββββββββββββββββββββββ ββββββββββββββββββββββ β +β β +β βββββββββββββββββββββββββββββββββββββββββββββββ β +β β Uptime Heatmap (7Γ24 grid, GitHub-style) β β +β βββββββββββββββββββββββββββββββββββββββββββββββ β +β β +β βββββββββββββββββββββββββββββββββββββββββββββββ β +β β Peer Interactions (ranked list) β β +β βββββββββββββββββββββββββββββββββββββββββββββββ β +βββββββββββββββββββββββββββββββββββββββββββββββββββ +``` + +### Time Range Selector +- Buttons: 24h | 7d | 30d | All +- Default: 7d +- Reloads data via API when changed + +### Chart Library +- **Chart.js v4** from CDN (unpkg): `https://unpkg.com/chart.js@4/dist/chart.umd.min.js` +- Add ` +
Skip to content @@ -75,15 +76,16 @@ - - - - - - - - - - + + + + + + + + + + +