fix: broadcast k6 stdout summary and show full stats

- Remove --out json=- (was flooding stdout and hiding summary)
- Broadcast both stdout (summary table) and stderr (live warnings)
- Add --summary-trend-stats with p(90)/p(95)/p(99)
- Parse checks, failed count/total, data received, all percentiles
- Strip ANSI codes before regex matching
- Add threshold pass/fail banner with per-threshold status
- Show all duration percentiles as individual cards

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-01 19:46:39 +02:00
parent 6848d69a9d
commit 0cd0f96bab
3 changed files with 85 additions and 23 deletions

View File

@@ -130,31 +130,63 @@ function renderSummary(summary) {
summaryPanel.style.display = '';
summaryCards.innerHTML = '';
// ---- Threshold pass/fail banner ----
if (summary.thresholds && summary.thresholds.length) {
const allPass = summary.thresholds.every(t => t.pass);
const banner = document.createElement('div');
banner.className = `threshold-banner ${allPass ? 'pass' : 'fail'}`;
banner.innerHTML = `<strong>${allPass ? '✓ All thresholds passed' : '✗ Some thresholds failed'}</strong>
<ul>${summary.thresholds.map(t =>
`<li class="${t.pass ? 'pass' : 'fail'}">${t.pass ? '✓' : '✗'} ${escHtml(t.label)}</li>`
).join('')}</ul>`;
summaryCards.appendChild(banner);
}
// ---- Metric cards ----
const cards = [];
if (summary.http_reqs) {
cards.push({ label: 'Total Requests', value: summary.http_reqs[0], cls: 'info' });
cards.push({ label: 'Req/s', value: summary.http_reqs[1], cls: 'info' });
}
if (summary.http_req_duration) {
cards.push({ label: 'Avg Duration', value: summary.http_req_duration[0], cls: 'info' });
cards.push({ label: 'p(95) Duration', value: summary.http_req_duration[5], cls: 'info' });
cards.push({ label: 'Max Duration', value: summary.http_req_duration[3], cls: 'info' });
cards.push({ label: 'Throughput', value: summary.http_reqs[1], cls: 'info' });
}
if (summary.http_req_failed) {
const pct = parseFloat(summary.http_req_failed[0]);
cards.push({ label: 'Failure Rate', value: summary.http_req_failed[0], cls: pct > 5 ? 'bad' : 'good' });
const detail = summary.http_req_failed[1] != null
? ` (${summary.http_req_failed[1]}/${summary.http_req_failed[2]})`
: '';
cards.push({ label: 'Failed Requests', value: summary.http_req_failed[0] + detail, cls: pct > 0 ? 'bad' : 'good' });
}
if (summary.checks) {
const pct = parseFloat(summary.checks[0]);
cards.push({ label: 'Checks Passed', value: `${summary.checks[0]} (${summary.checks[1]}/${summary.checks[2]})`, cls: pct < 100 ? 'bad' : 'good' });
}
if (summary.http_req_duration) {
const d = summary.http_req_duration;
// d = [avg, min, med, max, p90, p95, p99]
cards.push({ label: 'Avg Response', value: d[0], cls: 'info' });
cards.push({ label: 'Median (p50)', value: d[2], cls: 'info' });
cards.push({ label: 'p(90)', value: d[4], cls: 'info' });
cards.push({ label: 'p(95)', value: d[5], cls: 'info' });
cards.push({ label: 'p(99)', value: d[6], cls: 'info' });
cards.push({ label: 'Max Response', value: d[3], cls: 'info' });
}
if (summary.iterations) {
cards.push({ label: 'Iterations', value: summary.iterations[0], cls: 'info' });
cards.push({ label: 'Iter/s', value: summary.iterations[1], cls: 'info' });
}
if (summary.data_received) {
cards.push({ label: 'Data Received', value: summary.data_received[0], cls: 'info' });
}
const grid = document.createElement('div');
grid.className = 'summary-grid';
for (const c of cards) {
const div = document.createElement('div');
div.className = 'summary-card';
div.innerHTML = `<div class="label">${c.label}</div><div class="value ${c.cls}">${c.value}</div>`;
summaryCards.appendChild(div);
grid.appendChild(div);
}
summaryCards.appendChild(grid);
}
// ---- History ----

View File

@@ -150,10 +150,25 @@ textarea { font-family: var(--mono); resize: vertical; }
color: #a5f3fc;
}
/* THRESHOLD BANNER */
.threshold-banner {
border-radius: 8px;
padding: .85rem 1rem;
margin-bottom: 1rem;
border: 1px solid;
}
.threshold-banner.pass { background: rgba(34,197,94,.08); border-color: rgba(34,197,94,.3); color: var(--green); }
.threshold-banner.fail { background: rgba(239,68,68,.08); border-color: rgba(239,68,68,.3); color: var(--red); }
.threshold-banner ul { list-style: none; margin-top: .4rem; padding: 0; }
.threshold-banner li { font-size: .85rem; padding: .15rem 0; font-family: var(--mono); }
.threshold-banner li.pass { color: var(--green); }
.threshold-banner li.fail { color: var(--red); }
/* SUMMARY CARDS */
#summary-cards { margin-top: .5rem; }
.summary-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
gap: 1rem;
margin-top: .75rem;
}