diff --git a/public/app.js b/public/app.js index 24e56f3..bdfe1bc 100644 --- a/public/app.js +++ b/public/app.js @@ -89,6 +89,12 @@ const animate = () => { const openDiagnostics = () => { document.getElementById("diagnosticsModal").classList.add("active"); + // Ensure bandwidth graph is correctly sized and drawn after modal opens + setTimeout(() => { + if (typeof resizeBandwidthCanvas === "function") { + resizeBandwidthCanvas(); + } + }, 50); }; const closeDiagnostics = () => { @@ -587,7 +593,6 @@ terminalInput.addEventListener("keypress", async (e) => { } } - let scope = "GLOBAL"; let target = null; @@ -686,6 +691,21 @@ terminalInput.addEventListener("keypress", async (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) => { @@ -796,14 +816,19 @@ evtSource.onmessage = (event) => { d.invalidPoW.toLocaleString(); document.getElementById("diag-invalid-sig").innerText = d.invalidSig.toLocaleString(); - 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(); + + if (typeof addBandwidthData === "function") { + addBandwidthData(d.bytesReceived, d.bytesRelayed); + drawBandwidthGraph(); + document.getElementById("current-in").innerText = formatBandwidth( + d.bytesReceived + ); + document.getElementById("current-out").innerText = formatBandwidth( + d.bytesRelayed + ); + } } }; @@ -817,6 +842,135 @@ countEl.classList.add("loaded"); updateParticles(initialCount); animate(); +const bandwidthHistory = { timestamps: [], bytesIn: [], bytesOut: [] }; +let selectedTimeRange = 300; +const bandwidthCanvas = document.getElementById("bandwidthGraph"); +const bandwidthCtx = bandwidthCanvas ? bandwidthCanvas.getContext("2d") : null; +const bandwidthOverlay = document.getElementById("bandwidthOverlay"); + +function resizeBandwidthCanvas() { + if (!bandwidthCanvas) return; + const rect = bandwidthCanvas.getBoundingClientRect(); + bandwidthCanvas.width = rect.width; + bandwidthCanvas.height = rect.height; + drawBandwidthGraph(); +} + +window.addEventListener("resize", resizeBandwidthCanvas); +setTimeout(resizeBandwidthCanvas, 100); + +const toggleBandwidthGraph = () => { + if (!bandwidthOverlay) return; + bandwidthOverlay.classList.toggle("collapsed"); + const closeBtn = document.querySelector(".bandwidth-overlay .close-btn"); + if (closeBtn) { + closeBtn.textContent = bandwidthOverlay.classList.contains("collapsed") + ? "+" + : "−"; + } + // Redraw after transition + setTimeout(resizeBandwidthCanvas, 350); +}; + +window.toggleBandwidthGraph = toggleBandwidthGraph; + +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(); + }); +}); + +if (timePills.length > 0) { + 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 = () => { + if (!bandwidthCanvas || !bandwidthCtx) return; + 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"); +}; + const themes = [ "hypermind.css", "hypermind-classic.css", @@ -843,11 +997,11 @@ function showThemeNotification(themeName) { notification.classList.remove("hidden"); notification.offsetHeight; - + notification.classList.add("show"); if (notificationTimeout) clearTimeout(notificationTimeout); - + notificationTimeout = setTimeout(() => { notification.classList.remove("show"); setTimeout(() => { diff --git a/public/index.html b/public/index.html index 24ddb76..53d0fec 100644 --- a/public/index.html +++ b/public/index.html @@ -128,18 +128,41 @@ Invalid Signatures 0 -
- Bandwidth In - 0 KB -
-
- Bandwidth Out - 0 KB -
LEAVE Messages 0
+ +
+
Bandwidth
+
+ +
+ +
last 10 seconds
diff --git a/public/style.css b/public/style.css index 3673fd0..13cb04e 100644 --- a/public/style.css +++ b/public/style.css @@ -156,6 +156,62 @@ a { color: var(--color-text-anchor-link); text-decoration: none; border-bottom: margin-top: 1rem; } +.bandwidth-overlay { + padding-top: .5rem; +} +.bandwidth-title { + font-size: 0.85rem; + color: var(--color-modal-stat-label); + margin-bottom: 0.5rem; + letter-spacing: 0.5px; +} +.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 0 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; } .theme-btn { position: fixed; bottom: 1.5rem;