diff --git a/backend/server.js b/backend/server.js index 669da12..2116ebf 100644 --- a/backend/server.js +++ b/backend/server.js @@ -171,12 +171,13 @@ app.post('/api/tests', (req, res) => { // Live metric aggregation state const live = { - durations: [], // http_req_duration values (ms) - p90durations: [], // sampled for percentile calc + durations: [], totalReqs: 0, failedReqs: 0, checksTotal: 0, checksPassed: 0, + bytesIn: 0, // data_received (bytes) + bytesOut: 0, // data_sent (bytes) startTime: Date.now(), lastBroadcast: 0, }; @@ -317,6 +318,10 @@ function ingestPoint(obj, live) { } else if (metric === 'checks') { live.checksTotal++; if (data.value === 1) live.checksPassed++; + } else if (metric === 'data_received') { + live.bytesIn += data.value; + } else if (metric === 'data_sent') { + live.bytesOut += data.value; } } @@ -334,6 +339,12 @@ function computeLiveMetrics(live) { const p95 = pct(95); const score = computeScore({ p95, httpErrRate, checksRate }); + const bwInMBps = +(live.bytesIn / elapsed / 1048576).toFixed(2); // MB/s + const bwOutMBps = +(live.bytesOut / elapsed / 1048576).toFixed(2); + const avgRespKB = live.totalReqs > 0 + ? +(live.bytesIn / live.totalReqs / 1024).toFixed(1) + : 0; + return { totalReqs: live.totalReqs, reqPerSec: +(live.totalReqs / elapsed).toFixed(1), @@ -349,6 +360,9 @@ function computeLiveMetrics(live) { p99: +pct(99).toFixed(1), max: sorted.length ? +sorted[sorted.length - 1].toFixed(1) : 0, score, + bwInMBps, + bwOutMBps, + avgRespKB, }; } diff --git a/frontend/app.js b/frontend/app.js index 8a659c2..f56ffc3 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -92,6 +92,7 @@ function updateGauges(m) { setStat('sc-p90', fmtMs(m.p90)); setStat('sc-p95', fmtMs(m.p95)); setStat('sc-p99', fmtMs(m.p99)); + updateBandwidth(m); // colour p95 cell based on threshold (5 s) const p95cell = document.getElementById('sc-p95'); @@ -101,6 +102,23 @@ function updateGauges(m) { } } +function updateBandwidth(m) { + const inEl = document.getElementById('bw-in'); + const outEl = document.getElementById('bw-out'); + const sizeEl = document.getElementById('bw-size'); + const warn = document.getElementById('bw-warning'); + if (!inEl) return; + + inEl.textContent = m.bwInMBps + ' MB/s'; + outEl.textContent = m.bwOutMBps + ' MB/s'; + sizeEl.textContent = m.avgRespKB + ' KB'; + + // Flag if inbound bandwidth looks high (>20 MB/s = ~160 Mbps — notable) + const high = m.bwInMBps > 20; + inEl.style.color = m.bwInMBps > 50 ? 'var(--red)' : m.bwInMBps > 20 ? 'var(--yellow)' : 'var(--green)'; + warn.style.display = high ? '' : 'none'; +} + function resetGauges() { ['gauge-score','gauge-checks','gauge-http'].forEach(id => { const el = document.getElementById(id); @@ -114,6 +132,14 @@ function resetGauges() { ['sc-reqs','sc-rps','sc-avg','sc-p90','sc-p95','sc-p99'].forEach(id => setStat(id, '--')); const tb = document.getElementById('threshold-banner'); if (tb) { tb.style.display = 'none'; tb.innerHTML = ''; } + const bwIn = document.getElementById('bw-in'); + if (bwIn) { bwIn.textContent = '-- MB/s'; bwIn.style.color = ''; } + const bwOut = document.getElementById('bw-out'); + if (bwOut) bwOut.textContent = '-- MB/s'; + const bwSize = document.getElementById('bw-size'); + if (bwSize) bwSize.textContent = '-- KB'; + const bwWarn = document.getElementById('bw-warning'); + if (bwWarn) bwWarn.style.display = 'none'; } function renderThresholds(summary) { diff --git a/frontend/index.html b/frontend/index.html index f33cfc5..75ce88a 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -176,6 +176,27 @@ + +
+
+ ↓ RX + -- MB/s +
+
+
+ ↑ TX + -- MB/s +
+
+
+ Avg size + -- KB +
+ +
+ diff --git a/frontend/style.css b/frontend/style.css index fd53ecf..d71e76a 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -307,6 +307,42 @@ textarea { font-family: var(--mono); resize: vertical; } letter-spacing: .05em; } +/* ── BANDWIDTH ROW ──────────────────────────────────────────────────────────── */ +.bw-row { + display: flex; + align-items: center; + gap: 1rem; + padding: .8rem 1rem; + margin-top: .75rem; + background: var(--bg); + border: 1px solid var(--border); + border-radius: 8px; + flex-wrap: wrap; +} +.bw-item { display: flex; align-items: center; gap: .5rem; } +.bw-label { + font-size: .72rem; + text-transform: uppercase; + letter-spacing: .06em; + color: var(--muted); +} +.bw-val { + font-size: .95rem; + font-weight: 700; + font-family: var(--mono); + transition: color .4s; +} +.bw-divider { width: 1px; height: 1.2rem; background: var(--border); flex-shrink: 0; } +.bw-warning { + font-size: .78rem; + color: var(--yellow); + background: rgba(245,158,11,.08); + border: 1px solid rgba(245,158,11,.25); + border-radius: 6px; + padding: .25rem .65rem; + margin-left: auto; +} + /* ── THRESHOLD BANNER ───────────────────────────────────────────────────────── */ #threshold-banner { border-radius: 8px;