diff --git a/internal/gateway/ui/index.html b/internal/gateway/ui/index.html
index 3a42bc7..b6ff288 100644
--- a/internal/gateway/ui/index.html
+++ b/internal/gateway/ui/index.html
@@ -110,6 +110,12 @@ pre.configbox { background:var(--card2); border:1px solid var(--line); border-ra
.kpi .k-sub { font-size:11.5px; color:var(--muted); margin-top:4px; }
.kpi .k-chart { margin-top:8px; position:relative; height:86px; }
.kpi canvas { width:100%; height:86px; display:block; }
+.kpi.kpi-status .k-chart-pie { height:110px; }
+.kpi.kpi-status canvas { height:110px; }
+.status-legend { display:flex; flex-wrap:wrap; gap:5px 9px; margin-top:6px; justify-content:center; }
+.status-legend .lg { display:inline-flex; align-items:center; gap:5px; font-size:11px; color:var(--muted); white-space:nowrap; }
+.status-legend .lg i { width:9px; height:9px; border-radius:2px; flex:0 0 auto; }
+.status-legend .lg b { color:var(--fg); font-variant-numeric:tabular-nums; }
.kpi.kpi-exit { cursor:pointer; border-color:var(--err); }
.kpi.kpi-exit:hover { border-color:var(--err); box-shadow:0 0 0 3px rgba(244,67,54,.15); }
.kpi.kpi-exit .k-lab::after { content:'×'; color:var(--err); font-weight:800; font-size:15px; margin-left:auto; }
@@ -783,10 +789,9 @@ async function paintStats() {
-
${t('dashStatus')}
-
${fmtN((st.by_status||[]).reduce((a,x)=>a+x.reqs,0))}
-
-
+
${t('dashStatus')}
+
+
`;
paintModelTable(st.by_model || []);
paintSrcTable(st.by_source || []);
@@ -832,11 +837,13 @@ function paintSrcTable(rows) {
${fmtMs(fmtLat(r.latency_sum_ms, r.reqs))} | ${fmtMs(r.latency_max_ms)} | `).join('') + '';
}
function paintStatusTable(rows) {
+ const total = rows.reduce((m, x) => m + x.reqs, 0) || 1;
+ const totalEl = $('#tb-status-total');
+ if (totalEl) totalEl.textContent = fmtN(total) + ' ' + t('thReqs');
const el = $('#tb-status'); if (!el) return;
if (!rows.length) { el.innerHTML = ''; return; }
- const total = rows.reduce((m, x) => m + x.reqs, 0) || 1;
- const palette = ['#3fb27f', '#4fa3d8', '#f0a45c', '#e06c6c', '#b06ce0', '#8a9bb5'];
- el.innerHTML = rows.map((r, i) => `
+ const palette = ['#3fb27f', '#4fa3d8', '#f0a45c', '#e06c6c', '#b06ce0', '#8a9bb5', '#6fbfa0', '#d0a45c'];
+ el.innerHTML = rows.map((r, i) => `
${esc(r.name)}${fmtN(r.reqs)}`).join('');
}
// sparkline: turn a value array into a compact polyline (area fill under accent)
@@ -931,18 +938,34 @@ function drawStatusChart(rows) {
const ctx = cv.getContext('2d');
const dpr = window.devicePixelRatio || 1;
const parent = cv.parentElement, rect = parent.getBoundingClientRect();
- cv.width = rect.width * dpr; cv.height = 86 * dpr; ctx.scale(dpr, dpr);
- ctx.clearRect(0, 0, cv.width, cv.height);
- // stacked bar of request counts by status code
- const total = rows.reduce((a,x)=>a+x.reqs,0) || 1;
- const palette = ['#3fb27f','#4fa3d8','#f0a45c','#e06c6c','#b06ce0','#8a9bb5'];
- let x = 0;
- rows.forEach((r,i) => {
- const w = Math.max(2, r.reqs / total * rect.width);
- ctx.fillStyle = palette[i%palette.length];
- ctx.fillRect(x, 0, w, 86);
- x += w;
+ const H = rect.height || 110;
+ cv.width = rect.width * dpr; cv.height = H * dpr; ctx.scale(dpr, dpr);
+ ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
+ const total = (rows||[]).reduce((a,x)=>a+x.reqs,0);
+ if (!total) return;
+ const palette = ['#3fb27f', '#4fa3d8', '#f0a45c', '#e06c6c', '#b06ce0', '#8a9bb5', '#6fbfa0', '#d0a45c'];
+ const r = Math.min(rect.width, H) / 2 - 2;
+ const cx = rect.width / 2, cy = H / 2;
+ let a0 = -Math.PI / 2;
+ rows.forEach((row, i) => {
+ const sweep = row.reqs / total * Math.PI * 2;
+ ctx.beginPath();
+ ctx.moveTo(cx, cy);
+ ctx.arc(cx, cy, r, a0, a0 + sweep);
+ ctx.closePath();
+ ctx.fillStyle = palette[i % palette.length];
+ ctx.fill();
+ ctx.strokeStyle = 'var(--card)'; ctx.lineWidth = 1.5; ctx.stroke();
+ a0 += sweep;
});
+ // center label = total
+ ctx.fillStyle = 'var(--fg)';
+ ctx.font = '700 16px ui-monospace,Menlo,Consolas,monospace';
+ ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
+ ctx.fillText(fmtN(total), cx, cy - 7);
+ ctx.fillStyle = 'var(--muted)';
+ ctx.font = '10px ui-monospace,Menlo,Consolas,monospace';
+ ctx.fillText(t('thReqs'), cx, cy + 9);
}
function paintKeyTable(rows, keyNames) {
const el = $('#tb-key'); if (!el) return;