From ac0caf9e0c2ed979806fa2fe5d5b85d14cc2a4a0 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 11 Aug 2026 16:58:44 +0800 Subject: [PATCH] fix(ui): status block = pie chart (no labels) with color legend below - rewrite drawStatusChart to a Canvas pie (no per-slice labels), center shows total requests - status card: title + total caption + tall pie area (110px) + color legend row below (swatch + code + count), each item has title tooltip with ok/err - store status swatch palette and reuse across pie/legend for color consistency - deployed (bak .bak.20260811m), server active --- internal/gateway/ui/index.html | 59 +++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 18 deletions(-) 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;