web UI
This commit is contained in:
BIN
billing.sqlite
BIN
billing.sqlite
Binary file not shown.
Binary file not shown.
@@ -106,7 +106,6 @@
|
|||||||
<section data-tab="overview" id="account-section"><h2>Account</h2><div id="account">loading…</div></section>
|
<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>Tracker hive</h2><div id="hive" class="empty">loading…</div></section>
|
||||||
<section data-tab="overview"><h2>Nodes & coverage</h2><div id="nodes" class="empty">loading…</div></section>
|
<section data-tab="overview"><h2>Nodes & 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"><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="overview" class="wide"><h2>Call wall</h2><div id="call-wall" class="empty">loading...</div></section>
|
||||||
<section data-tab="chat" class="wide">
|
<section data-tab="chat" class="wide">
|
||||||
@@ -134,10 +133,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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"><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>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="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" 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"><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>
|
<section data-tab="admin" class="wide"><h2>Console output</h2><div id="console" class="console empty">admin login required</div></section>
|
||||||
</main>
|
</main>
|
||||||
@@ -442,22 +444,89 @@ function renderCallWall(consoleData, stats) {
|
|||||||
$("call-wall").innerHTML = html;
|
$("call-wall").innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
function groupUsageByDayModel(records) {
|
function startOfLocalDay(tsSec) {
|
||||||
const groups = new Map();
|
const d = new Date(tsSec * 1000);
|
||||||
|
d.setHours(0, 0, 0, 0);
|
||||||
|
return d.getTime() / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
for (const u of records) {
|
||||||
const day = new Date((u.ts || 0) * 1000).toLocaleDateString();
|
const ts = Number(u.ts || 0);
|
||||||
const model = u.model || "?";
|
const tokens = Number(u.total_tokens || 0);
|
||||||
const key = day + "\0" + model;
|
const cost = Number(u.cost || 0);
|
||||||
const g = groups.get(key) || { day, model, requests: 0, tokens: 0, cost: 0 };
|
total.requests += 1;
|
||||||
g.requests += 1;
|
total.tokens += tokens;
|
||||||
g.tokens += Number(u.total_tokens || 0);
|
total.cost += cost;
|
||||||
g.cost += Number(u.cost || 0);
|
if (ts >= now - 30 * daySec) {
|
||||||
groups.set(key, g);
|
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 Array.from(groups.values()).sort((a, b) => {
|
return [...daily, last7, last30, total];
|
||||||
const dayCmp = b.day.localeCompare(a.day);
|
}
|
||||||
return dayCmp !== 0 ? dayCmp : a.model.localeCompare(b.model);
|
|
||||||
});
|
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) {
|
function renderBillingUsage(records) {
|
||||||
@@ -527,7 +596,8 @@ function updateSectionVisibility() {
|
|||||||
for (const section of document.querySelectorAll("main section[data-tab]")) {
|
for (const section of document.querySelectorAll("main section[data-tab]")) {
|
||||||
const onTab = section.dataset.tab === dashboardTab;
|
const onTab = section.dataset.tab === dashboardTab;
|
||||||
const adminOnly = section.hasAttribute("data-admin-only");
|
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) {
|
if (!enabled) {
|
||||||
accountUsageRecords = [];
|
accountUsageRecords = [];
|
||||||
renderBillingUsage([]);
|
renderBillingUsage([]);
|
||||||
|
renderUsageSummary([]);
|
||||||
|
renderNodeThroughput(null);
|
||||||
if (dashboardTab === "billing") switchDashboardTab("overview");
|
if (dashboardTab === "billing") switchDashboardTab("overview");
|
||||||
} else {
|
} else {
|
||||||
updateSectionVisibility();
|
updateSectionVisibility();
|
||||||
@@ -803,21 +875,9 @@ async function renderAccountPanel() {
|
|||||||
} else {
|
} else {
|
||||||
html += '<div class="empty">no active keys</div>';
|
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;
|
$("account").innerHTML = html;
|
||||||
|
renderUsageSummary(accountUsageRecords);
|
||||||
|
renderNodeThroughput(lastStats);
|
||||||
renderBillingUsage(accountUsageRecords);
|
renderBillingUsage(accountUsageRecords);
|
||||||
renderChatAuthHint();
|
renderChatAuthHint();
|
||||||
renderChatModels();
|
renderChatModels();
|
||||||
@@ -926,8 +986,7 @@ async function refresh() {
|
|||||||
renderStats(stats);
|
renderStats(stats);
|
||||||
renderCallWall(consoleData, stats);
|
renderCallWall(consoleData, stats);
|
||||||
renderConsole(consoleData);
|
renderConsole(consoleData);
|
||||||
const throughputEl = $("account-throughput");
|
renderNodeThroughput(stats);
|
||||||
if (throughputEl) throughputEl.innerHTML = renderThroughputHtml(stats);
|
|
||||||
renderChatModels();
|
renderChatModels();
|
||||||
renderChatHistory();
|
renderChatHistory();
|
||||||
$("refreshed").textContent = "refreshed " + new Date().toLocaleTimeString();
|
$("refreshed").textContent = "refreshed " + new Date().toLocaleTimeString();
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ PANELS = [
|
|||||||
"Tracker hive", "Nodes & coverage", "Client balances",
|
"Tracker hive", "Nodes & coverage", "Client balances",
|
||||||
"Node pending payouts", "Settlement history",
|
"Node pending payouts", "Settlement history",
|
||||||
"Strikes / bans / forfeitures", "Model usage", "Call wall",
|
"Strikes / bans / forfeitures", "Model usage", "Call wall",
|
||||||
"Request history", "node throughput",
|
"Usage summary", "Node throughput", "Request history",
|
||||||
"Chat / inference",
|
"Chat / inference",
|
||||||
"Console output",
|
"Console output",
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user