/** * Verbatim port of the v1 budget-panel client JS (old src/server/board.ts, * lines 1466–1750). Kept byte-identical on purpose: session/total baseline * logic, donut signature throttling and the reset handler are battle-tested. * * Depends on page-scope helpers provided by the columns/data-layer port: * esc, getJSON, hashColor, agentSpec, toast. Mount points expected in the DOM: * #budgetRows, #budgetTotal, #budgetReset, [data-budget-mode]. */ export const v1BudgetJs = ` // ── Roster ────────────────────────────────────────────────────────────── // AGENTS backs the budget panel + resolves a title's "name:" prefix to a // known agent when the architect drags a card into In Progress. var AGENTS = []; var BUDGET = null; var budgetMode = localStorage.getItem('agenthub-budget-mode') || 'session'; var donutMetric = localStorage.getItem('agenthub-donut-metric') || 'tokens'; var lastDonutSignature = ''; var lastDonutAt = 0; var DONUT_REFRESH_MS = 180000; async function loadAgents() { try { AGENTS = await getJSON('/agents'); } catch (_) { AGENTS = []; } } // ── Budget panel ──────────────────────────────────────────────────────── function fmtTokens(n) { n = Number(n) || 0; if (n >= 1e6) return (n / 1e6).toFixed(2) + 'M'; if (n >= 1e3) return (n / 1e3).toFixed(1) + 'k'; return String(Math.round(n)); } function fmtEur(n) { return '\\u20ac' + (Number(n) || 0).toFixed(2); } function maxTok(list) { return list.reduce(function(m, a) { return Math.max(m, a.tokens || 0); }, 0); } function cloneAgent(a) { return { name: a.name, role: a.role, model: a.model, kind: a.kind, tokens: Number(a.tokens) || 0, realTokens: Number(a.realTokens) || 0, estimatedTokens: Number(a.estimatedTokens) || 0, estimated: !!a.estimated, costEur: Number(a.costEur) || 0, taskCount: Number(a.taskCount) || 0 }; } function currentSnapshot(rep) { var byAgent = {}; (rep && rep.agents ? rep.agents : []).forEach(function(a) { byAgent[a.name] = { tokens: Number(a.tokens) || 0, costEur: Number(a.costEur) || 0 }; }); return { createdAt: Date.now(), byAgent: byAgent }; } function readBaseline() { try { return JSON.parse(localStorage.getItem('agenthub-budget-baseline') || 'null'); } catch (_) { return null; } } function writeBaseline(rep) { localStorage.setItem('agenthub-budget-baseline', JSON.stringify(currentSnapshot(rep))); } function applyBudgetMode(rep) { var agents = (rep && rep.agents ? rep.agents : []).map(cloneAgent); if (budgetMode === 'session') { var base = readBaseline(); if (!base) { writeBaseline(rep); base = readBaseline(); } agents.forEach(function(a) { var b = base && base.byAgent ? base.byAgent[a.name] : null; var baseTokens = b ? Number(b.tokens) || 0 : 0; var baseCost = b ? Number(b.costEur) || 0 : 0; a.tokens = Math.max(0, a.tokens - baseTokens); a.costEur = Math.max(0, a.costEur - baseCost); a.realTokens = Math.min(a.realTokens, a.tokens); a.estimatedTokens = Math.max(0, a.tokens - a.realTokens); a.estimated = a.estimated && a.estimatedTokens > 0; }); } var totals = { tokens: agents.reduce(function(s, a) { return s + (a.tokens || 0); }, 0), costEur: agents.reduce(function(s, a) { return s + (a.costEur || 0); }, 0), estimated: agents.some(function(a) { return a.estimated; }) }; return { agents: agents, totals: totals }; } function companySpec(kind) { var key = String(kind || '').toLowerCase(); var map = { anthropic: { name: 'Anthropic', color: '#D97757' }, openai: { name: 'OpenAI', color: '#10A37F' }, moonshot: { name: 'Moonshot', color: '#7C3AED' } }; return map[key] || { name: key ? key.charAt(0).toUpperCase() + key.slice(1) : 'Unknown', color: hashColor(key || 'unknown') }; } function donutPolar(cx, cy, r, angleDeg) { var rad = angleDeg * Math.PI / 180; return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) }; } function donutArc(cx, cy, r, startDeg, endDeg) { var start = donutPolar(cx, cy, r, startDeg); var end = donutPolar(cx, cy, r, endDeg); var largeArc = endDeg - startDeg > 180 ? 1 : 0; return 'M ' + start.x.toFixed(2) + ' ' + start.y.toFixed(2) + ' A ' + r + ' ' + r + ' 0 ' + largeArc + ' 1 ' + end.x.toFixed(2) + ' ' + end.y.toFixed(2); } function aggregateByCompany(agents, field) { var byKind = {}; (agents || []).forEach(function(a) { var kind = String(a.kind || '').toLowerCase(); if (!kind || !(a.tokens > 0)) return; var spec = companySpec(kind); if (!byKind[kind]) byKind[kind] = { kind: kind, name: spec.name, color: spec.color, value: 0 }; byKind[kind].value += Number(a[field]) || 0; }); return Object.keys(byKind).map(function(k) { return byKind[k]; }).sort(function(a, b) { return b.value - a.value; }); } function halfDonut(title, rows, centerValue, centerLabel, formatValue) { var total = rows.reduce(function(s, x) { return s + (x.value || 0); }, 0); var safeTotal = Math.max(1, total); var cx = 110, cy = 122, r = 90, stroke = 18; var angle = 180; var paths = ''; rows.forEach(function(row, idx) { var start = angle; var end = angle + 180 * ((row.value || 0) / safeTotal); angle = end; if (end <= start + 0.4) return; paths += ''; }); if (!rows.length) paths += ''; var legend = rows.length ? rows.map(function(row) { var pct = total > 0 ? Math.round((row.value / total) * 100) : 0; return '
' + '' + esc(row.name) + '' + '' + esc(formatValue(row.value)) + ' · ' + pct + '%
'; }).join('') : '
no provider data
'; return '
' + '

' + esc(title) + '

' + '
' + '
' + '' + paths + '0' + '' + esc(centerLabel) + '' + '' + '
' + '
' + legend + '
' + '
'; } function animateCounts(root) { var nodes = (root || document).querySelectorAll('[data-count-to]'); nodes.forEach(function(node) { var target = Number(node.getAttribute('data-count-to')) || 0; var format = node.getAttribute('data-count-format'); var start = performance.now(); function step(now) { var p = Math.min(1, (now - start) / 900); var eased = 1 - Math.pow(1 - p, 3); var val = target * eased; node.textContent = format === 'eur' ? fmtEur(val) : fmtTokens(val); if (p < 1) requestAnimationFrame(step); } requestAnimationFrame(step); }); } function signatureFor(rows) { return rows.map(function(r) { return r.kind + ':' + Math.round((Number(r.value) || 0) * 100); }).join('|'); } function shouldRenderDonut(nextSig, force) { if (force) return true; var now = Date.now(); if (!lastDonutSignature || now - lastDonutAt > DONUT_REFRESH_MS) return true; if (nextSig === lastDonutSignature) return false; var prev = {}; lastDonutSignature.split('|').forEach(function(part) { if (!part) return; var bits = part.split(':'); prev[bits[0]] = Number(bits[1]) || 0; }); var changed = false; nextSig.split('|').forEach(function(part) { if (!part) return; var bits = part.split(':'); var oldVal = prev[bits[0]] || 0; var newVal = Number(bits[1]) || 0; if (Math.abs(newVal - oldVal) / Math.max(1, oldVal) > 0.10) changed = true; }); return changed; } function renderDonut(active, force) { var wrap = document.getElementById('donutWrap'); if (!wrap) return; var field = donutMetric === 'cost' ? 'costEur' : 'tokens'; var companies = aggregateByCompany(active, field); var sig = donutMetric + ':' + signatureFor(companies); if (!shouldRenderDonut(sig, force)) return; lastDonutSignature = sig; lastDonutAt = Date.now(); var center = active.reduce(function(s, a) { return s + (Number(a[field]) || 0); }, 0); wrap.innerHTML = halfDonut(donutMetric === 'cost' ? 'Cost by Company' : 'Tokens by Company', companies, center, donutMetric === 'cost' ? 'EUR' : 'tokens', donutMetric === 'cost' ? fmtEur : fmtTokens); animateCounts(wrap); } function agentBarHtml(a, mx) { var spec = agentSpec(a.name); var est = a.estimated ? '~ estimated' : 'real'; var pct = mx > 0 ? Math.round(((a.tokens || 0) / mx) * 1000) / 10 : 0; var company = companySpec(a.kind); return '
' + '
' + esc(spec.initials) + '' + '' + esc(a.name) + '' + '' + esc(company.name + (a.model ? ' · ' + a.model : '')) + '
' + '
' + '
' + fmtTokens(a.tokens) + ' tok' + est + ' · ' + fmtEur(a.costEur) + '
' + '
'; } function renderAgentBars(active) { var host = document.getElementById('agentBars'); if (!host) return; var before = {}; host.querySelectorAll('[data-agent-row]').forEach(function(el) { before[el.getAttribute('data-agent-row')] = el.getBoundingClientRect(); }); var mx = maxTok(active) || 1; var sorted = active.slice().sort(function(a, b) { return (b.tokens || 0) - (a.tokens || 0) || a.name.localeCompare(b.name); }); host.innerHTML = '

Token-Verbrauch pro Agent

' + sorted.map(function(a) { return agentBarHtml(a, mx); }).join(''); host.querySelectorAll('[data-agent-row]').forEach(function(el) { var key = el.getAttribute('data-agent-row'); var old = before[key]; if (!old) return; var now = el.getBoundingClientRect(); var dy = old.top - now.top; if (!dy) return; el.style.transform = 'translateY(' + dy + 'px)'; requestAnimationFrame(function() { el.style.transform = ''; }); }); } function updateBudgetButtons() { document.querySelectorAll('[data-budget-mode]').forEach(function(btn) { btn.classList.toggle('active', btn.getAttribute('data-budget-mode') === budgetMode); }); document.querySelectorAll('[data-donut-tab]').forEach(function(btn) { btn.classList.toggle('active', btn.getAttribute('data-donut-tab') === donutMetric); }); } function renderBudget(rep, opts) { opts = opts || {}; BUDGET = rep; var rows = document.getElementById('budgetRows'); var total = document.getElementById('budgetTotal'); if (!rows || !total) return; var scoped = applyBudgetMode(rep); var active = scoped.agents.filter(function(a) { return a.kind && a.tokens > 0; }); updateBudgetButtons(); if (!active.length) { rows.innerHTML = '
no real agent token data in this ' + (budgetMode === 'session' ? 'session' : 'total range') + '
'; total.textContent = ''; lastDonutSignature = ''; return; } if (!document.getElementById('donutWrap') || !document.getElementById('agentBars')) { rows.innerHTML = '
'; } renderDonut(active, opts.forceDonut); renderAgentBars(active); total.innerHTML = (scoped.totals.estimated ? '~' : '') + fmtTokens(scoped.totals.tokens) + ' tok \\u00b7 \\u2248 ' + fmtEur(scoped.totals.costEur) + ''; } async function refreshBudget() { try { renderBudget(await getJSON('/budget')); } catch (_) {} } document.addEventListener('click', function(e) { var modeBtn = e.target.closest && e.target.closest('[data-budget-mode]'); if (modeBtn) { budgetMode = modeBtn.getAttribute('data-budget-mode') || 'session'; localStorage.setItem('agenthub-budget-mode', budgetMode); lastDonutSignature = ''; if (BUDGET) renderBudget(BUDGET, { forceDonut: true }); return; } var tabBtn = e.target.closest && e.target.closest('[data-donut-tab]'); if (tabBtn) { donutMetric = tabBtn.getAttribute('data-donut-tab') || 'tokens'; localStorage.setItem('agenthub-donut-metric', donutMetric); if (BUDGET) renderBudget(BUDGET, { forceDonut: true }); return; } var reset = e.target.closest && e.target.closest('#budgetReset'); if (reset) { if (BUDGET) { writeBaseline(BUDGET); budgetMode = 'session'; localStorage.setItem('agenthub-budget-mode', budgetMode); lastDonutSignature = ''; renderBudget(BUDGET, { forceDonut: true }); toast('Token session reset'); } } }); `;