Skip to content

Commit ceb87c3

Browse files
committed
feat: Stat-Cards um Dauer und Fehlerrate erweitern
Zwei neue Stat-Cards: Letzte Dauer (formatiert) und Fehlerrate (failed/total). Beide mit eigener Sparkline. renderRunSparkline zu renderSparkline(cardId, getValue, opts) generalisiert mit optionalem fixedMin/Max. ep_test_history speichert jetzt auch duration_ms. applyLatestRunToCards befuellt die Cards beim Page-Load aus der History.
1 parent 5e10f00 commit ceb87c3

2 files changed

Lines changed: 93 additions & 16 deletions

File tree

static/js/app.js

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ let currentRunId = null;
66
let eventSource = null;
77
let savedEnvironments = {};
88
let editingEnvName = null;
9+
let runStartTime = null;
910

1011
// SVG-Icons (Feather/Lucide-Stil, currentColor)
1112
const ICON_EDIT = '<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"></path><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5z"></path></svg>';
@@ -37,7 +38,8 @@ document.addEventListener("DOMContentLoaded", () => {
3738
loadReports();
3839
loadScreenshots();
3940
loadJiraConfig();
40-
renderRunSparkline();
41+
applyLatestRunToCards();
42+
renderAllSparklines();
4143
syncThemeToggleState();
4244

4345
// Enter-Taste im URL-Feld startet Tests
@@ -287,6 +289,7 @@ async function runTests() {
287289
setBtnLoading("btnRunTests", true);
288290
document.getElementById("btnCancel").style.display = "";
289291
setConnectionStatus("running");
292+
runStartTime = Date.now();
290293

291294
// Ergebnisse zuruecksetzen
292295
clearResults();
@@ -485,12 +488,24 @@ async function onTestsCompleted(data) {
485488

486489
const total = (data.passed || 0) + (data.failed || 0) + (data.skipped || 0);
487490
const pct = total > 0 ? Math.round(((data.passed || 0) / total) * 100) : 0;
491+
const errorPct = total > 0 ? Math.round(((data.failed || 0) / total) * 100) : 0;
492+
const durationMs = runStartTime ? Date.now() - runStartTime : null;
493+
runStartTime = null;
494+
488495
document.getElementById("lastRunStatus").textContent = `${pct}% bestanden`;
496+
document.getElementById("lastRunDuration").textContent = durationMs ? formatDuration(durationMs) : "--";
497+
document.getElementById("lastRunErrorRate").textContent = total > 0 ? `${errorPct}%` : "--";
489498

490499
if (!cancelled && total > 0) {
491-
addRunToHistory({ pct: pct, passed: data.passed || 0, failed: data.failed || 0, total: total });
500+
addRunToHistory({
501+
pct: pct,
502+
passed: data.passed || 0,
503+
failed: data.failed || 0,
504+
total: total,
505+
duration_ms: durationMs,
506+
});
492507
}
493-
renderRunSparkline();
508+
renderAllSparklines();
494509

495510
stopLiveBrowser();
496511

@@ -1321,14 +1336,34 @@ function addRunToHistory(run) {
13211336
try { localStorage.setItem(HISTORY_KEY, JSON.stringify(list)); } catch (e) {}
13221337
}
13231338

1324-
function renderRunSparkline() {
1325-
const card = document.getElementById("cardLastRun");
1339+
function formatDuration(ms) {
1340+
if (ms == null || isNaN(ms) || ms < 0) return "--";
1341+
const totalSec = Math.round(ms / 1000);
1342+
if (totalSec < 60) return `${totalSec}s`;
1343+
const min = Math.floor(totalSec / 60);
1344+
const sec = totalSec % 60;
1345+
if (min < 60) return `${min}m ${sec.toString().padStart(2, "0")}s`;
1346+
const h = Math.floor(min / 60);
1347+
const m = min % 60;
1348+
return `${h}h ${m.toString().padStart(2, "0")}m`;
1349+
}
1350+
1351+
function renderAllSparklines() {
1352+
renderSparkline("cardLastRun", r => r.pct, { fixedMin: 0, fixedMax: 100, label: "Pass-Rate-Trend", fmt: v => `${Math.round(v)}%` });
1353+
renderSparkline("cardDuration", r => (r.duration_ms != null ? r.duration_ms : null), { label: "Dauer-Trend", fmt: v => formatDuration(v) });
1354+
renderSparkline("cardErrorRate", r => r.total > 0 ? (r.failed / r.total) * 100 : 0, { fixedMin: 0, fixedMax: 100, label: "Fehlerraten-Trend", fmt: v => `${Math.round(v)}%` });
1355+
}
1356+
1357+
function renderSparkline(cardId, getValue, opts = {}) {
1358+
const card = document.getElementById(cardId);
13261359
if (!card) return;
13271360
let svg = card.querySelector(".stat-sparkline");
13281361
let empty = card.querySelector(".stat-sparkline-empty");
1362+
13291363
const history = getRunHistory();
1364+
const values = history.map(getValue).filter(v => v != null && !isNaN(v));
13301365

1331-
if (history.length < 2) {
1366+
if (values.length < 2) {
13321367
if (svg) svg.remove();
13331368
if (!empty) {
13341369
empty = document.createElement("span");
@@ -1341,14 +1376,13 @@ function renderRunSparkline() {
13411376
if (empty) empty.remove();
13421377

13431378
const w = 80, h = 28, pad = 2;
1344-
const pcts = history.map(r => r.pct);
1345-
const min = Math.min(...pcts, 0);
1346-
const max = Math.max(...pcts, 100);
1379+
const min = opts.fixedMin !== undefined ? Math.min(...values, opts.fixedMin) : Math.min(...values);
1380+
const max = opts.fixedMax !== undefined ? Math.max(...values, opts.fixedMax) : Math.max(...values);
13471381
const range = max - min || 1;
1348-
const stepX = (w - pad * 2) / (pcts.length - 1);
1349-
const points = pcts.map((p, i) => {
1382+
const stepX = (w - pad * 2) / (values.length - 1);
1383+
const points = values.map((v, i) => {
13501384
const x = pad + i * stepX;
1351-
const y = h - pad - ((p - min) / range) * (h - pad * 2);
1385+
const y = h - pad - ((v - min) / range) * (h - pad * 2);
13521386
return [x, y];
13531387
});
13541388
const linePath = points.map(([x, y], i) => `${i === 0 ? "M" : "L"}${x.toFixed(1)} ${y.toFixed(1)}`).join(" ");
@@ -1359,15 +1393,33 @@ function renderRunSparkline() {
13591393
svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
13601394
svg.setAttribute("class", "stat-sparkline");
13611395
svg.setAttribute("viewBox", `0 0 ${w} ${h}`);
1362-
svg.setAttribute("aria-label", "Trend der letzten Testläufe");
1396+
svg.setAttribute("aria-label", opts.label || "Trend");
13631397
card.appendChild(svg);
13641398
}
13651399
svg.innerHTML = `
13661400
<path class="area" d="${areaPath}"></path>
13671401
<path class="line" d="${linePath}"></path>
13681402
<circle cx="${last[0].toFixed(1)}" cy="${last[1].toFixed(1)}" r="2.5"></circle>
13691403
`;
1370-
svg.setAttribute("title", history.map(r => `${r.pct}%`).join(" → "));
1404+
const fmt = opts.fmt || (v => v);
1405+
svg.setAttribute("title", values.map(fmt).join(" → "));
1406+
}
1407+
1408+
// Beibehalten als Alias, falls noch irgendwo referenziert
1409+
function renderRunSparkline() {
1410+
renderAllSparklines();
1411+
}
1412+
1413+
// Initial-State der Stat-Cards aus History befüllen
1414+
function applyLatestRunToCards() {
1415+
const history = getRunHistory();
1416+
if (history.length === 0) return;
1417+
const last = history[history.length - 1];
1418+
const total = last.total || 0;
1419+
const failed = last.failed || 0;
1420+
document.getElementById("lastRunStatus").textContent = `${last.pct}% bestanden`;
1421+
document.getElementById("lastRunDuration").textContent = formatDuration(last.duration_ms);
1422+
document.getElementById("lastRunErrorRate").textContent = total > 0 ? `${Math.round((failed / total) * 100)}%` : "--";
13711423
}
13721424

13731425
// ========== Screenshot Drag&Drop ==========

templates/web/index.html

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,15 +193,40 @@ <h2>Chatbot-URL</h2>
193193
<div class="card stat-card" id="cardLastRun">
194194
<div class="stat-icon" aria-hidden="true">
195195
<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
196-
<circle cx="12" cy="12" r="10"></circle>
197-
<polyline points="12 6 12 12 16 14"></polyline>
196+
<polyline points="23 6 13.5 15.5 8.5 10.5 1 18"></polyline>
197+
<polyline points="17 6 23 6 23 12"></polyline>
198198
</svg>
199199
</div>
200200
<div class="stat-content">
201201
<div class="stat-value" id="lastRunStatus">--</div>
202202
<div class="stat-label">Letzter Testlauf</div>
203203
</div>
204204
</div>
205+
<div class="card stat-card" id="cardDuration">
206+
<div class="stat-icon" aria-hidden="true">
207+
<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
208+
<circle cx="12" cy="12" r="10"></circle>
209+
<polyline points="12 6 12 12 16 14"></polyline>
210+
</svg>
211+
</div>
212+
<div class="stat-content">
213+
<div class="stat-value" id="lastRunDuration">--</div>
214+
<div class="stat-label">Letzte Dauer</div>
215+
</div>
216+
</div>
217+
<div class="card stat-card" id="cardErrorRate">
218+
<div class="stat-icon" aria-hidden="true">
219+
<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
220+
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
221+
<line x1="12" y1="9" x2="12" y2="13"></line>
222+
<line x1="12" y1="17" x2="12.01" y2="17"></line>
223+
</svg>
224+
</div>
225+
<div class="stat-content">
226+
<div class="stat-value" id="lastRunErrorRate">--</div>
227+
<div class="stat-label">Fehlerrate</div>
228+
</div>
229+
</div>
205230
</section>
206231

207232
<!-- Live-Browser-Ansicht (MFA / Login) -->

0 commit comments

Comments
 (0)