From d57e530c4e202c4ecca37a5feacc41b0c0679083 Mon Sep 17 00:00:00 2001 From: stedrow Date: Sun, 4 Jan 2026 15:04:38 -0500 Subject: [PATCH] (feat): Bandwidth Graph Overlay --- public/app.js | 145 +++++++++++++++++++++++++++++++++++++++++----- public/index.html | 23 ++++++++ public/style.css | 56 ++++++++++++++++++ 3 files changed, 209 insertions(+), 15 deletions(-) diff --git a/public/app.js b/public/app.js index 239964d..3bb72fe 100644 --- a/public/app.js +++ b/public/app.js @@ -99,6 +99,21 @@ document.addEventListener('keydown', (e) => { } }); +const formatBandwidth = (bytes, short = false) => { + const kb = bytes / 1024; + const mb = kb / 1024; + const gb = mb / 1024; + const space = short ? '' : ' '; + + if (gb >= 1) { + return gb.toFixed(short ? 1 : 2) + space + 'GB'; + } else if (mb >= 1) { + return mb.toFixed(short ? 1 : 2) + space + 'MB'; + } else { + return kb.toFixed(short ? 0 : 1) + space + 'KB'; + } +}; + const evtSource = new EventSource("/events"); evtSource.onmessage = (event) => { @@ -117,21 +132,7 @@ evtSource.onmessage = (event) => { if (data.diagnostics) { const d = data.diagnostics; - - const formatBandwidth = (bytes) => { - const kb = bytes / 1024; - const mb = kb / 1024; - const gb = mb / 1024; - - if (gb >= 1) { - return gb.toFixed(2) + ' GB'; - } else if (mb >= 1) { - return mb.toFixed(2) + ' MB'; - } else { - return kb.toFixed(1) + ' KB'; - } - }; - + document.getElementById('diag-heartbeats-rx').innerText = d.heartbeatsReceived.toLocaleString(); document.getElementById('diag-heartbeats-tx').innerText = d.heartbeatsRelayed.toLocaleString(); document.getElementById('diag-new-peers').innerText = d.newPeersAdded.toLocaleString(); @@ -141,6 +142,12 @@ evtSource.onmessage = (event) => { document.getElementById('diag-bandwidth-in').innerText = formatBandwidth(d.bytesReceived); document.getElementById('diag-bandwidth-out').innerText = formatBandwidth(d.bytesRelayed); document.getElementById('diag-leave').innerText = d.leaveMessages.toLocaleString(); + + addBandwidthData(d.bytesReceived, d.bytesRelayed); + drawBandwidthGraph(); + + document.getElementById('current-in').innerText = formatBandwidth(d.bytesReceived); + document.getElementById('current-out').innerText = formatBandwidth(d.bytesRelayed); } }; @@ -154,3 +161,111 @@ countEl.classList.add('loaded'); updateParticles(initialCount); animate(); +const bandwidthHistory = { timestamps: [], bytesIn: [], bytesOut: [] }; +let selectedTimeRange = 300; +const bandwidthCanvas = document.getElementById('bandwidthGraph'); +const bandwidthCtx = bandwidthCanvas.getContext('2d'); +const bandwidthOverlay = document.getElementById('bandwidthOverlay'); + +function resizeBandwidthCanvas() { + const rect = bandwidthCanvas.getBoundingClientRect(); + bandwidthCanvas.width = rect.width; + bandwidthCanvas.height = rect.height; + drawBandwidthGraph(); +} + +window.addEventListener('resize', resizeBandwidthCanvas); +setTimeout(resizeBandwidthCanvas, 100); + +const toggleBandwidthGraph = () => { + bandwidthOverlay.classList.toggle('collapsed'); + document.querySelector('.bandwidth-overlay .close-btn').textContent = + bandwidthOverlay.classList.contains('collapsed') ? '+' : '−'; +}; + +const timePills = document.querySelectorAll('.time-pill'); +timePills.forEach(pill => { + pill.addEventListener('click', (e) => { + e.stopPropagation(); + timePills.forEach(p => p.classList.remove('active')); + pill.classList.add('active'); + const value = pill.dataset.value; + selectedTimeRange = value === 'all' ? 'all' : parseInt(value); + drawBandwidthGraph(); + }); +}); + +timePills[0].classList.add('active'); + +const addBandwidthData = (bytesIn, bytesOut) => { + bandwidthHistory.timestamps.push(Date.now()); + bandwidthHistory.bytesIn.push(bytesIn); + bandwidthHistory.bytesOut.push(bytesOut); + + if (bandwidthHistory.timestamps.length > 360) { + bandwidthHistory.timestamps.shift(); + bandwidthHistory.bytesIn.shift(); + bandwidthHistory.bytesOut.shift(); + } +}; + +const getFilteredData = () => { + if (selectedTimeRange === 'all') return bandwidthHistory; + + const cutoff = Date.now() - (selectedTimeRange * 1000); + const startIndex = bandwidthHistory.timestamps.findIndex(t => t >= cutoff); + + if (startIndex === -1) return bandwidthHistory; + + return { + timestamps: bandwidthHistory.timestamps.slice(startIndex), + bytesIn: bandwidthHistory.bytesIn.slice(startIndex), + bytesOut: bandwidthHistory.bytesOut.slice(startIndex) + }; +}; + +const drawBandwidthGraph = () => { + const w = bandwidthCanvas.width; + const h = bandwidthCanvas.height; + + if (w === 0 || h === 0) return; + + const pad = { t: 10, r: 10, b: 20, l: 50 }; + + bandwidthCtx.clearRect(0, 0, w, h); + + const data = getFilteredData(); + if (data.timestamps.length < 2) return; + + const max = Math.max(...data.bytesIn, ...data.bytesOut); + if (max === 0) return; + + bandwidthCtx.fillStyle = '#9ca3af'; + bandwidthCtx.font = '10px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto'; + bandwidthCtx.textAlign = 'right'; + [max, max / 2, 0].forEach((val, i) => { + bandwidthCtx.fillText(formatBandwidth(val, true), pad.l - 5, pad.t + ((h - pad.t - pad.b) / 2) * i + 4); + }); + + const drawLine = (points, color) => { + bandwidthCtx.strokeStyle = color; + bandwidthCtx.lineWidth = 2; + bandwidthCtx.beginPath(); + points.forEach((val, i) => { + const x = pad.l + (i / (points.length - 1)) * (w - pad.l - pad.r); + const y = pad.t + (h - pad.t - pad.b) - (val / max) * (h - pad.t - pad.b); + i === 0 ? bandwidthCtx.moveTo(x, y) : bandwidthCtx.lineTo(x, y); + }); + bandwidthCtx.stroke(); + + bandwidthCtx.lineTo(pad.l + (w - pad.l - pad.r), pad.t + (h - pad.t - pad.b)); + bandwidthCtx.lineTo(pad.l, pad.t + (h - pad.t - pad.b)); + bandwidthCtx.closePath(); + bandwidthCtx.fillStyle = color + '33'; + bandwidthCtx.fill(); + }; + + drawLine(data.bytesIn, '#60a5fa'); + drawLine(data.bytesOut, '#f97316'); +}; + diff --git a/public/index.html b/public/index.html index 77d90c0..a7dbd43 100644 --- a/public/index.html +++ b/public/index.html @@ -65,6 +65,29 @@ +
+
+ +
+ +
diff --git a/public/style.css b/public/style.css index cd016e1..4bac6f4 100644 --- a/public/style.css +++ b/public/style.css @@ -85,3 +85,59 @@ a { color: #4b5563; text-decoration: none; border-bottom: 1px dotted #4b5563; } color: #333; margin-top: 1rem; } + +.bandwidth-overlay { + position: fixed; + bottom: 0; + left: 0; + right: 0; + z-index: 999; + background: transparent; +} +.bandwidth-graph-container { + padding: 0.5rem 0 0; + transition: max-height 0.3s ease-in-out, opacity 0.3s ease-in-out; + max-height: 150px; + opacity: 1; + overflow: hidden; +} +.bandwidth-overlay.collapsed .bandwidth-graph-container { max-height: 0; opacity: 0; padding: 0; } +.bandwidth-overlay .close-btn { + position: static; + font-size: 0.65rem; + font-weight: bold; + padding: 0.2rem 0.5rem; + color: #9ca3af; + background: #1a1a1a; + border: 1px solid #333; + border-radius: 3px; + cursor: pointer; + transition: all 0.2s; + outline: none; +} +.bandwidth-overlay .close-btn:hover { color: #cbd5e1; border-color: #4b5563; } +#bandwidthGraph { width: 100%; height: 100px; display: block; background: transparent; } +.bandwidth-footer { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem 1.5rem 0.75rem; +} +.bandwidth-legend { display: flex; gap: 1.5rem; } +.legend-item { display: flex; align-items: center; gap: 0.4rem; font-size: 0.7rem; } +.legend-color { width: 10px; height: 10px; border-radius: 2px; } +.time-pills { display: flex; gap: 0.3rem; } +.bandwidth-overlay.collapsed .time-pill { display: none; } +.time-pill { + background: transparent; + color: #4b5563; + border: 1px solid #333; + padding: 0.2rem 0.5rem; + font-size: 0.65rem; + border-radius: 3px; + cursor: pointer; + transition: all 0.2s; + outline: none; +} +.time-pill:hover { color: #9ca3af; border-color: #4b5563; } +.time-pill.active { background: #1a1a1a; color: #4ade80; border-color: #4ade80; }