From ab7a83c50d34acbd82bf7b1727dbdc584f2f0b35 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Sat, 14 Mar 2026 11:34:15 -0700 Subject: [PATCH] feat: stacked area chart, framework breakdown on active sessions, tool bar visualization --- cmd/web-ui/static/app.js | 56 +++++++++++++++++++++++++++---------- cmd/web-ui/static/style.css | 21 ++++++++++++++ 2 files changed, 62 insertions(+), 15 deletions(-) diff --git a/cmd/web-ui/static/app.js b/cmd/web-ui/static/app.js index f9d2a3d..9e0ada6 100644 --- a/cmd/web-ui/static/app.js +++ b/cmd/web-ui/static/app.js @@ -943,6 +943,7 @@
Active Sessions
-
+
Runs Today
@@ -1133,6 +1134,14 @@ if (errEl) { errEl.classList.toggle('has-errors', s.errors_today > 0); } + + const subEl = document.getElementById('dash-active-sub'); + if (subEl && s.by_framework) { + const parts = Object.entries(s.by_framework) + .filter(([, v]) => v.runs > 0) + .map(([name, v]) => escapeHTML(name) + ' ' + v.runs); + subEl.textContent = parts.length > 0 ? parts.join(' / ') : ''; + } } async function loadTimeseries() { @@ -1154,11 +1163,15 @@ function buildChartData() { const ts = dashboardState.timeseries; if (!ts || !ts.series || ts.series.length === 0) return null; + // Stacked: errors on bottom, then tools, then runs on top + const errors = ts.series.map(b => b.errors); + const tools = ts.series.map((b, i) => b.tools + errors[i]); + const runs = ts.series.map((b, i) => b.runs + tools[i]); return [ ts.series.map(b => Math.floor(new Date(b.ts).getTime() / 1000)), - ts.series.map(b => b.runs), - ts.series.map(b => b.tools), - ts.series.map(b => b.errors), + runs, + tools, + errors, ]; } @@ -1211,22 +1224,26 @@ { label: 'Runs', stroke: '#34d399', - width: 2, - fill: 'rgba(52, 211, 153, 0.08)', + width: 1.5, + fill: 'rgba(52, 211, 153, 0.15)', }, { label: 'Tools', stroke: '#22d3ee', - width: 2, - fill: 'rgba(34, 211, 238, 0.08)', + width: 1.5, + fill: 'rgba(34, 211, 238, 0.15)', }, { label: 'Errors', stroke: '#f87171', - width: 2, - fill: 'rgba(248, 113, 113, 0.08)', + width: 1.5, + fill: 'rgba(248, 113, 113, 0.2)', }, ], + bands: [ + { series: [1, 2], fill: 'rgba(52, 211, 153, 0.15)' }, + { series: [2, 3], fill: 'rgba(34, 211, 238, 0.15)' }, + ], }; dashboardChart = new uPlot(opts, data, container); @@ -1354,12 +1371,21 @@ return; } - list.innerHTML = topTools.map(([name, count]) => ` -
  • - ${escapeHTML(name)} - ${count} -
  • - `).join(''); + const maxCount = topTools[0][1]; + list.innerHTML = topTools.map(([name, count]) => { + const pct = maxCount > 0 ? (count / maxCount * 100) : 0; + return ` +
  • +
    + ${escapeHTML(name)} + ${count} +
    +
    +
    +
    +
  • + `; + }).join(''); } route(); diff --git a/cmd/web-ui/static/style.css b/cmd/web-ui/static/style.css index e01c213..47e0de5 100644 --- a/cmd/web-ui/static/style.css +++ b/cmd/web-ui/static/style.css @@ -889,6 +889,27 @@ tr.clickable:hover td:first-child { border-radius: 4px; } +.stat-list-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.stat-list-bar-track { + height: 3px; + background: var(--surface-2); + border-radius: 2px; + margin-top: 0.3rem; + overflow: hidden; +} + +.stat-list-bar-fill { + height: 100%; + background: var(--accent); + border-radius: 2px; + transition: width 0.3s ease; +} + .event-icon { width: 18px; height: 18px;