antern-bot / static /metrics.html
johnpitteera's picture
Upload folder using huggingface_hub
2dd2de0 verified
Raw
History Blame Contribute Delete
8.57 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Antern Bot — Metrics</title>
<style>
:root { --bg:#0f1115; --panel:#171a21; --panel2:#1f242e; --border:#2a303c; --text:#e8eaed; --muted:#9aa3b2; --accent:#4f8cff; --ok:#38d39f; --warn:#ffb020; --bad:#ff6b6b; }
* { box-sizing:border-box; }
body { margin:0; background:var(--bg); color:var(--text); font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; padding:28px; }
h1 { font-size:22px; margin:0 0 4px; display:flex; align-items:center; gap:10px; }
h1 .logo { width:30px;height:30px;border-radius:8px;background:linear-gradient(135deg,#4f8cff,#8a5cff);display:flex;align-items:center;justify-content:center;font-weight:700; }
.sub { color:var(--muted); font-size:13px; margin-bottom:22px; }
.grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(170px,1fr)); gap:14px; margin-bottom:26px; }
.card { background:var(--panel); border:1px solid var(--border); border-radius:12px; padding:16px 18px; }
.card .label { color:var(--muted); font-size:12px; text-transform:uppercase; letter-spacing:.04em; }
.card .value { font-size:26px; font-weight:700; margin-top:6px; }
.card .value.small { font-size:20px; }
h2 { font-size:15px; color:var(--muted); margin:24px 0 10px; font-weight:600; }
table { width:100%; border-collapse:collapse; background:var(--panel); border:1px solid var(--border); border-radius:12px; overflow:hidden; }
th,td { padding:10px 14px; text-align:left; border-bottom:1px solid var(--border); font-size:13.5px; }
th { background:var(--panel2); color:var(--muted); font-weight:600; }
td.num,th.num { text-align:right; font-variant-numeric:tabular-nums; }
tr:last-child td { border-bottom:none; }
.bar { height:8px; background:var(--panel2); border-radius:5px; overflow:hidden; margin-top:8px; }
.bar > span { display:block; height:100%; background:var(--accent); }
.muted { color:var(--muted); }
.pill { display:inline-block; padding:2px 9px; border-radius:20px; font-size:12px; font-weight:600; }
.pill.ok { background:rgba(56,211,159,.15); color:var(--ok); }
.pill.bad { background:rgba(255,107,107,.15); color:var(--bad); }
#updated { color:var(--muted); font-size:12px; }
.err { background:var(--panel); border:1px solid var(--border); border-radius:12px; padding:20px; }
.err code { background:var(--panel2); padding:2px 6px; border-radius:5px; }
.empty { color:var(--muted); padding:14px; }
</style>
</head>
<body>
<h1><span class="logo">A</span> Antern Bot — Live Metrics</h1>
<div class="sub">Auto-refreshing every 5s · <span id="updated"></span></div>
<div id="content"><div class="empty">Loading…</div></div>
<script>
const token = new URLSearchParams(location.search).get("token") || "";
const $ = (id) => document.getElementById(id);
function commas(n) { return (n ?? 0).toLocaleString("en-US"); }
function money(n) { return "$" + (n ?? 0).toFixed(4); }
function uptime(s) {
s = Math.floor(s || 0);
const h = Math.floor(s/3600), m = Math.floor((s%3600)/60), sec = s%60;
return (h?h+"h ":"") + (m?m+"m ":"") + sec + "s";
}
function esc(s){ return (s==null?"":String(s)).replace(/[&<>"]/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c])); }
function render(d, recent) {
const errPill = d.errors > 0 ? `<span class="pill bad">${d.errors}</span>` : `<span class="pill ok">0</span>`;
const cards = [
["Requests", commas(d.requests)],
["Sessions", commas(d.sessions)],
["Errors", errPill, true],
["Total tokens", commas(d.tokens_total)],
["Total cost", money(d.total_cost_usd)],
["Avg latency", commas(Math.round(d.avg_latency_ms)) + " ms"],
["Avg tokens / chat", commas(Math.round(d.avg_tokens_per_request))],
["Avg cost / chat", money(d.avg_cost_per_request_usd)],
["Uptime", uptime(d.uptime_seconds)],
];
let html = '<div class="grid">';
for (const [label, value, raw] of cards) {
html += `<div class="card"><div class="label">${label}</div><div class="value ${String(value).length>10?'small':''}">${raw?value:esc(value)}</div></div>`;
}
html += '</div>';
// Reliability + pricing table
html += '<h2>Reliability & pricing</h2><table><tbody>';
html += `<tr><td>Error rate</td><td class="num">${((d.error_rate||0)*100).toFixed(1)}%</td></tr>`;
html += `<tr><td>Blocked queries (security)</td><td class="num">${commas(d.blocked_queries)}</td></tr>`;
html += `<tr><td>SQL errors</td><td class="num">${commas(d.sql_errors)}</td></tr>`;
html += `<tr><td>Price (input / output per 1M tokens)</td><td class="num">$${d.price_per_1m?.input} / $${d.price_per_1m?.output}</td></tr>`;
html += '</tbody></table>';
// System
const cpu = d.system?.cpu_pct ?? 0, ram = d.system?.ram_pct ?? 0;
html += '<h2>System</h2><table><tbody>';
html += `<tr><td style="width:140px">CPU</td><td>${cpu}%<div class="bar"><span style="width:${cpu}%"></span></div></td></tr>`;
html += `<tr><td>RAM</td><td>${ram}%<div class="bar"><span style="width:${ram}%"></span></div></td></tr>`;
html += '</tbody></table>';
// Top sessions / cost per customer
html += '<h2>Top sessions (cost per customer)</h2>';
const s = d.top_sessions || [];
if (!s.length) {
html += '<div class="empty">No sessions yet — ask a few questions in the chat, then refresh.</div>';
} else {
html += '<table><thead><tr><th>Session</th><th class="num">Chats</th><th class="num">Prompt</th><th class="num">Completion</th><th class="num">Total tokens</th><th class="num">Cost</th></tr></thead><tbody>';
for (const r of s) {
html += `<tr><td>${esc(r.session_id)}</td><td class="num">${commas(r.requests)}</td><td class="num">${commas(r.prompt)}</td><td class="num">${commas(r.completion)}</td><td class="num">${commas(r.total)}</td><td class="num">${money(r.cost_usd)}</td></tr>`;
}
html += '</tbody></table>';
}
// Recent chats (from the durable log)
html += '<h2>Recent chats</h2>';
if (!recent || !recent.length) {
html += '<div class="empty">No chats logged yet.</div>';
} else {
html += '<table><thead><tr><th>Time</th><th>Session</th><th>Question</th><th>Output</th><th class="num">Latency</th><th class="num">Tokens</th><th class="num">Cost</th><th>Status</th></tr></thead><tbody>';
for (const c of recent) {
const t = c.ts ? new Date(c.ts).toLocaleTimeString() : "";
const out = c.presentation || "text";
let status = '<span class="pill ok">ok</span>';
if (c.error) status = '<span class="pill bad">error</span>';
else if (c.security_events) status = '<span class="pill bad">blocked</span>';
else if (c.sql_errors) status = '<span class="pill" style="background:rgba(255,176,32,.15);color:var(--warn)">sql err</span>';
const q = (c.question || "");
const qShort = esc(q.slice(0, 60)) + (q.length > 60 ? "…" : "");
html += `<tr><td class="muted">${t}</td><td>${esc(c.session_id)}</td><td>${qShort}</td><td>${esc(out)}</td><td class="num">${commas(Math.round(c.latency_ms||0))} ms</td><td class="num">${commas(c.tokens||0)}</td><td class="num">${money(c.cost_usd||0)}</td><td>${status}</td></tr>`;
}
html += '</tbody></table>';
}
$("content").innerHTML = html;
$("updated").textContent = "updated " + new Date().toLocaleTimeString();
}
async function load() {
const H = { headers: { "ngrok-skip-browser-warning": "true" } };
const q = token ? ("?token=" + encodeURIComponent(token)) : "";
try {
const r = await fetch("/api/metrics" + q, H);
if (r.status === 401) {
$("content").innerHTML = '<div class="err">🔒 <b>Unauthorized.</b> Add your metrics token to the URL:<br><br><code>'
+ location.pathname + '?token=YOUR_TOKEN</code></div>';
return;
}
const d = await r.json();
let recent = [];
try {
const rUrl = "/api/metrics/recent?limit=20" + (token ? ("&token=" + encodeURIComponent(token)) : "");
const rr = await fetch(rUrl, H);
if (rr.ok) recent = (await rr.json()).recent || [];
} catch (e) { /* recent is best-effort */ }
render(d, recent);
} catch (e) {
$("content").innerHTML = '<div class="err">⚠ Cannot reach the server. Is the bot running on this host?</div>';
}
}
load();
setInterval(load, 5000);
</script>
</body>
</html>