<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>bctx · local dashboard</title>
<link rel="icon" href="data:,">
<style>
:root { --bg:#f6f7f8; --card:#fff; --ink:#15191c; --muted:#66727a; --line:#dde2e4; --accent:#17685a; }
@media (prefers-color-scheme: dark) {
:root { --bg:#0f1416; --card:#161d20; --ink:#e7edee; --muted:#8a9a9f; --line:#2a3438; --accent:#4fb8a2; }
}
* { box-sizing: border-box; }
body { margin:0; background:var(--bg); color:var(--ink); font:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; }
main { max-width:1000px; margin:0 auto; padding:28px 20px 60px; display:flex; flex-direction:column; gap:22px; }
h1 { margin:0; font-size:20px; } .sub { color:var(--muted); margin:0; }
.tiles { display:grid; grid-template-columns:repeat(auto-fit,minmax(170px,1fr)); gap:12px; }
.tile { background:var(--card); border:1px solid var(--line); border-radius:8px; padding:14px; }
.tile b { display:block; font-size:22px; font-variant-numeric:tabular-nums; }
.tile span { color:var(--muted); font-size:12px; text-transform:uppercase; letter-spacing:.06em; }
section { background:var(--card); border:1px solid var(--line); border-radius:8px; padding:16px; overflow-x:auto; }
h2 { margin:0 0 10px; font-size:14px; }
table { border-collapse:collapse; width:100%; font-variant-numeric:tabular-nums; }
th, td { text-align:left; padding:6px 8px; border-bottom:1px solid var(--line); white-space:nowrap; }
th { color:var(--muted); font-weight:600; font-size:12px; }
td.num, th.num { text-align:right; }
td.args { max-width:340px; overflow:hidden; text-overflow:ellipsis; color:var(--muted); }
svg text { fill:var(--muted); font-size:10px; }
.empty { color:var(--muted); }
</style>
</head>
<body>
<main>
<div><h1>bctx · local dashboard</h1><p class="sub">Read from ~/.bctx/executions.db on this machine. This page sends nothing anywhere.</p><p class="empty" id="status"></p></div>
<div class="tiles" id="tiles"></div>
<section><h2>Tokens per day (last 30 days)</h2><div id="chart"></div></section>
<section><h2>By program</h2><table id="programs"></table></section>
<section><h2>Recent commands</h2><table id="recent"></table></section>
</main>
<script>
const fmt = n => n >= 1e6 ? (n / 1e6).toFixed(1) + 'M' : n >= 1e3 ? (n / 1e3).toFixed(1) + 'k' : String(n);
function el(tag, text, cls) { const e = document.createElement(tag); if (text !== undefined) e.textContent = text; if (cls) e.className = cls; return e; }
function table(node, headers, rows) {
node.textContent = '';
if (!rows.length) { node.appendChild(el('caption', 'No commands recorded yet.', 'empty')); return; }
const head = el('tr'); headers.forEach(([label, num]) => head.appendChild(el('th', label, num ? 'num' : '')));
node.appendChild(head);
rows.forEach(cells => { const tr = el('tr'); cells.forEach(([v, cls]) => tr.appendChild(el('td', v, cls))); node.appendChild(tr); });
}
function chart(node, days) {
if (!days.length) { node.appendChild(el('p', 'No data yet.', 'empty')); return; }
const W = 940, H = 180, pad = 24, bw = (W - pad) / days.length;
const max = Math.max(...days.map(d => d.tokens_before), 1);
const ns = 'http://www.w3.org/2000/svg';
const svg = document.createElementNS(ns, 'svg');
svg.setAttribute('viewBox', `0 0 ${W} ${H + 18}`); svg.setAttribute('width', '100%');
days.forEach((d, i) => {
const x = pad + i * bw, hb = (d.tokens_before / max) * H, ha = (d.tokens_after / max) * H;
const before = document.createElementNS(ns, 'rect');
before.setAttribute('x', x + 2); before.setAttribute('y', H - hb); before.setAttribute('width', Math.max(bw - 4, 1));
before.setAttribute('height', hb); before.setAttribute('fill', 'var(--line)');
const after = document.createElementNS(ns, 'rect');
after.setAttribute('x', x + 2); after.setAttribute('y', H - ha); after.setAttribute('width', Math.max(bw - 4, 1));
after.setAttribute('height', ha); after.setAttribute('fill', 'var(--accent)');
const tip = document.createElementNS(ns, 'title');
tip.textContent = `${d.day}: ${fmt(d.tokens_before)} raw → ${fmt(d.tokens_after)} sent`;
before.appendChild(tip); svg.append(before, after);
if (i % Math.ceil(days.length / 6) === 0) {
const t = document.createElementNS(ns, 'text');
t.setAttribute('x', x + 2); t.setAttribute('y', H + 14); t.textContent = d.day.slice(5); svg.appendChild(t);
}
});
node.appendChild(svg);
}
function render(s) {
const t = s.totals, tiles = document.getElementById('tiles');
tiles.textContent = ''; document.getElementById('chart').textContent = '';
[['Commands', fmt(t.commands)], ['Raw tokens', fmt(t.tokens_before)], ['Sent to model', fmt(t.tokens_after)],
['Saved', fmt(t.saved) + ' (' + t.saved_pct.toFixed(1) + '%)']].forEach(([label, value]) => {
const d = el('div', undefined, 'tile'); d.append(el('b', value), el('span', label)); tiles.appendChild(d);
});
chart(document.getElementById('chart'), s.by_day);
table(document.getElementById('programs'), [['Program'], ['Raw', true], ['Sent', true], ['Avg saved', true]],
s.by_program.map(p => [[p.program], [fmt(p.tokens_before), 'num'], [fmt(p.tokens_after), 'num'], [p.avg_pct.toFixed(1) + '%', 'num']]));
table(document.getElementById('recent'), [['When'], ['Program'], ['Args'], ['Raw', true], ['Sent', true], ['Exit', true]],
s.recent.map(r => [[r.timestamp.slice(0, 16).replace('T', ' ')], [r.program], [r.args, 'args'],
[fmt(r.tokens_before), 'num'], [fmt(r.tokens_after), 'num'], [String(r.exit_code), 'num']]));
}
function load() {
const status = document.getElementById('status');
fetch('api/summary').then(r => { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
.then(s => { status.textContent = ''; render(s); })
.catch(() => { status.textContent = 'The local server has stopped. Run `bctx dashboard` again to reopen it.'; });
}
load(); setInterval(load, 60000);
</script>
</body>
</html>