bctx 0.1.39

bctx CLI — intercept CLI commands and compress output for LLM coding agents
<!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.'; });
}
// Refreshing every minute keeps the numbers current and the server running while this tab is open.
load(); setInterval(load, 60000);
</script>
</body>
</html>