feat(board): drop budget über-tabs, Verlauf chart lives in donut card as third tab
This commit is contained in:
parent
5dd5806640
commit
964ddff2f2
@ -1,38 +1,26 @@
|
||||
import { throughputSeries, areaPath, dayStart } from './viewmodel.js';
|
||||
|
||||
/**
|
||||
* Sidebar skeleton: budget card (über-tabs Token Insights | Verlauf; insight
|
||||
* is the default) + live feed card. The insight pane keeps the v1 mount
|
||||
* points (#budgetRows, #budgetTotal, #budgetReset, [data-budget-mode]) that
|
||||
* the ported v1 budget JS (v1Budget.ts) drives; donut sub-tabs (Token/Kosten)
|
||||
* are rendered by that same ported code.
|
||||
* Sidebar skeleton: budget card + live feed card. The budget card has no tab
|
||||
* bar of its own — the Token/Kosten/Verlauf tabs live inside the donut card
|
||||
* (rendered client-side by the ported v1 budget JS in v1Budget.ts). The
|
||||
* subhead keeps the v1 mount points: [data-budget-mode] (Session/Gesamt),
|
||||
* #budgetTotal, #budgetReset; #budgetRows is where the donut card mounts.
|
||||
*/
|
||||
export function sidebarHtml(): string {
|
||||
return `
|
||||
<aside class="b2-side">
|
||||
<div class="b2-panel b2-glass" id="b2Budget">
|
||||
<div class="b2-tabhead">
|
||||
<div class="b2-subhead">
|
||||
<h6>Budget</h6>
|
||||
<div class="b2-otabs">
|
||||
<button type="button" data-otab="insight" class="active">Token Insights</button>
|
||||
<button type="button" data-otab="verlauf">Verlauf</button>
|
||||
</div>
|
||||
</div>
|
||||
<div data-opane="insight">
|
||||
<div class="b2-subhead">
|
||||
<span class="seg" aria-label="Token insight range">
|
||||
<button type="button" data-budget-mode="session" class="active">Session</button>
|
||||
<button type="button" data-budget-mode="total">Gesamt</button>
|
||||
</span>
|
||||
<span class="total" id="budgetTotal"></span>
|
||||
<button type="button" class="reset-btn" id="budgetReset">Reset</button>
|
||||
</div>
|
||||
<div id="budgetRows"><div class="budget-empty">no agent activity yet</div></div>
|
||||
</div>
|
||||
<div data-opane="verlauf" hidden>
|
||||
<div id="b2Throughput"></div>
|
||||
<div class="b2-chart-tip" id="b2ThroughputTip"></div>
|
||||
<span class="seg" aria-label="Token insight range">
|
||||
<button type="button" data-budget-mode="session" class="active">Session</button>
|
||||
<button type="button" data-budget-mode="total">Gesamt</button>
|
||||
</span>
|
||||
<span class="total" id="budgetTotal"></span>
|
||||
<button type="button" class="reset-btn" id="budgetReset">Reset</button>
|
||||
</div>
|
||||
<div id="budgetRows"><div class="budget-empty">no agent activity yet</div></div>
|
||||
</div>
|
||||
<div class="b2-panel b2-glass">
|
||||
<h6>Live</h6>
|
||||
@ -42,8 +30,8 @@ export function sidebarHtml(): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline script: über-tab switching (persisted in localStorage), throughput
|
||||
* area chart, live feed — followed by the ported v1 budget JS.
|
||||
* Inline script: throughput area chart renderer (used by the Verlauf tab in
|
||||
* the donut card) + live feed, followed by the ported v1 budget JS.
|
||||
*/
|
||||
export function sidebarJs(v1BudgetJs: string): string {
|
||||
return `
|
||||
@ -51,29 +39,10 @@ var DAY_MS = 24 * 3600 * 1000;
|
||||
${dayStart.toString()}
|
||||
${throughputSeries.toString()}
|
||||
${areaPath.toString()}
|
||||
(function () {
|
||||
var OTAB_KEY = 'agenthub-budget-otab';
|
||||
function activate(name) {
|
||||
document.querySelectorAll('[data-otab]').forEach(function (b) {
|
||||
b.classList.toggle('active', b.getAttribute('data-otab') === name);
|
||||
});
|
||||
document.querySelectorAll('[data-opane]').forEach(function (p) {
|
||||
p.hidden = p.getAttribute('data-opane') !== name;
|
||||
});
|
||||
}
|
||||
document.addEventListener('click', function (e) {
|
||||
var btn = e.target.closest && e.target.closest('[data-otab]');
|
||||
if (!btn) return;
|
||||
var name = btn.getAttribute('data-otab');
|
||||
localStorage.setItem(OTAB_KEY, name);
|
||||
activate(name);
|
||||
});
|
||||
activate(localStorage.getItem(OTAB_KEY) || 'insight');
|
||||
})();
|
||||
window.__b2RenderThroughput = function (tasks) {
|
||||
var box = document.getElementById('b2Throughput');
|
||||
var tip = document.getElementById('b2ThroughputTip');
|
||||
if (!box) return;
|
||||
if (!box) return; // Verlauf tab not mounted right now — nothing to update
|
||||
var s = throughputSeries(tasks, 14);
|
||||
var p = areaPath(s, 220, 80);
|
||||
box.innerHTML = '<svg width="100%" height="80" viewBox="0 0 220 80" preserveAspectRatio="none">'
|
||||
|
||||
@ -309,22 +309,9 @@ export function boardV2Css(): string {
|
||||
position: sticky; top: 84px; max-height: calc(100vh - 102px); overflow-y: auto; }
|
||||
.b2-panel { padding: 13px 15px; }
|
||||
.b2-panel h6 { margin: 0; font-size: 11px; text-transform: uppercase; letter-spacing: .12em; color: var(--b2-muted); }
|
||||
.b2-tabhead { display: flex; align-items: center; gap: 8px; }
|
||||
.b2-otabs, .b2-stabs { display: flex; gap: 4px; }
|
||||
.b2-otabs { margin-left: auto; }
|
||||
.b2-otabs button, .b2-stabs button {
|
||||
min-height: 26px; padding: 0 10px; border-radius: 999px; cursor: pointer;
|
||||
border: 1px solid transparent; background: transparent;
|
||||
color: var(--b2-muted); font: 600 11px/1 var(--sans);
|
||||
transition: color 140ms, background 140ms, border-color 140ms;
|
||||
}
|
||||
.b2-otabs button.active, .b2-stabs button.active {
|
||||
color: var(--b2-text); background: rgba(255,255,255,.09); border-color: rgba(255,255,255,.14);
|
||||
}
|
||||
.b2-subhead { display: flex; align-items: center; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
|
||||
.b2-subhead { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
.b2-subhead .seg { margin-left: auto; }
|
||||
[data-opane] { margin-top: 10px; }
|
||||
[data-opane][hidden] { display: none; }
|
||||
#budgetRows { margin-top: 10px; }
|
||||
.b2-chart-tip { color: var(--b2-muted); font-size: 11px; text-align: right; margin-top: 5px; font-family: var(--mono); }
|
||||
|
||||
/* Budget internals (ported v1: seg, reset, donuts, legend, agent bars) */
|
||||
|
||||
@ -114,6 +114,17 @@ export const v1BudgetJs = `
|
||||
});
|
||||
return Object.keys(byKind).map(function(k) { return byKind[k]; }).sort(function(a, b) { return b.value - a.value; });
|
||||
}
|
||||
// The donut card hosts three tabs: Token | Kosten | Verlauf (throughput chart).
|
||||
function donutTabsHtml() {
|
||||
return '<div class="donut-tabs">' +
|
||||
'<button type="button" data-donut-tab="tokens" class="' + (donutMetric === 'tokens' ? 'active' : '') + '">Token</button>' +
|
||||
'<button type="button" data-donut-tab="cost" class="' + (donutMetric === 'cost' ? 'active' : '') + '">Kosten</button>' +
|
||||
'<button type="button" data-donut-tab="verlauf" class="' + (donutMetric === 'verlauf' ? 'active' : '') + '">Verlauf</button>' +
|
||||
'</div>';
|
||||
}
|
||||
function donutCardShell(title, bodyHtml) {
|
||||
return '<div class="donut-card"><h3 class="donut-title">' + esc(title) + '</h3>' + donutTabsHtml() + bodyHtml + '</div>';
|
||||
}
|
||||
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);
|
||||
@ -136,7 +147,7 @@ export const v1BudgetJs = `
|
||||
}).join('') : '<div class="empty">no provider data</div>';
|
||||
return '<div class="donut-card">' +
|
||||
'<h3 class="donut-title">' + esc(title) + '</h3>' +
|
||||
'<div class="donut-tabs"><button type="button" data-donut-tab="tokens" class="' + (donutMetric === 'tokens' ? 'active' : '') + '">Token</button><button type="button" data-donut-tab="cost" class="' + (donutMetric === 'cost' ? 'active' : '') + '">Kosten</button></div>' +
|
||||
donutTabsHtml() +
|
||||
'<div class="half-donut">' +
|
||||
'<svg viewBox="0 0 220 132" role="img" aria-label="' + esc(title) + '">' + paths +
|
||||
'<text class="donut-value" x="110" y="102" text-anchor="middle" dominant-baseline="central" data-count-to="' + esc(centerValue) + '" data-count-format="' + (formatValue === fmtEur ? 'eur' : 'tokens') + '">0</text>' +
|
||||
@ -189,6 +200,20 @@ export const v1BudgetJs = `
|
||||
function renderDonut(active, force) {
|
||||
var wrap = document.getElementById('donutWrap');
|
||||
if (!wrap) return;
|
||||
// Verlauf tab: throughput area chart instead of the donut. Static
|
||||
// signature — the chart content itself updates via __b2RenderThroughput
|
||||
// on task refreshes, so the 3s budget poll must not rebuild the card.
|
||||
if (donutMetric === 'verlauf') {
|
||||
if (lastDonutSignature === 'verlauf' && !force) return;
|
||||
lastDonutSignature = 'verlauf';
|
||||
lastDonutAt = Date.now();
|
||||
wrap.innerHTML = donutCardShell('Verlauf · erledigte Tasks',
|
||||
'<div id="b2Throughput"></div><div class="b2-chart-tip" id="b2ThroughputTip"></div>');
|
||||
if (window.__b2RenderThroughput) {
|
||||
window.__b2RenderThroughput(typeof lastTasks !== 'undefined' ? lastTasks : []);
|
||||
}
|
||||
return;
|
||||
}
|
||||
var field = donutMetric === 'cost' ? 'costEur' : 'tokens';
|
||||
var companies = aggregateByCompany(active, field);
|
||||
var sig = donutMetric + ':' + signatureFor(companies);
|
||||
@ -250,17 +275,28 @@ export const v1BudgetJs = `
|
||||
var scoped = applyBudgetMode(rep);
|
||||
var active = scoped.agents.filter(function(a) { return a.kind && a.tokens > 0; });
|
||||
updateBudgetButtons();
|
||||
if (!active.length) {
|
||||
rows.innerHTML = '<div class="budget-empty">no real agent token data in this ' + (budgetMode === 'session' ? 'session' : 'total range') + '</div>';
|
||||
total.textContent = '';
|
||||
lastDonutSignature = '';
|
||||
return;
|
||||
}
|
||||
// The card skeleton (and with it the Token/Kosten/Verlauf tabs) always
|
||||
// exists — even in the empty state the Verlauf tab must stay reachable.
|
||||
if (!document.getElementById('donutWrap') || !document.getElementById('agentBars')) {
|
||||
rows.innerHTML = '<div class="donut-grid"><div id="donutWrap"></div></div><div class="agent-bars" id="agentBars"></div>';
|
||||
}
|
||||
var bars = document.getElementById('agentBars');
|
||||
if (!active.length) {
|
||||
if (donutMetric === 'verlauf') {
|
||||
renderDonut(active, opts.forceDonut);
|
||||
} else {
|
||||
lastDonutSignature = '';
|
||||
document.getElementById('donutWrap').innerHTML = donutCardShell('Token Insights',
|
||||
'<div class="budget-empty">no real agent token data in this ' + (budgetMode === 'session' ? 'session' : 'total range') + '</div>');
|
||||
}
|
||||
if (bars) bars.innerHTML = '';
|
||||
total.textContent = '';
|
||||
return;
|
||||
}
|
||||
renderDonut(active, opts.forceDonut);
|
||||
renderAgentBars(active);
|
||||
// Agent bars belong to the token insights; hidden on the Verlauf tab.
|
||||
if (donutMetric === 'verlauf') { if (bars) bars.innerHTML = ''; }
|
||||
else renderAgentBars(active);
|
||||
total.innerHTML = (scoped.totals.estimated ? '~' : '') + fmtTokens(scoped.totals.tokens) +
|
||||
' tok <small>\\u00b7 \\u2248 ' + fmtEur(scoped.totals.costEur) + '</small>';
|
||||
}
|
||||
|
||||
@ -3,11 +3,10 @@ import { sidebarHtml, sidebarJs } from '../src/server/board/sidebar.js';
|
||||
import { v1BudgetJs } from '../src/server/board/v1Budget.js';
|
||||
|
||||
describe('sidebarHtml', () => {
|
||||
it('renders budget card with über-tabs and insight as default', () => {
|
||||
it('renders budget card without über-tabs (tabs live in the donut card)', () => {
|
||||
const h = sidebarHtml();
|
||||
expect(h).toContain('id="b2Budget"');
|
||||
expect(h).toContain('data-otab="insight"');
|
||||
expect(h).toContain('data-otab="verlauf"');
|
||||
expect(h).not.toContain('data-otab');
|
||||
expect(h).toContain('id="budgetReset"');
|
||||
expect(h).toContain('id="b2Feed"');
|
||||
});
|
||||
@ -22,8 +21,8 @@ describe('sidebarHtml', () => {
|
||||
|
||||
describe('sidebarJs', () => {
|
||||
const js = sidebarJs(v1BudgetJs);
|
||||
it('persists the über-tab and renders throughput', () => {
|
||||
expect(js).toContain('agenthub-budget-otab'); // localStorage key
|
||||
it('renders throughput and feed without über-tab logic', () => {
|
||||
expect(js).not.toContain('agenthub-budget-otab');
|
||||
expect(js).toContain('function throughputSeries');
|
||||
expect(js).toContain('function dayStart');
|
||||
expect(js).toContain('__b2RenderThroughput');
|
||||
@ -36,4 +35,11 @@ describe('sidebarJs', () => {
|
||||
expect(js).toContain('budgetReset');
|
||||
expect(js).toContain('renderBudget');
|
||||
});
|
||||
it('hosts the three tabs Token/Kosten/Verlauf inside the donut card', () => {
|
||||
expect(js).toContain('data-donut-tab="tokens"');
|
||||
expect(js).toContain('data-donut-tab="cost"');
|
||||
expect(js).toContain('data-donut-tab="verlauf"');
|
||||
expect(js).toContain('donutTabsHtml');
|
||||
expect(js).toContain('donutCardShell');
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user