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>
This commit is contained in:
2026-05-01 20:24:59 +02:00
parent cf51a4620b
commit 175ba3ec7a
4 changed files with 645 additions and 229 deletions

View File

@@ -108,14 +108,83 @@
<div id="result-panel" style="display:none">
<div class="result-header">
<h2>Test Running</h2>
<h2>Test Results</h2>
<span id="result-status" class="badge running">running</span>
</div>
<pre id="output-log"></pre>
<div id="summary-panel" style="display:none">
<h3>Summary</h3>
<div id="summary-cards" class="summary-grid"></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>