headroom-gui 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.html ADDED
@@ -0,0 +1,1068 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" class="h-full">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Headroom · Monitor</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script>
9
+ tailwind.config = {
10
+ theme: { extend: { fontFamily: { mono: ['"IBM Plex Mono"', 'ui-monospace', 'monospace'] } } }
11
+ }
12
+ </script>
13
+ <link rel="preconnect" href="https://fonts.googleapis.com">
14
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
15
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
16
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
17
+ <style>
18
+ * { font-family: 'Inter', sans-serif; }
19
+ .mono { font-family: 'IBM Plex Mono', monospace; }
20
+ .nav-item { display:flex; align-items:center; gap:10px; padding:7px 12px; border-radius:8px; font-size:13px; font-weight:500; color:#94a3b8; cursor:pointer; transition:all .15s; text-decoration:none; }
21
+ .nav-item:hover { background:#1e293b; color:#e2e8f0; }
22
+ .nav-item.active { background:#2563eb; color:#fff; }
23
+ .nav-item .ep { font-family:'IBM Plex Mono',monospace; font-size:10px; opacity:.65; margin-left:auto; }
24
+ .nav-group { font-size:10px; font-weight:700; letter-spacing:.1em; text-transform:uppercase; color:#475569; padding:8px 12px 4px; margin-top:8px; }
25
+ @keyframes pulse-dot { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:.4;transform:scale(.7)} }
26
+ .pulse-ok { animation: pulse-dot 2s ease-in-out infinite; }
27
+ @keyframes spin { to { transform:rotate(360deg) } }
28
+ .loading-spin { animation: spin 1s linear infinite; }
29
+ .metric-bar { height:4px; background:#e2e8f0; border-radius:9999px; overflow:hidden; }
30
+ .metric-bar-fill { height:100%; border-radius:9999px; transition:width .5s ease; }
31
+ </style>
32
+ </head>
33
+ <body class="h-full flex bg-gray-50 antialiased" style="font-family:'Inter',sans-serif">
34
+
35
+ <!-- ═══ SIDEBAR ═══ -->
36
+ <aside class="w-56 bg-slate-900 flex flex-col h-screen sticky top-0 flex-shrink-0">
37
+
38
+ <!-- Logo -->
39
+ <div class="px-4 pt-5 pb-4 border-b border-slate-800">
40
+ <div class="flex items-center gap-2.5 mb-3">
41
+ <div class="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center flex-shrink-0">
42
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
43
+ </div>
44
+ <div>
45
+ <div class="text-white font-semibold text-sm leading-none">Headroom</div>
46
+ <div class="text-slate-500 text-xs leading-none mt-0.5">Monitor</div>
47
+ </div>
48
+ </div>
49
+ <div class="mono text-xs text-slate-500 truncate" id="sb-proxy-url">127.0.0.1:8787</div>
50
+ </div>
51
+
52
+ <!-- Live badge -->
53
+ <div class="px-4 py-2.5 border-b border-slate-800">
54
+ <div id="live-pill" class="flex items-center gap-2 text-xs font-semibold text-amber-400">
55
+ <span class="w-2 h-2 rounded-full bg-current pulse-ok"></span>
56
+ <span id="live-text">Connecting…</span>
57
+ </div>
58
+ </div>
59
+
60
+ <!-- Nav -->
61
+ <nav class="flex-1 px-3 py-2 overflow-y-auto" id="sidebar-nav"></nav>
62
+
63
+ <!-- Controls -->
64
+ <div class="px-4 py-4 border-t border-slate-800 space-y-3">
65
+ <div class="flex items-center gap-2">
66
+ <span class="text-slate-500 text-xs">every</span>
67
+ <input id="iv-in" type="number" value="5" min="1" max="999"
68
+ class="w-10 bg-slate-800 border border-slate-700 text-white mono text-xs rounded px-1.5 py-1 text-center focus:outline-none focus:border-blue-500">
69
+ <span class="text-slate-500 text-xs">s</span>
70
+ <button id="toggle-btn" onclick="toggleAuto()"
71
+ class="ml-auto text-xs font-medium px-2.5 py-1 rounded bg-emerald-900/60 text-emerald-400 border border-emerald-800 hover:bg-emerald-800 transition-colors">⏸</button>
72
+ </div>
73
+ <button onclick="doRefresh()"
74
+ class="w-full text-xs font-semibold py-1.5 rounded bg-blue-600 hover:bg-blue-700 text-white transition-colors">
75
+ ↺ Refresh Now
76
+ </button>
77
+ <div class="mono text-xs text-slate-600 truncate" id="last-upd">—</div>
78
+ </div>
79
+ </aside>
80
+
81
+ <!-- ═══ MAIN ═══ -->
82
+ <div class="flex-1 flex flex-col overflow-hidden">
83
+
84
+ <!-- Top bar -->
85
+ <header class="bg-white border-b border-gray-200 px-6 h-14 flex items-center gap-3 flex-shrink-0">
86
+ <h1 class="text-sm font-semibold text-gray-800" id="page-title">Overview</h1>
87
+ <div class="flex items-center gap-1.5 ml-2" id="page-badges"></div>
88
+ </header>
89
+
90
+ <!-- Content -->
91
+ <main class="flex-1 overflow-y-auto p-6" id="content">
92
+ <div class="flex items-center justify-center h-full text-gray-400 text-sm">Loading…</div>
93
+ </main>
94
+ </div>
95
+
96
+ <script>
97
+ // ═══ CONFIG & STATE ═══
98
+ const BASE = '/api';
99
+ const cache = {}; // { path: { data, ok, status, ts } }
100
+ const charts = {}; // Chart.js instances keyed by ID
101
+ let view = 'overview';
102
+ let autoRef = true;
103
+ let ivMs = 5000;
104
+ let timer = null;
105
+
106
+ // ═══ NAV DEFINITION ═══
107
+ const NAV = [
108
+ { id:'overview', label:'Overview', ep:null, group:null },
109
+ { id:'livez', label:'Liveness', ep:'/livez', group:'Status' },
110
+ { id:'readyz', label:'Readiness', ep:'/readyz', group:null },
111
+ { id:'health', label:'Health', ep:'/health', group:null },
112
+ { id:'stats', label:'Statistics', ep:'/stats', group:'Analytics' },
113
+ { id:'history', label:'History', ep:'/stats-history', group:null },
114
+ { id:'metrics', label:'Prometheus', ep:'/metrics', group:null },
115
+ ];
116
+
117
+ // ═══ DOM HELPERS ═══
118
+ const $ = id => document.getElementById(id);
119
+ const esc = s => String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
120
+
121
+ function setContent(html) { $('content').innerHTML = html; }
122
+
123
+ function dig(obj, path) {
124
+ if (!obj || !path) return undefined;
125
+ return path.split('.').reduce((o,k) => o?.[k], obj);
126
+ }
127
+ function pick(obj, ...paths) {
128
+ for (const p of paths) { const v = dig(obj,p); if (v != null) return v; }
129
+ return null;
130
+ }
131
+
132
+ // ═══ FORMAT ═══
133
+ const fmtTokens = n => {
134
+ if (n == null || !isFinite(n)) return '—';
135
+ if (Math.abs(n) >= 1e6) return (n/1e6).toFixed(2)+'M';
136
+ if (Math.abs(n) >= 1e3) return (n/1e3).toFixed(1)+'K';
137
+ return n.toLocaleString();
138
+ };
139
+ const fmtNum = n => {
140
+ if (n == null || !isFinite(n)) return '—';
141
+ if (Math.abs(n) >= 1e9) return (n/1e9).toFixed(2)+'B';
142
+ if (Math.abs(n) >= 1e6) return (n/1e6).toFixed(2)+'M';
143
+ if (Math.abs(n) >= 1e3) return (n/1e3).toFixed(1)+'K';
144
+ return String(Math.round(n*100)/100);
145
+ };
146
+ const fmtPct = (n, d=1) => {
147
+ if (n == null || !isFinite(n)) return '—';
148
+ return (n > 1 ? n : n*100).toFixed(d)+'%';
149
+ };
150
+ const fmtMs = n => {
151
+ if (n == null || !isFinite(n)) return '—';
152
+ return n < 1000 ? n.toFixed(1)+'ms' : (n/1000).toFixed(2)+'s';
153
+ };
154
+ const fmtDur = s => {
155
+ if (s == null) return '—';
156
+ s = Math.round(s);
157
+ if (s < 60) return s+'s';
158
+ if (s < 3600) return `${Math.floor(s/60)}m ${s%60}s`;
159
+ if (s < 86400) return `${Math.floor(s/3600)}h ${Math.floor((s%3600)/60)}m`;
160
+ return `${Math.floor(s/86400)}d ${Math.floor((s%86400)/3600)}h`;
161
+ };
162
+ const fmtUSD = n => {
163
+ if (n == null || !isFinite(n)) return '—';
164
+ return n < 0.001 ? '<$0.001' : '$'+n.toFixed(4);
165
+ };
166
+ const humanize = s => s.replace(/([A-Z])/g,' $1').replace(/_/g,' ').replace(/\b\w/g,c=>c.toUpperCase()).trim();
167
+
168
+ // ═══ UI COMPONENTS ═══
169
+ const OK_W = new Set(['ok','okay','healthy','up','running','alive','ready','true','pass','passing','loaded','enabled','available']);
170
+ const ERR_W = new Set(['error','unhealthy','down','dead','fail','failing','false','disabled','missing','critical','not ready']);
171
+ const WARN_W= new Set(['degraded','warn','warning','partial','unknown','pending']);
172
+
173
+ function statusCls(v) {
174
+ const s = String(v).toLowerCase().trim();
175
+ if (OK_W.has(s)) return 'ok';
176
+ if (ERR_W.has(s)) return 'err';
177
+ if (WARN_W.has(s)) return 'warn';
178
+ return null;
179
+ }
180
+
181
+ const CLS = {
182
+ ok: 'bg-emerald-50 text-emerald-700 border border-emerald-200',
183
+ err: 'bg-red-50 text-red-700 border border-red-200',
184
+ warn: 'bg-amber-50 text-amber-700 border border-amber-200',
185
+ blue: 'bg-blue-50 text-blue-700 border border-blue-200',
186
+ gray: 'bg-gray-100 text-gray-500 border border-gray-200',
187
+ };
188
+
189
+ function badge(text, cls='gray') {
190
+ return `<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold ${CLS[cls]||CLS.gray}">${esc(text)}</span>`;
191
+ }
192
+ function autoBadge(v) { return badge(v, statusCls(v)||'gray'); }
193
+
194
+ function card(title, body, sub='') {
195
+ return `<div class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
196
+ <div class="px-5 py-3 border-b border-gray-100 bg-gray-50/70 flex items-center gap-2">
197
+ <span class="text-xs font-semibold text-gray-400 uppercase tracking-wider">${title}</span>
198
+ ${sub?`<span class="text-xs text-gray-400 ml-1">· ${sub}</span>`:''}
199
+ </div>
200
+ <div class="p-5">${body}</div>
201
+ </div>`;
202
+ }
203
+
204
+ function statCard(label, value, sub='', color='text-blue-600') {
205
+ return `<div class="bg-white rounded-xl border border-gray-200 shadow-sm p-5 flex flex-col gap-1.5">
206
+ <span class="text-xs font-semibold text-gray-400 uppercase tracking-wider">${esc(label)}</span>
207
+ <span class="mono text-2xl font-semibold ${color} leading-none">${esc(String(value))}</span>
208
+ ${sub?`<span class="text-xs text-gray-400">${esc(String(sub))}</span>`:''}
209
+ </div>`;
210
+ }
211
+
212
+ function kv(key, val, valHtml='') {
213
+ return `<div class="flex items-center justify-between py-2 border-b border-gray-50 last:border-0 gap-4">
214
+ <span class="text-xs text-gray-400 shrink-0">${esc(humanize(key))}</span>
215
+ <span class="text-xs font-medium text-gray-700 text-right truncate">${valHtml || esc(String(val))}</span>
216
+ </div>`;
217
+ }
218
+
219
+ function kvList(pairs) {
220
+ return `<div class="divide-y divide-gray-50">${pairs.map(([k,v,vh])=>kv(k,v,vh)).join('')}</div>`;
221
+ }
222
+
223
+ function bar(pct, color='bg-blue-500') {
224
+ const w = Math.min(100,Math.max(0,pct||0));
225
+ return `<div class="metric-bar w-full"><div class="${color} metric-bar-fill" style="width:${w.toFixed(1)}%"></div></div>`;
226
+ }
227
+
228
+ function errState(msg, hint='') {
229
+ return `<div class="flex flex-col items-center justify-center py-16 gap-3 text-center">
230
+ <div class="w-10 h-10 rounded-full bg-red-50 border border-red-100 flex items-center justify-center text-red-500 text-lg font-bold">✕</div>
231
+ <div class="mono text-sm text-red-600 max-w-sm">${esc(msg)}</div>
232
+ ${hint?`<div class="text-xs text-gray-400 max-w-sm leading-relaxed">${esc(hint)}</div>`:''}
233
+ </div>`;
234
+ }
235
+
236
+ function checkRow(label, obj) {
237
+ const ok = obj?.ready ?? obj?.available ?? true;
238
+ const st = obj?.status ?? (ok ? 'healthy' : 'error');
239
+ const sc = statusCls(st) || (ok?'ok':'err');
240
+ const icon = sc==='ok' ? '✓' : sc==='err' ? '✕' : '~';
241
+ const iconCls = sc==='ok' ? 'text-emerald-600 bg-emerald-50' : sc==='err' ? 'text-red-600 bg-red-50' : 'text-amber-600 bg-amber-50';
242
+ const extra = obj?.backend ? ` · ${obj.backend}` : obj?.source ? ` · ${obj.source}` : '';
243
+ return `<div class="flex items-center gap-3 p-3 bg-gray-50 rounded-lg border border-gray-100">
244
+ <div class="w-6 h-6 rounded-full ${iconCls} flex items-center justify-center text-xs font-bold flex-shrink-0">${icon}</div>
245
+ <div class="flex-1 min-w-0">
246
+ <div class="text-sm font-medium text-gray-700">${esc(humanize(label))}</div>
247
+ ${extra?`<div class="text-xs text-gray-400 mono">${esc(extra)}</div>`:''}
248
+ </div>
249
+ ${autoBadge(st)}
250
+ </div>`;
251
+ }
252
+
253
+ // ═══ SIDEBAR RENDER ═══
254
+ function renderSidebar() {
255
+ let html = '';
256
+ let lastGroup = null;
257
+ for (const item of NAV) {
258
+ if (item.group && item.group !== lastGroup) {
259
+ html += `<div class="nav-group">${item.group}</div>`;
260
+ lastGroup = item.group;
261
+ }
262
+ const isActive = view === item.id;
263
+ const activeCls = isActive ? ' active' : '';
264
+ const epHtml = item.ep ? `<span class="ep mono">${item.ep}</span>` : '';
265
+ html += `<div class="nav-item${activeCls}" onclick="navigate('${item.id}')">${esc(item.label)}${epHtml}</div>`;
266
+ }
267
+ $('sidebar-nav').innerHTML = html;
268
+ }
269
+
270
+ // ═══ LIVE PILL ═══
271
+ function setLive(state) { // 'ok'|'err'|'connecting'
272
+ const el = $('live-pill');
273
+ const txt = $('live-text');
274
+ if (!el) return;
275
+ const map = {
276
+ ok: ['text-emerald-400','Live'],
277
+ err: ['text-red-400','Unreachable'],
278
+ connecting: ['text-amber-400','Connecting…'],
279
+ };
280
+ const [cls, label] = map[state] || map.connecting;
281
+ el.className = `flex items-center gap-2 text-xs font-semibold ${cls}`;
282
+ txt.textContent = label;
283
+ }
284
+
285
+ // ═══ FETCH & CACHE ═══
286
+ async function get(path) {
287
+ const r = await fetch(BASE + path, { cache:'no-store' });
288
+ const ct = r.headers.get('content-type') || '';
289
+ const isJ = ct.includes('json');
290
+ const data = isJ ? await r.json() : await r.text();
291
+ return { ok:r.ok, status:r.status, isJ, data };
292
+ }
293
+
294
+ async function fetchPath(path) {
295
+ try {
296
+ const r = await get(path);
297
+ cache[path] = { ok:r.ok, status:r.status, data:r.data, isJ:r.isJ, ts:Date.now() };
298
+ return cache[path];
299
+ } catch(e) {
300
+ cache[path] = { ok:false, status:0, data:null, error:e.message, ts:Date.now() };
301
+ return cache[path];
302
+ }
303
+ }
304
+
305
+ // ═══ NAVIGATION & REFRESH ═══
306
+ const VIEW_ENDPOINTS = {
307
+ overview: ['/livez','/readyz','/stats'],
308
+ livez: ['/livez'],
309
+ readyz: ['/readyz'],
310
+ health: ['/health'],
311
+ stats: ['/stats'],
312
+ history: ['/stats-history'],
313
+ metrics: ['/metrics'],
314
+ };
315
+
316
+ function navigate(id) {
317
+ view = id;
318
+ const def = NAV.find(n=>n.id===id);
319
+ $('page-title').textContent = def?.label || id;
320
+ $('page-badges').innerHTML = def?.ep ? `<span class="mono text-xs text-gray-400">${def.ep}</span>` : '';
321
+ renderSidebar();
322
+ doRefresh();
323
+ }
324
+
325
+ async function doRefresh() {
326
+ const paths = VIEW_ENDPOINTS[view] || [];
327
+ await Promise.all(paths.map(p => fetchPath(p)));
328
+ renderView();
329
+ const liveCache = cache['/livez'];
330
+ if (liveCache) setLive(liveCache.ok ? 'ok' : 'err');
331
+ $('last-upd').textContent = new Date().toLocaleTimeString();
332
+ }
333
+
334
+ function renderView() {
335
+ destroyAllCharts();
336
+ switch(view) {
337
+ case 'overview': viewOverview(); break;
338
+ case 'livez': viewLivez(); break;
339
+ case 'readyz': viewReadyz(); break;
340
+ case 'health': viewHealth(); break;
341
+ case 'stats': viewStats(); break;
342
+ case 'history': viewHistory(); break;
343
+ case 'metrics': viewMetrics(); break;
344
+ }
345
+ }
346
+
347
+ // ═══ AUTO-REFRESH ═══
348
+ function schedNext() {
349
+ if (timer) clearTimeout(timer);
350
+ if (!autoRef) return;
351
+ timer = setTimeout(() => doRefresh().then(schedNext), ivMs);
352
+ }
353
+ function toggleAuto() {
354
+ autoRef = !autoRef;
355
+ const btn = $('toggle-btn');
356
+ btn.textContent = autoRef ? '⏸' : '▶';
357
+ btn.className = autoRef
358
+ ? 'ml-auto text-xs font-medium px-2.5 py-1 rounded bg-emerald-900/60 text-emerald-400 border border-emerald-800 hover:bg-emerald-800 transition-colors'
359
+ : 'ml-auto text-xs font-medium px-2.5 py-1 rounded bg-amber-900/60 text-amber-400 border border-amber-800 hover:bg-amber-800 transition-colors';
360
+ if (autoRef) doRefresh().then(schedNext);
361
+ else if (timer) { clearTimeout(timer); timer = null; }
362
+ }
363
+ $('iv-in').addEventListener('change', function() {
364
+ const v = parseInt(this.value);
365
+ if (v >= 1) { ivMs = v*1000; if (autoRef) schedNext(); }
366
+ });
367
+
368
+ // ═══ CHART HELPERS ═══
369
+ function destroyAllCharts() {
370
+ Object.values(charts).forEach(c => { try { c.destroy(); } catch(e){} });
371
+ Object.keys(charts).forEach(k => delete charts[k]);
372
+ }
373
+
374
+ function makeChart(id, config) {
375
+ const ctx = $(id);
376
+ if (!ctx) return;
377
+ if (charts[id]) { try { charts[id].destroy(); } catch(e){} }
378
+ charts[id] = new Chart(ctx, config);
379
+ }
380
+
381
+ const BLUE_GRADIENT = ctx => {
382
+ const g = ctx.createLinearGradient(0,0,0,ctx.canvas.height);
383
+ g.addColorStop(0,'rgba(37,99,235,.15)');
384
+ g.addColorStop(1,'rgba(37,99,235,0)');
385
+ return g;
386
+ };
387
+
388
+ // ═══ VIEW: OVERVIEW ═══
389
+ function viewOverview() {
390
+ const ls = cache['/livez'] || {};
391
+ const rs = cache['/readyz'] || {};
392
+ const ss = cache['/stats'] || {};
393
+ const d = ss.data || {};
394
+
395
+ // Extract key metrics
396
+ const summaryDisplay = dig(d,'summary.display') || '';
397
+ const tokensSaved = pick(d,'tokens.saved','tokens_saved','savings.by_layer.compression.all_layers_tokens');
398
+ const savingsPct = pick(d,'tokens.savings_percent','tokens.all_layers_savings_percent','savings_percent');
399
+ const activePct = pick(d,'tokens.active_savings_percent');
400
+ const reqTotal = pick(d,'requests.total','total_requests','summary.by_request.total');
401
+ const reqCached = pick(d,'requests.cached','cached_requests');
402
+ const costSaved = pick(d,'cost.savings_usd');
403
+ const costTotal = pick(d,'cost.total_cost_usd');
404
+ const overheadAvg = pick(d,'overhead.average_ms');
405
+ const latAvg = pick(d,'latency.average_ms');
406
+ const ccrEntries = pick(d,'compression.ccr_entries');
407
+ const ccrRetr = pick(d,'compression.ccr_retrievals');
408
+ const feedbackRate = pick(d,'feedback_loop.global_retrieval_rate','telemetry.global_retrieval_rate');
409
+
410
+ // Strategy data
411
+ const byStrategy = d.compressions_by_strategy || dig(d,'summary.by_transform') || {};
412
+ const tokByStrategy = d.tokens_saved_by_strategy || {};
413
+ const stratTotal = Object.values(byStrategy).reduce((a,b)=>a+b,0) || 1;
414
+
415
+ const strategyNames = {
416
+ smart_crusher: 'SmartCrusher',
417
+ code_compressor: 'CodeCompressor',
418
+ kompress_base: 'Kompress-base',
419
+ llmlingua: 'LLMLingua',
420
+ };
421
+
422
+ const livez = ls.ok;
423
+ const readyz = rs.ok;
424
+
425
+ // Status strip
426
+ const statusStrip = `<div class="flex items-center gap-3 flex-wrap">
427
+ <div class="flex items-center gap-2 px-3 py-1.5 rounded-lg border ${livez?'border-emerald-200 bg-emerald-50':'border-red-200 bg-red-50'}">
428
+ <span class="w-2 h-2 rounded-full ${livez?'bg-emerald-500 pulse-ok':'bg-red-500'}"></span>
429
+ <span class="text-xs font-semibold ${livez?'text-emerald-700':'text-red-700'}">Liveness ${livez?'OK':'DOWN'}</span>
430
+ </div>
431
+ <div class="flex items-center gap-2 px-3 py-1.5 rounded-lg border ${readyz?'border-emerald-200 bg-emerald-50':'border-red-200 bg-red-50'}">
432
+ <span class="w-2 h-2 rounded-full ${readyz?'bg-emerald-500 pulse-ok':'bg-red-500'}"></span>
433
+ <span class="text-xs font-semibold ${readyz?'text-emerald-700':'text-red-700'}">Readiness ${readyz?'READY':'NOT READY'}</span>
434
+ </div>
435
+ ${activePct!=null?`<div class="flex items-center gap-2 px-3 py-1.5 rounded-lg border border-blue-200 bg-blue-50">
436
+ <span class="text-xs font-semibold text-blue-700">Active compression: ${fmtPct(activePct)}</span>
437
+ </div>`:''}
438
+ ${overheadAvg!=null?`<div class="flex items-center gap-2 px-3 py-1.5 rounded-lg border border-gray-200 bg-gray-50">
439
+ <span class="text-xs text-gray-500">Overhead <span class="font-semibold text-gray-700 mono">${fmtMs(overheadAvg)}</span></span>
440
+ </div>`:''}
441
+ </div>`;
442
+
443
+ // Hero banner
444
+ const heroBanner = summaryDisplay ? `
445
+ <div class="bg-gradient-to-r from-blue-600 to-blue-700 rounded-xl p-5 mb-6 text-white">
446
+ <div class="text-xs font-semibold text-blue-200 uppercase tracking-wider mb-1">Session Summary</div>
447
+ <div class="text-lg font-semibold mono">${esc(summaryDisplay)}</div>
448
+ </div>` : '';
449
+
450
+ // Stat cards
451
+ const cards4 = `<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
452
+ ${statCard('Tokens Saved', fmtTokens(tokensSaved), savingsPct!=null?fmtPct(savingsPct)+' reduction':'all layers', 'text-blue-600')}
453
+ ${statCard('Requests', fmtNum(reqTotal), reqCached!=null?`${fmtNum(reqCached)} cache hits`:'', 'text-purple-600')}
454
+ ${statCard('Cost Saved', fmtUSD(costSaved), costTotal!=null?`of ${fmtUSD(costTotal)} total`:'', 'text-emerald-600')}
455
+ ${statCard('Avg Latency', fmtMs(latAvg), overheadAvg!=null?`+${fmtMs(overheadAvg)} overhead`:'', 'text-amber-600')}
456
+ </div>`;
457
+
458
+ // Strategy breakdown
459
+ const strategyRows = Object.entries(byStrategy).map(([k,count]) => {
460
+ const name = strategyNames[k] || humanize(k);
461
+ const tokSaved = tokByStrategy[k];
462
+ const pct = count / stratTotal * 100;
463
+ return `<div class="space-y-1.5">
464
+ <div class="flex items-center justify-between text-xs">
465
+ <span class="font-medium text-gray-700">${esc(name)}</span>
466
+ <span class="mono text-gray-500">${count} calls${tokSaved?` · ${fmtTokens(tokSaved)} saved`:''}</span>
467
+ </div>
468
+ ${bar(pct)}
469
+ </div>`;
470
+ });
471
+
472
+ // CCR card
473
+ const ccrCard = (ccrEntries != null) ? `
474
+ <div class="grid grid-cols-3 gap-3 mt-4">
475
+ <div class="bg-gray-50 rounded-lg p-3 text-center">
476
+ <div class="mono text-lg font-semibold text-gray-800">${fmtNum(ccrEntries)}</div>
477
+ <div class="text-xs text-gray-400 mt-0.5">CCR Entries</div>
478
+ </div>
479
+ <div class="bg-gray-50 rounded-lg p-3 text-center">
480
+ <div class="mono text-lg font-semibold text-gray-800">${fmtNum(ccrRetr)}</div>
481
+ <div class="text-xs text-gray-400 mt-0.5">Retrievals</div>
482
+ </div>
483
+ <div class="bg-gray-50 rounded-lg p-3 text-center ${feedbackRate!=null&&feedbackRate>0.1?'bg-amber-50':''}">
484
+ <div class="mono text-lg font-semibold ${feedbackRate!=null&&feedbackRate>0.1?'text-amber-600':'text-gray-800'}">${fmtPct(feedbackRate)}</div>
485
+ <div class="text-xs text-gray-400 mt-0.5">Retrieval Rate</div>
486
+ </div>
487
+ </div>` : '';
488
+
489
+ // Error state if no stats
490
+ if (!ss.ok && ss.ts) {
491
+ setContent(`
492
+ ${heroBanner}
493
+ <div class="space-y-6">
494
+ <div class="mb-4">${statusStrip}</div>
495
+ ${card('Statistics Unavailable', errState(ss.error||'HTTP '+ss.status,
496
+ 'The headroom proxy is reachable but /stats returned an error. Try refreshing.'))}
497
+ </div>`);
498
+ return;
499
+ }
500
+
501
+ setContent(`
502
+ ${heroBanner}
503
+ <div class="space-y-6">
504
+ <div>${statusStrip}</div>
505
+ ${cards4}
506
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
507
+ ${card('Compression Strategies', strategyRows.length
508
+ ? `<div class="space-y-4">${strategyRows.join('')}</div>${ccrCard}`
509
+ : '<div class="text-sm text-gray-400">No compression data yet</div>')}
510
+ ${card('Live History', `<div style="position:relative;height:160px"><canvas id="ov-hist-chart"></canvas></div>`, 'Tokens saved')}
511
+ </div>
512
+ </div>`);
513
+
514
+ // History sparkline from stats.savings_history or local history
515
+ requestAnimationFrame(() => {
516
+ const hist = d.savings_history || [];
517
+ if (!hist.length) return;
518
+ const labels = hist.map(h => {
519
+ const t = h.timestamp || h.ts;
520
+ return t ? new Date(t).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'}) : '';
521
+ });
522
+ const vals = hist.map(h => h.tokens_saved || h.value || 0);
523
+ makeChart('ov-hist-chart', {
524
+ type:'line',
525
+ data:{ labels, datasets:[{
526
+ data:vals, borderColor:'#2563eb', borderWidth:2, fill:true,
527
+ backgroundColor: ctx => BLUE_GRADIENT(ctx.chart.ctx),
528
+ tension:0.4, pointRadius:0, pointHoverRadius:4
529
+ }]},
530
+ options:{
531
+ responsive:true, maintainAspectRatio:false,
532
+ plugins:{ legend:{display:false}, tooltip:{ callbacks:{ label:c=>` ${fmtTokens(c.parsed.y)} saved` } } },
533
+ scales:{
534
+ x:{ grid:{display:false}, ticks:{font:{size:10},maxTicksLimit:8,maxRotation:0} },
535
+ y:{ grid:{color:'#f3f4f6'}, ticks:{font:{size:10},callback:v=>fmtTokens(v)} }
536
+ }
537
+ }
538
+ });
539
+ });
540
+ }
541
+
542
+ // ═══ VIEW: LIVENESS ═══
543
+ function viewLivez() {
544
+ const c = cache['/livez'] || {};
545
+ if (c.error) { setContent(errState(c.error,'Cannot reach headroom proxy. Is it running?')); return; }
546
+ const d = c.data || {};
547
+ const isJ = c.isJ;
548
+ const ok = c.ok;
549
+
550
+ const data = isJ && typeof d === 'object' ? d : {};
551
+ const status = data.status || (ok ? 'healthy' : 'error');
552
+ const service = data.service || 'headroom-proxy';
553
+ const version = data.version;
554
+ const uptime = data.uptime_seconds;
555
+ const alive = data.alive ?? ok;
556
+ const ts = data.timestamp;
557
+
558
+ const scalars = Object.entries(data).filter(([k,v]) =>
559
+ !['status','service','version','uptime_seconds','alive','timestamp'].includes(k) && typeof v !== 'object'
560
+ );
561
+
562
+ setContent(`
563
+ <div class="max-w-lg space-y-4">
564
+ <div class="bg-white rounded-xl border ${ok?'border-emerald-200':'border-red-200'} shadow-sm p-8 text-center">
565
+ <div class="w-20 h-20 rounded-full ${ok?'bg-emerald-50 border-2 border-emerald-200':'bg-red-50 border-2 border-red-200'} flex items-center justify-center mx-auto mb-4">
566
+ <span class="text-3xl font-bold ${ok?'text-emerald-600':'text-red-600'}">${ok?'✓':'✕'}</span>
567
+ </div>
568
+ <div class="text-2xl font-bold ${ok?'text-emerald-700':'text-red-700'} mb-1">${ok?'ALIVE':'DOWN'}</div>
569
+ <div class="mono text-sm text-gray-400">HTTP ${c.status}</div>
570
+ </div>
571
+ ${card('Process Info', kvList([
572
+ ['Service', service],
573
+ ...(version ? [['Version', version]] : []),
574
+ ...(uptime != null ? [['Uptime', fmtDur(uptime)]] : []),
575
+ ...(alive != null ? [['Alive', alive, autoBadge(String(alive))]] : []),
576
+ ...(ts ? [['Timestamp', new Date(ts).toLocaleString()]] : []),
577
+ ...scalars.map(([k,v]) => [k, v, autoBadge(v)]),
578
+ ]))}
579
+ </div>`);
580
+ }
581
+
582
+ // ═══ VIEW: READINESS ═══
583
+ function viewReadyz() {
584
+ const c = cache['/readyz'] || {};
585
+ if (c.error) { setContent(errState(c.error)); return; }
586
+ const d = c.data || {};
587
+ const ok = c.ok;
588
+ const checks = d.checks || {};
589
+ const runtime = d.runtime || {};
590
+ const exec = runtime.compression_executor || {};
591
+ const ws = runtime.websocket_sessions || {};
592
+ const pre = runtime.anthropic_pre_upstream || {};
593
+ const rust = d.rust_core;
594
+
595
+ setContent(`
596
+ <div class="space-y-5">
597
+ <!-- Overall status -->
598
+ <div class="flex items-center gap-4 bg-white rounded-xl border ${ok?'border-emerald-200':'border-red-200'} shadow-sm p-5">
599
+ <div class="w-12 h-12 rounded-full ${ok?'bg-emerald-50':'bg-red-50'} flex items-center justify-center text-xl font-bold ${ok?'text-emerald-600':'text-red-600'} flex-shrink-0">${ok?'✓':'✕'}</div>
600
+ <div>
601
+ <div class="text-base font-semibold ${ok?'text-emerald-700':'text-red-700'}">${ok?'READY':'NOT READY'}</div>
602
+ <div class="text-sm text-gray-400">${d.service||'headroom-proxy'} ${d.version?'v'+d.version:''} · HTTP ${c.status}</div>
603
+ ${d.uptime_seconds!=null?`<div class="text-xs text-gray-400 mono mt-0.5">Uptime: ${fmtDur(d.uptime_seconds)}</div>`:''}
604
+ </div>
605
+ ${rust?`<div class="ml-auto">${badge('Rust: '+rust, statusCls(rust)||'gray')}</div>`:''}
606
+ </div>
607
+
608
+ <!-- Checks grid -->
609
+ ${Object.keys(checks).length ? card('Subsystem Checks',
610
+ `<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
611
+ ${Object.entries(checks).map(([k,v])=>checkRow(k,v)).join('')}
612
+ </div>`) : ''}
613
+
614
+ <!-- Runtime executor -->
615
+ ${exec.max_workers!=null ? card('Compression Executor', kvList([
616
+ ['Workers', exec.max_workers],
617
+ ['In-flight', `${exec.in_flight||0} / ${exec.in_flight_max||'—'}`, `<span class="mono text-xs text-gray-700">${exec.in_flight||0} <span class="text-gray-400">/ ${exec.in_flight_max||'—'}</span></span>`],
618
+ ['Queued', `${exec.queued||0} / ${exec.queued_max||'—'}`, `<span class="mono text-xs text-gray-700">${exec.queued||0} <span class="text-gray-400">/ ${exec.queued_max||'—'}</span></span>`],
619
+ ['Run Time (total)', fmtMs(exec.run_seconds_total*1000), `<span class="mono text-xs text-gray-700">${fmtMs((exec.run_seconds_total||0)*1000)}</span>`],
620
+ ['Max Run Time', fmtMs((exec.run_seconds_max||0)*1000)],
621
+ ['Source', exec.source||'—'],
622
+ ])) : ''}
623
+
624
+ <!-- WebSocket sessions -->
625
+ ${ws.active_sessions!=null ? card('WebSocket Sessions', kvList([
626
+ ['Active Sessions', ws.active_sessions],
627
+ ['Active Relay Tasks', ws.active_relay_tasks||0],
628
+ ])) : ''}
629
+
630
+ <!-- Anthropic concurrency -->
631
+ ${pre.enabled ? card('Anthropic Concurrency', kvList([
632
+ ['Enabled', pre.enabled, autoBadge(String(pre.enabled))],
633
+ ['Concurrency', pre.resolved_concurrency||'—'],
634
+ ['Source', pre.source||'—'],
635
+ ['Acquire Timeout', fmtDur(pre.acquire_timeout_seconds)],
636
+ ])) : ''}
637
+ </div>`);
638
+ }
639
+
640
+ // ═══ VIEW: HEALTH ═══
641
+ function viewHealth() {
642
+ const c = cache['/health'] || {};
643
+ if (c.error) { setContent(errState(c.error)); return; }
644
+ const d = c.data || {};
645
+ const ok = c.ok;
646
+
647
+ // /health returns simpler shape per docs
648
+ const status = d.status || (ok?'healthy':'error');
649
+ const optimize = d.optimize;
650
+ const stats = d.stats || {};
651
+ const sc = statusCls(status)||'gray';
652
+
653
+ const scalars = Object.entries(d).filter(([k,v]) =>
654
+ !['status','optimize','stats','checks','runtime','config'].includes(k) && typeof v !== 'object'
655
+ );
656
+ const config = d.config || {};
657
+
658
+ setContent(`
659
+ <div class="space-y-5 max-w-2xl">
660
+ <div class="bg-white rounded-xl border ${sc==='ok'?'border-emerald-200':'border-red-200'} shadow-sm p-6">
661
+ <div class="flex items-center gap-4">
662
+ <div class="w-14 h-14 rounded-full ${sc==='ok'?'bg-emerald-50':'bg-red-50'} flex items-center justify-center text-2xl font-bold ${sc==='ok'?'text-emerald-600':'text-red-600'} flex-shrink-0">${sc==='ok'?'✓':'✕'}</div>
663
+ <div class="flex-1">
664
+ <div class="text-xl font-bold ${sc==='ok'?'text-emerald-700':'text-red-700'}">${esc(String(status).toUpperCase())}</div>
665
+ <div class="flex items-center gap-2 mt-1 flex-wrap">
666
+ ${optimize!=null ? badge(optimize?'Optimization ON':'Optimization OFF', optimize?'ok':'warn') : ''}
667
+ ${badge('HTTP '+c.status,'gray')}
668
+ </div>
669
+ </div>
670
+ </div>
671
+ </div>
672
+
673
+ ${stats.total_requests!=null || stats.tokens_saved!=null ? card('Quick Stats', `
674
+ <div class="grid grid-cols-3 gap-3">
675
+ ${stats.total_requests!=null?`<div class="bg-gray-50 rounded-lg p-3 text-center"><div class="mono text-xl font-semibold text-gray-800">${fmtNum(stats.total_requests)}</div><div class="text-xs text-gray-400 mt-0.5">Requests</div></div>`:''}
676
+ ${stats.tokens_saved!=null?`<div class="bg-blue-50 rounded-lg p-3 text-center"><div class="mono text-xl font-semibold text-blue-700">${fmtTokens(stats.tokens_saved)}</div><div class="text-xs text-gray-400 mt-0.5">Tokens Saved</div></div>`:''}
677
+ ${stats.savings_percent!=null?`<div class="bg-emerald-50 rounded-lg p-3 text-center"><div class="mono text-xl font-semibold text-emerald-700">${fmtPct(stats.savings_percent)}</div><div class="text-xs text-gray-400 mt-0.5">Savings</div></div>`:''}
678
+ </div>`) : ''}
679
+
680
+ ${scalars.length ? card('Details', kvList(scalars.map(([k,v]) => [k, v, autoBadge(v)]))) : ''}
681
+ ${Object.keys(config).length ? card('Config', kvList(
682
+ Object.entries(config).filter(([,v])=>typeof v!=='object').map(([k,v])=>[k,v,autoBadge(v)])
683
+ )) : ''}
684
+ </div>`);
685
+ }
686
+
687
+ // ═══ VIEW: STATISTICS ═══
688
+ function viewStats() {
689
+ const c = cache['/stats'] || {};
690
+ if (c.error) { setContent(errState(c.error)); return; }
691
+ if (!c.ok) { setContent(errState('HTTP '+c.status)); return; }
692
+ const d = c.data || {};
693
+
694
+ // Parse all fields
695
+ const tok = {
696
+ saved: pick(d,'tokens.saved','tokens_saved'),
697
+ input: pick(d,'tokens.input','tokens_input'),
698
+ output: pick(d,'tokens.output','tokens_output'),
699
+ savedPct: pick(d,'tokens.savings_percent','savings_percent'),
700
+ activePct: pick(d,'tokens.active_savings_percent'),
701
+ allPct: pick(d,'tokens.all_layers_savings_percent'),
702
+ proxyBefore: pick(d,'tokens.proxy_total_before_compression'),
703
+ };
704
+ const req = {
705
+ total: pick(d,'requests.total','total_requests'),
706
+ cached: pick(d,'requests.cached','cached_requests'),
707
+ failed: pick(d,'requests.failed','failed_requests'),
708
+ rateLim: pick(d,'requests.rate_limited'),
709
+ byProv: pick(d,'requests.by_provider'),
710
+ byModel: pick(d,'requests.by_model'),
711
+ };
712
+ const lat = {
713
+ avg: pick(d,'latency.average_ms'),
714
+ min: pick(d,'latency.min_ms'),
715
+ max: pick(d,'latency.max_ms'),
716
+ };
717
+ const ovh = { avg: pick(d,'overhead.average_ms'), min: pick(d,'overhead.min_ms'), max: pick(d,'overhead.max_ms') };
718
+ const ttfb = { avg: pick(d,'ttfb.average_ms'), min: pick(d,'ttfb.min_ms'), max: pick(d,'ttfb.max_ms') };
719
+ const cost = {
720
+ total: pick(d,'cost.total_cost_usd'),
721
+ saved: pick(d,'cost.savings_usd'),
722
+ pct: pick(d,'cost.savings_percent'),
723
+ prefix: pick(d,'prefix_cache.totals.net_savings_usd','cost.prefix_cache_cost_usd'),
724
+ inp: pick(d,'cost.input_cost_usd'),
725
+ out: pick(d,'cost.output_cost_usd'),
726
+ };
727
+ const cache_ = {
728
+ hits: pick(d,'cache.hits','compression_cache.total_hits'),
729
+ misses: pick(d,'cache.misses','compression_cache.total_misses'),
730
+ hitRate: pick(d,'cache.hit_rate','compression_cache.hit_rate'),
731
+ entries: pick(d,'cache.entries','compression_cache.total_entries'),
732
+ tokSaved: pick(d,'cache.tokens_saved','compression_cache.total_tokens_saved'),
733
+ };
734
+ const ccr = {
735
+ entries: pick(d,'compression.ccr_entries'),
736
+ max: pick(d,'compression.ccr_max_entries'),
737
+ origToks: pick(d,'compression.original_tokens_cached'),
738
+ compToks: pick(d,'compression.compressed_tokens_cached'),
739
+ retrievals:pick(d,'compression.ccr_retrievals'),
740
+ };
741
+ const fb = {
742
+ tools: pick(d,'feedback_loop.tools_tracked'),
743
+ total: pick(d,'feedback_loop.total_compressions'),
744
+ retr: pick(d,'feedback_loop.total_retrievals'),
745
+ rate: pick(d,'feedback_loop.global_retrieval_rate'),
746
+ highRetr: pick(d,'feedback_loop.tools_with_high_retrieval'),
747
+ };
748
+ const byStrat = d.compressions_by_strategy || dig(d,'summary.by_transform') || {};
749
+ const tokByStrat = d.tokens_saved_by_strategy || {};
750
+ const stratTotal = Object.values(byStrat).reduce((a,b)=>a+b,0)||1;
751
+ const maxTokStrat = Math.max(...Object.values(tokByStrat),1);
752
+
753
+ const stratNames = { smart_crusher:'SmartCrusher', code_compressor:'CodeCompressor', kompress_base:'Kompress-base', llmlingua:'LLMLingua', cache_aligner:'CacheAligner', intelligent_context:'IntelligentContext' };
754
+
755
+ setContent(`
756
+ <div class="space-y-5">
757
+
758
+ <!-- Token Economics -->
759
+ <div>
760
+ <div class="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-3">Token Economics</div>
761
+ <div class="grid grid-cols-2 lg:grid-cols-4 gap-3 mb-3">
762
+ ${statCard('Tokens Saved', fmtTokens(tok.saved), tok.savedPct!=null?fmtPct(tok.savedPct)+' reduction':'', 'text-blue-600')}
763
+ ${statCard('Active Savings', tok.activePct!=null?fmtPct(tok.activePct):'—', 'on compressible content', 'text-blue-600')}
764
+ ${statCard('Input', fmtTokens(tok.input), 'before compression', 'text-gray-600')}
765
+ ${statCard('Output', fmtTokens(tok.output), 'after compression', 'text-gray-600')}
766
+ </div>
767
+ ${tok.proxyBefore!=null?`<div class="bg-blue-50 border border-blue-100 rounded-lg px-4 py-2 text-xs text-blue-700 mono">Before compression: ${fmtTokens(tok.proxyBefore)} → After: ${fmtTokens(tok.output)} · All layers saved: ${fmtPct(tok.allPct)}</div>`:''}
768
+ </div>
769
+
770
+ <!-- Strategy breakdown + chart -->
771
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
772
+ ${card('By Strategy', Object.keys(byStrat).length ? `
773
+ <div class="space-y-4">
774
+ ${Object.entries(byStrat).map(([k,count]) => {
775
+ const name = stratNames[k]||humanize(k);
776
+ const ts2 = tokByStrat[k];
777
+ const pct = count/stratTotal*100;
778
+ const tpct = ts2 ? ts2/maxTokStrat*100 : 0;
779
+ return `<div class="space-y-1.5">
780
+ <div class="flex justify-between text-xs">
781
+ <span class="font-medium text-gray-700">${esc(name)}</span>
782
+ <span class="mono text-gray-400">${count} · ${ts2?fmtTokens(ts2)+' saved':'—'}</span>
783
+ </div>
784
+ <div class="flex gap-1.5 items-center">
785
+ ${bar(pct,'bg-blue-400')}
786
+ <span class="mono text-xs text-gray-400 w-8 text-right">${pct.toFixed(0)}%</span>
787
+ </div>
788
+ </div>`;
789
+ }).join('')}
790
+ </div>` : '<div class="text-sm text-gray-400">No data</div>')}
791
+ ${card('Strategy Chart', `<div style="position:relative;height:200px"><canvas id="strat-chart"></canvas></div>`)}
792
+ </div>
793
+
794
+ <!-- Requests -->
795
+ <div>
796
+ <div class="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-3">Requests</div>
797
+ <div class="grid grid-cols-2 lg:grid-cols-4 gap-3">
798
+ ${statCard('Total', fmtNum(req.total), '', 'text-gray-700')}
799
+ ${statCard('Cache Hits', fmtNum(req.cached), req.total?fmtPct(req.cached/req.total)+' hit rate':'', 'text-emerald-600')}
800
+ ${statCard('Failed', fmtNum(req.failed), '', req.failed?'text-red-500':'text-gray-500')}
801
+ ${statCard('Rate Limited',fmtNum(req.rateLim), '', req.rateLim?'text-amber-600':'text-gray-500')}
802
+ </div>
803
+ ${req.byProv ? `<div class="mt-3 flex flex-wrap gap-2">
804
+ ${Object.entries(req.byProv).map(([p,n])=>`<span class="mono text-xs bg-gray-100 text-gray-600 border border-gray-200 px-2 py-1 rounded">${esc(p)}: ${fmtNum(n)}</span>`).join('')}
805
+ </div>` : ''}
806
+ </div>
807
+
808
+ <!-- Latency & Performance -->
809
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
810
+ ${card('Request Latency', kvList([
811
+ ['Average', fmtMs(lat.avg)],
812
+ ['Minimum', fmtMs(lat.min)],
813
+ ['Maximum', fmtMs(lat.max)],
814
+ ]))}
815
+ ${card('Headroom Overhead', kvList([
816
+ ['Average', fmtMs(ovh.avg)],
817
+ ['Minimum', fmtMs(ovh.min)],
818
+ ['Maximum', fmtMs(ovh.max)],
819
+ ]),'subtitle: time spent optimizing')}
820
+ ${card('Time to First Byte', kvList([
821
+ ['Average', fmtMs(ttfb.avg)],
822
+ ['Minimum', fmtMs(ttfb.min)],
823
+ ['Maximum', fmtMs(ttfb.max)],
824
+ ]))}
825
+ </div>
826
+
827
+ <!-- Semantic Cache -->
828
+ ${cache_.hits!=null ? `<div>
829
+ <div class="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-3">Semantic Cache</div>
830
+ <div class="grid grid-cols-2 lg:grid-cols-4 gap-3">
831
+ ${statCard('Hit Rate', fmtPct(cache_.hitRate!=null?cache_.hitRate/100:null),'', 'text-emerald-600')}
832
+ ${statCard('Hits', fmtNum(cache_.hits), '', 'text-gray-700')}
833
+ ${statCard('Misses', fmtNum(cache_.misses), '', 'text-gray-500')}
834
+ ${statCard('Tokens Saved', fmtTokens(cache_.tokSaved), '', 'text-blue-600')}
835
+ </div>
836
+ </div>` : ''}
837
+
838
+ <!-- Cost -->
839
+ ${cost.total!=null ? `<div>
840
+ <div class="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-3">Cost Analysis</div>
841
+ <div class="grid grid-cols-2 lg:grid-cols-4 gap-3">
842
+ ${statCard('Total Cost', fmtUSD(cost.total), '', 'text-gray-700')}
843
+ ${statCard('Cost Saved', fmtUSD(cost.saved), cost.pct!=null?fmtPct(cost.pct)+' savings':'', 'text-emerald-600')}
844
+ ${statCard('Prefix Cache', fmtUSD(cost.prefix),'KV cache discount', 'text-purple-600')}
845
+ ${statCard('Est. Without Headroom', fmtUSD((cost.total||0)+(cost.saved||0)), '', 'text-gray-500')}
846
+ </div>
847
+ </div>` : ''}
848
+
849
+ <!-- CCR & Feedback -->
850
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
851
+ ${ccr.entries!=null ? card('CCR — Compress-Cache-Retrieve', kvList([
852
+ ['Cached Originals', `${fmtNum(ccr.entries)} / ${fmtNum(ccr.max)}`, `<span class="mono text-xs text-gray-700">${fmtNum(ccr.entries)}<span class="text-gray-400"> / ${fmtNum(ccr.max)}</span></span>`],
853
+ ['Original Tokens', fmtTokens(ccr.origToks)],
854
+ ['Compressed Tokens', fmtTokens(ccr.compToks)],
855
+ ['Retrievals', ccr.retrievals, `<span class="mono text-xs ${ccr.retrievals>0?'text-amber-600':'text-gray-700'}">${fmtNum(ccr.retrievals)}</span>`],
856
+ ]),'5-min TTL cache of original content for LLM retrieval') : ''}
857
+
858
+ ${fb.tools!=null ? card('Feedback Loop', kvList([
859
+ ['Tools Tracked', fb.tools],
860
+ ['Global Retrieval Rate', fmtPct(fb.rate), `<span class="mono text-xs ${fb.rate>0.1?'text-amber-600':'text-emerald-600'}">${fmtPct(fb.rate)}</span>`],
861
+ ['High-Retrieval Tools', fb.highRetr, `<span class="mono text-xs ${fb.highRetr>0?'text-amber-600':'text-gray-700'}">${fmtNum(fb.highRetr)}</span>`],
862
+ ['Total Compressions', fb.total],
863
+ ['Total Retrievals', fb.retr],
864
+ ]),'Low retrieval rate = compression is not too aggressive') : ''}
865
+ </div>
866
+
867
+ </div>`);
868
+
869
+ // Strategy chart
870
+ requestAnimationFrame(() => {
871
+ const labels = Object.keys(byStrat).map(k=>stratNames[k]||humanize(k));
872
+ const vals = Object.values(byStrat);
873
+ if (!labels.length) return;
874
+ makeChart('strat-chart', {
875
+ type:'doughnut',
876
+ data:{ labels, datasets:[{ data:vals, backgroundColor:['#2563eb','#7c3aed','#059669','#d97706','#dc2626','#0891b2'], borderWidth:0 }] },
877
+ options:{
878
+ responsive:true, maintainAspectRatio:false, cutout:'60%',
879
+ plugins:{ legend:{ position:'right', labels:{ font:{size:11}, padding:12, boxWidth:10 } } }
880
+ }
881
+ });
882
+ });
883
+ }
884
+
885
+ // ═══ VIEW: HISTORY ═══
886
+ let histSeries = 'history';
887
+ function viewHistory() {
888
+ const c = cache['/stats-history'] || {};
889
+ if (c.error) { setContent(errState(c.error)); return; }
890
+
891
+ const d = c.data || {};
892
+ const arr = Array.isArray(d) ? d : (d.history||d.entries||d.items||d.data||[]);
893
+ const sess = d.display_session || d.session || null;
894
+ const periods = d.totals_by_period || {};
895
+
896
+ const seriesBtns = ['history','hourly','daily','weekly','monthly'].map(s =>
897
+ `<button onclick="changeHistSeries('${s}')"
898
+ class="px-3 py-1.5 text-xs font-semibold rounded-lg transition-colors ${histSeries===s
899
+ ?'bg-blue-600 text-white'
900
+ :'bg-white text-gray-500 border border-gray-200 hover:border-gray-300'}">
901
+ ${s.charAt(0).toUpperCase()+s.slice(1)}
902
+ </button>`
903
+ ).join('');
904
+
905
+ const sessCards = sess ? Object.entries(sess).filter(([,v])=>typeof v!=='object'&&v!=null).map(([k,v])=>`
906
+ <div class="bg-gray-50 rounded-lg p-3 text-center border border-gray-100">
907
+ <div class="mono text-lg font-semibold text-gray-800">${
908
+ k.includes('percent')||k.includes('ratio') ? fmtPct(v) :
909
+ k.includes('token') ? fmtTokens(v) :
910
+ k.includes('second') ? fmtDur(v) :
911
+ fmtNum(v)
912
+ }</div>
913
+ <div class="text-xs text-gray-400 mt-0.5">${humanize(k)}</div>
914
+ </div>`).join('') : '';
915
+
916
+ const periodRows = Object.entries(periods).map(([period, data]) => {
917
+ if (!data || typeof data !== 'object') return '';
918
+ const saved = data.tokens_saved??data.total_tokens??null;
919
+ const pct = data.savings_percent??null;
920
+ return `<div class="flex items-center justify-between py-2 border-b border-gray-50 last:border-0">
921
+ <span class="text-xs font-semibold text-gray-500 uppercase">${period}</span>
922
+ <div class="flex items-center gap-3">
923
+ ${saved!=null?`<span class="mono text-xs text-blue-600 font-semibold">${fmtTokens(saved)} saved</span>`:''}
924
+ ${pct!=null?badge(fmtPct(pct),'blue'):''}
925
+ </div>
926
+ </div>`;
927
+ }).join('');
928
+
929
+ setContent(`
930
+ <div class="space-y-5">
931
+ <div class="flex items-center gap-2 flex-wrap">
932
+ <span class="text-xs text-gray-400 font-medium">Series:</span>
933
+ ${seriesBtns}
934
+ </div>
935
+
936
+ ${card('Compression History', `<div style="position:relative;height:240px"><canvas id="hist-chart"></canvas></div>`,
937
+ arr.length ? arr.length+' data points' : 'No data')}
938
+
939
+ ${sessCards ? card('Current Session', `<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">${sessCards}</div>`) : ''}
940
+ ${periodRows ? card('Period Totals', `<div>${periodRows}</div>`) : ''}
941
+ </div>`);
942
+
943
+ requestAnimationFrame(() => {
944
+ if (!arr.length) return;
945
+ const labels = arr.map(h => {
946
+ const t = h.timestamp||h.time||h.ts;
947
+ if (!t) return '';
948
+ const dt = new Date(t);
949
+ return histSeries==='history'
950
+ ? dt.toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'})
951
+ : dt.toLocaleDateString([],{month:'short',day:'numeric'});
952
+ });
953
+ const vals = arr.map(h => h.tokens_saved||h.value||0);
954
+ makeChart('hist-chart', {
955
+ type:'line',
956
+ data:{ labels, datasets:[{
957
+ label:'Tokens Saved',
958
+ data:vals,
959
+ borderColor:'#2563eb', borderWidth:2.5,
960
+ fill:true,
961
+ backgroundColor: ctx => BLUE_GRADIENT(ctx.chart.ctx),
962
+ tension:0.4, pointRadius:arr.length>60?0:3,
963
+ pointBackgroundColor:'#2563eb', pointBorderColor:'#fff', pointBorderWidth:2,
964
+ pointHoverRadius:5
965
+ }]},
966
+ options:{
967
+ responsive:true, maintainAspectRatio:false,
968
+ plugins:{
969
+ legend:{display:false},
970
+ tooltip:{ mode:'index', intersect:false, callbacks:{ label:c=>` ${fmtTokens(c.parsed.y)} saved` } }
971
+ },
972
+ scales:{
973
+ x:{ grid:{display:false}, ticks:{font:{size:10},maxTicksLimit:10,maxRotation:0} },
974
+ y:{ grid:{color:'#f3f4f6'}, ticks:{font:{size:10},callback:v=>fmtTokens(v)} }
975
+ }
976
+ }
977
+ });
978
+ });
979
+ }
980
+
981
+ window.changeHistSeries = async (s) => {
982
+ histSeries = s;
983
+ await fetchPath('/stats-history?series='+s).then(r => { cache['/stats-history']=r; });
984
+ viewHistory();
985
+ };
986
+
987
+ // ═══ VIEW: PROMETHEUS ═══
988
+ function viewMetrics() {
989
+ const c = cache['/metrics'] || {};
990
+ if (c.error) { setContent(errState(c.error)); return; }
991
+
992
+ const text = c.isJ ? JSON.stringify(c.data,null,2) : (c.data||'');
993
+ const ms = parseProm(text);
994
+
995
+ if (!ms.length) {
996
+ setContent(`<div class="space-y-3">
997
+ <div class="text-xs font-semibold text-gray-400 uppercase tracking-wider">${ms.length} metrics</div>
998
+ <pre class="bg-white border border-gray-200 rounded-xl p-5 text-xs mono text-gray-600 overflow-auto max-h-96 whitespace-pre-wrap">${esc(text)}</pre>
999
+ </div>`);
1000
+ return;
1001
+ }
1002
+
1003
+ const maxN = Math.max(...ms.map(m=>m.num).filter(n=>isFinite(n)&&n>0),1);
1004
+
1005
+ setContent(`
1006
+ <div class="space-y-3">
1007
+ <div class="text-xs text-gray-400">${ms.length} metric series</div>
1008
+ <div class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
1009
+ <div class="overflow-auto max-h-[70vh]">
1010
+ <table class="w-full text-sm">
1011
+ <thead>
1012
+ <tr class="border-b border-gray-200 bg-gray-50/80 sticky top-0">
1013
+ <th class="text-left px-5 py-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Metric</th>
1014
+ <th class="text-left px-4 py-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Labels</th>
1015
+ <th class="text-right px-4 py-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Value</th>
1016
+ <th class="px-4 py-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Type</th>
1017
+ <th class="px-4 py-3 w-24"></th>
1018
+ </tr>
1019
+ </thead>
1020
+ <tbody class="divide-y divide-gray-50">
1021
+ ${ms.map(m => {
1022
+ const pct = isFinite(m.num)&&m.num>0 ? Math.min(100,m.num/maxN*100) : 0;
1023
+ return `<tr class="hover:bg-gray-50/80 transition-colors">
1024
+ <td class="px-5 py-3">
1025
+ <div class="mono text-xs font-medium text-blue-700">${esc(m.name)}</div>
1026
+ ${m.help?`<div class="text-xs text-gray-400 italic mt-0.5">${esc(m.help)}</div>`:''}
1027
+ </td>
1028
+ <td class="px-4 py-3 mono text-xs text-gray-400">${m.labels||'<span class="text-gray-200">—</span>'}</td>
1029
+ <td class="px-4 py-3 mono text-xs font-semibold text-blue-600 text-right">${esc(m.value)}</td>
1030
+ <td class="px-4 py-3">${m.type?badge(m.type,'gray'):''}</td>
1031
+ <td class="px-4 py-3">
1032
+ <div class="metric-bar w-20"><div class="metric-bar-fill bg-blue-400" style="width:${pct.toFixed(1)}%"></div></div>
1033
+ </td>
1034
+ </tr>`;
1035
+ }).join('')}
1036
+ </tbody>
1037
+ </table>
1038
+ </div>
1039
+ </div>
1040
+ </div>`);
1041
+ }
1042
+
1043
+ function parseProm(text) {
1044
+ const out = []; let help='', type='';
1045
+ for (const line of (text||'').split('\n')) {
1046
+ if (line.startsWith('# HELP ')) { help = line.slice(7).replace(/^\S+\s/,''); }
1047
+ else if (line.startsWith('# TYPE ')) { type=(line.split(' ')[3]||'').trim(); }
1048
+ else if (line && !line.startsWith('#')) {
1049
+ const m = line.match(/^([^{}\s]+)(\{[^}]*\})?\s+([\d.eE+\-NaInfinity]+)/);
1050
+ if (m) { out.push({name:m[1],labels:m[2]||'',value:m[3],num:parseFloat(m[3]),help,type}); help=''; }
1051
+ }
1052
+ }
1053
+ return out;
1054
+ }
1055
+
1056
+ // ═══ INIT ═══
1057
+ renderSidebar();
1058
+ navigate('overview');
1059
+ schedNext();
1060
+
1061
+ function schedNext() {
1062
+ if (timer) clearTimeout(timer);
1063
+ if (!autoRef) return;
1064
+ timer = setTimeout(() => doRefresh().then(schedNext), ivMs);
1065
+ }
1066
+ </script>
1067
+ </body>
1068
+ </html>