File size: 8,565 Bytes
2dd2de0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
<!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>