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 @@
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 `
+
+
+
+
+ `;
+ }).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;