From fa6b121a302dde0b10bf701f8e962fda3d82607a Mon Sep 17 00:00:00 2001 From: atryan Date: Sat, 15 Aug 2026 02:40:02 +0000 Subject: [PATCH] fix: keep usage dashboard current --- apps/server/src/usage/UsageService.test.ts | 81 +++++++++++++++++++++ apps/server/src/usage/UsageService.ts | 69 ++++++++++++++++-- apps/web/src/components/usage/UsagePage.tsx | 2 + 3 files changed, 144 insertions(+), 8 deletions(-) create mode 100644 apps/server/src/usage/UsageService.test.ts diff --git a/apps/server/src/usage/UsageService.test.ts b/apps/server/src/usage/UsageService.test.ts new file mode 100644 index 000000000000..9db4d31054cb --- /dev/null +++ b/apps/server/src/usage/UsageService.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, it } from "@effect/vitest"; +import { ServerSettings } from "@t3tools/contracts"; +import * as Schema from "effect/Schema"; + +import { configuredUsageProviders } from "./UsageService.ts"; + +const decodeSettings = Schema.decodeSync(ServerSettings); + +describe("configuredUsageProviders", () => { + it("uses every enabled supported provider instance", () => { + const settings = decodeSettings({ + providerInstances: { + "codex-awtprod": { + driver: "codex", + enabled: true, + config: { homePath: "/providers/codex/awtprod/.codex" }, + }, + "codex-secondary": { + driver: "codex", + config: { homePath: "/providers/codex/secondary/.codex" }, + }, + "claude-awtprod": { + driver: "claudeAgent", + enabled: true, + config: { homePath: "/providers/claude/awtprod/.claude" }, + }, + disabled: { + driver: "claudeAgent", + enabled: false, + config: { homePath: "/providers/claude/disabled/.claude" }, + }, + kimi: { driver: "kimi", enabled: true, config: {} }, + }, + }); + + expect( + configuredUsageProviders(settings).map(({ provider, config }) => [provider, config.homePath]), + ).toEqual([ + ["codex", "/providers/codex/awtprod/.codex"], + ["codex", "/providers/codex/secondary/.codex"], + ["claude", "/providers/claude/awtprod/.claude"], + ]); + }); + + it("falls back per driver when only legacy settings exist", () => { + const settings = decodeSettings({ + providers: { + codex: { homePath: "/legacy/.codex" }, + claudeAgent: { homePath: "/legacy/.claude" }, + }, + }); + + expect( + configuredUsageProviders(settings).map(({ provider, config }) => [provider, config.homePath]), + ).toEqual([ + ["claude", "/legacy/.claude"], + ["codex", "/legacy/.codex"], + ]); + }); + + it("uses an explicit legacy home when a wrapper instance does not declare one", () => { + const settings = decodeSettings({ + providers: { + codex: { homePath: "/providers/codex/awtprod/.codex" }, + }, + providerInstances: { + codex: { + driver: "codex", + enabled: true, + config: { binaryPath: "/runtime/provider-bin/codex" }, + }, + }, + }); + + expect( + configuredUsageProviders(settings) + .filter(({ provider }) => provider === "codex") + .map(({ config }) => config.homePath), + ).toEqual(["/providers/codex/awtprod/.codex"]); + }); +}); diff --git a/apps/server/src/usage/UsageService.ts b/apps/server/src/usage/UsageService.ts index 0e8fbb4adb30..e71ec5b3e088 100644 --- a/apps/server/src/usage/UsageService.ts +++ b/apps/server/src/usage/UsageService.ts @@ -14,7 +14,10 @@ import * as NodeOS from "node:os"; import { + ClaudeSettings, + CodexSettings, USAGE_CONTRACT_VERSION, + type ServerSettings as ServerSettingsDocument, type UsageProviderKind, type UsageSource, type UsageSummary, @@ -93,6 +96,49 @@ export class UsageService extends Context.Service< } >()("@awtprod/command-center/usage/UsageService") {} +type UsageProviderSettings = + | { readonly provider: "claude"; readonly config: ClaudeSettings } + | { readonly provider: "codex"; readonly config: CodexSettings }; + +const decodeClaudeSettings = Schema.decodeUnknownOption(ClaudeSettings); +const decodeCodexSettings = Schema.decodeUnknownOption(CodexSettings); + +/** Resolve every enabled provider instance the transcript dashboard supports. */ +export function configuredUsageProviders( + settings: ServerSettingsDocument, +): readonly UsageProviderSettings[] { + const configured: UsageProviderSettings[] = []; + let hasClaudeInstance = false; + let hasCodexInstance = false; + + for (const instance of Object.values(settings.providerInstances)) { + if (instance.enabled === false) continue; + if (instance.driver === "claudeAgent") { + const config = decodeClaudeSettings(instance.config); + if (Option.isSome(config) && config.value.homePath.length > 0) { + hasClaudeInstance = true; + configured.push({ provider: "claude", config: config.value }); + } + } else if (instance.driver === "codex") { + const config = decodeCodexSettings(instance.config); + if (Option.isSome(config) && config.value.homePath.length > 0) { + hasCodexInstance = true; + configured.push({ provider: "codex", config: config.value }); + } + } + } + + // Preserve mixed and legacy settings during the provider-instance migration. + if (!hasClaudeInstance && settings.providers.claudeAgent.enabled) { + configured.push({ provider: "claude", config: settings.providers.claudeAgent }); + } + if (!hasCodexInstance && settings.providers.codex.enabled) { + configured.push({ provider: "codex", config: settings.providers.codex }); + } + + return configured; +} + /** Empty summary, for suites that only need the RPC surface to resolve. */ export const layerTest = Layer.succeed( UsageService, @@ -215,14 +261,21 @@ export const make = Effect.gen(function* () { ), ); - const claudeHome = yield* resolveClaudeHomePath(settings.providers.claudeAgent); - const claudeDir = yield* resolveClaudeTranscriptDir(claudeHome); - const codexLayout = yield* resolveCodexHomeLayout(settings.providers.codex); - - return [ - { provider: "claude" as const, dir: claudeDir }, - { provider: "codex" as const, dir: path.join(codexLayout.sharedHomePath, "sessions") }, - ]; + const dirs: Array<{ readonly provider: UsageProviderKind; readonly dir: string }> = []; + const seen = new Set(); + for (const provider of configuredUsageProviders(settings)) { + const dir = + provider.provider === "claude" + ? yield* resolveClaudeHomePath(provider.config).pipe( + Effect.flatMap(resolveClaudeTranscriptDir), + ) + : path.join((yield* resolveCodexHomeLayout(provider.config)).sharedHomePath, "sessions"); + const key = `${provider.provider}:${dir}`; + if (seen.has(key)) continue; + seen.add(key); + dirs.push({ provider: provider.provider, dir }); + } + return dirs; }); /** diff --git a/apps/web/src/components/usage/UsagePage.tsx b/apps/web/src/components/usage/UsagePage.tsx index 5e9034bb2af0..f01d2619246a 100644 --- a/apps/web/src/components/usage/UsagePage.tsx +++ b/apps/web/src/components/usage/UsagePage.tsx @@ -7,6 +7,7 @@ import type { DailyTotals, HourlyTotals } from "@t3tools/shared/usageMerge"; import { isElectron } from "../../env"; import { cn } from "../../lib/utils"; import { useUsage, type EnvironmentUsageStatus } from "../../state/usage"; +import { useLiveRefresh } from "../../hooks/useLiveRefresh"; import { enumerateDays, enumerateHourStarts, @@ -99,6 +100,7 @@ export function UsagePage() { setWindowSelection({ days: windowDays, window: nextWindow }); } }; + useLiveRefresh(refreshWindow, { key: "usage-dashboard" }); return (