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;