diff --git a/billing.sqlite b/billing.sqlite index 85ceb51..2a00b87 100644 Binary files a/billing.sqlite and b/billing.sqlite differ diff --git a/meshnet_registry.sqlite3 b/meshnet_registry.sqlite3 index 7a6d3a4..8060b82 100644 Binary files a/meshnet_registry.sqlite3 and b/meshnet_registry.sqlite3 differ diff --git a/packages/tracker/meshnet_tracker/dashboard.html b/packages/tracker/meshnet_tracker/dashboard.html index 2180367..decdad3 100644 --- a/packages/tracker/meshnet_tracker/dashboard.html +++ b/packages/tracker/meshnet_tracker/dashboard.html @@ -106,7 +106,6 @@

Account

loading…

Tracker hive

loading…

Nodes & coverage

loading…
-

Strikes / bans / forfeitures

loading…

Model usage (RPM)

loading…

Call wall

loading...
@@ -134,10 +133,13 @@
+

Usage summary

login required
+

Node throughput

login required

Request history

login required

Node pending payouts

admin login required

Settlement history

admin login required

All accounts (admin)

+

Strikes / bans / forfeitures

admin login required

Client balances

admin login required

Console output

admin login required
@@ -442,22 +444,89 @@ function renderCallWall(consoleData, stats) { $("call-wall").innerHTML = html; } -function groupUsageByDayModel(records) { - const groups = new Map(); +function startOfLocalDay(tsSec) { + 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) { - 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); + 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 Array.from(groups.values()).sort((a, b) => { - const dayCmp = b.day.localeCompare(a.day); - return dayCmp !== 0 ? dayCmp : a.model.localeCompare(b.model); - }); + return [...daily, last7, last30, total]; +} + +function renderUsageSummary(records) { + const el = $("usage-summary"); + if (!el) return; + if (!sessionToken) { + el.innerHTML = '
login required
'; + return; + } + if (!records.length) { + el.innerHTML = '
no billed requests yet
'; + return; + } + const rows = summarizeUsageBuckets(records).map(b => [ + esc(b.label), + `${b.requests}`, + `${esc(String(b.tokens))}`, + `${usdt(b.cost)}`, + ]); + el.innerHTML = + '
per-request detail on Request history below
' + + table(["period", "requests", "tokens", "cost (USDT)"], rows); +} + +function renderNodeThroughput(stats) { + const el = $("node-throughput"); + if (!el) return; + if (!sessionToken) { + el.innerHTML = '
login required
'; + 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 += '
no active keys
'; } - html += '
node throughput
' + - `
${renderThroughputHtml(lastStats)}
`; - const grouped = groupUsageByDayModel(accountUsageRecords); - if (grouped.length) { - html += '
usage by day / model ' + - '(full request list on Billing tab)
' + - table(["day", "model", "requests", "tokens", "cost (USDT)"], grouped.map(g => [ - esc(g.day), - esc(short(g.model, 28)), - `${g.requests}`, - `${esc(String(g.tokens))}`, - `${usdt(g.cost)}`, - ])); - } $("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(); diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index 4a6cabd..ff8029d 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -13,7 +13,7 @@ PANELS = [ "Tracker hive", "Nodes & 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", ]