(feat): Bandwidth Graph Overlay

This commit is contained in:
stedrow
2026-01-04 15:04:38 -05:00
parent b15787abca
commit d57e530c4e
3 changed files with 209 additions and 15 deletions
+130 -15
View File
@@ -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');
};
+23
View File
@@ -65,6 +65,29 @@
</div>
</div>
<div id="bandwidthOverlay" class="bandwidth-overlay">
<div class="bandwidth-graph-container">
<canvas id="bandwidthGraph"></canvas>
</div>
<div class="bandwidth-footer">
<div class="bandwidth-legend">
<div class="legend-item">
<span class="legend-color" style="background: #60a5fa;"></span>
<span class="stat-value">In: <span id="current-in">0 KB</span></span>
</div>
<div class="legend-item">
<span class="legend-color" style="background: #f97316;"></span>
<span class="stat-value">Out: <span id="current-out">0 KB</span></span>
</div>
</div>
<div class="time-pills">
<button class="time-pill" data-value="300">5m</button>
<button class="time-pill" data-value="1800">30m</button>
<button class="time-pill" data-value="all">All</button>
<button class="close-btn" onclick="toggleBandwidthGraph()"></button>
</div>
</div>
</div>
<script src="/app.js"></script>
</body>
+56
View File
@@ -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; }