This commit is contained in:
Dobromir Popov
2026-07-07 17:54:22 +03:00
parent 08e9c22ccf
commit fdeb881c83
4 changed files with 92 additions and 33 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -106,7 +106,6 @@
<section data-tab="overview" id="account-section"><h2>Account</h2><div id="account">loading…</div></section>
<section data-tab="overview"><h2>Tracker hive</h2><div id="hive" class="empty">loading…</div></section>
<section data-tab="overview"><h2>Nodes &amp; coverage</h2><div id="nodes" class="empty">loading…</div></section>
<section data-tab="overview"><h2>Strikes / bans / forfeitures</h2><div id="fraud" class="empty">loading…</div></section>
<section data-tab="overview"><h2>Model usage (RPM)</h2><div id="stats" class="empty">loading…</div></section>
<section data-tab="overview" class="wide"><h2>Call wall</h2><div id="call-wall" class="empty">loading...</div></section>
<section data-tab="chat" class="wide">
@@ -134,10 +133,13 @@
</div>
</div>
</section>
<section data-tab="billing" data-logged-in-only><h2>Usage summary</h2><div id="usage-summary" class="empty">login required</div></section>
<section data-tab="billing" data-logged-in-only><h2>Node throughput</h2><div id="node-throughput" class="empty">login required</div></section>
<section data-tab="billing"><h2>Request history</h2><div id="billing-usage" class="empty">login required</div></section>
<section data-tab="billing" data-admin-only><h2>Node pending payouts</h2><div id="pending" class="empty">admin login required</div></section>
<section data-tab="billing" data-admin-only><h2>Settlement history</h2><div id="settlements" class="empty">admin login required</div></section>
<section data-tab="admin" id="admin-section"><h2>All accounts (admin)</h2><div id="admin" class="empty"></div></section>
<section data-tab="admin" data-admin-only><h2>Strikes / bans / forfeitures</h2><div id="fraud" class="empty">admin login required</div></section>
<section data-tab="admin"><h2>Client balances</h2><div id="clients" class="empty">admin login required</div></section>
<section data-tab="admin" class="wide"><h2>Console output</h2><div id="console" class="console empty">admin login required</div></section>
</main>
@@ -442,22 +444,89 @@ function renderCallWall(consoleData, stats) {
$("call-wall").innerHTML = html;
}
function groupUsageByDayModel(records) {
const groups = new Map();
for (const u of records) {
const day = new Date((u.ts || 0) * 1000).toLocaleDateString();
const model = u.model || "?";
const key = day + "\0" + model;
const g = groups.get(key) || { day, model, requests: 0, tokens: 0, cost: 0 };
g.requests += 1;
g.tokens += Number(u.total_tokens || 0);
g.cost += Number(u.cost || 0);
groups.set(key, g);
function startOfLocalDay(tsSec) {
const d = new Date(tsSec * 1000);
d.setHours(0, 0, 0, 0);
return d.getTime() / 1000;
}
return Array.from(groups.values()).sort((a, b) => {
const dayCmp = b.day.localeCompare(a.day);
return dayCmp !== 0 ? dayCmp : a.model.localeCompare(b.model);
});
function formatUsageDayLabel(tsSec) {
return new Date(tsSec * 1000).toLocaleDateString();
}
function summarizeUsageBuckets(records) {
const now = Date.now() / 1000;
const todayStart = startOfLocalDay(now);
const daySec = 86400;
const empty = () => ({ requests: 0, tokens: 0, cost: 0 });
const daily = [0, 1, 2].map(offset => ({
label: offset === 0 ? "Today" : offset === 1 ? "Yesterday" : formatUsageDayLabel(todayStart - offset * daySec),
...empty(),
}));
const last7 = { label: "Last 7 days", ...empty() };
const last30 = { label: "Last 30 days", ...empty() };
const total = { label: "All time", ...empty() };
for (const u of records) {
const ts = Number(u.ts || 0);
const tokens = Number(u.total_tokens || 0);
const cost = Number(u.cost || 0);
total.requests += 1;
total.tokens += tokens;
total.cost += cost;
if (ts >= now - 30 * daySec) {
last30.requests += 1;
last30.tokens += tokens;
last30.cost += cost;
}
if (ts >= now - 7 * daySec) {
last7.requests += 1;
last7.tokens += tokens;
last7.cost += cost;
}
for (let offset = 0; offset < 3; offset++) {
const start = todayStart - offset * daySec;
const end = start + daySec;
if (ts >= start && ts < end) {
daily[offset].requests += 1;
daily[offset].tokens += tokens;
daily[offset].cost += cost;
}
}
}
return [...daily, last7, last30, total];
}
function renderUsageSummary(records) {
const el = $("usage-summary");
if (!el) return;
if (!sessionToken) {
el.innerHTML = '<div class="empty">login required</div>';
return;
}
if (!records.length) {
el.innerHTML = '<div class="empty">no billed requests yet</div>';
return;
}
const rows = summarizeUsageBuckets(records).map(b => [
esc(b.label),
`<span class="num">${b.requests}</span>`,
`<span class="num">${esc(String(b.tokens))}</span>`,
`<span class="num">${usdt(b.cost)}</span>`,
]);
el.innerHTML =
'<div class="dim" style="margin-bottom:6px">per-request detail on Request history below</div>' +
table(["period", "requests", "tokens", "cost (USDT)"], rows);
}
function renderNodeThroughput(stats) {
const el = $("node-throughput");
if (!el) return;
if (!sessionToken) {
el.innerHTML = '<div class="empty">login required</div>';
return;
}
el.innerHTML = renderThroughputHtml(stats);
}
function renderBillingUsage(records) {
@@ -527,7 +596,8 @@ function updateSectionVisibility() {
for (const section of document.querySelectorAll("main section[data-tab]")) {
const onTab = section.dataset.tab === dashboardTab;
const adminOnly = section.hasAttribute("data-admin-only");
section.hidden = !onTab || (adminOnly && !isAdmin);
const loggedInOnly = section.hasAttribute("data-logged-in-only");
section.hidden = !onTab || (adminOnly && !isAdmin) || (loggedInOnly && !isLoggedIn);
}
}
@@ -608,6 +678,8 @@ function setLoggedInMode(enabled) {
if (!enabled) {
accountUsageRecords = [];
renderBillingUsage([]);
renderUsageSummary([]);
renderNodeThroughput(null);
if (dashboardTab === "billing") switchDashboardTab("overview");
} else {
updateSectionVisibility();
@@ -803,21 +875,9 @@ async function renderAccountPanel() {
} else {
html += '<div class="empty">no active keys</div>';
}
html += '<div style="margin-top:8px"><b class="dim">node throughput</b></div>' +
`<div id="account-throughput">${renderThroughputHtml(lastStats)}</div>`;
const grouped = groupUsageByDayModel(accountUsageRecords);
if (grouped.length) {
html += '<div style="margin-top:8px"><b class="dim">usage by day / model</b> ' +
'<span class="dim">(full request list on Billing tab)</span></div>' +
table(["day", "model", "requests", "tokens", "cost (USDT)"], grouped.map(g => [
esc(g.day),
esc(short(g.model, 28)),
`<span class="num">${g.requests}</span>`,
`<span class="num">${esc(String(g.tokens))}</span>`,
`<span class="num">${usdt(g.cost)}</span>`,
]));
}
$("account").innerHTML = html;
renderUsageSummary(accountUsageRecords);
renderNodeThroughput(lastStats);
renderBillingUsage(accountUsageRecords);
renderChatAuthHint();
renderChatModels();
@@ -926,8 +986,7 @@ async function refresh() {
renderStats(stats);
renderCallWall(consoleData, stats);
renderConsole(consoleData);
const throughputEl = $("account-throughput");
if (throughputEl) throughputEl.innerHTML = renderThroughputHtml(stats);
renderNodeThroughput(stats);
renderChatModels();
renderChatHistory();
$("refreshed").textContent = "refreshed " + new Date().toLocaleTimeString();

View File

@@ -13,7 +13,7 @@ PANELS = [
"Tracker hive", "Nodes &amp; coverage", "Client balances",
"Node pending payouts", "Settlement history",
"Strikes / bans / forfeitures", "Model usage", "Call wall",
"Request history", "node throughput",
"Usage summary", "Node throughput", "Request history",
"Chat / inference",
"Console output",
]