From 8ab2bcda88ccb634164f484df443b78926f3b205 Mon Sep 17 00:00:00 2001 From: sting-raider Date: Wed, 4 Nov 2026 18:23:03 +0530 Subject: [PATCH 1/3] feat(web): add all-time usage window Add All time with first-usage-day trimming, year-aware chart labels, saved preferences, and the next Usage period shortcut. Keep old live transcripts warm in memory so refresh does not reparse them, without changing persisted retention. Model: gpt-6.1-sol. Harness: Codex in T3 Code. --- apps/server/src/usage/UsageService.test.ts | 57 +++++++++++++++++++ apps/server/src/usage/UsageService.ts | 30 ++++++++-- apps/web/src/components/usage/UsagePage.tsx | 21 +++---- .../usage/UsageProviderChart.test.ts | 13 ++++- .../components/usage/UsageProviderChart.tsx | 9 ++- .../usage/usagePagePreferences.test.ts | 13 ++++- .../components/usage/usagePagePreferences.ts | 3 +- .../src/components/usage/usageShortcuts.ts | 4 +- .../src/components/usage/usageWindow.test.ts | 45 +++++++++++++++ apps/web/src/components/usage/usageWindow.ts | 29 ++++++++++ apps/web/src/keybindings.test.ts | 12 +++- packages/contracts/src/keybindings.ts | 1 + packages/shared/src/keybindings.ts | 1 + 13 files changed, 211 insertions(+), 27 deletions(-) create mode 100644 apps/web/src/components/usage/usageWindow.test.ts create mode 100644 apps/web/src/components/usage/usageWindow.ts diff --git a/apps/server/src/usage/UsageService.test.ts b/apps/server/src/usage/UsageService.test.ts index 9cf97cd4c7c4d..2386ba3979b28 100644 --- a/apps/server/src/usage/UsageService.test.ts +++ b/apps/server/src/usage/UsageService.test.ts @@ -3062,6 +3062,63 @@ describe("UsageService", () => { }).pipe(Effect.scoped), ); + it.live( + "reuses old live transcripts across all-time refreshes without retaining deleted history", + () => + Effect.gen(function* () { + yield* TestClock.setTime(Date.parse("2026-12-01T00:00:00Z")); + const { transcript, settings, home } = yield* setup; + const lastWrite = Date.parse("2026-08-01T10:00:00Z") / 1000; + yield* Effect.promise(async () => { + await NodeFSP.writeFile(transcript, claudeLine(1, 5)); + await NodeFSP.utimes(transcript, lastWrite, lastWrite); + }); + yield* Effect.gen(function* () { + const service = yield* UsageService.make; + const allTime = { + ...WINDOW, + sinceDay: UsageDay.make("1970-01-01"), + untilDay: UsageDay.make("2026-12-01"), + }; + const open = vi.spyOn(NodeFSP, "open"); + yield* Effect.addFinalizer(() => Effect.sync(() => open.mockRestore())); + open.mockClear(); + assert.strictEqual(totalOutputTokens(yield* service.readSummary(allTime)), 5); + assert.isTrue(open.mock.calls.some(([path]) => path === transcript)); + + open.mockClear(); + for (const sinceDay of ["2026-11-02", "2026-09-03"]) { + const recent = { ...allTime, sinceDay: UsageDay.make(sinceDay) }; + assert.deepStrictEqual((yield* service.readSummary(recent)).buckets, []); + } + assert.strictEqual( + totalOutputTokens(yield* service.readSummary({ ...allTime, awaitRefresh: true })), + 5, + ); + assert.isFalse(open.mock.calls.some(([path]) => path === transcript)); + + yield* Effect.promise(async () => { + await NodeFSP.appendFile(transcript, claudeLine(2, 7)); + await NodeFSP.utimes(transcript, lastWrite + 86400, lastWrite + 86400); + }); + assert.strictEqual(totalOutputTokens(yield* service.readSummary(allTime)), 12); + yield* Effect.promise(async () => { + await NodeFSP.writeFile(transcript, claudeLine(3, 9)); + await NodeFSP.utimes(transcript, lastWrite + 172800, lastWrite + 172800); + }); + assert.strictEqual(totalOutputTokens(yield* service.readSummary(allTime)), 9); + + yield* Effect.promise(() => NodeFSP.rm(transcript)); + assert.deepStrictEqual((yield* service.readSummary(allTime)).buckets, []); + yield* service.awaitPersisted; + const restarted = yield* UsageService.make; + assert.deepStrictEqual((yield* restarted.readSummary(allTime)).buckets, []); + }).pipe( + Effect.provide(layerService({ prefix: "usage-service-all-time-test", home, settings })), + ); + }).pipe(Effect.scoped, Effect.provide(TestClock.layer())), + ); + it.live("reports saved usage of a removed directory only for windows it reaches", () => Effect.gen(function* () { yield* TestClock.setTime(Date.parse("2026-08-12T00:00:00Z")); diff --git a/apps/server/src/usage/UsageService.ts b/apps/server/src/usage/UsageService.ts index 7e2e8586ed95f..f9b2ffebe09b1 100644 --- a/apps/server/src/usage/UsageService.ts +++ b/apps/server/src/usage/UsageService.ts @@ -158,6 +158,8 @@ export const make = Effect.gen(function* () { const readerContext = yield* Effect.context(); const fileCache: ScanCache = new Map(); + // Old live files stay warm without enlarging persisted history or normal-window cache walks. + const historicalFileCache: ScanCache = new Map(); // Reads from concurrent windows can still hold the original record after it is corrected. const resolvedRecords = new WeakMap(); const correctedRecord = (record: UsageRecord) => resolvedRecords.get(record) ?? record; @@ -453,7 +455,7 @@ export const make = Effect.gen(function* () { readonly failed?: true; readonly update?: { readonly entry: CachedFile; readonly replaces: CachedFile | undefined }; }> { - const cached = fileCache.get(filePath); + const cached = fileCache.get(filePath) ?? historicalFileCache.get(filePath); // Provider is part of the identity: if both providers were ever pointed // at one directory, a hit parsed by the other parser must not be reused. if ( @@ -559,6 +561,7 @@ export const make = Effect.gen(function* () { const scanTranscriptDir = Effect.fn("UsageService.scanTranscriptDir")(function* ( source: TranscriptSource, windowStartMs: number, + retentionCutoffMs: number, ) { const { provider, format, dir, volumeId, fileName } = source; const exists = yield* fileSystem @@ -568,6 +571,17 @@ export const make = Effect.gen(function* () { const { files, failedPaths } = yield* Effect.promise(() => listTranscriptFiles(dir, windowStartMs, fileName === undefined ? undefined : { fileName }), ); + if (windowStartMs < retentionCutoffMs && failedPaths === 0) { + const livePaths = new Set(files.map((file) => file.path)); + for (const [filePath, entry] of historicalFileCache) { + if ( + entry.mtimeMs >= windowStartMs && + isWithinDirectory(filePath, dir) && + !livePaths.has(filePath) + ) + historicalFileCache.delete(filePath); + } + } const read = yield* Effect.forEach( files, (file) => @@ -581,18 +595,21 @@ export const make = Effect.gen(function* () { // A scan of another window may have cached its own read of this file // meanwhile. Then keep whichever read saw the later file, so a slower // scan never replaces newer usage with older. - const current = fileCache.get(path); + const current = fileCache.get(path) ?? historicalFileCache.get(path); if ( current === update.replaces || current === undefined || isLaterRead(update.entry, current) ) { - fileCache.set(path, { + const cache = update.entry.mtimeMs < retentionCutoffMs ? historicalFileCache : fileCache; + if (cache === fileCache) historicalFileCache.delete(path); + else if (fileCache.delete(path)) cacheDirty = true; + cache.set(path, { ...update.entry, records: correctedBatch(update.entry.records), tailRecords: correctedBatch(update.entry.tailRecords), }); - cacheDirty = true; + if (cache === fileCache) cacheDirty = true; } return { path, records, malformedRecords }; }); @@ -650,7 +667,10 @@ export const make = Effect.gen(function* () { // Scan independent sources together, directories sequentially to bound open files; preserve order for first-copy dedupe. const [transcripts, scanDirs] = yield* Effect.all( - [Effect.forEach(dirs, (dir) => scanTranscriptDir(dir, windowStartMs)), scans], + [ + Effect.forEach(dirs, (dir) => scanTranscriptDir(dir, windowStartMs, retentionCutoffMs)), + scans, + ], { concurrency: "unbounded" }, ); const scanned: readonly ScannedDir[] = [...transcripts, ...scanDirs.flat()]; diff --git a/apps/web/src/components/usage/UsagePage.tsx b/apps/web/src/components/usage/UsagePage.tsx index b4ec17a8940cf..9a3d1ad802757 100644 --- a/apps/web/src/components/usage/UsagePage.tsx +++ b/apps/web/src/components/usage/UsagePage.tsx @@ -55,7 +55,6 @@ import { formatTokens, formatUsageContractMismatch, formatUsd, - makeWindow, usageSourceMessages, } from "@t3tools/shared/usageFormat"; import { Button, InlineButton } from "../ui/button"; @@ -85,6 +84,7 @@ import { WorkspacePageContainer } from "../WorkspacePageContainer"; import { UsageLimitsSection } from "./UsageLimits"; import { UsagePriceOverrides } from "./UsagePriceOverrides"; import { UsageProviderChart } from "./UsageProviderChart"; +import { makeUsageWindow, trimUsageWindow } from "./usageWindow"; import { SpeedPremium, UsageModelDialog } from "./UsageModelDialog"; import { UsageShareBar } from "./UsageShareBar"; import { @@ -188,11 +188,7 @@ export function UsagePage() { }; const [windowSelection, setWindowSelection] = useState(() => ({ days: preferences.windowDays, - window: makeWindow( - preferences.windowDays, - undefined, - preferences.windowDays === 1 ? "hour" : "day", - ), + window: makeUsageWindow(preferences.windowDays), })); const metric = preferences.metric; const showingLimits = metric === "limits"; @@ -204,7 +200,6 @@ export function UsagePage() { useState | null>(null); const hiddenProviders = new Set(preferences.hiddenProviders); const { days: windowDays, window } = windowSelection; - const isPast24Hours = windowDays === 1; const { merged: answeredUsage, environments, @@ -214,7 +209,7 @@ export function UsagePage() { refresh, } = useUsage(window, selectedEnvironmentIds, hiddenProviders); const merged = shown?.merged ?? answeredUsage; - const shownWindow = shown?.window ?? window; + const shownWindow = trimUsageWindow(shown?.window ?? window, merged.daily); const shownHourly = shownWindow.resolution === "hour"; const refreshingUsage = isRefreshing && !showingLimits; const showingKept = shown !== null && shown.window !== window; @@ -243,7 +238,7 @@ export function UsagePage() { updatePreferences({ windowDays: days }); setWindowSelection({ days, - window: makeWindow(days, undefined, days === 1 ? "hour" : "day"), + window: makeUsageWindow(days), }); }; const selectMetric = (nextMetric: UsageMetric) => { @@ -309,7 +304,7 @@ export function UsagePage() { }); return; } - const nextWindow = makeWindow(windowDays, undefined, isPast24Hours ? "hour" : "day"); + const nextWindow = makeUsageWindow(windowDays); if ( nextWindow.sinceDay !== window.sinceDay || nextWindow.untilDay !== window.untilDay || @@ -361,7 +356,7 @@ export function UsagePage() { ? `${formatDateTimeShort(shownWindow.sinceTime, shownWindow.timeZone, hour12)} to ${formatDateTimeShort(shownWindow.untilTime, shownWindow.timeZone, hour12)}` : `${formatDayShort(shownWindow.sinceDay)} to ${formatDayShort(shownWindow.untilDay)}`; const topbarContent = ( -
+

Usage

@@ -394,7 +389,7 @@ export function UsagePage() { {windowLabel} ) : null} -
+
{/* Limits are always current, so they have no period. The period sits before the metric tabs so dropping it never moves them. */} {showingLimits ? null : ( @@ -440,7 +435,7 @@ export function UsagePage() {
-
+
{showingLimits ? null : (