-
Notifications
You must be signed in to change notification settings - Fork 667
feat(usage): add date filters and daily model breakdown in dashboard (#1058) #1079
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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", | ||||||||||||||||||||||
| "usage.range.yesterday": "Yesterday", | ||||||||||||||||||||||
| "usage.filterByDate": "Filtered for {date}", | ||||||||||||||||||||||
| "usage.clearDateFilter": "Clear date filter", "Available history", | ||||||||||||||||||||||
|
Comment on lines
+661
to
+665
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win Fix the broken Every locale file assigns no value to
🐛 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
Suggested change
🧰 Tools🪛 Biome (2.5.6)[error] 662-662: expected (parse) [error] 665-665: expected (parse) 📍 Affects 6 files
🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||||||||||||||||||||||
| "usage.historyTruncated": "Totals cover available history only because older usage was not loaded.", | ||||||||||||||||||||||
| "usage.range.30d": "30d", | ||||||||||||||||||||||
| "usage.range.7d": "7d", | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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 { | ||||||
|
|
@@ -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 => { | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 🔧 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
Suggested change
🤖 Prompt for AI Agents |
||||||
| const label = choice === "all" ? t("usage.range.available") : t(`usage.range.${choice}`); | ||||||
| return ( | ||||||
| <button | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 { | ||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
After adding Useful? React with 👍 / 👎. |
||
| } | ||
| return "30d"; | ||
| } | ||
|
|
||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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 }; | ||
|
|
@@ -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 | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This leaves the locale object syntactically invalid:
usage.range.availablehas 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 👍 / 👎.