// Dashboard — KPIs, time-series charts, breakdown, recent requests, health. import { h, mount, money, moneyEl, num, ms, pct, timeAgo, toast, confirmDialog } from "../ui.js"; import { t, trTier } from "../i18n.js"; import { api } from "../api.js"; import { lineChart, barList } from "../charts.js"; import { SITE_URL } from "../app.js"; const RANGES = ["1h", "24h", "7d"]; const GROUPS = ["model", "tier", "label", "account"]; function kpi(big, lbl) { return h("div", { class: "kpi" }, h("div", { class: "big" }, big), h("div", { class: "lbl" }, lbl)); } function seg(options, current, onPick, labelFn) { return h( "div", { class: "seg" }, ...options.map((o) => h("button", { class: o === current ? "active" : "", onClick: () => onPick(o) }, labelFn ? labelFn(o) : o) ) ); } export async function renderDashboard() { let range = "24h"; let groupby = "model"; const root = h("div"); // ── host metrics: CPU sparkline + RAM / disk bars, refreshed every 2 s ── const sysHost = h("div", {}); const cpuHistory = []; // last ~60 samples (2 min) function fmtG(n) { return (n / 1e9).toFixed(1) + " GB"; } function bar(frac, cls) { return h("div", { class: "sysbar" }, h("i", { class: cls || "", style: `width:${Math.min(100, frac * 100)}%` })); } function sparkline(values, w, hgt) { const c = document.createElement("canvas"); c.width = w; c.height = hgt; const ctx = c.getContext("2d"); const accent = getComputedStyle(document.documentElement).getPropertyValue("--accent").trim() || "#5b8def"; ctx.strokeStyle = accent; ctx.lineWidth = 1.5; ctx.beginPath(); const n = Math.max(values.length, 2); values.forEach((v, i) => { const x = (i / (n - 1)) * (w - 2) + 1; const y = hgt - 2 - (Math.min(v, 100) / 100) * (hgt - 4); if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.stroke(); return c; } async function refreshSystem() { let sys; try { sys = await api.system(); } catch { return; } if (!sys.ts) return; cpuHistory.push(sys.cpu_percent || 0); if (cpuHistory.length > 60) cpuHistory.shift(); const ramFrac = sys.ram_total ? sys.ram_used / sys.ram_total : 0; const diskFrac = sys.disk_total ? (sys.disk_total - sys.disk_free) / sys.disk_total : 0; mount( sysHost, h( "div", { class: "card" }, h("div", { class: "card-title" }, t("dash.sys.title")), h( "div", { class: "sysgrid" }, h("div", { class: "sysitem" }, h("div", { class: "flex" }, h("span", { class: "k grow" }, `CPU · ${sys.cores} ${t("dash.sys.cores")}`), h("span", { class: "v mono" }, (sys.cpu_percent || 0).toFixed(0) + "%")), sparkline(cpuHistory, 220, 36), h("div", { class: "muted small" }, `load ${Number(sys.load1 || 0).toFixed(1)}`)), h("div", { class: "sysitem" }, h("div", { class: "flex" }, h("span", { class: "k grow" }, "RAM"), h("span", { class: "v mono" }, `${fmtG(sys.ram_used)} / ${fmtG(sys.ram_total)}`)), bar(ramFrac, ramFrac > 0.9 ? "bad" : ""), h("div", { class: "muted small" }, t("dash.sys.gateway") + " " + (sys.process_rss / 1e6).toFixed(0) + " MB")), h("div", { class: "sysitem" }, h("div", { class: "flex" }, h("span", { class: "k grow" }, t("dash.sys.disk")), h("span", { class: "v mono" }, t("dash.sys.free") + " " + fmtG(sys.disk_free)), ), bar(diskFrac, diskFrac > 0.92 ? "bad" : ""), h("div", { class: "muted small" }, `${fmtG(sys.disk_total - sys.disk_free)} / ${fmtG(sys.disk_total)}`)) ) ) ); } const sysTimer = setInterval(() => { if (!root.isConnected) { clearInterval(sysTimer); return; } refreshSystem(); }, 2000); refreshSystem(); async function load() { const rangeSecs = range; let stats, recent, health; try { [stats, recent, health] = await Promise.all([ api.stats(rangeSecs, groupby), api.requests(40, 0), api.health().catch(() => null), ]); } catch (e) { mount(root, h("div", { class: "callout" }, String(e.message || e))); return; } const tot = stats.totals || {}; const series = stats.series || []; const breakdown = (stats.breakdown || []).map((b) => ({ key: b.key || t("common.none"), value: b.requests })); mount( root, h( "div", { class: "page-head" }, h( "div", { class: "flex" }, h("div", { class: "grow" }, h("h2", {}, t("dash.title")), h("p", {}, t("dash.subtitle"))), h("div", { class: "flex", style: "gap:8px;align-items:center" }, seg(RANGES, range, (r) => { range = r; load(); }, (r) => t("dash.range." + r)), h("button", { class: "btn sm danger", onClick: async () => { if (!(await confirmDialog(t("dash.clearConfirm")))) return; try { await api.clearStats(); toast(t("dash.cleared"), "good"); await load(); } catch (e) { toast(String(e.message || e), "bad"); } }, }, t("dash.clear"))) ) ), // KPIs h( "div", { class: "kpis" }, kpi(num(tot.requests), t("dash.kpi.requests")), kpi(num(tot.total_tokens), t("dash.kpi.tokens")), kpi(moneyEl(tot.cost_usd), t("dash.kpi.cost")), kpi(ms(tot.avg_latency_ms), t("dash.kpi.latency")), kpi(pct(tot.success_rate), t("dash.kpi.success")), kpi(num(tot.failovers), t("dash.kpi.failovers")) ), sysHost, h( "div", { class: "grid cols-2" }, h( "div", { class: "card" }, h("div", { class: "card-head" }, h("h3", {}, t("dash.series.title"))), series.length < 2 ? h("div", { class: "empty" }, t("dash.series.empty")) : lineChart(series, "requests") ), h( "div", { class: "card" }, h("div", { class: "card-head" }, h("h3", {}, t("dash.series.cost"))), series.length < 2 ? h("div", { class: "empty" }, t("dash.series.empty")) : lineChart(series, "cost_usd") ) ), // breakdown h( "div", { class: "card" }, h( "div", { class: "card-head" }, h("h3", {}, t("dash.breakdown.title")), h( "div", { class: "right" }, seg(GROUPS, groupby, (g) => { groupby = g; load(); }, (g) => t("dash.groupby." + g)) ) ), barList(breakdown, num) ), // semantic cache stats.cache && stats.cache.enabled ? h( "div", { class: "card" }, h("div", { class: "card-head" }, h("h3", {}, t("dash.cache"))), h( "div", { class: "kpis" }, kpi(pct(stats.cache.hit_rate), t("dash.cache.hitRate")), kpi(num(stats.cache.hits), t("dash.cache.hits")), kpi(money(stats.cache.saved_usd), t("dash.cache.saved")) ) ) : null, // recent h( "div", { class: "card" }, h("div", { class: "card-head" }, h("h3", {}, t("dash.recent.title"))), recentTable(recent.requests || []) ), health ? healthCard(health) : null ); } await load(); return root; } function recentTable(rows) { if (!rows.length) return h("div", { class: "empty" }, t("dash.recent.empty")); return h( "div", { class: "table-wrap" }, h( "table", {}, h( "thead", {}, h( "tr", {}, h("th", {}, t("dash.col.time")), h("th", {}, t("dash.col.model")), h("th", {}, t("dash.col.tier")), h("th", {}, t("dash.col.task")), h("th", {}, t("dash.col.tokens")), h("th", {}, t("dash.col.cost")), h("th", {}, t("dash.col.latency")), h("th", {}, t("dash.col.outcome")) ) ), h( "tbody", {}, ...rows.map((r) => h( "tr", {}, h("td", { class: "mono" }, timeAgo(r.ts)), h("td", {}, r.model_id || "—", r.failover ? h("span", { class: "badge warn", style: "margin-left:6px" }, "fo") : null), h("td", {}, r.tier ? h("span", { class: "badge tier-" + r.tier }, trTier(r.tier)) : "—", r.score > 0 ? h("span", { class: "muted small mono", style: "margin-left:6px" }, r.score.toFixed(2)) : null), h("td", {}, r.task_label || "—"), h("td", { class: "mono" }, `${r.prompt_tokens}/${r.completion_tokens}`), h("td", { class: "mono" }, money(r.cost_usd)), h("td", { class: "mono" }, ms(r.latency_ms)), h("td", {}, h("span", { class: "badge " + (r.outcome === "ok" ? "ok" : "error") }, r.outcome)) ) ) ) ) ); } function keySourceLabel(src) { if (src === "store") return t("models.form.secretStored"); if (src === "env") return t("models.form.secretEnv"); return t("models.form.secretMissing"); } function healthCard(hh) { const r = hh.router || {}; const keySrc = r.key_source || "none"; const err = r.last_error; // problems fixed on the site (get / pay for a key) get the top-up link const needsSite = !r.reachable || keySrc === "missing" || (err && ["auth", "payment", "quota"].includes(err.kind)); return h( "div", { class: "card" }, h("div", { class: "card-head" }, h("h3", {}, t("dash.health.title"))), h( "div", { class: "flex wrap", style: "margin-bottom:12px" }, h( "span", { class: "badge " + (r.reachable ? "ok" : "bad"), title: r.url || "" }, t("status.router") + ": " + (r.reachable ? t("dash.health.reachable") : t("dash.health.unreachable")) ), keySrc !== "none" ? h( "span", { class: "badge " + keyBadge(keySrc), title: r.key_env || "" }, t("models.col.key") + ": " + keySourceLabel(keySrc) ) : null, err ? h("span", { class: "badge bad" }, t("router.probe." + (err.kind || "error"))) : null, needsSite ? h("a", { href: SITE_URL, target: "_blank", rel: "noopener" }, t("dash.health.getKey") + " ↗") : null ), h( "div", { class: "table-wrap" }, h( "table", {}, h("thead", {}, h("tr", {}, h("th", {}, t("models.col.id")), h("th", {}, t("models.col.provider")), h("th", {}, t("models.col.model")), h("th", {}, t("models.col.key")))), h( "tbody", {}, ...(hh.models || []).map((m) => h( "tr", {}, h("td", { class: "mono" }, m.id), h("td", {}, m.provider), h("td", { class: "mono" }, m.model), h("td", {}, h("span", { class: "badge " + keyBadge(m.key_source) }, m.key_source)) ) ) ) ) ) ); } function keyBadge(src) { if (src === "store") return "store"; if (src === "env") return "env"; if (src === "missing") return "bad"; return ""; }