Skip to content
Closed
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
6 changes: 5 additions & 1 deletion gui/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -627,7 +627,11 @@ export const de: Record<TKey, string> = {
"usage.empty": "Noch keine Nutzung erfasst. Sende eine Anfrage über den Proxy, um Aktivität hier zu sehen.",
"usage.loadError": "Nutzungsdaten konnten nicht geladen werden.",
"usage.range.all": "Alle",
"usage.range.available": "Verfügbarer Verlauf",
"usage.range.available":
"usage.range.1d": "Heute",
"usage.range.yesterday": "Gestern",
"usage.filterByDate": "Gefiltered nach {date}",
"usage.clearDateFilter": "Datumsfilter zurücksetzen", "Verfügbarer Verlauf",
"usage.historyTruncated": "Die Summen beziehen sich nur auf den verfügbaren Verlauf, da ältere Nutzungsdaten nicht geladen wurden.",
"usage.range.30d": "30d",
"usage.range.7d": "7d",
Expand Down
6 changes: 5 additions & 1 deletion gui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -658,7 +658,11 @@ export const en = {
"usage.empty": "No usage recorded yet. Send a request through the proxy to see activity here.",
"usage.loadError": "Could not load usage data.",
"usage.range.all": "All",
"usage.range.available": "Available history",
"usage.range.available":
"usage.range.1d": "Today",
Comment on lines +661 to +662

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Restore the missing available-range locale value

This leaves the locale object syntactically invalid: usage.range.available has no value, so the following "usage.range.1d" key is parsed where an expression is expected. The same malformed splice appears in the other locale files, so the GUI cannot parse the i18n modules until "Available history" is restored as the value and the new strings remain as separate locale entries.

AGENTS.md reference: gui/AGENTS.md:L15-L17

Useful? React with 👍 / 👎.

"usage.range.yesterday": "Yesterday",
"usage.filterByDate": "Filtered for {date}",
"usage.clearDateFilter": "Clear date filter", "Available history",
Comment on lines +661 to +665

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.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Fix the broken usage.range.available entry in all six locale catalogs.

Every locale file assigns no value to "usage.range.available": and instead appends the previous localized "Available history" string as an orphaned literal after usage.clearDateFilter's value. Biome reports a parse error in each file (expected ',' but instead found ':' on the usage.range.available line, and expected ':' but instead found ',' on the usage.clearDateFilter line). Because every catalog is typed as Record<TKey, string>, this syntax error breaks compilation of the whole GUI, not just the usage page. Assign the localized "Available history" string directly to usage.range.available and remove the trailing orphan string in each file.

  • gui/src/i18n/en.ts#L661-L665: change to "usage.range.available": "Available history", and drop the trailing , "Available history" after usage.clearDateFilter's value.
  • gui/src/i18n/de.ts#L630-L634: change to "usage.range.available": "Verfügbarer Verlauf", and drop the trailing , "Verfügbarer Verlauf" after usage.clearDateFilter's value.
  • gui/src/i18n/ja.ts#L619-L623: change to "usage.range.available": "利用可能な履歴", and drop the trailing , "利用可能な履歴" after usage.clearDateFilter's value.
  • gui/src/i18n/ko.ts#L652-L656: change to "usage.range.available": "사용 가능한 기록", and drop the trailing , "사용 가능한 기록" after usage.clearDateFilter's value.
  • gui/src/i18n/ru.ts#L651-L655: change to "usage.range.available": "Доступная история", and drop the trailing , "Доступная история" after usage.clearDateFilter's value.
  • gui/src/i18n/zh.ts#L645-L649: change to "usage.range.available": "可用历史", and drop the trailing , "可用历史" after usage.clearDateFilter's value.
🐛 Proposed fix (example shown for en.ts; apply the equivalent per-locale string to each file)
   "usage.range.all": "All",
-  "usage.range.available":
+  "usage.range.available": "Available history",
   "usage.range.1d": "Today",
   "usage.range.yesterday": "Yesterday",
   "usage.filterByDate": "Filtered for {date}",
-  "usage.clearDateFilter": "Clear date filter", "Available history",
+  "usage.clearDateFilter": "Clear date filter",
   "usage.historyTruncated": "Totals cover available history only because older usage was not loaded.",

Based on the static analysis hints (Biome parse errors reported at the matching line numbers in each of the six files).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"usage.range.available":
"usage.range.1d": "Today",
"usage.range.yesterday": "Yesterday",
"usage.filterByDate": "Filtered for {date}",
"usage.clearDateFilter": "Clear date filter", "Available history",
"usage.range.available": "Available history",
"usage.range.1d": "Today",
"usage.range.yesterday": "Yesterday",
"usage.filterByDate": "Filtered for {date}",
"usage.clearDateFilter": "Clear date filter",
🧰 Tools
🪛 Biome (2.5.6)

[error] 662-662: expected , but instead found :

(parse)


[error] 665-665: expected : but instead found ,

(parse)

📍 Affects 6 files
  • gui/src/i18n/en.ts#L661-L665 (this comment)
  • gui/src/i18n/de.ts#L630-L634
  • gui/src/i18n/ja.ts#L619-L623
  • gui/src/i18n/ko.ts#L652-L656
  • gui/src/i18n/ru.ts#L651-L655
  • gui/src/i18n/zh.ts#L645-L649
🤖 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/src/i18n/en.ts` around lines 661 - 665, Fix the malformed
usage.range.available entries in gui/src/i18n/en.ts:661-665,
gui/src/i18n/de.ts:630-634, gui/src/i18n/ja.ts:619-623,
gui/src/i18n/ko.ts:652-656, gui/src/i18n/ru.ts:651-655, and
gui/src/i18n/zh.ts:645-649 by assigning each locale’s “Available history”
translation to usage.range.available and removing the orphaned trailing literal
after usage.clearDateFilter in every file.

Source: Linters/SAST tools

"usage.historyTruncated": "Totals cover available history only because older usage was not loaded.",
"usage.range.30d": "30d",
"usage.range.7d": "7d",
Expand Down
6 changes: 5 additions & 1 deletion gui/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -616,7 +616,11 @@ export const ja: Record<TKey, string> = {
"usage.empty": "まだ使用量が記録されていません。プロキシ経由でリクエストを送信するとここにアクティビティが表示されます。",
"usage.loadError": "使用量データを読み込めませんでした。",
"usage.range.all": "すべて",
"usage.range.available": "利用可能な履歴",
"usage.range.available":
"usage.range.1d": "今日",
"usage.range.yesterday": "昨日",
"usage.filterByDate": "{date} でフィルタ中",
"usage.clearDateFilter": "日付フィルタを解除", "利用可能な履歴",
"usage.historyTruncated": "古い利用履歴が読み込まれていないため、合計は利用可能な履歴のみを対象とします。",
"usage.range.30d": "30日",
"usage.range.7d": "7日",
Expand Down
6 changes: 5 additions & 1 deletion gui/src/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -649,7 +649,11 @@ export const ko: Record<TKey, string> = {
"usage.empty": "아직 기록된 사용량이 없습니다. 프록시로 요청을 보내면 여기에 표시됩니다.",
"usage.loadError": "사용량 데이터를 불러오지 못했습니다.",
"usage.range.all": "전체",
"usage.range.available": "사용 가능한 기록",
"usage.range.available":
"usage.range.1d": "오늘",
"usage.range.yesterday": "어제",
"usage.filterByDate": "{date} 필터링 됨",
"usage.clearDateFilter": "날짜 필터 초기화", "사용 가능한 기록",
"usage.historyTruncated": "이전 사용 기록을 불러오지 않아 합계는 사용 가능한 기록만 포함합니다.",
"usage.range.30d": "30일",
"usage.range.7d": "7일",
Expand Down
6 changes: 5 additions & 1 deletion gui/src/i18n/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -648,7 +648,11 @@ export const ru: Record<TKey, string> = {
"usage.empty": "Данных об использовании пока нет. Отправьте запрос через прокси, чтобы увидеть здесь активность.",
"usage.loadError": "Не удалось загрузить данные об использовании.",
"usage.range.all": "Все",
"usage.range.available": "Доступная история",
"usage.range.available":
"usage.range.1d": "Сегодня",
"usage.range.yesterday": "Вчера",
"usage.filterByDate": "Фильтр по дате: {date}",
"usage.clearDateFilter": "Сбросить фильтр по дате", "Доступная история",
"usage.historyTruncated": "Итоги охватывают только доступную историю, поскольку старые данные не загружены.",
"usage.range.30d": "30 дн.",
"usage.range.7d": "7 дн.",
Expand Down
6 changes: 5 additions & 1 deletion gui/src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -642,7 +642,11 @@ export const zh: Record<TKey, string> = {
"usage.empty": "尚无用量记录。通过代理发送请求后将在此显示。",
"usage.loadError": "无法加载用量数据。",
"usage.range.all": "全部",
"usage.range.available": "可用历史",
"usage.range.available":
"usage.range.1d": "今天",
"usage.range.yesterday": "昨天",
"usage.filterByDate": "按日期筛选: {date}",
"usage.clearDateFilter": "清除日期筛选", "可用历史",
"usage.historyTruncated": "由于未加载较早的使用记录,合计仅涵盖可用历史。",
"usage.range.30d": "30 天",
"usage.range.7d": "7 天",
Expand Down
4 changes: 2 additions & 2 deletions gui/src/pages/Usage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { DataSurfaceSkeleton } from "../components/data-surface";
import { SectionTabs } from "../components/section-tabs";
import { sectionAnchorId } from "../section-anchors";

type Range = "all" | "30d" | "7d";
type Range = "all" | "30d" | "7d" | "yesterday" | "1d";
type UsageSurface = "all" | "codex" | "claude" | "grok";

interface UsageSummaryTotals {
Expand Down Expand Up @@ -244,7 +244,7 @@ function UsageFilters({
})}
</div>
<div className="usage-segmented" role="group" aria-label={t("usage.title")}>
{(["all", "30d", "7d"] as Range[]).map(choice => {
{(["all", "30d", "7d", "yesterday", "1d"] as Range[]).map(choice => {

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.

🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Range selector order does not match the PR's described filter order.

The array ["all", "30d", "7d", "yesterday", "1d"] renders buttons in this order: All, 30d, 7d, Yesterday, Today. The PR summary and issue #1058 describe the intended order as Today, Yesterday, Last 7 Days, Last 30 Days, then All.

🔧 Proposed fix to match the described order
-        {(["all", "30d", "7d", "yesterday", "1d"] as Range[]).map(choice => {
+        {(["1d", "yesterday", "7d", "30d", "all"] as Range[]).map(choice => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{(["all", "30d", "7d", "yesterday", "1d"] as Range[]).map(choice => {
{(["1d", "yesterday", "7d", "30d", "all"] as Range[]).map(choice => {
🤖 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/src/pages/Usage.tsx` at line 247, Update the range choices mapped in the
Usage page to render in the intended order: 1d (Today), yesterday, 7d, 30d, then
all. Keep the existing Range mapping and button behavior unchanged.

const label = choice === "all" ? t("usage.range.available") : t(`usage.range.${choice}`);
return (
<button
Expand Down
4 changes: 2 additions & 2 deletions src/server/management/logs-usage-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ function usageSummaryExpiresAt(
now: number,
): number {
let expiresAt = nextLocalMidnight(now);
const windowMs = range === "7d" ? 7 * USAGE_DAY_MS : range === "30d" ? 30 * USAGE_DAY_MS : null;
const windowMs = range === "1d" ? USAGE_DAY_MS : range === "yesterday" ? 2 * USAGE_DAY_MS : range === "7d" ? 7 * USAGE_DAY_MS : range === "30d" ? 30 * USAGE_DAY_MS : null;
if (windowMs === null) return expiresAt;
for (const entry of entries) {
if (!usageEntryMatchesSurface(entry, surface)) continue;
Expand All @@ -116,7 +116,7 @@ function usageSummaryExpiresAt(
}

function refreshedUsageSummary<T extends UsageSummary & { historyTruncated: boolean }>(summary: T, range: UsageRange, now: number): T {
const since = range === "7d" ? now - 7 * USAGE_DAY_MS : range === "30d" ? now - 30 * USAGE_DAY_MS : null;
const since = range === "1d" ? now - USAGE_DAY_MS : range === "yesterday" ? now - 2 * USAGE_DAY_MS : range === "7d" ? now - 7 * USAGE_DAY_MS : range === "30d" ? now - 30 * USAGE_DAY_MS : null;
return { ...summary, since, generatedAt: now };
}

Expand Down
13 changes: 9 additions & 4 deletions src/usage/summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { usageDisplayTotalTokens } from "./totals";
import type { PersistedUsageEntry, UsageStatus } from "./log";
import { estimateComboCost, estimateRequestCost, serviceTierContext } from "./cost";

export type UsageRange = "7d" | "30d" | "all";
export type UsageRange = "1d" | "yesterday" | "7d" | "30d" | "all";
export type UsageSurface = "all" | "codex" | "claude" | "grok";

export interface UsageSummaryTotals {
Expand Down Expand Up @@ -104,7 +104,9 @@ function retainedBreakdownRows<T>(
}

export function parseRange(input: string | null | undefined): UsageRange {
if (input === "7d" || input === "30d" || input === "all") return input;
if (input === "1d" || input === "today" || input === "yesterday" || input === "7d" || input === "30d" || input === "all") {
return input === "today" ? "1d" : input;
Comment on lines +107 to +108

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 Keep observe usage ranges in sync

After adding 1d/today/yesterday here, the existing headless usage command still rejects anything except 7d, 30d, or all in src/cli/observe.ts:134 (and the ocx usage help advertises the same older set). As a result, ocx observe usage --range 1d or --range yesterday fails locally before reaching the now-supported API, so users outside the dashboard cannot access these new windows; update the CLI validator and help alongside the shared range parser.

Useful? React with 👍 / 👎.

}
return "30d";
}

Expand All @@ -113,7 +115,9 @@ export function parseUsageSurface(input: string | null | undefined): UsageSurfac
return "all";
}

function rangeWindow(range: UsageRange, now: number): { since: number | null; days: number } {
function rangeWindow(range: UsageRange, now: number): { since: number | null; until?: number | null; days: number } {
if (range === "1d") return { since: now - DAY_MS, days: 1 };
if (range === "yesterday") return { since: now - 2 * DAY_MS, until: now - DAY_MS, days: 1 };
Comment on lines +119 to +120

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 Anchor Today and Yesterday to calendar days

These ranges are surfaced in the dashboard as Today and Yesterday, but the API filters them as rolling 24-hour windows from the current time. For example, at 10:00 local time, Today includes requests from yesterday after 10:00, while Yesterday includes part of the day before yesterday and excludes yesterday after 10:00, so the totals and daily breakdown do not match the selected date; use local start-of-day boundaries instead.

Useful? React with 👍 / 👎.

if (range === "7d") return { since: now - 7 * DAY_MS, days: 7 };
if (range === "30d") return { since: now - 30 * DAY_MS, days: 30 };
return { since: null, days: 0 };
Expand Down Expand Up @@ -553,9 +557,10 @@ export function summarizeUsage(
now: number,
surface: UsageSurface = "all",
): UsageSummary {
const { since } = rangeWindow(range, now);
const { since, until } = rangeWindow(range, now);
const filteredEntries = entries.filter(entry => {
if (since !== null && entry.timestamp < since) return false;
if (until !== undefined && until !== null && entry.timestamp >= until) return false;
if (surface === "claude") return entry.surface === "claude" || entry.surface === "claude-desktop";
if (surface === "grok") return entry.surface === "grok";
// Codex = the historical unlabelled bucket. Before the grok tag existed every
Expand Down
23 changes: 22 additions & 1 deletion tests/usage-summary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ function entry(overrides: Partial<PersistedUsageEntry> & { ts: number }): Persis
}

describe("parseRange", () => {
test("accepts 7d / 30d / all", () => {
test("accepts 1d / yesterday / 7d / 30d / all", () => {
expect(parseRange("1d")).toBe("1d");
expect(parseRange("today")).toBe("1d");
expect(parseRange("yesterday")).toBe("yesterday");
expect(parseRange("7d")).toBe("7d");
expect(parseRange("30d")).toBe("30d");
expect(parseRange("all")).toBe("all");
Expand Down Expand Up @@ -704,4 +707,22 @@ describe("summarizeUsage", () => {
});
});


test("1d and yesterday ranges filter entries by day window", () => {
const DAY_MS = 86_400_000;
const entries: PersistedUsageEntry[] = [
entry({ ts: FIXED_NOW - 1000, usageStatus: "reported", usage: { inputTokens: 10, outputTokens: 5 }, totalTokens: 15 }),
entry({ ts: FIXED_NOW - (DAY_MS + 1000), usageStatus: "reported", usage: { inputTokens: 20, outputTokens: 10 }, totalTokens: 30 }),
entry({ ts: FIXED_NOW - (3 * DAY_MS), usageStatus: "reported", usage: { inputTokens: 40, outputTokens: 20 }, totalTokens: 60 }),
];

const today = summarizeUsage(entries, "1d", FIXED_NOW);
expect(today.summary.requests).toBe(1);
expect(today.summary.totalTokens).toBe(15);

const yesterday = summarizeUsage(entries, "yesterday", FIXED_NOW);
expect(yesterday.summary.requests).toBe(1);
expect(yesterday.summary.totalTokens).toBe(30);
});

});
Loading