# 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 `