mirror of
https://github.com/Routstr/routstrd.git
synced 2026-07-30 15:46:14 +00:00
feat(monitor): consume /usage/summary with legacy fallback
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { getVisibleTabs } from "./constants.ts";
|
||||
import type { Tab } from "./types.ts";
|
||||
import { fetchBalance, fetchClients, fetchStatus, fetchUsage, hasAnyNpubs, isDaemonRunning, type BalanceInfo, type ClientInfo, type StatusInfo } from "./data.ts";
|
||||
import { fetchBalance, fetchClients, fetchStatus, fetchUsage, fetchUsageSummary, hasAnyNpubs, isDaemonRunning, type BalanceInfo, type ClientInfo, type StatusInfo } from "./data.ts";
|
||||
import {
|
||||
applyScrollToContent,
|
||||
exitSearchMode,
|
||||
@@ -89,7 +89,7 @@ export async function runUsageTui(): Promise<void> {
|
||||
const height = getHeight();
|
||||
|
||||
if (forceFetch || shouldUpdate) {
|
||||
stats = await fetchUsage(10000);
|
||||
stats = await fetchUsageSummary() ?? await fetchUsage(10000);
|
||||
balance = await fetchBalance();
|
||||
status = await fetchStatus();
|
||||
clients = await fetchClients();
|
||||
|
||||
+26
-1
@@ -1,6 +1,6 @@
|
||||
import type { UsageTrackingEntry } from "../../daemon/types.ts";
|
||||
import { callDaemon, isDaemonRunning } from "../../utils/daemon-client.ts";
|
||||
import type { ClientStats, DayStats, ModelStats, NpubStats, ProviderStats, UsageStats } from "./types.ts";
|
||||
import type { ClientStats, DayStats, ModelStats, NpubStats, ProviderStats, UsageStats, UsageSummary } from "./types.ts";
|
||||
|
||||
export { isDaemonRunning };
|
||||
|
||||
@@ -112,6 +112,31 @@ export async function fetchUsage(limit = 10000): Promise<UsageStats | null> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchUsageSummary(): Promise<UsageStats | null> {
|
||||
try {
|
||||
const running = await isDaemonRunning();
|
||||
if (!running) return null;
|
||||
|
||||
const tz = new Date().getTimezoneOffset();
|
||||
const result = await callDaemon(`/usage/summary?tz=${tz}`);
|
||||
if (result.error) return null;
|
||||
|
||||
const summary = result.output as UsageSummary | undefined;
|
||||
if (!summary || typeof summary.totals !== "object") return null;
|
||||
|
||||
return {
|
||||
entries: summary.recent,
|
||||
totalEntries: summary.totals.requests,
|
||||
totalSatsCost: summary.totals.satsCost,
|
||||
recentSatsCost: summary.totals.satsCost,
|
||||
limit: 50,
|
||||
summary,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getTodayStart(): number {
|
||||
const now = new Date();
|
||||
return new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
||||
|
||||
+136
-92
@@ -240,7 +240,7 @@ export function renderOverview(stats: UsageStats, balance: BalanceInfo | null, s
|
||||
output = renderBox(statusLines, width, "System Status") + "\n" + output;
|
||||
}
|
||||
|
||||
const modelStats = getModelStats(stats.entries);
|
||||
const modelStats = stats.summary?.models ?? getModelStats(stats.entries);
|
||||
if (modelStats.length > 0) {
|
||||
const maxCost = modelStats[0]!.satsCost;
|
||||
const totalCost = Math.max(totalVisibleCost, 1);
|
||||
@@ -259,7 +259,7 @@ export function renderOverview(stats: UsageStats, balance: BalanceInfo | null, s
|
||||
output += "\n" + renderBox(modelLines, width, "Top Models by Cost");
|
||||
}
|
||||
|
||||
const clientStats = getClientStats(stats.entries);
|
||||
const clientStats = stats.summary?.clients ?? getClientStats(stats.entries);
|
||||
if (clientStats.length > 0) {
|
||||
const maxCost = clientStats[0]!.satsCost;
|
||||
const totalCost = Math.max(totalVisibleCost, 1);
|
||||
@@ -282,19 +282,40 @@ export function renderOverview(stats: UsageStats, balance: BalanceInfo | null, s
|
||||
}
|
||||
|
||||
export function renderToday(stats: UsageStats, width: number): string {
|
||||
const hourly = getHourlyToday(stats.entries);
|
||||
const todayStart = getTodayStart();
|
||||
const currentHour = new Date().getHours();
|
||||
const todayStats = { date: formatDate(Date.now()), requests: 0, satsCost: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
||||
const todayDateStr = formatDate(Date.now());
|
||||
|
||||
for (const entry of stats.entries) {
|
||||
if (entry.timestamp >= todayStart) {
|
||||
todayStats.requests++;
|
||||
todayStats.satsCost += entry.satsCost;
|
||||
todayStats.promptTokens += entry.promptTokens;
|
||||
todayStats.completionTokens += entry.completionTokens;
|
||||
todayStats.totalTokens += entry.totalTokens;
|
||||
let todayStats: { date: string; requests: number; satsCost: number; promptTokens: number; completionTokens: number; totalTokens: number };
|
||||
let recentDays: Array<{ date: string; requests: number; satsCost: number; totalTokens: number; promptTokens: number; completionTokens: number }>;
|
||||
let hourlyMap: Map<number, { requests: number; satsCost: number }>;
|
||||
|
||||
if (stats.summary) {
|
||||
// Use server-aggregated data
|
||||
const { days, hoursToday } = stats.summary;
|
||||
// days[0] is most-recent-first; check if it's today
|
||||
const todayDayStat = days[0]?.date === todayDateStr ? days[0] : undefined;
|
||||
todayStats = todayDayStat
|
||||
? { date: todayDayStat.date, requests: todayDayStat.requests, satsCost: todayDayStat.satsCost, promptTokens: todayDayStat.promptTokens, completionTokens: todayDayStat.completionTokens, totalTokens: todayDayStat.totalTokens }
|
||||
: { date: todayDateStr, requests: 0, satsCost: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
||||
recentDays = days.slice(1, 7);
|
||||
hourlyMap = new Map(hoursToday.map((h) => [h.hour, { requests: h.requests, satsCost: h.satsCost }]));
|
||||
} else {
|
||||
// Fallback: compute from raw entries
|
||||
const todayStart = getTodayStart();
|
||||
todayStats = { date: todayDateStr, requests: 0, satsCost: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
||||
for (const entry of stats.entries) {
|
||||
if (entry.timestamp >= todayStart) {
|
||||
todayStats.requests++;
|
||||
todayStats.satsCost += entry.satsCost;
|
||||
todayStats.promptTokens += entry.promptTokens;
|
||||
todayStats.completionTokens += entry.completionTokens;
|
||||
todayStats.totalTokens += entry.totalTokens;
|
||||
}
|
||||
}
|
||||
recentDays = Array.from(getDayStats(stats.entries).values()).slice(1, 7);
|
||||
hourlyMap = new Map(
|
||||
Array.from(getHourlyToday(stats.entries).entries()).map(([h, s]) => [h, { requests: s.requests, satsCost: s.satsCost }])
|
||||
);
|
||||
}
|
||||
|
||||
const summaryLines = [
|
||||
@@ -306,18 +327,17 @@ export function renderToday(stats: UsageStats, width: number): string {
|
||||
|
||||
let output = renderBox(summaryLines, width, "Today");
|
||||
|
||||
const days = Array.from(getDayStats(stats.entries).values()).slice(0, 7);
|
||||
if (days.length > 1) {
|
||||
const dayLines = days.slice(1).map((d) => `${d.date}: ${formatReqs(d.requests)} req, ${formatCost(d.satsCost)} sats, ${formatNumber(d.totalTokens)} tokens`);
|
||||
if (recentDays.length > 0) {
|
||||
const dayLines = recentDays.map((d) => `${d.date}: ${formatReqs(d.requests)} req, ${formatCost(d.satsCost)} sats, ${formatNumber(d.totalTokens)} tokens`);
|
||||
output += "\n" + renderBox(dayLines, width, "Recent Days");
|
||||
}
|
||||
|
||||
const hourLines: string[] = [];
|
||||
const maxHourCost = Math.max(...Array.from(hourly.values()).map((h) => h.satsCost), 1);
|
||||
const maxHourCost = Math.max(...Array.from(hourlyMap.values()).map((h) => h.satsCost), 1);
|
||||
const totalTodayCost = Math.max(todayStats.satsCost, 1);
|
||||
const hourLabels: string[] = [];
|
||||
for (let h = currentHour; h >= 0; h--) {
|
||||
const hStat = hourly.get(h);
|
||||
const hStat = hourlyMap.get(h);
|
||||
const reqs = hStat?.requests || 0;
|
||||
const cost = hStat?.satsCost || 0;
|
||||
hourLabels.push(`${h.toString().padStart(2, "0")}:00 (${formatReqs(reqs)} req, ${formatCost(cost)} sats) `);
|
||||
@@ -325,7 +345,7 @@ export function renderToday(stats: UsageStats, width: number): string {
|
||||
const maxHourLabel = Math.max(...hourLabels.map((l) => l.length));
|
||||
startBarSection("hourly", maxHourLabel);
|
||||
for (let i = currentHour; i >= 0; i--) {
|
||||
const hStat = hourly.get(i);
|
||||
const hStat = hourlyMap.get(i);
|
||||
const reqs = hStat?.requests || 0;
|
||||
const cost = hStat?.satsCost || 0;
|
||||
hourLines.push(renderBarChart(
|
||||
@@ -346,7 +366,7 @@ export function renderToday(stats: UsageStats, width: number): string {
|
||||
}
|
||||
|
||||
export function renderModels(stats: UsageStats, width: number): string {
|
||||
const modelStats = getModelStats(stats.entries);
|
||||
const modelStats = stats.summary?.models ?? getModelStats(stats.entries);
|
||||
if (modelStats.length === 0) return renderBox(["No model data available"], width, "Models");
|
||||
|
||||
// Use totalSatsCost (all-time) for percentage calculations to match header
|
||||
@@ -373,7 +393,7 @@ export function renderModels(stats: UsageStats, width: number): string {
|
||||
}
|
||||
|
||||
export function renderProviders(stats: UsageStats, width: number): string {
|
||||
const providerStats = getProviderStats(stats.entries);
|
||||
const providerStats = stats.summary?.providers ?? getProviderStats(stats.entries);
|
||||
if (providerStats.length === 0) return renderBox(["No provider data available"], width, "Providers");
|
||||
|
||||
const lines: string[] = [];
|
||||
@@ -389,8 +409,8 @@ export function renderProviders(stats: UsageStats, width: number): string {
|
||||
}
|
||||
|
||||
export function renderTokens(stats: UsageStats, width: number): string {
|
||||
const totals = getTotals(stats.entries);
|
||||
const modelStats = getModelStats(stats.entries);
|
||||
const totals = stats.summary?.totals ?? getTotals(stats.entries);
|
||||
const modelStats = stats.summary?.models ?? getModelStats(stats.entries);
|
||||
const summaryLines = [
|
||||
`${COLORS.bold}Total Prompt Tokens:${COLORS.reset} ${formatNumber(totals.promptTokens)}`,
|
||||
`${COLORS.bold}Total Completion Tokens:${COLORS.reset} ${formatNumber(totals.completionTokens)}`,
|
||||
@@ -410,23 +430,25 @@ export function renderTokens(stats: UsageStats, width: number): string {
|
||||
output += "\n" + renderBox(tokenLines, width, "Tokens by Model");
|
||||
}
|
||||
|
||||
const sizeBuckets = {
|
||||
tiny: { min: 0, max: 1000, count: 0, cost: 0 },
|
||||
small: { min: 1000, max: 10000, count: 0, cost: 0 },
|
||||
medium: { min: 10000, max: 50000, count: 0, cost: 0 },
|
||||
large: { min: 50000, max: 100000, count: 0, cost: 0 },
|
||||
huge: { min: 100000, max: Infinity, count: 0, cost: 0 },
|
||||
};
|
||||
|
||||
for (const entry of stats.entries) {
|
||||
for (const bucket of Object.values(sizeBuckets)) {
|
||||
if (entry.totalTokens >= bucket.min && entry.totalTokens < bucket.max) {
|
||||
bucket.count++;
|
||||
bucket.cost += entry.satsCost;
|
||||
break;
|
||||
const sizeBuckets = stats.summary?.sizeBuckets ?? (() => {
|
||||
const b = {
|
||||
tiny: { min: 0, max: 1000, count: 0, cost: 0 },
|
||||
small: { min: 1000, max: 10000, count: 0, cost: 0 },
|
||||
medium: { min: 10000, max: 50000, count: 0, cost: 0 },
|
||||
large: { min: 50000, max: 100000, count: 0, cost: 0 },
|
||||
huge: { min: 100000, max: Infinity, count: 0, cost: 0 },
|
||||
};
|
||||
for (const entry of stats.entries) {
|
||||
for (const bucket of Object.values(b)) {
|
||||
if (entry.totalTokens >= bucket.min && entry.totalTokens < bucket.max) {
|
||||
bucket.count++;
|
||||
bucket.cost += entry.satsCost;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return b;
|
||||
})();
|
||||
|
||||
const sizeLines = Object.entries(sizeBuckets).map(([name, bucket]) => `${name.padEnd(6)}: ${formatReqs(bucket.count).padStart(5)} reqs, ${formatCost(bucket.cost)} sats`);
|
||||
output += "\n" + renderBox(sizeLines, width, "Request Size Distribution");
|
||||
@@ -434,7 +456,7 @@ export function renderTokens(stats: UsageStats, width: number): string {
|
||||
}
|
||||
|
||||
export function renderClients(stats: UsageStats, width: number): string {
|
||||
const clientStats = getClientStats(stats.entries);
|
||||
const clientStats = stats.summary?.clients ?? getClientStats(stats.entries);
|
||||
if (clientStats.length === 0) return renderBox(["No client data available (API key auth not used)"], width, "Client Breakdown");
|
||||
|
||||
// Use totalSatsCost (all-time) for percentage calculations to match header
|
||||
@@ -478,31 +500,43 @@ export function renderClients(stats: UsageStats, width: number): string {
|
||||
endBarSection("client-detail");
|
||||
|
||||
let output = renderBox(lines, width, "Client Breakdown");
|
||||
const clientModelMap = new Map<string, Map<string, { requests: number; satsCost: number; tokens: number }>>();
|
||||
|
||||
for (const entry of stats.entries) {
|
||||
const client = entry.client || "unknown";
|
||||
const model = entry.modelId;
|
||||
if (!clientModelMap.has(client)) clientModelMap.set(client, new Map());
|
||||
const modelMap = clientModelMap.get(client)!;
|
||||
const existing = modelMap.get(model) || { requests: 0, satsCost: 0, tokens: 0 };
|
||||
modelMap.set(model, {
|
||||
requests: existing.requests + 1,
|
||||
satsCost: existing.satsCost + entry.satsCost,
|
||||
tokens: existing.tokens + entry.totalTokens,
|
||||
});
|
||||
}
|
||||
|
||||
const clientModelLines: string[] = [];
|
||||
for (const topClient of clientStats.slice(0, 3)) {
|
||||
const modelMap = clientModelMap.get(topClient.client);
|
||||
if (!modelMap) continue;
|
||||
const models = Array.from(modelMap.entries()).sort((a, b) => b[1].satsCost - a[1].satsCost).slice(0, 5);
|
||||
clientModelLines.push(`${COLORS.bold}${topClient.client}${COLORS.reset} (${formatReqs(topClient.requests)} reqs, ${formatCost(topClient.satsCost)} sats)`);
|
||||
for (const [model, data] of models) {
|
||||
clientModelLines.push(` ${(MODEL_COLORS[model] || MODEL_COLORS.default)}${model.padEnd(18)}${COLORS.reset} ${formatNumber(data.tokens).padEnd(8)} tokens ${formatCost(data.satsCost)} sats`);
|
||||
|
||||
if (stats.summary) {
|
||||
// Use pre-aggregated topModels from summary
|
||||
for (const topClient of stats.summary.clients.slice(0, 3)) {
|
||||
if (topClient.topModels.length === 0) continue;
|
||||
clientModelLines.push(`${COLORS.bold}${topClient.client}${COLORS.reset} (${formatReqs(topClient.requests)} reqs, ${formatCost(topClient.satsCost)} sats)`);
|
||||
for (const m of topClient.topModels) {
|
||||
clientModelLines.push(` ${(MODEL_COLORS[m.modelId] || MODEL_COLORS.default)}${m.modelId.padEnd(18)}${COLORS.reset} ${formatNumber(m.totalTokens).padEnd(8)} tokens ${formatCost(m.satsCost)} sats`);
|
||||
}
|
||||
clientModelLines.push("");
|
||||
}
|
||||
} else {
|
||||
// Fallback: compute from raw entries
|
||||
const clientModelMap = new Map<string, Map<string, { requests: number; satsCost: number; tokens: number }>>();
|
||||
for (const entry of stats.entries) {
|
||||
const client = entry.client || "unknown";
|
||||
const model = entry.modelId;
|
||||
if (!clientModelMap.has(client)) clientModelMap.set(client, new Map());
|
||||
const modelMap = clientModelMap.get(client)!;
|
||||
const existing = modelMap.get(model) || { requests: 0, satsCost: 0, tokens: 0 };
|
||||
modelMap.set(model, {
|
||||
requests: existing.requests + 1,
|
||||
satsCost: existing.satsCost + entry.satsCost,
|
||||
tokens: existing.tokens + entry.totalTokens,
|
||||
});
|
||||
}
|
||||
for (const topClient of clientStats.slice(0, 3)) {
|
||||
const modelMap = clientModelMap.get(topClient.client);
|
||||
if (!modelMap) continue;
|
||||
const models = Array.from(modelMap.entries()).sort((a, b) => b[1].satsCost - a[1].satsCost).slice(0, 5);
|
||||
clientModelLines.push(`${COLORS.bold}${topClient.client}${COLORS.reset} (${formatReqs(topClient.requests)} reqs, ${formatCost(topClient.satsCost)} sats)`);
|
||||
for (const [model, data] of models) {
|
||||
clientModelLines.push(` ${(MODEL_COLORS[model] || MODEL_COLORS.default)}${model.padEnd(18)}${COLORS.reset} ${formatNumber(data.tokens).padEnd(8)} tokens ${formatCost(data.satsCost)} sats`);
|
||||
}
|
||||
clientModelLines.push("");
|
||||
}
|
||||
clientModelLines.push("");
|
||||
}
|
||||
|
||||
if (clientModelLines.length > 0) {
|
||||
@@ -512,7 +546,7 @@ export function renderClients(stats: UsageStats, width: number): string {
|
||||
}
|
||||
|
||||
export function renderNpubs(stats: UsageStats, clients: ClientInfo[], width: number): string {
|
||||
const npubStats = getNpubStats(stats.entries, clients);
|
||||
const npubStats = stats.summary?.npubs ?? getNpubStats(stats.entries, clients);
|
||||
if (npubStats.length === 0) return renderBox(["No npub data available"], width, "Npub Breakdown");
|
||||
|
||||
const totalCost = stats.totalSatsCost;
|
||||
@@ -557,38 +591,48 @@ export function renderNpubs(stats: UsageStats, clients: ClientInfo[], width: num
|
||||
endBarSection("npub-detail");
|
||||
|
||||
let output = renderBox(lines, width, "Npub Breakdown");
|
||||
|
||||
// Top models per npub (same pattern as clients tab)
|
||||
const npubModelMap = new Map<string, Map<string, { requests: number; satsCost: number; tokens: number }>>();
|
||||
const clientNpubMap = new Map<string, string>();
|
||||
for (const c of clients) {
|
||||
if (c.ownerNpub) clientNpubMap.set(c.clientId, c.ownerNpub);
|
||||
}
|
||||
|
||||
for (const entry of stats.entries) {
|
||||
const npub = clientNpubMap.get(entry.client || "");
|
||||
if (!npub) continue;
|
||||
const model = entry.modelId;
|
||||
if (!npubModelMap.has(npub)) npubModelMap.set(npub, new Map());
|
||||
const modelMap = npubModelMap.get(npub)!;
|
||||
const existing = modelMap.get(model) || { requests: 0, satsCost: 0, tokens: 0 };
|
||||
modelMap.set(model, {
|
||||
requests: existing.requests + 1,
|
||||
satsCost: existing.satsCost + entry.satsCost,
|
||||
tokens: existing.tokens + entry.totalTokens,
|
||||
});
|
||||
}
|
||||
|
||||
const npubModelLines: string[] = [];
|
||||
for (const topNpub of npubStats.slice(0, 5)) {
|
||||
const modelMap = npubModelMap.get(topNpub.npub);
|
||||
if (!modelMap) continue;
|
||||
const models = Array.from(modelMap.entries()).sort((a, b) => b[1].satsCost - a[1].satsCost).slice(0, 5);
|
||||
npubModelLines.push(`${COLORS.bold}${truncateNpub(topNpub.npub)}${COLORS.reset} (${formatReqs(topNpub.requests)} reqs, ${formatCost(topNpub.satsCost)} sats)`);
|
||||
for (const [model, data] of models) {
|
||||
npubModelLines.push(` ${(MODEL_COLORS[model] || MODEL_COLORS.default)}${model.padEnd(18)}${COLORS.reset} ${formatNumber(data.tokens).padEnd(8)} tokens ${formatCost(data.satsCost)} sats`);
|
||||
|
||||
if (stats.summary) {
|
||||
// Use pre-aggregated topModels from summary
|
||||
for (const topNpub of stats.summary.npubs.slice(0, 5)) {
|
||||
if (topNpub.topModels.length === 0) continue;
|
||||
npubModelLines.push(`${COLORS.bold}${truncateNpub(topNpub.npub)}${COLORS.reset} (${formatReqs(topNpub.requests)} reqs, ${formatCost(topNpub.satsCost)} sats)`);
|
||||
for (const m of topNpub.topModels) {
|
||||
npubModelLines.push(` ${(MODEL_COLORS[m.modelId] || MODEL_COLORS.default)}${m.modelId.padEnd(18)}${COLORS.reset} ${formatNumber(m.totalTokens).padEnd(8)} tokens ${formatCost(m.satsCost)} sats`);
|
||||
}
|
||||
npubModelLines.push("");
|
||||
}
|
||||
} else {
|
||||
// Fallback: compute from raw entries
|
||||
const npubModelMap = new Map<string, Map<string, { requests: number; satsCost: number; tokens: number }>>();
|
||||
const clientNpubMap = new Map<string, string>();
|
||||
for (const c of clients) {
|
||||
if (c.ownerNpub) clientNpubMap.set(c.clientId, c.ownerNpub);
|
||||
}
|
||||
for (const entry of stats.entries) {
|
||||
const npub = clientNpubMap.get(entry.client || "");
|
||||
if (!npub) continue;
|
||||
const model = entry.modelId;
|
||||
if (!npubModelMap.has(npub)) npubModelMap.set(npub, new Map());
|
||||
const modelMap = npubModelMap.get(npub)!;
|
||||
const existing = modelMap.get(model) || { requests: 0, satsCost: 0, tokens: 0 };
|
||||
modelMap.set(model, {
|
||||
requests: existing.requests + 1,
|
||||
satsCost: existing.satsCost + entry.satsCost,
|
||||
tokens: existing.tokens + entry.totalTokens,
|
||||
});
|
||||
}
|
||||
for (const topNpub of npubStats.slice(0, 5)) {
|
||||
const modelMap = npubModelMap.get(topNpub.npub);
|
||||
if (!modelMap) continue;
|
||||
const models = Array.from(modelMap.entries()).sort((a, b) => b[1].satsCost - a[1].satsCost).slice(0, 5);
|
||||
npubModelLines.push(`${COLORS.bold}${truncateNpub(topNpub.npub)}${COLORS.reset} (${formatReqs(topNpub.requests)} reqs, ${formatCost(topNpub.satsCost)} sats)`);
|
||||
for (const [model, data] of models) {
|
||||
npubModelLines.push(` ${(MODEL_COLORS[model] || MODEL_COLORS.default)}${model.padEnd(18)}${COLORS.reset} ${formatNumber(data.tokens).padEnd(8)} tokens ${formatCost(data.satsCost)} sats`);
|
||||
}
|
||||
npubModelLines.push("");
|
||||
}
|
||||
npubModelLines.push("");
|
||||
}
|
||||
|
||||
if (npubModelLines.length > 0) {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { UsageTrackingEntry } from "../../daemon/types.ts";
|
||||
import type { UsageSummary } from "../../daemon/http/usage-summary.ts";
|
||||
|
||||
export type { UsageSummary };
|
||||
|
||||
export interface UsageStats {
|
||||
entries: UsageTrackingEntry[];
|
||||
@@ -6,6 +9,8 @@ export interface UsageStats {
|
||||
totalSatsCost: number;
|
||||
recentSatsCost: number;
|
||||
limit: number;
|
||||
/** Present when data was fetched via /usage/summary (server-aggregated path). */
|
||||
summary?: UsageSummary;
|
||||
}
|
||||
|
||||
export interface DayStats {
|
||||
|
||||
Reference in New Issue
Block a user