Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions gui/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,7 @@ export const de: Record<TKey, string> = {
"usage.range.all": "Alle",
"usage.range.available": "Verfügbarer Verlauf",
"usage.historyTruncated": "Die Summen beziehen sich nur auf den verfügbaren Verlauf, da ältere Nutzungsdaten nicht geladen wurden.",
"usage.historyTruncatedWindow": "Die geladenen Zeilen haben Anfragestartzeiten zwischen {start} und {end}. Frühere Dateieinträge wurden durch das Leselimit ausgelassen, daher kann jeder gewählte Zeitraum unvollständig sein.",
"usage.range.30d": "30d",
"usage.range.7d": "7d",
"usage.card.requests": "Anfragen",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,7 @@ export const en = {
"usage.range.all": "All",
"usage.range.available": "Available history",
"usage.historyTruncated": "Totals cover available history only because older usage was not loaded.",
"usage.historyTruncatedWindow": "Loaded rows have request start times ranging from {start} to {end}. Earlier file entries were omitted by the read limit, so any selected range may be incomplete.",
"usage.range.30d": "30d",
"usage.range.7d": "7d",
"usage.card.requests": "Requests",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,7 @@ export const ja: Record<TKey, string> = {
"usage.range.all": "すべて",
"usage.range.available": "利用可能な履歴",
"usage.historyTruncated": "古い利用履歴が読み込まれていないため、合計は利用可能な履歴のみを対象とします。",
"usage.historyTruncatedWindow": "読み込まれた行のリクエスト開始時刻は {start} から {end} の範囲です。読み取り上限によりファイル前方の記録が除外されているため、選択した期間は不完全な場合があります。",
"usage.range.30d": "30日",
"usage.range.7d": "7日",
"usage.card.requests": "リクエスト",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -759,6 +759,7 @@ export const ko: Record<TKey, string> = {
"usage.range.all": "전체",
"usage.range.available": "사용 가능한 기록",
"usage.historyTruncated": "이전 사용 기록을 불러오지 않아 합계는 사용 가능한 기록만 포함합니다.",
"usage.historyTruncatedWindow": "불러온 기록의 요청 시작 시각은 {start}부터 {end} 사이입니다. 읽기 한도 때문에 파일 앞부분의 기록이 빠졌으므로 선택한 기간이 완전하지 않을 수 있습니다.",
"usage.range.30d": "30일",
"usage.range.7d": "7일",
"usage.card.requests": "요청",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -758,6 +758,7 @@ export const ru: Record<TKey, string> = {
"usage.range.all": "Все",
"usage.range.available": "Доступная история",
"usage.historyTruncated": "Итоги охватывают только доступную историю, поскольку старые данные не загружены.",
"usage.historyTruncatedWindow": "У загруженных записей время начала запроса находится в диапазоне от {start} до {end}. Более ранние записи файла пропущены из-за лимита чтения, поэтому выбранный период может быть неполным.",
"usage.range.30d": "30 дн.",
"usage.range.7d": "7 дн.",
"usage.card.requests": "Запросы",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/tr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,7 @@ export const tr: Record<TKey, string> = {
"usage.range.all": "Tümü",
"usage.range.available": "Mevcut geçmiş",
"usage.historyTruncated": "Toplamlar yalnızca mevcut geçmişi kapsar.",
"usage.historyTruncatedWindow": "Yüklenen satırların istek başlangıç zamanları {start} ile {end} arasındadır. Dosyanın önceki kayıtları okuma sınırı nedeniyle atlandı, bu yüzden seçilen aralık eksik olabilir.",
"usage.range.30d": "30 gün",
"usage.range.7d": "7 gün",
"usage.card.requests": "İstekler",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/zh-TW.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1586,6 +1586,7 @@ export const zhTW: Record<TKey, string> = {
"provider.name.volcengineAgentPlan": "Volcengine Ark Agent Plan",
"usage.range.available": "可用歷史紀錄",
"usage.historyTruncated": "總計僅涵蓋可用歷史紀錄,因為較舊的用量未被載入。",
"usage.historyTruncatedWindow": "已載入紀錄的請求開始時間介於 {start} 到 {end} 之間。受讀取上限限制,檔案較前的項目已被略過,所選期間可能不完整。",
"codexAuth.autoSwitchQuotaDesc": "配額:使用率達 {threshold}% 或以上時,下一個請求可能移至用量較低的合格帳號,包括已綁定的任務;Go/Free 僅使用 30 天。",
"codexAuth.autoSwitchQuotaOffDesc": "基於用量的主動切換已關閉。新增/未綁定分派與故障恢復仍然適用。",
"codexAuth.autoSwitchRoundRobinDesc": "輪詢分派不使用此閾值;它會繼續輪換新增/未綁定的任務。",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,7 @@ export const zh: Record<TKey, string> = {
"usage.range.all": "全部",
"usage.range.available": "可用历史",
"usage.historyTruncated": "由于未加载较早的使用记录,合计仅涵盖可用历史。",
"usage.historyTruncatedWindow": "已加载记录的请求开始时间介于 {start} 到 {end} 之间。受读取上限限制,文件较前的条目已被省略,所选时间范围可能不完整。",
"usage.range.30d": "30 天",
"usage.range.7d": "7 天",
"usage.card.requests": "请求数",
Expand Down
33 changes: 32 additions & 1 deletion gui/src/pages/Usage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ interface UsageResponse {
truncatedPrefixBytes: number;
entriesTruncated: boolean;
entriesDropped: number;
// Bounds of the rows the bounded reader loaded, before any range or surface filtering.
// Describes the read, not the query, and is never a completeness claim (#1497).
// Optional because a dashboard can talk to a proxy that predates these fields.
snapshotWindowStart?: number | null;
snapshotWindowEnd?: number | null;
error?: string;
}

Expand Down Expand Up @@ -816,7 +821,24 @@ export default function Usage({ apiBase }: { apiBase: string }) {
) : (
<>
{state.showError && <Notice tone="err">{t("usage.loadError")}</Notice>}
{data?.historyTruncated && <Notice tone="ok">{t("usage.historyTruncated")}</Notice>}
{data?.historyTruncated && (
// Naming the loaded window is the point: without it, `30d` and "Available history"
// look identical on a busy installation even though both may cover far less than
// they claim (#1497). `warn` rather than `ok` because a total that silently omits
// in-range rows is a caveat, not a status update.
<Notice tone="warn">
{(() => {
// Both bounds must be renderable before the detailed wording is used: an older
// proxy omits the fields entirely, and a hand-edited row can carry a timestamp
// outside Date's range. Either way the generic string is the honest fallback.
const start = renderableInstant(data.snapshotWindowStart);
const end = renderableInstant(data.snapshotWindowEnd);
return start !== null && end !== null
? t("usage.historyTruncatedWindow", { start, end })
: t("usage.historyTruncated");
})()}
</Notice>
)}
<UsageWorkspaceBody
data={data}
heatmap={heatmap}
Expand All @@ -835,3 +857,12 @@ export default function Usage({ apiBase }: { apiBase: string }) {
</>
);
}
function renderableInstant(value: number | null | undefined): string | null {
// The reader preserves whatever timestamp a row carries, including hand-edited values far
// outside Date's supported range. A presence check alone would then render the literal
// string "Invalid Date" in a notice whose whole job is to be trustworthy, so the bound is
// only used once it round-trips through Date.
if (typeof value !== "number" || !Number.isFinite(value)) return null;
const at = new Date(value);
return Number.isFinite(at.getTime()) ? at.toLocaleString() : null;
}
91 changes: 91 additions & 0 deletions gui/tests/usage-layout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ test("usage workspace i18n keys exist in every locale", async () => {
expect(dict).toContain('"usage.workspace.report":');
expect(dict).toContain('"usage.range.available":');
expect(dict).toContain('"usage.historyTruncated":');
expect(dict).toContain('"usage.historyTruncatedWindow":');
expect(dict).toContain('"api.attribution.totalRequestsAvailable":');
}
});
Expand Down Expand Up @@ -143,6 +144,96 @@ test("Usage renders Available history and a persistent qualification when histor
}
});

test("Usage names the loaded window when history is truncated", async () => {
const page = await Bun.file(new URL("../src/pages/Usage.tsx", import.meta.url)).text();

// #1497: when the proxy reports the window it actually loaded, the notice must name that
// window instead of the generic wording — otherwise `30d` and "Available history" stay
// indistinguishable on a busy installation. `!= null` keeps an older proxy that omits the
// fields on the generic string rather than rendering "Invalid Date".
expect(page).toContain("usage.historyTruncatedWindow");
// Presence alone is not enough: a hand-edited row can carry a timestamp outside Date's
// range, so both bounds must round-trip through Date before the detailed wording is used.
expect(page).toContain("function renderableInstant");
expect(page).toContain("Number.isFinite(at.getTime())");
// A total that silently omits in-range rows is a caveat, not a status update.
expect(page).toContain('<Notice tone="warn">');
});

test("Usage falls back to the generic caveat when a reported bound is unrenderable", async () => {
const globalKeys = ["document", "window", "navigator", "localStorage", "IS_REACT_ACT_ENVIRONMENT"] as const;
const previous = Object.fromEntries(globalKeys.map(key => [key, Reflect.get(globalThis, key)]));
const originalFetch = globalThis.fetch;
const testWindow = new Window({ url: "http://localhost/" });
Object.defineProperties(globalThis, {
document: { configurable: true, value: testWindow.document },
window: { configurable: true, value: testWindow },
navigator: { configurable: true, value: testWindow.navigator },
localStorage: { configurable: true, value: testWindow.localStorage },
});
Comment on lines +164 to +173

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Restore the original global-property shape.

Lines 164-165 save only values. Lines 231-233 then define every global key, even when that key did not exist before this test. This leaks own window, document, navigator, localStorage, or IS_REACT_ACT_ENVIRONMENT properties into later tests.

Save each original property descriptor. Restore the descriptor when it exists. Delete the property when it did not exist.

Proposed fix
-  const previous = Object.fromEntries(globalKeys.map(key => [key, Reflect.get(globalThis, key)]));
+  const previous = Object.fromEntries(
+    globalKeys.map(key => [key, Object.getOwnPropertyDescriptor(globalThis, key)]),
+  );
...
   for (const key of globalKeys) {
-    Object.defineProperty(globalThis, key, { configurable: true, value: previous[key] });
+    const descriptor = previous[key];
+    if (descriptor) Object.defineProperty(globalThis, key, descriptor);
+    else Reflect.deleteProperty(globalThis, key);
   }

Also applies to: 231-233

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gui/tests/usage-layout.test.ts` around lines 164 - 173, Update the
global-state setup and teardown around globalKeys to save each property’s
original descriptor with Object.getOwnPropertyDescriptor(globalThis, key),
rather than only its value. In the cleanup logic, restore existing descriptors
with Object.defineProperty and delete keys whose descriptors were undefined,
preserving the original own-property shape for document, window, navigator,
localStorage, and IS_REACT_ACT_ENVIRONMENT.

(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
clearClientResourceStoresForTests();
// A timestamp beyond Date's supported range. Rendering it would put the literal string
// "Invalid Date" inside a notice whose entire purpose is to be trustworthy.
globalThis.fetch = (async () => Response.json({
range: "30d",
surface: "all",
since: null,
generatedAt: Date.now(),
summary: {
requests: 0,
measuredRequests: 0,
reportedRequests: 0,
unreportedRequests: 0,
unsupportedRequests: 0,
estimatedRequests: 0,
inputTokens: 0,
outputTokens: 0,
cachedInputTokens: 0,
reasoningOutputTokens: 0,
totalTokens: 0,
coverageRatio: 1,
},
days: [],
models: [],
providers: [],
historyTruncated: true,
truncatedPrefixBytes: 1,
entriesTruncated: false,
entriesDropped: 0,
snapshotWindowStart: 1e18,
snapshotWindowEnd: 1e18,
})) as typeof fetch;

const container = document.createElement("div");
document.body.append(container);
const { createRoot } = await import("react-dom/client");
const root = createRoot(container);
try {
await act(async () => {
root.render(createElement(LanguageProvider, null, createElement(Usage, { apiBase: "http://usage-invalid-window-test" })));
});
const deadline = Date.now() + 1_000;
while (!(container.textContent ?? "").includes("Totals cover available history only")) {
if (Date.now() >= deadline) throw new Error("Usage fallback qualification did not render");
await act(async () => {
await new Promise<void>(resolve => testWindow.setTimeout(resolve, 10));
});
}
expect(container.textContent).not.toContain("Invalid Date");
expect(container.textContent).not.toContain("request start times ranging");
} finally {
await act(async () => { root.unmount(); });
container.remove();
globalThis.fetch = originalFetch;
clearClientResourceStoresForTests();
testWindow.close();
for (const key of globalKeys) {
Object.defineProperty(globalThis, key, { configurable: true, value: previous[key] });
}
}
});

test("Usage source marks keep brand colors and invert only the monochrome Grok mark", async () => {
const page = await Bun.file(new URL("../src/pages/Usage.tsx", import.meta.url)).text();
const css = await Bun.file(new URL("../src/styles.css", import.meta.url)).text();
Expand Down
28 changes: 28 additions & 0 deletions src/server/management/logs-usage-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,29 @@ function refreshedUsageSummary<T extends UsageSummary & { historyTruncated: bool
return { ...summary, since, generatedAt: now };
}

/**
* Timestamp bounds of the rows the bounded reader actually loaded.
*
* Deliberately computed over the whole snapshot, BEFORE `summarizeUsage` applies the range
* and surface predicates: truncation is a property of the read, not of the query, so the
* window that matters to a client is the one the reader could see. It is not a completeness
* claim and must never be presented as one. `usage.jsonl` is appended when a request
* COMPLETES while each row carries the request START time, so a long-running request can be
* appended after shorter ones that started later — meaning the oldest loaded timestamp does
* not bound what the dropped prefix contains (#1497).
*/
function snapshotWindow(entries: PersistedUsageEntry[]): { start: number | null; end: number | null } {
let start: number | null = null;
let end: number | null = null;
for (const entry of entries) {
const at = entry.timestamp;
if (typeof at !== "number" || !Number.isFinite(at)) continue;
if (start === null || at < start) start = at;
if (end === null || at > end) end = at;
}
return { start, end };
}

export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Response | null> {
const { req, url, config, deps, syncClaudeAgentDefsBestEffort } = ctx;

Expand Down Expand Up @@ -209,12 +232,15 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
const overlayVersion = userCostOverlayVersion();
const snapshot = await readUsageSnapshotForManagement(effectiveReadLimit);
const revisionReadAt = Date.now();
const window = snapshotWindow(snapshot.entries);
const summary = {
...summarizeUsage(snapshot.entries, range, now, surface),
historyTruncated: snapshot.truncatedPrefixBytes > 0 || snapshot.entriesTruncated,
truncatedPrefixBytes: snapshot.truncatedPrefixBytes,
entriesTruncated: snapshot.entriesTruncated,
entriesDropped: snapshot.entriesDropped,
snapshotWindowStart: window.start,
snapshotWindowEnd: window.end,
Comment on lines +242 to +243

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Document the new usage snapshot-window contract

These fields add a user-visible management API contract whose pre-filter semantics and nullable bounds are not documented in docs-site/src/content/docs/reference/management-api.md or its translated versions, leaving API consumers unable to discover or correctly interpret them. Update the management API reference alongside this response change.

AGENTS.md reference: AGENTS.md:L234-L235

Useful? React with 👍 / 👎.

};
if (userCostOverlayVersion() !== overlayVersion) {
// The overlay changed while the summary was being computed, so this
Expand Down Expand Up @@ -266,6 +292,8 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
truncatedPrefixBytes: 0,
entriesTruncated: false,
entriesDropped: 0,
snapshotWindowStart: null,
snapshotWindowEnd: null,
error: "read_failed",
});
}
Expand Down
Loading
Loading