'use strict'; // ─── Tab switching ──────────────────────────────────────────────────────────── document.querySelectorAll('.tab-btn').forEach(btn => { btn.addEventListener('click', () => { const tab = btn.dataset.tab; document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); btn.classList.add('active'); document.getElementById(`tab-${tab}`).classList.add('active'); if (tab === 'history') loadHistory(); }); }); // ─── Sync range ↔ number ────────────────────────────────────────────────────── function syncRangeNumber(rangeId, numId) { const range = document.getElementById(rangeId); const num = document.getElementById(numId); range.addEventListener('input', () => num.value = range.value); num.addEventListener('input', () => range.value = num.value); } syncRangeNumber('vus', 'vus-num'); syncRangeNumber('duration', 'duration-num'); // ─── Show/hide body for non-GET ─────────────────────────────────────────────── const methodSel = document.getElementById('httpMethod'); const bodyGroup = document.getElementById('body-group'); methodSel.addEventListener('change', () => { bodyGroup.style.display = (methodSel.value !== 'GET' && methodSel.value !== 'HEAD') ? '' : 'none'; }); // ─── gzip hint ──────────────────────────────────────────────────────────────── const gzipChk = document.getElementById('gzip'); const gzipHint = document.getElementById('gzip-hint'); gzipChk.addEventListener('change', () => { gzipHint.textContent = gzipChk.checked ? 'k6 default — server may compress response' : 'Sends Accept-Encoding: identity — forces uncompressed response'; }); // ─── Device hint ───────────────────────────────────────────────────────────── const uaHint = document.getElementById('ua-hint'); document.querySelectorAll('input[name="device"]').forEach(radio => { radio.addEventListener('change', () => { uaHint.textContent = radio.value === 'mobile' ? 'iPhone Safari 17 (iOS 17.4)' : 'Chrome 124 on Windows'; }); }); // ─── Gauge engine ───────────────────────────────────────────────────────────── const CIRC = 2 * Math.PI * 50; // r=50 → 314.16 function setGauge(id, pct, color) { const el = document.getElementById(id); if (!el) return; const ring = el.querySelector('.gauge-ring'); const val = el.querySelector('.gauge-value'); const clamped = Math.max(0, Math.min(100, pct)); ring.style.strokeDasharray = CIRC; ring.style.strokeDashoffset = CIRC * (1 - clamped / 100); ring.style.stroke = color; val.textContent = Math.round(clamped); } function gaugeColor(val, good, warn) { if (val >= good) return 'var(--green)'; if (val >= warn) return 'var(--yellow)'; return 'var(--red)'; } function fmtMs(ms) { if (!ms || ms === 0) return '--'; return ms >= 1000 ? (ms / 1000).toFixed(2) + 's' : Math.round(ms) + 'ms'; } function setStat(id, val) { const el = document.getElementById(id); if (el) el.querySelector('.sc-val').textContent = val; } function updateGauges(m) { if (!m || m.totalReqs === 0) return; setGauge('gauge-score', m.score, gaugeColor(m.score, 90, 50)); setGauge('gauge-checks', m.checksRate, gaugeColor(m.checksRate,98, 90)); setGauge('gauge-http', m.httpOkRate, gaugeColor(m.httpOkRate,99, 95)); setStat('sc-reqs', m.totalReqs.toLocaleString()); setStat('sc-rps', m.reqPerSec + '/s'); setStat('sc-avg', fmtMs(m.avg)); 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'); if (p95cell) { p95cell.querySelector('.sc-val').style.color = m.p95 < 1000 ? 'var(--green)' : m.p95 < 3000 ? 'var(--yellow)' : 'var(--red)'; } } 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); if (!el) return; const ring = el.querySelector('.gauge-ring'); ring.style.strokeDasharray = CIRC; ring.style.strokeDashoffset = CIRC; ring.style.stroke = 'var(--border)'; el.querySelector('.gauge-value').textContent = '--'; }); ['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) { const tb = document.getElementById('threshold-banner'); if (!tb) return; const thresholds = summary.thresholds; if (!thresholds || !thresholds.length) return; const allPass = thresholds.every(t => t.pass); tb.className = `threshold-banner ${allPass ? 'pass' : 'fail'}`; tb.innerHTML = `${allPass ? '✓ All thresholds passed' : '✗ Some thresholds failed'}
Failed to load history.
'; return; } if (!tests.length) { container.innerHTML = 'No tests yet.
'; return; } container.innerHTML = ''; for (const t of tests) { const item = document.createElement('div'); item.className = 'history-item'; item.dataset.id = t.id; const date = new Date(t.created_at).toLocaleString(); const dur = t.finished_at ? `${Math.round((t.finished_at - t.created_at) / 1000)}s` : `${t.duration}s`; // Score mini-gauge from saved live data const live = t.summary && t.summary.live; const scorePct = live ? live.score : null; const scoreColor = scorePct === null ? 'var(--border)' : scorePct >= 90 ? 'var(--green)' : scorePct >= 50 ? 'var(--yellow)' : 'var(--red)'; const miniGauge = ` `; let metaLine = ''; if (live) { metaLine = `${live.totalReqs.toLocaleString()} reqs · ${live.reqPerSec}/s · p95 ${fmtMs(live.p95)}`; } else if (t.summary && t.summary.http_reqs) { metaLine = `${t.summary.http_reqs[0]} reqs @ ${t.summary.http_reqs[1]}`; } const deviceBadge = t.device === 'mobile' ? '📱 mobile' : '🖥 desktop'; const cacheBadge = t.cache_mode === 'bust' ? 'cache-bust' : t.cache_mode === 'no-cache' ? 'no-cache' : 'cached'; const gzipBadge = t.gzip ? 'gzip' : 'no-gzip'; item.innerHTML = `${escHtml(test.output || '(no output)')}