Files
six.k/frontend/index.html
Malin 175ba3ec7a feat: animated live gauge circles (PageSpeed-style)
Backend:
- Re-add --out json=- but properly route: JSON lines → metric
  aggregation, non-JSON lines → text log (summary text lands in log)
- Aggregate http_req_duration / http_reqs / http_req_failed / checks
  per test into live state (capped 50k samples)
- Broadcast { metrics: {...} } SSE events every second with
  score, checksRate, httpOkRate, p90/p95/p99, req/s etc.
- computeScore() composite 0-100 based on p95 + error rate

Frontend:
- 3 animated SVG ring gauges: Score (/100), Checks OK (%), HTTP OK (%)
- Smooth CSS transition on stroke-dashoffset (0.7s cubic-bezier)
- Pulse animation while test is live, stops on completion
- Color: green ≥90/98/99, yellow mid-range, red below thresholds
- Stats strip: Requests, Req/s, Avg, p(90), p(95), p(99)
- p(95) cell color-coded green/yellow/red vs latency
- Threshold pass/fail banner at bottom of gauges panel
- Raw output collapsed in <details> by default
- History items show mini score ring gauge (44px) inline
- History detail expands 3 medium gauges + stat grid

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 20:24:59 +02:00

206 lines
7.8 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>k6 Load Tester</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="app">
<header>
<h1>&#9889; k6 Load Tester</h1>
<nav>
<button class="tab-btn active" data-tab="run">Run Test</button>
<button class="tab-btn" data-tab="history">History</button>
</nav>
</header>
<!-- RUN TAB -->
<section id="tab-run" class="tab active">
<form id="test-form">
<div class="form-grid">
<div class="form-group full">
<label for="url">Target URL</label>
<input type="url" id="url" placeholder="https://example.com" required />
</div>
<div class="form-group">
<label for="httpMethod">HTTP Method</label>
<select id="httpMethod">
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="PATCH">PATCH</option>
<option value="DELETE">DELETE</option>
<option value="HEAD">HEAD</option>
</select>
</div>
<div class="form-group">
<label for="vus">Virtual Users (VUs)</label>
<div class="range-row">
<input type="range" id="vus" min="1" max="500" value="10" />
<input type="number" id="vus-num" min="1" max="500" value="10" />
</div>
</div>
<div class="form-group">
<label for="duration">Duration (seconds)</label>
<div class="range-row">
<input type="range" id="duration" min="5" max="300" value="30" />
<input type="number" id="duration-num" min="5" max="300" value="30" />
</div>
</div>
<div class="form-group">
<label for="rpsLimit">Max RPS <span class="hint">(0 = unlimited)</span></label>
<input type="number" id="rpsLimit" min="0" value="0" />
</div>
<div class="form-group">
<label for="cacheMode">Cache Mode</label>
<select id="cacheMode">
<option value="normal">Normal (allow cache)</option>
<option value="no-cache">No-cache headers (revalidate)</option>
<option value="bust">Cache-bust URL + headers (full bypass)</option>
</select>
</div>
<div class="form-group">
<label for="device">Device</label>
<div class="device-toggle">
<input type="radio" name="device" id="device-desktop" value="desktop" checked />
<label for="device-desktop" class="device-btn">&#x1F5A5; Desktop</label>
<input type="radio" name="device" id="device-mobile" value="mobile" />
<label for="device-mobile" class="device-btn">&#128241; Mobile</label>
</div>
<div class="hint" id="ua-hint">Chrome 124 on Windows</div>
</div>
<div class="form-group">
<label>Encoding</label>
<div class="toggle-row">
<label class="toggle">
<input type="checkbox" id="gzip" checked />
<span class="toggle-track"></span>
<span class="toggle-label">Accept gzip / br</span>
</label>
</div>
<div class="hint" id="gzip-hint">k6 default — server may compress response</div>
</div>
<div class="form-group full" id="body-group" style="display:none">
<label for="requestBody">Request Body</label>
<textarea id="requestBody" rows="4" placeholder='{"key": "value"}'></textarea>
</div>
<div class="form-group full">
<label for="headers">
Custom Headers <span class="hint">(JSON, optional — merged with above options)</span>
</label>
<input type="text" id="headers" placeholder='{"Authorization": "Bearer token"}' />
</div>
</div>
<button type="submit" id="start-btn" class="btn-primary">&#9654; Run Test</button>
</form>
<div id="result-panel" style="display:none">
<div class="result-header">
<h2>Test Results</h2>
<span id="result-status" class="badge running">running</span>
</div>
<!-- Gauge circles -->
<div id="gauges-panel">
<div class="gauges-main">
<div class="gauge-wrap" id="gauge-score">
<svg viewBox="0 0 120 120" class="gauge-svg" aria-label="Performance score">
<circle class="gauge-track" cx="60" cy="60" r="50"/>
<circle class="gauge-ring" cx="60" cy="60" r="50"/>
</svg>
<div class="gauge-inner">
<span class="gauge-value" aria-live="polite">--</span>
<span class="gauge-sub">/100</span>
<span class="gauge-title">Score</span>
</div>
</div>
<div class="gauge-wrap" id="gauge-checks">
<svg viewBox="0 0 120 120" class="gauge-svg" aria-label="Checks passed">
<circle class="gauge-track" cx="60" cy="60" r="50"/>
<circle class="gauge-ring" cx="60" cy="60" r="50"/>
</svg>
<div class="gauge-inner">
<span class="gauge-value" aria-live="polite">--</span>
<span class="gauge-sub">%</span>
<span class="gauge-title">Checks OK</span>
</div>
</div>
<div class="gauge-wrap" id="gauge-http">
<svg viewBox="0 0 120 120" class="gauge-svg" aria-label="HTTP success rate">
<circle class="gauge-track" cx="60" cy="60" r="50"/>
<circle class="gauge-ring" cx="60" cy="60" r="50"/>
</svg>
<div class="gauge-inner">
<span class="gauge-value" aria-live="polite">--</span>
<span class="gauge-sub">%</span>
<span class="gauge-title">HTTP OK</span>
</div>
</div>
</div>
<!-- Live stat strip -->
<div class="stats-strip" id="stats-strip">
<div class="stat-cell" id="sc-reqs">
<span class="sc-val">--</span><span class="sc-lbl">Requests</span>
</div>
<div class="stat-cell" id="sc-rps">
<span class="sc-val">--</span><span class="sc-lbl">Req / s</span>
</div>
<div class="stat-cell" id="sc-avg">
<span class="sc-val">--</span><span class="sc-lbl">Avg</span>
</div>
<div class="stat-cell" id="sc-p90">
<span class="sc-val">--</span><span class="sc-lbl">p(90)</span>
</div>
<div class="stat-cell" id="sc-p95">
<span class="sc-val">--</span><span class="sc-lbl">p(95)</span>
</div>
<div class="stat-cell" id="sc-p99">
<span class="sc-val">--</span><span class="sc-lbl">p(99)</span>
</div>
</div>
<!-- Threshold results -->
<div id="threshold-banner" style="display:none"></div>
</div>
<!-- Raw log (collapsible) -->
<details id="log-details">
<summary>Raw Output</summary>
<pre id="output-log"></pre>
</details>
</div>
</section>
<!-- HISTORY TAB -->
<section id="tab-history" class="tab">
<div class="history-header">
<h2>Test History</h2>
<button id="refresh-history" class="btn-secondary">&#8635; Refresh</button>
</div>
<div id="history-list">
<p class="empty">No tests yet.</p>
</div>
</section>
</div>
<script src="app.js"></script>
</body>
</html>