diff --git a/src/daemon/http/index.ts b/src/daemon/http/index.ts index b66c3fa..96b4d59 100644 --- a/src/daemon/http/index.ts +++ b/src/daemon/http/index.ts @@ -37,7 +37,6 @@ type DaemonDeps = { walletClient: CocodClient; walletAdapter: any; storageAdapter: any; - providerRegistry: any; discoveryAdapter: any; modelManager: any; ensureProvidersBootstrapped: () => Promise; @@ -292,7 +291,6 @@ export function createDaemonRequestHandler(deps: { walletClient: CocodClient; walletAdapter: any; storageAdapter: any; - providerRegistry: any; discoveryAdapter: any; modelManager: any; ensureProvidersBootstrapped: () => Promise; @@ -1268,7 +1266,6 @@ export function createDaemonRequestHandler(deps: { headers: incomingHeaders, walletAdapter: deps.walletAdapter, storageAdapter: deps.storageAdapter, - providerRegistry: deps.providerRegistry, discoveryAdapter: deps.discoveryAdapter, modelManager: deps.modelManager, debugLevel: "DEBUG", diff --git a/src/daemon/http/usage-summary.test.ts b/src/daemon/http/usage-summary.test.ts index 59f97df..f97a201 100644 --- a/src/daemon/http/usage-summary.test.ts +++ b/src/daemon/http/usage-summary.test.ts @@ -6,15 +6,27 @@ import { getUsageSummary, __resetUsageSummaryCacheForTest } from "./usage-summar // ─── Test fixtures ──────────────────────────────────────────────────────────── // -// All timestamps fall within 30 days of 2026-06-02 (today as of this writing). +// All timestamps are computed relative to "now" so the fixtures never age +// out of getUsageSummary's 30-day rolling window. We anchor to a recent UTC +// midnight ~10 days ago and keep the same hour-of-day structure the +// tz-bucketing assertions depend on. +// // tz = 300 (UTC-5 / EST). After shifting by -300min, UTC dates become: -// d1: 2026-05-20T10:00Z → shifted 2026-05-20T05:00Z → local day "2026-05-20", hour 05 -// d2: 2026-05-21T03:00Z → shifted 2026-05-20T22:00Z → local day "2026-05-20", hour 22 -// d3: 2026-05-21T06:00Z → shifted 2026-05-21T01:00Z → local day "2026-05-21", hour 01 +// d1: 10:00Z → local 05:00 → local day "" +// d2: 03:00Z → local 22:00 → local day "" +// d3: 06:00Z → local 01:00 → local day "" -const D1 = new Date("2026-05-20T10:00:00Z").getTime(); // local 2026-05-20, hour 05 -const D2 = new Date("2026-05-21T03:00:00Z").getTime(); // local 2026-05-20, hour 22 -const D3 = new Date("2026-05-21T06:00:00Z").getTime(); // local 2026-05-21, hour 01 +const _BASE_UTC_MIDNIGHT = Math.floor(Date.now() / 86_400_000) * 86_400_000; +const _ANCHOR_MS = _BASE_UTC_MIDNIGHT - 10 * 86_400_000; // 10 days ago UTC +const D1 = _ANCHOR_MS + 10 * 3_600_000; // 10:00Z → local day +const D2 = _ANCHOR_MS + 1 * 86_400_000 + 3 * 3_600_000; // 03:00Z next day → local day +const D3 = _ANCHOR_MS + 1 * 86_400_000 + 6 * 3_600_000; // 06:00Z next day → local day + +/** Compute the local-day date string the same way the SDK's day grouping does. */ +function localDayDateStr(ts: number, tzOffsetMinutes: number): string { + const d = new Date(ts - tzOffsetMinutes * 60_000); + return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}-${String(d.getUTCDate()).padStart(2, "0")}`; +} const BASE_ENTRY: Omit = { baseUrl: "https://api.openai.com/", @@ -152,23 +164,24 @@ describe("getUsageSummary", () => { const summary = await getUsageSummary(driver, CLIENTS, TZ); const { days } = summary; - // D1 (10:00Z) → local 2026-05-20, D2 (03:00Z) → local 2026-05-20, D3 (06:00Z) → local 2026-05-21 - // So: day 2026-05-20 has e1+e2, day 2026-05-21 has e3+e4 + // D1 (10:00Z) and D2 (next day 03:00Z) both bucket to local day ; + // D3 (next day 06:00Z) buckets to local day . + // So: day has e1+e2, day has e3+e4 expect(days.length).toBeGreaterThanOrEqual(2); - // Most-recent-first: 2026-05-21 first - expect(days[0]!.date).toBe("2026-05-21"); + // Most-recent-first: first + expect(days[0]!.date).toBe(localDayDateStr(D3, TZ)); expect(days[0]!.requests).toBe(2); // e3, e4 expect(days[0]!.satsCost).toBe(8 + 50); - expect(days[1]!.date).toBe("2026-05-20"); + expect(days[1]!.date).toBe(localDayDateStr(D1, TZ)); expect(days[1]!.requests).toBe(2); // e1, e2 expect(days[1]!.satsCost).toBe(10 + 20); }); it("returns empty hoursToday (test entries are not today)", async () => { const summary = await getUsageSummary(driver, CLIENTS, TZ); - // Test entries are in May 2026, not today (June 2026) + // Test entries are ~10 days ago, not today expect(summary.hoursToday).toHaveLength(0); }); diff --git a/src/daemon/index.ts b/src/daemon/index.ts index 9875400..f1d0de4 100644 --- a/src/daemon/index.ts +++ b/src/daemon/index.ts @@ -39,7 +39,6 @@ import { createBunSqliteDriver, createBunSqliteUsageTrackingDriver, createShardedDiscoveryAdapter, - createProviderRegistryFromDiscoveryAdapter, } from "@routstr/sdk/storage/bun"; import { createWalletAdapter } from "./wallet"; import type { AutoRefillConfig } from "./wallet/auto-refill"; @@ -82,7 +81,6 @@ async function main(): Promise { }); const discoveryAdapter = await createShardedDiscoveryAdapter({ driver: sqliteDriver }); - const providerRegistry = createProviderRegistryFromDiscoveryAdapter(discoveryAdapter, daemonSdkLogger); const storageAdapter = createStorageAdapterFromStore(store); const modelManager = new ModelManager(discoveryAdapter, { logger: daemonSdkLogger, @@ -91,7 +89,7 @@ async function main(): Promise { nostrRelays: config.relays, }); // Create shared ProviderManager for consistent failure tracking across all requests - const providerManager = new ProviderManager(providerRegistry, store, daemonSdkLogger); + const providerManager = new ProviderManager(discoveryAdapter, store, daemonSdkLogger); const { ensureProvidersBootstrapped, getRoutstr21Models, getModelProviders, refreshProvidersAndModels } = createModelService(modelManager, store); @@ -123,7 +121,7 @@ async function main(): Promise { const refundClient = new RoutstrClient( walletAdapter, storageAdapter, - providerRegistry, + discoveryAdapter, "min", "apikeys", { logger: daemonSdkLogger }, @@ -139,7 +137,6 @@ async function main(): Promise { walletClient, walletAdapter, storageAdapter, - providerRegistry, discoveryAdapter, modelManager, ensureProvidersBootstrapped, @@ -176,6 +173,13 @@ async function main(): Promise { ); refreshInterval = setInterval(async () => { + logger.log("Running scheduled Nostr event refresh..."); + try { + await modelManager.refreshNostrEvents(); + } catch (error) { + logger.error("Scheduled Nostr event refresh failed:", error); + } + logger.log("Running scheduled model refresh..."); try { await refreshModelsAndIntegrations(getRoutstr21Models, updatedConfig, "Scheduled"); @@ -268,6 +272,10 @@ async function main(): Promise { // Start the recurring model refresh job after initial bootstrap void ensureProvidersBootstrapped() .then(async () => { + // Catch up on any Nostr events published since last run + logger.log("Running initial Nostr event refresh..."); + await modelManager.refreshNostrEvents(); + startModelRefreshJob(); startRefundJob(); // Run an immediate refresh to populate models right away diff --git a/src/tui/usage/render.ts b/src/tui/usage/render.ts index 2fe7b41..727598e 100644 --- a/src/tui/usage/render.ts +++ b/src/tui/usage/render.ts @@ -278,18 +278,25 @@ export function renderToday(stats: UsageStats, width: number): string { const _now = new Date(); const todayDateStr = `${_now.getFullYear()}-${String(_now.getMonth() + 1).padStart(2, "0")}-${String(_now.getDate()).padStart(2, "0")}`; - 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; + let todayStats: { date: string; requests: number; satsCost: number; promptTokens: number; completionTokens: number; totalTokens: number } = { + date: todayDateStr, + requests: 0, + satsCost: 0, + promptTokens: 0, + completionTokens: 0, + totalTokens: 0, + }; + let recentDays: Array<{ date: string; requests: number; satsCost: number; totalTokens: number; promptTokens: number; completionTokens: number }> = []; + let hourlyMap: Map = new Map(); 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 }; + if (todayDayStat) { + todayStats = { date: todayDayStat.date, requests: todayDayStat.requests, satsCost: todayDayStat.satsCost, promptTokens: todayDayStat.promptTokens, completionTokens: todayDayStat.completionTokens, totalTokens: todayDayStat.totalTokens }; + } // Exclude today by date (it has its own box) rather than by position — // days[0] is only today when there was activity today. recentDays = days.filter((d) => d.date !== todayDateStr).slice(0, 6);