Skip to content

Commit d57e530

Browse files
committed
(feat): Bandwidth Graph Overlay
1 parent b15787a commit d57e530

File tree

3 files changed

+209
-15
lines changed

3 files changed

+209
-15
lines changed

public/app.js

Lines changed: 130 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,21 @@ document.addEventListener('keydown', (e) => {
9999
}
100100
});
101101

102+
const formatBandwidth = (bytes, short = false) => {
103+
const kb = bytes / 1024;
104+
const mb = kb / 1024;
105+
const gb = mb / 1024;
106+
const space = short ? '' : ' ';
107+
108+
if (gb >= 1) {
109+
return gb.toFixed(short ? 1 : 2) + space + 'GB';
110+
} else if (mb >= 1) {
111+
return mb.toFixed(short ? 1 : 2) + space + 'MB';
112+
} else {
113+
return kb.toFixed(short ? 0 : 1) + space + 'KB';
114+
}
115+
};
116+
102117
const evtSource = new EventSource("/events");
103118

104119
evtSource.onmessage = (event) => {
@@ -117,21 +132,7 @@ evtSource.onmessage = (event) => {
117132

118133
if (data.diagnostics) {
119134
const d = data.diagnostics;
120-
121-
const formatBandwidth = (bytes) => {
122-
const kb = bytes / 1024;
123-
const mb = kb / 1024;
124-
const gb = mb / 1024;
125-
126-
if (gb >= 1) {
127-
return gb.toFixed(2) + ' GB';
128-
} else if (mb >= 1) {
129-
return mb.toFixed(2) + ' MB';
130-
} else {
131-
return kb.toFixed(1) + ' KB';
132-
}
133-
};
134-
135+
135136
document.getElementById('diag-heartbeats-rx').innerText = d.heartbeatsReceived.toLocaleString();
136137
document.getElementById('diag-heartbeats-tx').innerText = d.heartbeatsRelayed.toLocaleString();
137138
document.getElementById('diag-new-peers').innerText = d.newPeersAdded.toLocaleString();
@@ -141,6 +142,12 @@ evtSource.onmessage = (event) => {
141142
document.getElementById('diag-bandwidth-in').innerText = formatBandwidth(d.bytesReceived);
142143
document.getElementById('diag-bandwidth-out').innerText = formatBandwidth(d.bytesRelayed);
143144
document.getElementById('diag-leave').innerText = d.leaveMessages.toLocaleString();
145+
146+
addBandwidthData(d.bytesReceived, d.bytesRelayed);
147+
drawBandwidthGraph();
148+
149+
document.getElementById('current-in').innerText = formatBandwidth(d.bytesReceived);
150+
document.getElementById('current-out').innerText = formatBandwidth(d.bytesRelayed);
144151
}
145152
};
146153

@@ -154,3 +161,111 @@ countEl.classList.add('loaded');
154161
updateParticles(initialCount);
155162
animate();
156163

164+
const bandwidthHistory = { timestamps: [], bytesIn: [], bytesOut: [] };
165+
let selectedTimeRange = 300;
166+
const bandwidthCanvas = document.getElementById('bandwidthGraph');
167+
const bandwidthCtx = bandwidthCanvas.getContext('2d');
168+
const bandwidthOverlay = document.getElementById('bandwidthOverlay');
169+
170+
function resizeBandwidthCanvas() {
171+
const rect = bandwidthCanvas.getBoundingClientRect();
172+
bandwidthCanvas.width = rect.width;
173+
bandwidthCanvas.height = rect.height;
174+
drawBandwidthGraph();
175+
}
176+
177+
window.addEventListener('resize', resizeBandwidthCanvas);
178+
setTimeout(resizeBandwidthCanvas, 100);
179+
180+
const toggleBandwidthGraph = () => {
181+
bandwidthOverlay.classList.toggle('collapsed');
182+
document.querySelector('.bandwidth-overlay .close-btn').textContent =
183+
bandwidthOverlay.classList.contains('collapsed') ? '+' : '−';
184+
};
185+
186+
const timePills = document.querySelectorAll('.time-pill');
187+
timePills.forEach(pill => {
188+
pill.addEventListener('click', (e) => {
189+
e.stopPropagation();
190+
timePills.forEach(p => p.classList.remove('active'));
191+
pill.classList.add('active');
192+
const value = pill.dataset.value;
193+
selectedTimeRange = value === 'all' ? 'all' : parseInt(value);
194+
drawBandwidthGraph();
195+
});
196+
});
197+
198+
timePills[0].classList.add('active');
199+
200+
const addBandwidthData = (bytesIn, bytesOut) => {
201+
bandwidthHistory.timestamps.push(Date.now());
202+
bandwidthHistory.bytesIn.push(bytesIn);
203+
bandwidthHistory.bytesOut.push(bytesOut);
204+
205+
if (bandwidthHistory.timestamps.length > 360) {
206+
bandwidthHistory.timestamps.shift();
207+
bandwidthHistory.bytesIn.shift();
208+
bandwidthHistory.bytesOut.shift();
209+
}
210+
};
211+
212+
const getFilteredData = () => {
213+
if (selectedTimeRange === 'all') return bandwidthHistory;
214+
215+
const cutoff = Date.now() - (selectedTimeRange * 1000);
216+
const startIndex = bandwidthHistory.timestamps.findIndex(t => t >= cutoff);
217+
218+
if (startIndex === -1) return bandwidthHistory;
219+
220+
return {
221+
timestamps: bandwidthHistory.timestamps.slice(startIndex),
222+
bytesIn: bandwidthHistory.bytesIn.slice(startIndex),
223+
bytesOut: bandwidthHistory.bytesOut.slice(startIndex)
224+
};
225+
};
226+
227+
const drawBandwidthGraph = () => {
228+
const w = bandwidthCanvas.width;
229+
const h = bandwidthCanvas.height;
230+
231+
if (w === 0 || h === 0) return;
232+
233+
const pad = { t: 10, r: 10, b: 20, l: 50 };
234+
235+
bandwidthCtx.clearRect(0, 0, w, h);
236+
237+
const data = getFilteredData();
238+
if (data.timestamps.length < 2) return;
239+
240+
const max = Math.max(...data.bytesIn, ...data.bytesOut);
241+
if (max === 0) return;
242+
243+
bandwidthCtx.fillStyle = '#9ca3af';
244+
bandwidthCtx.font = '10px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto';
245+
bandwidthCtx.textAlign = 'right';
246+
[max, max / 2, 0].forEach((val, i) => {
247+
bandwidthCtx.fillText(formatBandwidth(val, true), pad.l - 5, pad.t + ((h - pad.t - pad.b) / 2) * i + 4);
248+
});
249+
250+
const drawLine = (points, color) => {
251+
bandwidthCtx.strokeStyle = color;
252+
bandwidthCtx.lineWidth = 2;
253+
bandwidthCtx.beginPath();
254+
points.forEach((val, i) => {
255+
const x = pad.l + (i / (points.length - 1)) * (w - pad.l - pad.r);
256+
const y = pad.t + (h - pad.t - pad.b) - (val / max) * (h - pad.t - pad.b);
257+
i === 0 ? bandwidthCtx.moveTo(x, y) : bandwidthCtx.lineTo(x, y);
258+
});
259+
bandwidthCtx.stroke();
260+
261+
bandwidthCtx.lineTo(pad.l + (w - pad.l - pad.r), pad.t + (h - pad.t - pad.b));
262+
bandwidthCtx.lineTo(pad.l, pad.t + (h - pad.t - pad.b));
263+
bandwidthCtx.closePath();
264+
bandwidthCtx.fillStyle = color + '33';
265+
bandwidthCtx.fill();
266+
};
267+
268+
drawLine(data.bytesIn, '#60a5fa');
269+
drawLine(data.bytesOut, '#f97316');
270+
};
271+

public/index.html

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,29 @@
6565
</div>
6666
</div>
6767

68+
<div id="bandwidthOverlay" class="bandwidth-overlay">
69+
<div class="bandwidth-graph-container">
70+
<canvas id="bandwidthGraph"></canvas>
71+
</div>
72+
<div class="bandwidth-footer">
73+
<div class="bandwidth-legend">
74+
<div class="legend-item">
75+
<span class="legend-color" style="background: #60a5fa;"></span>
76+
<span class="stat-value">In: <span id="current-in">0 KB</span></span>
77+
</div>
78+
<div class="legend-item">
79+
<span class="legend-color" style="background: #f97316;"></span>
80+
<span class="stat-value">Out: <span id="current-out">0 KB</span></span>
81+
</div>
82+
</div>
83+
<div class="time-pills">
84+
<button class="time-pill" data-value="300">5m</button>
85+
<button class="time-pill" data-value="1800">30m</button>
86+
<button class="time-pill" data-value="all">All</button>
87+
<button class="close-btn" onclick="toggleBandwidthGraph()"></button>
88+
</div>
89+
</div>
90+
</div>
6891

6992
<script src="/app.js"></script>
7093
</body>

public/style.css

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,59 @@ a { color: #4b5563; text-decoration: none; border-bottom: 1px dotted #4b5563; }
8585
color: #333;
8686
margin-top: 1rem;
8787
}
88+
89+
.bandwidth-overlay {
90+
position: fixed;
91+
bottom: 0;
92+
left: 0;
93+
right: 0;
94+
z-index: 999;
95+
background: transparent;
96+
}
97+
.bandwidth-graph-container {
98+
padding: 0.5rem 0 0;
99+
transition: max-height 0.3s ease-in-out, opacity 0.3s ease-in-out;
100+
max-height: 150px;
101+
opacity: 1;
102+
overflow: hidden;
103+
}
104+
.bandwidth-overlay.collapsed .bandwidth-graph-container { max-height: 0; opacity: 0; padding: 0; }
105+
.bandwidth-overlay .close-btn {
106+
position: static;
107+
font-size: 0.65rem;
108+
font-weight: bold;
109+
padding: 0.2rem 0.5rem;
110+
color: #9ca3af;
111+
background: #1a1a1a;
112+
border: 1px solid #333;
113+
border-radius: 3px;
114+
cursor: pointer;
115+
transition: all 0.2s;
116+
outline: none;
117+
}
118+
.bandwidth-overlay .close-btn:hover { color: #cbd5e1; border-color: #4b5563; }
119+
#bandwidthGraph { width: 100%; height: 100px; display: block; background: transparent; }
120+
.bandwidth-footer {
121+
display: flex;
122+
justify-content: space-between;
123+
align-items: center;
124+
padding: 0.5rem 1.5rem 0.75rem;
125+
}
126+
.bandwidth-legend { display: flex; gap: 1.5rem; }
127+
.legend-item { display: flex; align-items: center; gap: 0.4rem; font-size: 0.7rem; }
128+
.legend-color { width: 10px; height: 10px; border-radius: 2px; }
129+
.time-pills { display: flex; gap: 0.3rem; }
130+
.bandwidth-overlay.collapsed .time-pill { display: none; }
131+
.time-pill {
132+
background: transparent;
133+
color: #4b5563;
134+
border: 1px solid #333;
135+
padding: 0.2rem 0.5rem;
136+
font-size: 0.65rem;
137+
border-radius: 3px;
138+
cursor: pointer;
139+
transition: all 0.2s;
140+
outline: none;
141+
}
142+
.time-pill:hover { color: #9ca3af; border-color: #4b5563; }
143+
.time-pill.active { background: #1a1a1a; color: #4ade80; border-color: #4ade80; }

0 commit comments

Comments
 (0)