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 @@
- Strikes / bans / forfeitures
loading…
Model usage (RPM)
loading…
+ Usage summary
login required
+ Node throughput
login required
Request history
login required
Node pending payouts
admin login required
Settlement history
admin login required
+ 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",
]