diff --git a/CHANGELOG.md b/CHANGELOG.md index 66d242c..908945f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -94,6 +94,10 @@ semantic versioning. - The orphaned `/stats` page, which was unreachable from the navigation and rendered stub charts that never populated. +- The dashboard's Live Activity feed. It opened three SocketIO subscriptions + and re-rendered on every packet to duplicate a page that already exists at + `/realtime`, so the dashboard now costs one snapshot read per poll and + nothing else. ### Deprecated diff --git a/docs/web-viewer.md b/docs/web-viewer.md index b1b13db..66f6bba 100644 --- a/docs/web-viewer.md +++ b/docs/web-viewer.md @@ -146,6 +146,10 @@ proxy_set_header X-Forwarded-Proto $scheme; plus a 30-day multibyte adoption trend - Busiest repeaters, and **one-hop neighbours** (24-hour or 7-day window) +The live packet feed lives on the **Real-time** page rather than here; the +dashboard reads a single snapshot per poll and holds no streaming +subscriptions. + Two measurement notes for the mesh charts: **Hops are derived, and the two path tables disagree on units.** diff --git a/modules/web_viewer/dashboard_stats.py b/modules/web_viewer/dashboard_stats.py index fbec82f..7a3faa6 100644 --- a/modules/web_viewer/dashboard_stats.py +++ b/modules/web_viewer/dashboard_stats.py @@ -856,10 +856,14 @@ class DashboardStatsService: ) ) - # Pad both onto one contiguous axis so the bars line up. - edges = [hop for series in distribution.values() for hop, _ in series] - if edges: - low, high = min(edges), max(edges) + # Pad both onto one contiguous axis so the bars line up, bounded by the + # hops that actually carry something: an axis that runs on past the last + # observation spends its width on nothing. + populated = [ + hop for series in distribution.values() for hop, count in series if count + ] + if populated: + low, high = min(populated), max(populated) for key, series in distribution.items(): counts = dict(series) distribution[key] = [[hop, counts.get(hop, 0)] for hop in range(low, high + 1)] diff --git a/modules/web_viewer/static/css/dashboard.css b/modules/web_viewer/static/css/dashboard.css index bc1b20d..44672ee 100644 --- a/modules/web_viewer/static/css/dashboard.css +++ b/modules/web_viewer/static/css/dashboard.css @@ -342,6 +342,29 @@ height: 240px; } +/* Cards in an equal-height row: let the chart or list take the slack instead + * of leaving it as dead space under a fixed-height canvas. Chart.js needs a + * positioned parent with a real height, which flex-basis: 0 + flex-grow gives + * it — plain height: 100% resolves against an auto-height parent and collapses. + */ +.card-body--fill { + display: flex; + flex-direction: column; +} + +.chart-box--fill { + flex: 1 1 0; + min-height: 200px; + height: auto; +} + +.mix-fill { + flex: 1 1 auto; + display: flex; + flex-direction: column; + justify-content: space-evenly; +} + .doughnut-box { position: relative; max-width: 190px; @@ -385,23 +408,6 @@ text-align: left; } -/* ── Live activity feed ────────────────────────────────────────────────── */ - -.live-feed { - height: 260px; - overflow-y: auto; - background-color: var(--bg-secondary); - padding: 0.5rem; -} - -.live-feed__entry { - padding: 3px 8px; - margin-bottom: 3px; - font-size: 0.82rem; - border-radius: 2px; - border-left: 3px solid var(--border-color); -} - @media (max-width: 575.98px) { .stat-tile__value { font-size: 1.35rem; diff --git a/modules/web_viewer/static/js/dashboard.js b/modules/web_viewer/static/js/dashboard.js index 09f2fbb..bc187a8 100644 --- a/modules/web_viewer/static/js/dashboard.js +++ b/modules/web_viewer/static/js/dashboard.js @@ -1001,89 +1001,6 @@ }, }; - // ── live activity feed ─────────────────────────────────────────────── - - function initLiveFeed() { - const feed = el('live-feed'); - const dot = el('live-dot'); - const countBadge = el('live-count'); - const placeholder = el('live-placeholder'); - if (!feed || !window.connectionManager) return; - - const TYPE_COLORS = { packet: '#fd7e14', command: '#198754', message: '#0dcaf0' }; - const activeFilters = { packet: true, command: true, message: true }; - const MAX_ENTRIES = 100; - let paused = false; - let total = 0; - - function applyFilters() { - Array.from(feed.children).forEach((node) => { - const type = node.dataset.type; - if (type) node.style.display = activeFilters[type] ? '' : 'none'; - }); - } - - document.querySelectorAll('.live-filter-cb').forEach((checkbox) => { - checkbox.addEventListener('change', () => { - activeFilters[checkbox.dataset.type] = checkbox.checked; - applyFilters(); - }); - }); - - function addEntry(label, text, type) { - if (paused) return; - if (placeholder && placeholder.parentNode) placeholder.remove(); - const node = document.createElement('div'); - node.dataset.type = type; - node.className = 'live-feed__entry'; - node.style.borderLeftColor = TYPE_COLORS[type] || '#6c757d'; - node.append( - makeTextElement('span', new Date().toLocaleTimeString(), 'text-muted me-2'), - makeTextElement('strong', label), - makeTextElement('span', ' ' + String(text ?? ''), 'text-muted') - ); - if (!activeFilters[type]) node.style.display = 'none'; - feed.insertBefore(node, feed.firstChild); - total += 1; - if (countBadge) countBadge.textContent = total; - while (feed.children.length > MAX_ENTRIES) feed.removeChild(feed.lastChild); - } - - el('live-pause-btn')?.addEventListener('click', () => { - paused = !paused; - const icon = el('live-pause-icon'); - if (icon) icon.className = paused ? 'fas fa-play' : 'fas fa-pause'; - }); - el('live-clear-btn')?.addEventListener('click', () => { - feed.replaceChildren(); - total = 0; - if (countBadge) countBadge.textContent = '0'; - }); - el('live-scroll-top')?.addEventListener('click', () => { feed.scrollTop = 0; }); - el('live-scroll-bottom')?.addEventListener('click', () => { feed.scrollTop = feed.scrollHeight; }); - - const socket = window.connectionManager.socket; - socket.on('connect', () => { - if (dot) dot.className = 'status-indicator status-connected ms-2'; - socket.emit('subscribe_packets'); - socket.emit('subscribe_commands'); - socket.emit('subscribe_messages'); - }); - socket.on('disconnect', () => { - if (dot) dot.className = 'status-indicator status-disconnected ms-2'; - }); - socket.on('packet_data', (d) => { - addEntry(d.payload_type_name || d.type_name || 'Packet', d.from_name || d.pubkey_prefix || '', 'packet'); - }); - socket.on('command_data', (d) => { - addEntry('Cmd: ' + (d.command || '?'), (d.user || '') + ' → ' + (d.channel || ''), 'command'); - }); - socket.on('message_data', (d) => { - const channel = d.channel ? '[' + d.channel + '] ' : (d.is_dm ? '[DM] ' : ''); - addEntry(d.sender || '?', channel + (d.content || ''), 'message'); - }); - } - // ── connected clients modal ────────────────────────────────────────── async function loadConnectedClients() { @@ -1178,7 +1095,6 @@ event.preventDefault(); loadConnectedClients(); }); - initLiveFeed(); initTooltips(); }); diff --git a/modules/web_viewer/templates/index.html b/modules/web_viewer/templates/index.html index 148ba47..fbc430a 100644 --- a/modules/web_viewer/templates/index.html +++ b/modules/web_viewer/templates/index.html @@ -137,12 +137,12 @@ -
Nodes by their closest advert path, and how far arriving flood packets had travelled
-