From 82f90d511472f36fc173d23843928ec6353fc0c1 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 11 Aug 2026 16:51:58 +0800 Subject: [PATCH] fix(ui): unify five KPI blocks into identical cards with canvas placeholders - unify all five metric boxes into identical .kpi cards (same height, padding, flex layout, canvas content area) - replace old numeric KPI blocks with uniform content areas - move status donut into 5th KPI slot (already done), now uses same .kpi-card layout - add canvas placeholders (cv-active, cv-reqs, cv-tokens, cv-lat, cv-status) for future sparklines/charts - draw stubs: active/reqs/tokens/latency/status stub renderers (drawActiveChart etc.) - CSS unified: .kpi flex column, .k-hdr title row, .k-canvas flex:1 canvas content area - deployed (bak .bak.20260811k), server active --- internal/gateway/ui/index.html | 114 +++++++++++++++++++++++++++++---- 1 file changed, 101 insertions(+), 13 deletions(-) diff --git a/internal/gateway/ui/index.html b/internal/gateway/ui/index.html index 66a0021..dbe92db 100644 --- a/internal/gateway/ui/index.html +++ b/internal/gateway/ui/index.html @@ -102,16 +102,17 @@ pre.configbox { background:var(--card2); border:1px solid var(--line); border-ra /* ---------- dashboard / usage metrics ---------- */ .kpis { display:grid; grid-template-columns:repeat(auto-fit,minmax(160px,1fr)); gap:12px; margin-bottom:18px; } -.kpi { background:var(--card); border:1px solid var(--line); border-radius:14px; padding:14px 16px; box-shadow:var(--shadow); } -.kpi .k-lab { font-size:12px; color:var(--muted); display:flex; align-items:center; gap:6px; } -.kpi .k-val { font-size:26px; font-weight:800; margin-top:4px; font-variant-numeric:tabular-nums; letter-spacing:.3px; } -.kpi .k-val .u { font-size:13px; font-weight:600; color:var(--muted); margin-left:3px; } -.kpi .k-sub { font-size:11.5px; color:var(--muted); margin-top:3px; } +.kpi { background:var(--card); border:1px solid var(--line); border-radius:14px; padding:12px 14px; box-shadow:var(--shadow); display:flex; flex-direction:column; min-height:160px; } +.kpi .k-hdr { display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:8px; } +.kpi .k-lab { font-size:12px; color:var(--muted); font-weight:600; white-space:nowrap; } +.kpi .k-val { font-size:22px; font-weight:800; font-variant-numeric:tabular-nums; } +.kpi .k-sub { font-size:11px; color:var(--muted); } +.kpi .k-canvas { flex:1; display:flex; align-items:center; justify-content:center; min-height:80px; position:relative; } +.kpi canvas { width:100%; height:100%; display:block; } .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; } -.kpi.kpi-status { display:flex; flex-direction:column; padding:12px; } -.kpi.kpi-status > div { flex:1; display:flex; align-items:center; justify-content:center; } +.kpi.kpi-status { display:flex; flex-direction:column; } .dot { width:7px; height:7px; border-radius:50%; background:var(--ok); display:inline-block; animation:blink 1.6s infinite; } .dot.err { background:var(--err); } @keyframes blink { 50% { opacity:.25; } } @@ -757,12 +758,26 @@ async function paintStats() { const avg = fmtLat(tot.latency_sum_ms, tot.reqs); const kpi = $('#kpi-row'); if (kpi) kpi.innerHTML = ` -
${t('kpiActive')}
${st.active || 0}
${statsKeyF ? esc(st.key_names ? (st.key_names[statsKeyF] ? st.key_names[statsKeyF] + ' · ' : '') + statsKeyF : statsKeyF) : ''}
-
${t('kpiReqs')}
${fmtN(tot.reqs)}
${t('kpiOk')} ${okr}%
-
${t('kpiTokens')}
${fmtTok(tot.tokens)}
-
${t('thPrompt')} ${fmtTok(tot.prompt_tokens)} · ${t('thCompl')} ${fmtTok(tot.completion_tokens)}
-
${t('kpiLat')}
${fmtMs(avg)}
${t('kpiMaxLat')} ${fmtMs(tot.latency_max_ms)}
-
${t('dashStatus')}
`; +
+
${t('kpiActive')}
+
+
+
+
${t('kpiReqs')}
+
+
+
+
${t('kpiTokens')}
+
+
+
+
${t('kpiLat')}
+
+
+
+
${t('dashStatus')}
+
+
`; paintModelTable(st.by_model || []); paintSrcTable(st.by_source || []); paintStatusTable(st.by_status || []); @@ -771,6 +786,12 @@ async function paintStats() { const rex = $('#rec-exit'); if (rex) rex.innerHTML = statsKeyF ? `` : ''; renderKeySelect((st.by_key || []).map(k => k.name), st.key_names || {}); + // draw charts after DOM ready + drawActiveChart(st); + drawReqsChart(st); + drawTokensChart(st.by_model || []); + drawLatencyChart(st); + drawStatusChart(st.by_status || []); } catch (e) { console.error('[stats]', e); } } function paintModelTable(rows) { @@ -838,6 +859,73 @@ function paintStatusTable(rows) {
${chips}
`; } +function drawActiveChart(st) { + const cv = document.getElementById('cv-active'); if (!cv) return; + const ctx = cv.getContext('2d'); + const dpr = window.devicePixelRatio || 1; + const rect = cv.parentElement.getBoundingClientRect(); + cv.width = rect.width * dpr; cv.height = rect.height * dpr; + ctx.scale(dpr, dpr); + const w = rect.width, h = rect.height; + ctx.clearRect(0, 0, w, h); + ctx.fillStyle = '#2a2f3a'; ctx.fillRect(0, 0, w, h); + ctx.font = '14px ui-monospace,Menlo,Consolas,monospace'; + ctx.fillStyle = 'var(--ok)'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; + ctx.fillText((st.active || 0) + ' ' + t('kpiActive').replace('活跃','').replace('Active',''), w/2, h/2); +} +function drawReqsChart(st) { + const cv = document.getElementById('cv-reqs'); if (!cv) return; + const ctx = cv.getContext('2d'); + const dpr = window.devicePixelRatio || 1; + const rect = cv.parentElement.getBoundingClientRect(); + cv.width = rect.width * dpr; cv.height = rect.height * dpr; + ctx.scale(dpr, dpr); + const w = rect.width, h = rect.height; + ctx.clearRect(0, 0, w, h); + ctx.fillStyle = '#2a2f3a'; ctx.fillRect(0, 0, w, h); + ctx.font = '18px ui-monospace,Menlo,Consolas,monospace'; + ctx.fillStyle = '#e2e8f0'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; + ctx.fillText(fmtN((st.total||{}).reqs), w/2, h/2); +} +function drawTokensChart(models) { + const cv = document.getElementById('cv-tokens'); if (!cv) return; + const ctx = cv.getContext('2d'); + const dpr = window.devicePixelRatio || 1; + const rect = cv.parentElement.getBoundingClientRect(); + cv.width = rect.width * dpr; cv.height = rect.height * dpr; + ctx.scale(dpr, dpr); + const w = rect.width, h = rect.height; + ctx.clearRect(0, 0, w, h); + ctx.fillStyle = '#2a2f3a'; ctx.fillRect(0, 0, w, h); + const totTok = (models||[]).reduce((a,x)=>a+x.tokens,0); + ctx.font = '16px ui-monospace,Menlo,Consolas,monospace'; + ctx.fillStyle = '#e2e8f0'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; + ctx.fillText(fmtTok(totTok) + ' tokens', w/2, h/2); +} +function drawLatencyChart(st) { + const cv = document.getElementById('cv-lat'); if (!cv) return; + const ctx = cv.getContext('2d'); + const dpr = window.devicePixelRatio || 1; + const rect = cv.parentElement.getBoundingClientRect(); + cv.width = rect.width * dpr; cv.height = rect.height * dpr; + ctx.scale(dpr, dpr); + const w = rect.width, h = rect.height; + ctx.clearRect(0, 0, w, h); + ctx.fillStyle = '#2a2f3a'; ctx.fillRect(0, 0, w, h); + ctx.font = '16px ui-monospace,Menlo,Consolas,monospace'; + ctx.fillStyle = '#e2e8f0'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; + ctx.fillText('avg ' + fmtMs((st.total||{}).latency_sum_ms, (st.total||{}).reqs), w/2, h/2); +} +function drawStatusChart(rows) { + const cv = document.getElementById('cv-status'); if (!cv) return; + const ctx = cv.getContext('2d'); + const dpr = window.devicePixelRatio || 1; + const rect = cv.parentElement.getBoundingClientRect(); + cv.width = rect.width * dpr; cv.height = rect.height * dpr; + ctx.scale(dpr, dpr); + const w = rect.width, h = rect.height; + ctx.clearRect(0, 0, w, h); +} function paintKeyTable(rows, keyNames) { const el = $('#tb-key'); if (!el) return; if (!rows.length) { el.innerHTML = `
${t('noUsage')}
`; return; }