<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>bctx dashboard</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{
--bg:#0d1117;--surface:#161b22;--border:#30363d;--text:#e6edf3;
--muted:#8b949e;--green:#3fb950;--blue:#58a6ff;--yellow:#d29922;
--red:#f85149;--accent:#388bfd;--sidebar-w:220px;
font-size:14px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;
}
body{background:var(--bg);color:var(--text);min-height:100vh;display:flex}
a{color:var(--accent);text-decoration:none}
.sidebar{width:var(--sidebar-w);min-height:100vh;background:var(--surface);
border-right:1px solid var(--border);display:flex;flex-direction:column;
flex-shrink:0;position:fixed;top:0;left:0;bottom:0;z-index:10}
.sidebar-logo{padding:20px 16px 16px;font-size:1.1rem;font-weight:700;
letter-spacing:-.5px;border-bottom:1px solid var(--border)}
.sidebar-logo span{color:var(--accent)}
.sidebar-nav{flex:1;padding:12px 0;overflow-y:auto}
.nav-section{padding:4px 16px 4px;font-size:.7rem;color:var(--muted);
text-transform:uppercase;letter-spacing:.8px;margin-top:8px}
.nav-item{display:flex;align-items:center;gap:10px;padding:8px 16px;
font-size:.85rem;color:var(--muted);cursor:pointer;border-radius:0;
transition:background .12s,color .12s;user-select:none}
.nav-item:hover{background:#1c2128;color:var(--text)}
.nav-item.active{color:var(--text);background:#1c2128;border-left:2px solid var(--accent);padding-left:14px}
.nav-icon{width:16px;text-align:center;flex-shrink:0;font-size:.9rem}
.sidebar-footer{padding:16px;border-top:1px solid var(--border)}
.sidebar-user{font-size:.8rem;color:var(--muted);margin-bottom:6px;
overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sidebar-user strong{color:var(--text);display:block}
.tier-badge{display:inline-block;padding:2px 10px;border-radius:20px;
font-size:.7rem;text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px;
background:var(--bg);border:1px solid var(--border);color:var(--muted)}
.tier-badge.beacon{border-color:#2d5a9e;color:var(--blue)}
.tier-badge.studio{border-color:#5a2d9e;color:#c084fc}
.tier-badge.enterprise{border-color:#9e2d2d;color:var(--red)}
.sign-out{background:none;border:none;color:var(--muted);font-size:.8rem;
cursor:pointer;padding:0;text-align:left}
.sign-out:hover{color:var(--text)}
.main{margin-left:var(--sidebar-w);flex:1;min-width:0;padding:28px 32px;max-width:1100px}
.page-title{font-size:1.25rem;font-weight:700;margin-bottom:24px}
.period-bar{display:flex;gap:6px;margin-bottom:20px}
.period-btn{background:var(--surface);border:1px solid var(--border);
border-radius:6px;color:var(--muted);padding:5px 14px;font-size:.8rem;cursor:pointer}
.period-btn:hover{border-color:var(--muted);color:var(--text)}
.period-btn.active{background:var(--accent);border-color:var(--accent);color:#fff}
.grid{display:grid;gap:16px;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));margin-bottom:24px}
.card{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:20px;position:relative}
.card-icon{font-size:1.1rem;margin-bottom:10px}
.card-label{font-size:.72rem;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px}
.card-value{font-size:1.8rem;font-weight:700;line-height:1}
.card-sub{font-size:.78rem;color:var(--muted);margin-top:6px}
.green{color:var(--green)}.blue{color:var(--blue)}.yellow{color:var(--yellow)}.purple{color:#c084fc}
.charts-row{display:grid;grid-template-columns:2fr 1fr;gap:16px;margin-bottom:24px}
@media(max-width:800px){.charts-row{grid-template-columns:1fr}}
.chart-card{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:20px}
.chart-title{font-size:.78rem;font-weight:600;color:var(--muted);text-transform:uppercase;
letter-spacing:.5px;margin-bottom:16px}
.chart-wrap{position:relative}
.donut-center{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
text-align:center;pointer-events:none}
.donut-pct{font-size:1.6rem;font-weight:700;color:var(--green);line-height:1}
.donut-label{font-size:.7rem;color:var(--muted)}
.gauge-wrap{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:20px;margin-bottom:24px}
.gauge-header{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:12px}
.gauge-title{font-size:.78rem;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.5px}
.gauge-subtitle{font-size:.75rem;color:var(--muted)}
.gauge-bar-bg{height:8px;border-radius:4px;background:#21262d;overflow:hidden}
.gauge-bar-fill{height:100%;border-radius:4px;background:linear-gradient(90deg,var(--green),var(--blue));transition:width .6s ease}
.gauge-bar-fill.danger{background:linear-gradient(90deg,var(--yellow),var(--red))}
.gauge-footer{display:flex;justify-content:space-between;margin-top:8px;font-size:.75rem;color:var(--muted)}
.table-card{background:var(--surface);border:1px solid var(--border);border-radius:8px;overflow:hidden;margin-bottom:24px}
.table-card table{width:100%;border-collapse:collapse}
.table-card th{padding:10px 16px;text-align:left;font-size:.72rem;color:var(--muted);
text-transform:uppercase;letter-spacing:.5px;border-bottom:1px solid var(--border);background:#0d1117}
.table-card td{padding:10px 16px;font-size:.85rem;border-bottom:1px solid #1c2128}
.table-card tr:last-child td{border-bottom:none}
.table-card tr:hover td{background:#1c2128}
.mini-bar-bg{height:6px;border-radius:3px;background:#21262d;overflow:hidden;min-width:80px}
.mini-bar-fill{height:100%;border-radius:3px;background:var(--green)}
.two-col{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:24px}
@media(max-width:700px){.two-col{grid-template-columns:1fr}}
.info-row{display:flex;justify-content:space-between;align-items:center;
padding:9px 0;border-bottom:1px solid var(--border);font-size:.85rem}
.info-row:last-child{border-bottom:none}
.info-label{color:var(--muted)}
.pill{padding:2px 10px;border-radius:20px;font-size:.72rem;font-weight:600}
.pill-green{background:#0d3320;color:var(--green);border:1px solid #1a5e38}
.pill-muted{background:#21262d;color:var(--muted);border:1px solid var(--border)}
.btn{background:var(--accent);color:#fff;border:none;border-radius:6px;padding:9px 18px;
font-size:.85rem;cursor:pointer;font-weight:600;display:inline-flex;align-items:center;gap:6px}
.btn:hover{background:#1f6feb}
.btn:disabled{opacity:.45;cursor:not-allowed}
.btn-full{width:100%;justify-content:center}
.btn-ghost{background:transparent;border:1px solid var(--border);color:var(--muted);font-weight:400}
.btn-ghost:hover{border-color:var(--muted);color:var(--text)}
.btn-sm{padding:6px 12px;font-size:.8rem}
.plan-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:12px;margin-bottom:24px}
.plan-card{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:16px}
.plan-card.current{border-color:var(--accent)}
.plan-name{font-weight:700;margin-bottom:3px}
.plan-price{color:var(--muted);font-size:.8rem;margin-bottom:12px}
.plan-features{list-style:none;font-size:.8rem;color:var(--muted);display:flex;flex-direction:column;gap:4px;margin-bottom:14px}
.plan-features li::before{content:"✓ ";color:var(--green)}
.plan-features li.locked::before{content:"✗ ";color:var(--border)}
.plan-features li.locked{opacity:.5}
.inline-form{display:flex;gap:8px;margin-top:12px}
.input-field{flex:1;background:#0d1117;border:1px solid var(--border);border-radius:6px;
color:var(--text);padding:8px 12px;font-size:.85rem;outline:none}
.input-field:focus{border-color:var(--accent)}
.member-list{display:flex;flex-direction:column;gap:6px;margin-bottom:16px}
.member-row{display:flex;align-items:center;justify-content:space-between;
background:#0d1117;border:1px solid var(--border);border-radius:6px;padding:9px 14px}
.team-id-box{background:#0d1117;border:1px solid var(--border);border-radius:6px;
padding:9px 14px;font-family:monospace;font-size:.8rem;color:var(--muted);
display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:14px}
.copy-btn{background:transparent;border:none;color:var(--accent);cursor:pointer;font-size:.75rem}
.gated-msg{text-align:center;padding:48px 20px}
.gated-msg h3{font-size:1rem;margin-bottom:8px}
.gated-msg p{color:var(--muted);font-size:.85rem;margin-bottom:20px;line-height:1.6}
#auth-screen{position:fixed;inset:0;background:var(--bg);display:flex;align-items:center;justify-content:center;z-index:100}
.auth-box{background:var(--surface);border:1px solid var(--border);border-radius:8px;
padding:32px;max-width:480px;width:100%;text-align:center}
.auth-box h2{margin-bottom:8px;font-size:1.2rem}
.auth-box p{color:var(--muted);margin-bottom:24px;font-size:.9rem;line-height:1.6}
.token-input{width:100%;background:#0d1117;border:1px solid var(--border);border-radius:6px;
color:var(--text);padding:10px 14px;font-size:.85rem;font-family:monospace;margin-bottom:12px;outline:none}
.token-input:focus{border-color:var(--accent)}
#error-msg{color:var(--red);font-size:.8rem;margin-top:6px;display:none}
.toast{position:fixed;bottom:24px;right:24px;background:#0d3320;color:var(--green);
border:1px solid var(--green);border-radius:6px;padding:10px 16px;font-size:.85rem;
opacity:0;transition:opacity .3s;pointer-events:none;z-index:200}
.toast.show{opacity:1}
.section{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:20px;margin-bottom:20px}
.section-title{font-size:.78rem;font-weight:600;color:var(--muted);text-transform:uppercase;
letter-spacing:.5px;margin-bottom:14px}
.empty{color:var(--muted);font-size:.85rem;text-align:center;padding:24px 0}
.upgrade-banner{background:#1c2230;border:1px solid #2d3a5a;border-radius:8px;
padding:12px 18px;margin-bottom:20px;display:flex;align-items:center;justify-content:space-between;gap:16px}
.upgrade-banner p{color:var(--muted);font-size:.85rem}
@media(max-width:700px){
.sidebar{transform:translateX(-100%)}
.main{margin-left:0;padding:16px}
}
</style>
</head>
<body>
<nav class="sidebar" id="sidebar">
<div class="sidebar-logo">bctx <span>dashboard</span></div>
<div class="sidebar-nav">
<div class="nav-section">Analytics</div>
<div class="nav-item active" onclick="nav('overview')">
<span class="nav-icon">◈</span> Overview
</div>
<div class="nav-item" onclick="nav('commands')">
<span class="nav-icon">⌘</span> Commands
</div>
<div class="nav-item" onclick="nav('performance')">
<span class="nav-icon">↗</span> Performance
</div>
<div class="nav-section">Account</div>
<div class="nav-item" onclick="nav('account')">
<span class="nav-icon">◎</span> Account
</div>
<div class="nav-item" onclick="nav('team')">
<span class="nav-icon">⬡</span> Team
</div>
</div>
<div class="sidebar-footer">
<div class="sidebar-user">
<strong id="sidebar-email">—</strong>
</div>
<span class="tier-badge" id="tier-badge">free</span><br>
<button class="sign-out" onclick="logout()">Sign out</button>
</div>
</nav>
<main class="main" id="main-content" style="display:none">
<div class="upgrade-banner" id="upgrade-banner" style="display:none">
<p>You're on the free tier — cloud sync, history & team features require Beacon+ ($9/mo).</p>
<button class="btn btn-sm" onclick="startCheckout('beacon')">Upgrade to Beacon</button>
</div>
<div id="page-overview" class="page">
<div class="page-title">Overview</div>
<div class="period-bar">
<button class="period-btn" onclick="setPeriod(7)">7d</button>
<button class="period-btn active" onclick="setPeriod(30)">30d</button>
<button class="period-btn" onclick="setPeriod(90)">90d</button>
<button class="period-btn" onclick="setPeriod(365)">All</button>
</div>
<div class="grid">
<div class="card">
<div class="card-icon">↑</div>
<div class="card-label">Tokens saved</div>
<div class="card-value green" id="ov-saved">—</div>
<div class="card-sub" id="ov-saved-pct">loading…</div>
</div>
<div class="card">
<div class="card-icon">$</div>
<div class="card-label">Cost avoided</div>
<div class="card-value yellow" id="ov-cost">—</div>
<div class="card-sub">at $15 / 1M tokens</div>
</div>
<div class="card">
<div class="card-icon">⚡</div>
<div class="card-label">Compression</div>
<div class="card-value purple" id="ov-compression">—</div>
<div class="card-sub">avg savings rate</div>
</div>
<div class="card">
<div class="card-icon">⌘</div>
<div class="card-label">Top skill</div>
<div class="card-value blue" id="ov-top" style="font-size:1.2rem">—</div>
<div class="card-sub">most active</div>
</div>
</div>
<div class="charts-row">
<div class="chart-card">
<div class="chart-title">Token savings</div>
<canvas id="line-chart" height="120"></canvas>
</div>
<div class="chart-card">
<div class="chart-title">Compression ratio</div>
<div class="chart-wrap">
<canvas id="donut-chart" height="180"></canvas>
<div class="donut-center">
<div class="donut-pct" id="donut-pct">—</div>
<div class="donut-label">saved</div>
</div>
</div>
</div>
</div>
<div class="gauge-wrap">
<div class="gauge-header">
<span class="gauge-title">Cloud token quota</span>
<span class="gauge-subtitle" id="gauge-reset">resets —</span>
</div>
<div class="gauge-bar-bg"><div class="gauge-bar-fill" id="gauge-fill" style="width:0%"></div></div>
<div class="gauge-footer">
<span id="gauge-used">0 used</span>
<span id="gauge-quota">— quota</span>
</div>
</div>
</div>
<div id="page-commands" class="page" style="display:none">
<div class="page-title">Commands</div>
<div class="period-bar">
<button class="period-btn" onclick="setPeriod(7)">7d</button>
<button class="period-btn active" onclick="setPeriod(30)">30d</button>
<button class="period-btn" onclick="setPeriod(90)">90d</button>
<button class="period-btn" onclick="setPeriod(365)">All</button>
</div>
<div class="table-card">
<table>
<thead>
<tr>
<th>Program</th>
<th>Tokens sent</th>
<th>Tokens saved</th>
<th>Compression</th>
<th>Runs</th>
</tr>
</thead>
<tbody id="cmd-table-body">
<tr><td colspan="5" style="text-align:center;color:var(--muted);padding:24px">loading…</td></tr>
</tbody>
</table>
</div>
</div>
<div id="page-performance" class="page" style="display:none">
<div class="page-title">Performance</div>
<div class="period-bar">
<button class="period-btn" onclick="setPeriod(7)">7d</button>
<button class="period-btn active" onclick="setPeriod(30)">30d</button>
<button class="period-btn" onclick="setPeriod(90)">90d</button>
<button class="period-btn" onclick="setPeriod(365)">All</button>
</div>
<div class="chart-card" style="margin-bottom:20px">
<div class="chart-title">Daily token savings</div>
<canvas id="perf-line-chart" height="140"></canvas>
</div>
<div class="chart-card">
<div class="chart-title">Daily compression rate (%)</div>
<canvas id="perf-compression-chart" height="120"></canvas>
</div>
</div>
<div id="page-account" class="page" style="display:none">
<div class="page-title">Account</div>
<div class="two-col">
<div class="card">
<div class="section-title" style="margin-bottom:12px">Your account</div>
<div class="info-row"><span class="info-label">Email</span><span id="acc-email">—</span></div>
<div class="info-row"><span class="info-label">Tier</span><span id="acc-tier">—</span></div>
<div class="info-row"><span class="info-label">Member since</span><span id="acc-created">—</span></div>
<div class="info-row"><span class="info-label">Cloud sync</span><span id="acc-sync">—</span></div>
<div class="info-row"><span class="info-label">Vault limit</span><span id="acc-vault">—</span></div>
<div class="info-row"><span class="info-label">Team vaults</span><span id="acc-team">—</span></div>
</div>
<div class="card">
<div class="section-title" style="margin-bottom:12px">Billing</div>
<div id="billing-free" style="display:none">
<p style="color:var(--muted);font-size:.85rem;line-height:1.6;margin-bottom:16px">
You're on the free tier. Upgrade to unlock cloud sync, extended history, and team vaults.
</p>
<button class="btn btn-full" style="margin-bottom:8px" onclick="startCheckout('beacon')">Upgrade to Beacon — $9/mo</button>
<button class="btn btn-full btn-ghost" onclick="startCheckout('studio')">Upgrade to Studio — $29/mo</button>
</div>
<div id="billing-paid" style="display:none">
<p style="color:var(--muted);font-size:.85rem;line-height:1.6;margin-bottom:16px">
Manage your subscription, download invoices, or update your payment method.
</p>
<button class="btn btn-full" onclick="openPortal()">Manage billing ↗</button>
</div>
</div>
</div>
<div class="section-title" style="margin-bottom:12px">Plans</div>
<div class="plan-cards">
<div class="plan-card" id="plan-free">
<div class="plan-name">Free</div><div class="plan-price">$0 / mo</div>
<ul class="plan-features">
<li>All 15 skills (local)</li><li>Local token dashboard</li>
<li>500 vault facts/project</li><li class="locked">Cloud sync</li>
<li class="locked">History > 7 days</li><li class="locked">Team vaults</li>
</ul>
<button class="btn btn-sm btn-ghost btn-full" disabled>Current plan</button>
</div>
<div class="plan-card" id="plan-beacon">
<div class="plan-name">Beacon</div><div class="plan-price">$9 / mo</div>
<ul class="plan-features">
<li>Everything in Free</li><li>Cloud sync (5 projects)</li>
<li>10,000 vault facts</li><li>90-day history</li>
<li>100K cloud tokens/mo</li><li class="locked">Team vaults</li>
</ul>
<button class="btn btn-sm btn-full" id="btn-beacon" onclick="startCheckout('beacon')">Upgrade</button>
</div>
<div class="plan-card" id="plan-studio">
<div class="plan-name">Studio</div><div class="plan-price">$29 / mo</div>
<ul class="plan-features">
<li>Everything in Beacon</li><li>Unlimited projects</li>
<li>Unlimited vault facts</li><li>365-day history</li>
<li>1M cloud tokens/mo</li><li>Team vaults</li>
</ul>
<button class="btn btn-sm btn-full" id="btn-studio" onclick="startCheckout('studio')">Upgrade</button>
</div>
</div>
</div>
<div id="page-team" class="page" style="display:none">
<div class="page-title">Team</div>
<div id="team-gated" class="gated-msg" style="display:none">
<h3>Team vaults require Studio</h3>
<p>Share context across your team with a shared Vault namespace.<br>
Teammates see the same project knowledge — no more re-explaining context.</p>
<button class="btn" onclick="startCheckout('studio')">Upgrade to Studio — $29/mo</button>
</div>
<div id="team-panel" style="display:none">
<div id="team-create" style="display:none">
<div class="section">
<div class="section-title">Create your team</div>
<div class="inline-form">
<input id="team-name-input" class="input-field" placeholder="e.g. Acme Engineering" maxlength="64">
<button class="btn" onclick="createTeam()">Create team</button>
</div>
<div id="team-create-error" style="color:var(--red);font-size:.8rem;margin-top:8px;display:none"></div>
</div>
</div>
<div id="team-info" style="display:none">
<div class="two-col">
<div class="card">
<div class="section-title" style="margin-bottom:12px">Team</div>
<div class="info-row"><span class="info-label">Name</span><span id="team-name-display">—</span></div>
<div class="info-row"><span class="info-label">Members</span><span id="team-member-count">—</span></div>
</div>
<div class="card">
<div class="section-title" style="margin-bottom:12px">Invite teammates</div>
<div class="team-id-box"><span id="team-id-share">—</span><button class="copy-btn" onclick="copyTeamId()">copy</button></div>
<div class="inline-form">
<input id="invite-email" class="input-field" placeholder="teammate@example.com" type="email">
<button class="btn btn-sm" onclick="inviteMember()">Invite</button>
</div>
<div id="invite-msg" style="font-size:.8rem;margin-top:8px;display:none"></div>
</div>
</div>
<div class="section"><div class="section-title">Members</div><div id="member-list" class="member-list"><div class="empty">loading…</div></div></div>
</div>
<div id="team-join" style="display:none">
<div class="section">
<div class="section-title">Join a team</div>
<p style="color:var(--muted);font-size:.85rem;margin-bottom:14px">Enter a Team ID shared by your team owner.</p>
<div class="inline-form">
<input id="join-team-id" class="input-field" placeholder="team-id…" style="font-family:monospace">
<button class="btn" onclick="joinTeam()">Join</button>
</div>
<div id="join-error" style="color:var(--red);font-size:.8rem;margin-top:8px;display:none"></div>
<p style="color:var(--muted);font-size:.75rem;margin-top:14px"><a href="#" onclick="showCreateTeam()">Create a team instead</a></p>
</div>
</div>
</div>
</div>
</main>
<div id="auth-screen">
<div class="auth-box">
<h2>bctx dashboard</h2>
<p>Paste your bctx access token, or run<br>
<code style="color:var(--green)">bctx dashboard</code> to open automatically.</p>
<input id="token-input" class="token-input" type="password" placeholder="paste token here…" autocomplete="off">
<div id="error-msg">Invalid token — check and try again.</div>
<button class="btn btn-full" onclick="doLogin()" style="margin-top:4px">Connect</button>
<button class="btn btn-full btn-ghost" style="margin-top:8px" onclick="useDemoData()">View demo data</button>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
const API = '';
let token = '';
let currentTier = 'free';
let currentTeamId = localStorage.getItem('bctx_team_id') || '';
let currentPeriod = 30;
let currentPage = 'overview';
let lineChart = null, donutChart = null, perfLineChart = null, perfCompressionChart = null;
const urlParams = new URLSearchParams(window.location.search);
const urlToken = urlParams.get('token');
if (urlToken) {
token = urlToken;
localStorage.setItem('bctx_token', token);
history.replaceState({}, '', window.location.pathname);
} else {
token = localStorage.getItem('bctx_token') || '';
}
function fmt(n) {
if (n >= 1e6) return (n/1e6).toFixed(1) + 'M';
if (n >= 1e3) return (n/1e3).toFixed(1) + 'K';
return String(n);
}
function showToast(msg) {
const t = document.getElementById('toast');
t.textContent = msg; t.classList.add('show');
setTimeout(() => t.classList.remove('show'), 2500);
}
async function apiFetch(path, opts = {}) {
const r = await fetch(API + path, {
...opts,
headers: { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json', ...(opts.headers||{}) }
});
if (!r.ok) throw new Error(r.status);
return r.json();
}
function nav(page) {
currentPage = page;
document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
const items = ['overview','commands','performance','account','team'];
const idx = items.indexOf(page);
if (idx >= 0) document.querySelectorAll('.nav-item')[idx].classList.add('active');
document.querySelectorAll('.page').forEach(p => p.style.display = 'none');
document.getElementById('page-' + page).style.display = '';
loadPage(page);
}
function loadPage(page) {
if (page === 'overview') loadOverview();
else if (page === 'commands') loadCommands();
else if (page === 'performance') loadPerformance();
else if (page === 'account') loadAccount();
else if (page === 'team') loadTeamPage();
}
function setPeriod(days) {
currentPeriod = days;
document.querySelectorAll('#page-' + currentPage + ' .period-btn').forEach((b, i) => {
b.classList.toggle('active', [7,30,90,365][i] === days);
});
loadPage(currentPage);
}
async function load() {
try {
const gauge = await apiFetch('/dashboard/gauge');
renderGauge(gauge);
document.getElementById('main-content').style.display = '';
document.getElementById('auth-screen').style.display = 'none';
nav('overview');
} catch(e) { showAuth(); }
}
async function loadOverview() {
try {
const [sum, hist] = await Promise.all([
apiFetch('/dashboard/summary'),
apiFetch('/dashboard/history?days=' + currentPeriod),
]);
const total = sum.tokens_used + sum.tokens_saved;
const pct = total > 0 ? (sum.tokens_saved / total * 100).toFixed(1) : '0.0';
document.getElementById('ov-saved').textContent = fmt(sum.tokens_saved);
document.getElementById('ov-saved-pct').textContent = pct + '% savings rate';
document.getElementById('ov-cost').textContent = '$' + sum.cost_usd_avoided.toFixed(2);
document.getElementById('ov-compression').textContent = pct + '%';
document.getElementById('ov-top').textContent = sum.top_lens || '—';
renderLineChart(hist);
renderDonut(sum.tokens_saved, sum.tokens_used);
} catch(e) { console.warn('overview load failed', e); }
}
function renderLineChart(buckets) {
const ctx = document.getElementById('line-chart').getContext('2d');
const labels = buckets.map(b => b.date.slice(5));
const data = buckets.map(b => b.tokens_saved);
if (lineChart) lineChart.destroy();
lineChart = new Chart(ctx, {
type: 'line',
data: {
labels,
datasets: [{
label: 'Tokens saved',
data,
borderColor: '#3fb950',
backgroundColor: 'rgba(63,185,80,.1)',
fill: true,
tension: 0.4,
pointRadius: 3,
pointBackgroundColor: '#3fb950',
}]
},
options: {
responsive: true,
plugins: { legend: { display: false } },
scales: {
x: { ticks: { color: '#8b949e', font: { size: 11 } }, grid: { color: '#21262d' } },
y: { ticks: { color: '#8b949e', font: { size: 11 }, callback: v => fmt(v) }, grid: { color: '#21262d' } }
}
}
});
}
function renderDonut(saved, sent) {
const ctx = document.getElementById('donut-chart').getContext('2d');
const total = saved + sent;
const pct = total > 0 ? (saved / total * 100).toFixed(1) : '0.0';
document.getElementById('donut-pct').textContent = pct + '%';
if (donutChart) donutChart.destroy();
donutChart = new Chart(ctx, {
type: 'doughnut',
data: {
datasets: [{
data: [saved, sent],
backgroundColor: ['#3fb950', '#21262d'],
borderWidth: 0,
hoverOffset: 4,
}]
},
options: {
cutout: '72%',
responsive: true,
plugins: { legend: { display: false }, tooltip: {
callbacks: { label: ctx => fmt(ctx.raw) + ' tokens' }
}}
}
});
}
function renderGauge(g) {
currentTier = g.tier;
const badge = document.getElementById('tier-badge');
badge.textContent = g.tier;
badge.className = 'tier-badge ' + (g.tier !== 'free' ? g.tier : '');
document.getElementById('gauge-reset').textContent = 'resets ' + g.reset_at;
document.getElementById('gauge-used').textContent = fmt(g.used_tokens) + ' used';
document.getElementById('upgrade-banner').style.display = g.tier === 'free' ? 'flex' : 'none';
if (g.quota_tokens > 0) {
document.getElementById('gauge-quota').textContent = fmt(g.quota_tokens) + ' quota';
const pct = Math.min(g.used_tokens / g.quota_tokens * 100, 100).toFixed(1);
const fill = document.getElementById('gauge-fill');
fill.style.width = pct + '%';
if (pct > 80) fill.classList.add('danger');
} else {
document.getElementById('gauge-quota').textContent = 'unlimited';
document.getElementById('gauge-fill').style.width = '5%';
}
}
async function loadCommands() {
try {
const stats = await apiFetch('/dashboard/commands?days=' + currentPeriod);
const tbody = document.getElementById('cmd-table-body');
if (!stats.length) {
tbody.innerHTML = '<tr><td colspan="5" style="text-align:center;color:var(--muted);padding:24px">No command data yet</td></tr>';
return;
}
const maxSaved = Math.max(...stats.map(s => s.tokens_saved), 1);
tbody.innerHTML = stats.map(s => `
<tr>
<td><strong>${s.program}</strong></td>
<td>${fmt(s.tokens_sent)}</td>
<td style="color:var(--green)">${fmt(s.tokens_saved)}</td>
<td>
<div style="display:flex;align-items:center;gap:8px">
<div class="mini-bar-bg"><div class="mini-bar-fill" style="width:${s.savings_pct.toFixed(0)}%"></div></div>
<span style="color:var(--muted);font-size:.8rem;min-width:32px">${s.savings_pct.toFixed(0)}%</span>
</div>
</td>
<td style="color:var(--muted)">${s.run_count}</td>
</tr>`).join('');
} catch(e) { console.warn('commands load failed', e); }
}
async function loadPerformance() {
try {
const hist = await apiFetch('/dashboard/history?days=' + currentPeriod);
renderPerfCharts(hist);
} catch(e) { console.warn('performance load failed', e); }
}
function renderPerfCharts(buckets) {
const labels = buckets.map(b => b.date.slice(5));
const savings = buckets.map(b => b.tokens_saved);
const compression = buckets.map(b => {
const total = b.tokens_sent + b.tokens_saved;
return total > 0 ? (b.tokens_saved / total * 100) : 0;
});
const ctx1 = document.getElementById('perf-line-chart').getContext('2d');
if (perfLineChart) perfLineChart.destroy();
perfLineChart = new Chart(ctx1, {
type: 'line',
data: {
labels,
datasets: [{
label: 'Tokens saved',
data: savings,
borderColor: '#58a6ff',
backgroundColor: 'rgba(88,166,255,.1)',
fill: true, tension: 0.4, pointRadius: 3, pointBackgroundColor: '#58a6ff',
}]
},
options: {
responsive: true,
plugins: { legend: { display: false } },
scales: {
x: { ticks: { color: '#8b949e', font: { size: 11 } }, grid: { color: '#21262d' } },
y: { ticks: { color: '#8b949e', font: { size: 11 }, callback: v => fmt(v) }, grid: { color: '#21262d' } }
}
}
});
const ctx2 = document.getElementById('perf-compression-chart').getContext('2d');
if (perfCompressionChart) perfCompressionChart.destroy();
perfCompressionChart = new Chart(ctx2, {
type: 'line',
data: {
labels,
datasets: [{
label: 'Compression %',
data: compression,
borderColor: '#3fb950',
backgroundColor: 'rgba(63,185,80,.08)',
fill: true, tension: 0.4, pointRadius: 3, pointBackgroundColor: '#3fb950',
}]
},
options: {
responsive: true,
plugins: { legend: { display: false } },
scales: {
x: { ticks: { color: '#8b949e', font: { size: 11 } }, grid: { color: '#21262d' } },
y: { min: 0, max: 100, ticks: { color: '#8b949e', font: { size: 11 }, callback: v => v + '%' }, grid: { color: '#21262d' } }
}
}
});
}
async function loadAccount() {
try {
const a = await apiFetch('/account/me');
document.getElementById('sidebar-email').textContent = a.email;
document.getElementById('acc-email').textContent = a.email;
document.getElementById('acc-tier').innerHTML =
`<span class="pill ${a.tier === 'free' ? 'pill-muted' : 'pill-green'}">${a.tier}</span>`;
document.getElementById('acc-created').textContent = a.created_at.slice(0,10);
document.getElementById('acc-sync').innerHTML =
a.cloud_sync_enabled ? '<span class="pill pill-green">enabled</span>' : '<span class="pill pill-muted">free only</span>';
document.getElementById('acc-vault').textContent =
a.vault_fact_limit ? fmt(a.vault_fact_limit) + ' facts/project' : 'unlimited';
document.getElementById('acc-team').innerHTML =
a.team_vault_enabled ? '<span class="pill pill-green">enabled</span>' : '<span class="pill pill-muted">Studio+</span>';
document.getElementById('billing-free').style.display = a.tier === 'free' ? '' : 'none';
document.getElementById('billing-paid').style.display = a.tier !== 'free' ? '' : 'none';
const tiers = ['free','beacon','studio','enterprise'];
const idx = tiers.indexOf(a.tier);
['free','beacon','studio'].forEach((t, i) => {
const card = document.getElementById('plan-' + t);
card.classList.toggle('current', t === a.tier);
const btn = document.getElementById('btn-' + t);
if (btn) { btn.textContent = t === a.tier ? 'Current plan' : (i < idx ? 'Downgrade' : 'Upgrade'); btn.disabled = t === a.tier; }
});
} catch(e) { console.warn('account load failed', e); }
}
async function startCheckout(tier) {
try {
const d = await apiFetch('/billing/checkout?tier=' + tier, { method: 'POST' });
if (d.url) window.location.href = d.url;
} catch(e) { showToast('Could not start checkout — try again'); }
}
async function openPortal() {
try {
const d = await apiFetch('/billing/portal', { method: 'POST' });
if (d.url) window.location.href = d.url;
} catch(e) { showToast('Could not open billing portal — try again'); }
}
async function loadTeamPage() {
try {
const a = await apiFetch('/account/me');
const hasTeam = a.team_vault_enabled;
document.getElementById('team-gated').style.display = hasTeam ? 'none' : '';
document.getElementById('team-panel').style.display = hasTeam ? '' : 'none';
if (hasTeam) {
if (currentTeamId) loadTeam(currentTeamId);
else {
document.getElementById('team-create').style.display = '';
document.getElementById('team-info').style.display = 'none';
document.getElementById('team-join').style.display = 'none';
}
}
} catch(e) {}
}
function showCreateTeam() {
document.getElementById('team-join').style.display = 'none';
document.getElementById('team-create').style.display = '';
}
async function createTeam() {
const name = document.getElementById('team-name-input').value.trim();
if (!name) return;
try {
const d = await apiFetch('/team', { method: 'POST', body: JSON.stringify({ name }) });
currentTeamId = d.team_id; localStorage.setItem('bctx_team_id', currentTeamId);
document.getElementById('team-create-error').style.display = 'none';
await loadTeam(currentTeamId); showToast('Team created!');
} catch(e) {
document.getElementById('team-create-error').textContent = 'Failed — try again';
document.getElementById('team-create-error').style.display = '';
}
}
async function joinTeam() {
const id = document.getElementById('join-team-id').value.trim();
if (!id) return;
try {
await loadTeam(id); currentTeamId = id; localStorage.setItem('bctx_team_id', id);
document.getElementById('join-error').style.display = 'none'; showToast('Joined team!');
} catch(e) {
document.getElementById('join-error').textContent = 'Team not found or no access.';
document.getElementById('join-error').style.display = '';
}
}
async function loadTeam(id) {
const d = await apiFetch('/team/' + id);
document.getElementById('team-create').style.display = 'none';
document.getElementById('team-join').style.display = 'none';
document.getElementById('team-info').style.display = '';
document.getElementById('team-name-display').textContent = d.name;
document.getElementById('team-member-count').textContent = d.member_count;
document.getElementById('team-id-share').textContent = id;
document.getElementById('member-list').innerHTML =
`<div class="member-row"><span>${d.owner_id}</span><span style="color:var(--muted);font-size:.75rem">owner</span></div>`;
}
async function inviteMember() {
const email = document.getElementById('invite-email').value.trim();
const msgEl = document.getElementById('invite-msg');
if (!email || !currentTeamId) return;
try {
const d = await apiFetch('/team/' + currentTeamId + '/invite', { method: 'POST', body: JSON.stringify({ email }) });
msgEl.style.color = d.ok ? 'var(--green)' : 'var(--muted)';
msgEl.textContent = d.ok ? email + ' added.' : 'User not found — they need to sign up with bctx first.';
msgEl.style.display = '';
if (d.ok) { document.getElementById('invite-email').value = ''; await loadTeam(currentTeamId); }
setTimeout(() => { msgEl.style.display = 'none'; }, 4000);
} catch(e) { msgEl.style.color='var(--red)'; msgEl.textContent='Invite failed'; msgEl.style.display=''; }
}
function copyTeamId() {
navigator.clipboard.writeText(document.getElementById('team-id-share').textContent)
.then(() => showToast('Team ID copied!'));
}
function showAuth() {
document.getElementById('main-content').style.display = 'none';
document.getElementById('auth-screen').style.display = 'flex';
}
function doLogin() {
const t = document.getElementById('token-input').value.trim();
if (!t) return;
token = t; localStorage.setItem('bctx_token', t);
document.getElementById('error-msg').style.display = 'none';
load().catch(() => { token=''; localStorage.removeItem('bctx_token'); document.getElementById('error-msg').style.display=''; });
}
function logout() { token=''; localStorage.removeItem('bctx_token'); showAuth(); }
function useDemoData() {
document.getElementById('main-content').style.display = '';
document.getElementById('auth-screen').style.display = 'none';
document.getElementById('sidebar-email').textContent = 'demo@example.com';
currentTier = 'beacon';
renderGauge({ tier:'beacon', used_tokens:48200, quota_tokens:100000, reset_at:'2026-06-01', overage_allowed:false });
nav('overview');
const buckets = [
{date:'2026-05-06',tokens_sent:6200,tokens_saved:31800},
{date:'2026-05-07',tokens_sent:8100,tokens_saved:44900},
{date:'2026-05-08',tokens_sent:5400,tokens_saved:29600},
{date:'2026-05-09',tokens_sent:9200,tokens_saved:51800},
{date:'2026-05-10',tokens_sent:7800,tokens_saved:43200},
{date:'2026-05-11',tokens_sent:6300,tokens_saved:35100},
{date:'2026-05-12',tokens_sent:5200,tokens_saved:25400},
];
const totalSaved = buckets.reduce((s,b)=>s+b.tokens_saved,0);
const totalSent = buckets.reduce((s,b)=>s+b.tokens_sent,0);
const pct = (totalSaved/(totalSaved+totalSent)*100).toFixed(1);
document.getElementById('ov-saved').textContent = fmt(totalSaved);
document.getElementById('ov-saved-pct').textContent = pct + '% savings rate';
document.getElementById('ov-cost').textContent = '$' + (totalSaved*0.000015).toFixed(2);
document.getElementById('ov-compression').textContent = pct + '%';
document.getElementById('ov-top').textContent = 'sieve';
renderLineChart(buckets);
renderDonut(totalSaved, totalSent);
}
token ? load() : showAuth();
</script>
</body>
</html>