diff --git a/internal/gateway/ui/index.html b/internal/gateway/ui/index.html
index 2bfbfba..831b7a4 100644
--- a/internal/gateway/ui/index.html
+++ b/internal/gateway/ui/index.html
@@ -102,9 +102,9 @@ pre.configbox { background:var(--card2); border:1px solid var(--line); border-ra
/* ---------- dashboard / usage metrics ---------- */
.kpi-skeletons { grid-column:1 / -1; display:grid; grid-template-columns:repeat(auto-fit,minmax(160px,1fr)); gap:12px; }
-.ksk { background:var(--card); border:1px solid var(--line); border-radius:14px; padding:14px 16px; min-height:186px; display:flex; flex-direction:column; gap:10px; }
+.ksk { background:var(--card); border:1px solid var(--line); border-radius:14px; padding:14px 16px; height:206px; display:flex; flex-direction:column; gap:10px; }
.ksk i { display:block; height:11px; border-radius:6px; background:linear-gradient(90deg,var(--card2) 25%,var(--line) 37%,var(--card2) 63%); background-size:400% 100%; animation:shimmer 1.2s infinite; }
-.ksk i:nth-child(1){ width:38%; align-self:flex-start; } .ksk i:nth-child(2){ width:68%; height:24px; } .ksk i:nth-child(3){ width:100%; height:86px; margin-top:4px; }
+.ksk i:nth-child(1){ width:38%; align-self:flex-start; } .ksk i:nth-child(2){ width:68%; height:24px; } .ksk i:nth-child(3){ width:100%; flex:1 1 auto; margin:6px 0 0; }
@keyframes shimmer { 0%{background-position:100% 0} 100%{background-position:0 0} }
/* entry / transition animations */
@keyframes fadeUp { from{opacity:0;transform:translateY(8px)} to{opacity:1;transform:none} }
@@ -114,17 +114,15 @@ pre.configbox { background:var(--card2); border:1px solid var(--line); border-ra
.tab-pane { animation:tabIn .25s ease; }
.chat-log .chat-empty { animation:fadeIn .3s ease; }
.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-hdr { display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:6px; }
+.kpi { background:var(--card); border:1px solid var(--line); border-radius:14px; padding:14px 16px; box-shadow:var(--shadow); display:flex; flex-direction:column; height:206px; }
+.kpi .k-hdr { display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:6px; flex:0 0 auto; }
.kpi .k-lab { font-size:12px; color:var(--muted); font-weight:600; white-space:nowrap; }
-.kpi .k-val { font-size:26px; font-weight:800; margin-top:4px; font-variant-numeric:tabular-nums; letter-spacing:.3px; }
+.kpi .k-val { font-size:26px; font-weight:800; margin-top:4px; font-variant-numeric:tabular-nums; letter-spacing:.3px; flex:0 0 auto; }
.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:4px; }
+.kpi .k-sub { font-size:11.5px; color:var(--muted); margin-top:4px; flex:0 0 auto; }
.kpi .k-sub.k2 { display:flex; flex-direction:column; align-items:flex-start; line-height:1.5; }
-.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; }
+.kpi .k-chart { position:relative; flex:1 1 auto; min-height:40px; margin-top:8px; }
+.kpi canvas { position:absolute; inset:0; width:100%; height:100%; display:block; }
.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; }
@@ -915,7 +913,7 @@ function drawActiveChart(st) {
const ctx = cv.getContext('2d');
const dpr = window.devicePixelRatio || 1;
const parent = cv.parentElement, rect = parent.getBoundingClientRect();
- const H = 86;
+ const H = rect.height || 86;
cv.width = rect.width * dpr; cv.height = H * dpr; ctx.scale(dpr, dpr);
ctx.clearRect(0, 0, cv.width, cv.height);
// sample: requests started in the last 3s (approx QPS). Records carry unix-ms time.
@@ -945,14 +943,15 @@ function drawReqsChart(st) {
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);
+ const H = rect.height || 86;
+ cv.width = rect.width * dpr; cv.height = H * dpr; ctx.scale(dpr, dpr);
ctx.clearRect(0, 0, cv.width, cv.height);
const recs = st.records || [];
// bucket by UTC day -> request counts (last 7 non-empty days)
const byDay = new Map();
recs.forEach(r => { const d = new Date(r.time).toISOString().slice(5,10); byDay.set(d, (byDay.get(d)||0)+1); });
const counts = Array.from(byDay.values()).slice(-7);
- dailyBars(ctx, counts, rect.width, 86, '#3fb27f');
+ dailyBars(ctx, counts, rect.width, H, '#3fb27f');
}
const tokPalette = ['#3fb27f','#4fa3d8','#f0a45c','#b06ce0'];
function drawTokensChart(models) {
@@ -960,7 +959,8 @@ function drawTokensChart(models) {
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);
+ const H = rect.height || 86;
+ cv.width = rect.width * dpr; cv.height = H * dpr; ctx.scale(dpr, dpr);
ctx.clearRect(0, 0, cv.width, cv.height);
const rows = (models||[]).slice().sort((a,b)=>b.tokens-a.tokens).slice(0,4);
const tot = rows.reduce((a,x)=>a+x.tokens,0) || 1;
@@ -968,7 +968,7 @@ function drawTokensChart(models) {
rows.forEach((m,i) => {
const w = Math.max(2, m.tokens / tot * rect.width);
ctx.fillStyle = tokPalette[i%tokPalette.length];
- ctx.fillRect(x, 0, w, 86);
+ ctx.fillRect(x, 0, w, H);
x += w;
});
// color-swatch legend instead of on-chart text
@@ -982,12 +982,13 @@ function drawLatencyChart(st) {
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);
+ const H = rect.height || 86;
+ cv.width = rect.width * dpr; cv.height = H * dpr; ctx.scale(dpr, dpr);
ctx.clearRect(0, 0, cv.width, cv.height);
const recs = st.records || [];
// per-record latency sparkline (last 40)
const lat = recs.slice(-40).map(r => r.latency_ms || 0);
- sparkline(ctx, lat, rect.width, 86, '#e06c6c');
+ sparkline(ctx, lat, rect.width, H, '#e06c6c');
}
function drawStatusChart(rows) {
const cv = document.getElementById('cv-status'); if (!cv) return;