feat(my-usage): cache statistics and timezone fixes#623
Conversation
Add cacheCreationTokens and cacheReadTokens fields to ModelBreakdownItem interface and related DB queries for displaying cache statistics in the statistics summary card modal. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use server timezone (TZ config) instead of browser locale when: - Filtering statistics by date (getMyStatsSummary) - Calculating daily quota time ranges (getTimeRangeForPeriod) This ensures consistent date interpretation across different client timezones. Fixes discrepancy between Daily Quota and Statistics Summary when user is in a different timezone than the server. Added comprehensive unit tests covering: - Date parsing in configured timezone - Timezone offset calculations - Day boundary edge cases
Remove duplicate expiration information: - Remove Key Expires chip from Welcome header (keep User Expires only) - Remove "Expiring Soon" warning block from Quota Usage cards Improve countdown display in ExpirationInfo component: - Add Clock icon - Increase font size to match date display - Use monospace font for better readability - Color-coded by status (emerald/amber/red) ExpirationInfo remains the single source of expiration data.
Output tokens are never cached (per Anthropic docs), so they should not be included in cache hit rate calculation. Changed formula from: cacheReadTokens / (input + output + cacheCreate + cacheRead) to: cacheReadTokens / (input + cacheCreate + cacheRead) Affects: - /my-usage model breakdown cache hit rate display - /dashboard/leaderboard provider cache hit rate ranking Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. 📝 WalkthroughWalkthrough在多语言使用统计中新增每模型详情模态本地化标签,扩展后端模型以采集缓存相关令牌(创建/读取),引入时区感知的日期解析,移除过期时间显示相关 props/逻辑,并调整缓存命中率的分母为“总输入令牌”。 Changes
Estimated code review effort🎯 3 (中等) | ⏱️ ~25 分钟 Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
Summary of ChangesHello @miraserver, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly enhances the "my-usage" page by introducing detailed cache token statistics and addressing critical timezone-related issues. It also refines the user interface for expiration information and corrects the cache hit rate calculation for improved accuracy across the application. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces several significant improvements, including the addition of cache token statistics, crucial timezone-related fixes for date filtering, and a substantial UI cleanup that centralizes expiration information. The correction to the cache hit rate formula is also a key fix. The code is well-written and the changes are well-documented. I have one suggestion to improve the clarity of a field name in the leaderboard logic to prevent potential confusion in the future.
src/repository/leaderboard.ts
Outdated
| cacheReadTokens: sumCacheReadTokens, | ||
| cacheCreationCost: sumCacheCreationCost, | ||
| totalTokens: sumTotalTokens, | ||
| totalTokens: sumTotalInputTokens, |
There was a problem hiding this comment.
The field totalTokens is assigned the value of sumTotalInputTokens, which correctly excludes output tokens for the cache hit rate calculation. However, the name totalTokens is misleading as it no longer represents the sum of all tokens. This could cause confusion for future developers using this data.
To improve clarity and maintainability, I recommend renaming this field to totalInputTokens to accurately reflect its contents. This change should be applied here, in the ProviderCacheHitRateLeaderboardEntry interface, and in the final mapping that returns the data.
| totalTokens: sumTotalInputTokens, | |
| totalInputTokens: sumTotalInputTokens, |
There was a problem hiding this comment.
Additional Comments (1)
-
src/repository/leaderboard.ts, line 430 (link)syntax: Update comment to match the corrected formula - should be
cache_read / (input + cache_creation + cache_read)without output tokens
13 files reviewed, 1 comment
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@src/app/`[locale]/my-usage/_components/statistics-summary-card.tsx:
- Around line 326-351: The clickable summary row uses a div with onClick but
lacks keyboard accessibility; update the root div (the element with onClick={()
=> setOpen(true)}) to add role="button", tabIndex={0}, and an onKeyDown handler
that triggers setOpen(true) when Enter or Space is pressed (handle Space by
preventing default to avoid page scroll), keeping existing classes like
cursor-pointer; ensure you reference the onClick target in the handler so
setOpen is invoked consistently.
In `@src/repository/leaderboard.ts`:
- Line 474: The ProviderCacheHitRateLeaderboardEntry currently sets totalTokens
to sumTotalInputTokens which only includes input-related tokens, causing
inconsistency with other leaderboards; rename the field to totalInputTokens
(update the ProviderCacheHitRateLeaderboardEntry type/interface and the place
where totalTokens is assigned to use totalInputTokens = sumTotalInputTokens) and
update all consumers/tests to reference totalInputTokens, or alternatively add a
clear JSDoc/comment on ProviderCacheHitRateLeaderboardEntry.totalTokens stating
it only includes input tokens if you prefer not to rename—ensure the symbol
names updated are ProviderCacheHitRateLeaderboardEntry, totalTokens (or new
totalInputTokens), and sumTotalInputTokens.
🧹 Nitpick comments (3)
src/app/[locale]/my-usage/_components/expiration-info.tsx (1)
59-65: 新增的倒计时样式映射实现合理颜色编码与状态类型一致(正常为翠绿色、警告为琥珀色、危险为红色),且与
statusStyles保持语义对齐。可选优化:
warning、danger、expired的样式与statusStyles重复,可考虑提取公共映射以减少重复代码。src/repository/leaderboard.ts (1)
56-58: 接口注释需要更新接口
ProviderCacheHitRateLeaderboardEntry中totalTokens字段的含义已变更(现仅包含输入 token),建议添加注释说明以避免混淆。建议的修改
export interface ProviderCacheHitRateLeaderboardEntry { providerId: number; providerName: string; totalRequests: number; cacheReadTokens: number; totalCost: number; cacheCreationCost: number; - totalTokens: number; + totalTokens: number; // 仅包含输入相关 token (input + cacheCreation + cacheRead),用于计算缓存命中率 cacheHitRate: number; // 0-1 之间的小数,UI 层负责格式化为百分比 }src/actions/my-usage.ts (1)
550-561: 建议提取日期解析逻辑为复用函数。此处的时区感知日期解析逻辑与
getMyUsageLogs(行 400-411)几乎相同。考虑提取为公共辅助函数以减少重复,例如:function parseDateRangeWithTimezone(startDate?: string, endDate?: string) { const timezone = getEnvConfig().TZ; const parsedStart = startDate ? fromZonedTime(`${startDate}T00:00:00`, timezone).getTime() : Number.NaN; const parsedEnd = endDate ? fromZonedTime(`${endDate}T00:00:00`, timezone).getTime() : Number.NaN; return { startTime: Number.isFinite(parsedStart) ? parsedStart : undefined, endTime: Number.isFinite(parsedEnd) ? parsedEnd + 24 * 60 * 60 * 1000 : undefined, }; }
- Fix outdated cache hit rate comment (leaderboard.ts:430) - Add keyboard accessibility to model breakdown rows (a11y) - Rename totalTokens -> totalInputTokens in ProviderCacheHitRateLeaderboardEntry - Extract parseDateRangeInServerTimezone helper to reduce duplication Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Review feedback addressedFixed all 4 issues from the code review: 1. Outdated comment (leaderboard.ts:430)
2. Accessibility (statistics-summary-card.tsx)
3. Field naming (ProviderCacheHitRateLeaderboardEntry)
4. Code duplication (my-usage.ts)
Breaking change: API response for |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@src/actions/my-usage.ts`:
- Around line 28-46: The parseDateRangeInServerTimezone function uses
date-fns-tz.fromZonedTime which is buggy for non-UTC system timezones; replace
calls to fromZonedTime in parseDateRangeInServerTimezone with
date-fns-tz.zonedTimeToUtc (or an equivalent safe function) so you convert the
local zone midnight string to a UTC timestamp using the configured timezone from
getEnvConfig().TZ, and keep the existing logic that returns startTime as the
midnight timestamp and endTime as that timestamp plus 24*60*60*1000 when endDate
is provided.
In `@src/app/`[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx:
- Around line 299-305: The column header currently uses t("columns.totalTokens")
but the cell/getValue use ProviderCacheHitRateEntry.totalInputTokens so update
the i18n key to reflect input-only tokens: add a new key (e.g.
"columns.totalInputTokens") to all language files and change the column header
invocation to t("columns.totalInputTokens"); ensure the new translations (e.g.
"总输入 Token 数"/"Total Input Tokens") are present in each locale and leave the
cell/getValue/sortKey referencing totalInputTokens unchanged.
| /** | ||
| * Parse date range strings to timestamps using server timezone (TZ config). | ||
| * Returns startTime as midnight and endTime as next day midnight (exclusive upper bound). | ||
| */ | ||
| function parseDateRangeInServerTimezone( | ||
| startDate?: string, | ||
| endDate?: string | ||
| ): { startTime?: number; endTime?: number } { | ||
| const timezone = getEnvConfig().TZ; | ||
| const parsedStart = startDate | ||
| ? fromZonedTime(`${startDate}T00:00:00`, timezone).getTime() | ||
| : Number.NaN; | ||
| const parsedEnd = endDate ? fromZonedTime(`${endDate}T00:00:00`, timezone).getTime() : Number.NaN; | ||
|
|
||
| return { | ||
| startTime: Number.isFinite(parsedStart) ? parsedStart : undefined, | ||
| endTime: Number.isFinite(parsedEnd) ? parsedEnd + 24 * 60 * 60 * 1000 : undefined, | ||
| }; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
date-fns-tz fromZonedTime undefined timezone behavior
💡 Result:
Short answer: fromZonedTime has a known implementation issue — it reads date parts with getHours()/getFullYear() (local-time getters) and then builds a UTC Date from those parts, so the result is only correct if the environment's system time zone is UTC. That makes conversions wrong when your system TZ ≠ UTC (including cases where you pass undefined/omit a timeZone). See the GitHub issue describing the problem and the library docs for the intended behavior. [1][2]
What to do:
- Always pass an explicit IANA time zone (e.g. "America/New_York"), and prefer toZonedTime / zonedTimeToUtc or formatInTimeZone for most flows. [2]
- If you need a safe conversion regardless of the environment, convert/parse with explicit offsets or use functions that operate on UTC inputs instead. [2][1]
Sources:
[1] GitHub issue describing the fromZonedTime bug.
[2] date-fns-tz README / docs (function descriptions: fromZonedTime, toZonedTime, formatInTimeZone).
🏁 Script executed:
# Check the function definition and export type in the file
cat -n src/actions/my-usage.ts | sed -n '25,50p'Repository: ding113/claude-code-hub
Length of output: 1184
🏁 Script executed:
# Find and examine getEnvConfig implementation
fd config -t f | grep -E "(config|lib)" | head -20Repository: ding113/claude-code-hub
Length of output: 772
🏁 Script executed:
# Check imports and overall structure
head -n 50 src/actions/my-usage.tsRepository: ding113/claude-code-hub
Length of output: 1963
🏁 Script executed:
# Find where parseDateRangeInServerTimezone is exported
grep -n "export.*parseDateRangeInServerTimezone" src/actions/my-usage.tsRepository: ding113/claude-code-hub
Length of output: 49
🏁 Script executed:
# Check getEnvConfig implementation and TZ default value
cat -n src/lib/config/config.ts | head -100Repository: ding113/claude-code-hub
Length of output: 367
🏁 Script executed:
# Verify export statement nearby
grep -n "export" src/actions/my-usage.ts | head -20Repository: ding113/claude-code-hub
Length of output: 954
🏁 Script executed:
# Check env.schema for TZ configuration
fd env.schema -t fRepository: ding113/claude-code-hub
Length of output: 95
🏁 Script executed:
# Look for TZ in the config files
rg "TZ" src/lib/config/ -A 3 -B 1Repository: ding113/claude-code-hub
Length of output: 547
🏁 Script executed:
# Check if parseDateRangeInServerTimezone is used in getMyUsageLogs and getMyStatsSummary
sed -n '407,430p' src/actions/my-usage.tsRepository: ding113/claude-code-hub
Length of output: 878
🏁 Script executed:
# Check if there are any comments or issues about timezone handling
rg "fromZonedTime\|zonedTimeToUtc\|toZonedTime" src/ -t ts -t tsxRepository: ding113/claude-code-hub
Length of output: 94
🏁 Script executed:
# Verify the code structure around the function to understand context
sed -n '550,570p' src/actions/my-usage.tsRepository: ding113/claude-code-hub
Length of output: 762
🏁 Script executed:
# Check if there are any other timezone-related utilities or fixes in the codebase
rg "timezone\|toZonedTime\|zonedTimeToUtc" src/ --type ts -C 2Repository: ding113/claude-code-hub
Length of output: 49
🏁 Script executed:
# Verify the specific issue: understand how fromZonedTime is being used and what the expected behavior is
cat -n src/actions/my-usage.ts | sed -n '1,10p'Repository: ding113/claude-code-hub
Length of output: 548
fromZonedTime 存在已知 Bug,会导致时区转换错误。
date-fns-tz 中的 fromZonedTime 函数存在实现缺陷:它使用本地时间的 getter 方法(getHours()、getFullYear() 等)而非正确的 UTC 转换,只在系统时区为 UTC 时才能正常工作。即使配置了 TZ: "Asia/Shanghai",如果部署环境的系统时区不是 UTC,该函数返回的时间戳仍会错误。这会导致 getMyUsageLogs 和 getMyStatsSummary 返回的日期范围查询结果不准确。
改用 zonedTimeToUtc 或其他库提供的安全函数替代 fromZonedTime。
🤖 Prompt for AI Agents
In `@src/actions/my-usage.ts` around lines 28 - 46, The
parseDateRangeInServerTimezone function uses date-fns-tz.fromZonedTime which is
buggy for non-UTC system timezones; replace calls to fromZonedTime in
parseDateRangeInServerTimezone with date-fns-tz.zonedTimeToUtc (or an equivalent
safe function) so you convert the local zone midnight string to a UTC timestamp
using the configured timezone from getEnvConfig().TZ, and keep the existing
logic that returns startTime as the midnight timestamp and endTime as that
timestamp plus 24*60*60*1000 when endDate is provided.
| { | ||
| header: t("columns.totalTokens"), | ||
| className: "text-right", | ||
| cell: (row) => formatTokenAmount((row as ProviderCacheHitRateEntry).totalTokens), | ||
| sortKey: "totalTokens", | ||
| getValue: (row) => (row as ProviderCacheHitRateEntry).totalTokens, | ||
| cell: (row) => formatTokenAmount((row as ProviderCacheHitRateEntry).totalInputTokens), | ||
| sortKey: "totalInputTokens", | ||
| getValue: (row) => (row as ProviderCacheHitRateEntry).totalInputTokens, | ||
| }, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's examine the specific file and lines
cat -n src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx | sed -n '295,310p'Repository: ding113/claude-code-hub
Length of output: 774
🏁 Script executed:
# Find and check the i18n message files for the columns.totalTokens key
fd -type f -name "*.json" -path "*/messages/*" | head -20Repository: ding113/claude-code-hub
Length of output: 237
🏁 Script executed:
# Search for columns.totalTokens in i18n files
rg "totalTokens" -t json src/Repository: ding113/claude-code-hub
Length of output: 49
🏁 Script executed:
# Find all message/i18n JSON files
find . -type f -name "*.json" -path "*messages*" | head -20Repository: ding113/claude-code-hub
Length of output: 705
🏁 Script executed:
# Search for totalTokens more broadly, not just in src/
rg "totalTokens" --type jsonRepository: ding113/claude-code-hub
Length of output: 1600
🏁 Script executed:
# Check what changed in the PR by looking at git diff for i18n files
git diff --name-only | grep -i messagesRepository: ding113/claude-code-hub
Length of output: 49
🏁 Script executed:
# Check the structure of dashboard.json files, specifically looking for columns key
rg -A 2 -B 2 "columns" messages/en/dashboard.json | head -50Repository: ding113/claude-code-hub
Length of output: 1045
🏁 Script executed:
# Let's look at the actual JSON structure around totalTokens in dashboard files
python3 << 'EOF'
import json
import glob
for file in glob.glob("messages/*/dashboard.json"):
try:
with open(file, 'r', encoding='utf-8') as f:
data = json.load(f)
# Check if 'columns' key exists
if 'columns' in data:
print(f"\n=== {file} ===")
print("columns keys:", list(data['columns'].keys()))
if 'totalTokens' in data['columns']:
print(f"columns.totalTokens = {data['columns']['totalTokens']}")
# Also check leaderboard section
if 'leaderboard' in data and isinstance(data['leaderboard'], dict):
if 'columns' in data['leaderboard']:
print(f"\n=== {file} (leaderboard.columns) ===")
print("leaderboard.columns keys:", list(data['leaderboard']['columns'].keys())[:10])
if 'totalTokens' in data['leaderboard']['columns']:
print(f"leaderboard.columns.totalTokens = {data['leaderboard']['columns']['totalTokens']}")
except Exception as e:
print(f"Error reading {file}: {e}")
EOFRepository: ding113/claude-code-hub
Length of output: 153
🏁 Script executed:
# Extract just the leaderboard section from one dashboard file
jq '.leaderboard' messages/en/dashboard.json 2>/dev/null | head -100Repository: ding113/claude-code-hub
Length of output: 2567
🏁 Script executed:
# Search for both totalTokens and totalInputTokens in the message files
rg "totalInputTokens" messages/Repository: ding113/claude-code-hub
Length of output: 49
🏁 Script executed:
# Check the exact structure - look for where columns.totalTokens is defined
rg -B 5 "totalTokens" messages/en/dashboard.json | grep -E "(leaderboard|columns|totalTokens)" | head -20Repository: ding113/claude-code-hub
Length of output: 140
列标题与实际数据语义不匹配,需要更新国际化 key
逻辑变更正确:将 totalInputTokens 用于缓存命中率计算符合 PR 目标,与已有经验一致(缓存计算仅包含输入相关令牌)。
但存在 UX 问题:第 300 行的列标题使用 t("columns.totalTokens"),该 i18n key 在所有语言文件中均定义为"总 Token 数"/"Total Tokens",隐含包含所有 Token 类型(输入+输出)。而实际显示的数据仅为 totalInputTokens(输入令牌),造成语义不符。
需修复:
- 创建新的 i18n key(如
columns.totalInputTokens),在 5 个语言文件中定义为"总输入 Token 数"等 - 或更新列标题代码为
t("columns.totalInputTokens")
否则用户会误解为该列显示全部 Token。
🤖 Prompt for AI Agents
In `@src/app/`[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx
around lines 299 - 305, The column header currently uses
t("columns.totalTokens") but the cell/getValue use
ProviderCacheHitRateEntry.totalInputTokens so update the i18n key to reflect
input-only tokens: add a new key (e.g. "columns.totalInputTokens") to all
language files and change the column header invocation to
t("columns.totalInputTokens"); ensure the new translations (e.g. "总输入 Token
数"/"Total Input Tokens") are present in each locale and leave the
cell/getValue/sortKey referencing totalInputTokens unchanged.
Keep returning totalTokens (deprecated) alongside totalInputTokens for API consumers that haven't migrated yet. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/repository/leaderboard.ts (1)
477-500: deprecated 的 totalTokens 语义已变更,兼容性不完整当前 totalTokens 直接等于 totalInputTokens,会让旧消费方拿到更小的值(不含 outputTokens),与“向后兼容”的预期不一致。建议保留旧 totalTokens 计算(含 outputTokens),同时新增 totalInputTokens 供新分母使用。
Proposed fix
const totalInputTokensExpr = sql<number>`( COALESCE(${messageRequest.inputTokens}, 0) + COALESCE(${messageRequest.cacheCreationInputTokens}, 0) + COALESCE(${messageRequest.cacheReadInputTokens}, 0) )`; + const totalTokensExpr = sql<number>`( + COALESCE(${messageRequest.inputTokens}, 0) + + COALESCE(${messageRequest.outputTokens}, 0) + + COALESCE(${messageRequest.cacheCreationInputTokens}, 0) + + COALESCE(${messageRequest.cacheReadInputTokens}, 0) + )`; const sumTotalInputTokens = sql<number>`COALESCE(sum(${totalInputTokensExpr})::double precision, 0::double precision)`; + const sumTotalTokens = sql<number>`COALESCE(sum(${totalTokensExpr})::double precision, 0::double precision)`; const rankings = await db .select({ providerId: messageRequest.providerId, providerName: providers.name, totalRequests: sql<number>`count(*)::double precision`, totalCost: sql<string>`COALESCE(sum(${messageRequest.costUsd}), 0)`, cacheReadTokens: sumCacheReadTokens, cacheCreationCost: sumCacheCreationCost, totalInputTokens: sumTotalInputTokens, + totalTokens: sumTotalTokens, cacheHitRate: cacheHitRateExpr, }) return rankings.map((entry) => ({ providerId: entry.providerId, providerName: entry.providerName, totalRequests: entry.totalRequests, totalCost: parseFloat(entry.totalCost), cacheReadTokens: entry.cacheReadTokens, cacheCreationCost: parseFloat(entry.cacheCreationCost), totalInputTokens: entry.totalInputTokens, - totalTokens: entry.totalInputTokens, // deprecated, for backward compatibility + totalTokens: entry.totalTokens, // deprecated, for backward compatibility cacheHitRate: Math.min(Math.max(entry.cacheHitRate ?? 0, 0), 1), }));
|
Regarding the timezone issue, can we modify |
I've already thought about this and tried it, but I still decided not to include it in this PR. I'll try to explain But the main issue is too many fixes - admin logs, my-usage logs and etc. admin dashboard logs TODAY - based on browser user time I think we should leave the server time and just write it in UI (make a tooltip) so that the user and the admin understand what time the TODAY/YESTERDAY button refers to I suggest moving the timezome variable to a separate PR later. |
* fix: 清理 usage-doc / big-screen i18n 硬编码 + 修复 Next.js params Promise 报错 * feat(leaderboard): add user tag and group filters for user ranking (#607) * feat(leaderboard): add user tag and group filters for user ranking (#606) Add filtering capability to the leaderboard user ranking by: - userTags: filter users by their tags (OR logic) - userGroups: filter users by their providerGroup (OR logic) Changes: - Repository: Add UserLeaderboardFilters interface and SQL filtering - Cache: Extend LeaderboardFilters and include filters in cache key - API: Parse userTags/userGroups query params (CSV format, max 20) - Frontend: Add TagInput filters (admin-only, user scope only) - i18n: Add translation keys for 5 languages Closes #606 * refactor: apply reviewer suggestions for leaderboard filters - Use JSONB ? operator instead of @> for better performance - Extract parseListParam helper to reduce code duplication * feat(leaderboard): add tag/group suggestions dropdown for better UX - Fetch all user tags and groups via getAllUserTags/getAllUserKeyGroups - Pass suggestions to TagInput for autocomplete dropdown - Validate input against available suggestions - Consistent with /dashboard/users filter behavior * docs: update Privnode offer details in README files * fix: 修复 1M 上下文标头兼容问题 * fix: resolve container name conflicts in multi-user environments (#625) * fix: resolve container name conflicts in multi-user environments - Add top-level `name` field with COMPOSE_PROJECT_NAME env var support - Remove hardcoded container_name from all services - Users can now set COMPOSE_PROJECT_NAME in .env for complete isolation Closes #624 * fix: add top-level name field for project isolation Add `name: ${COMPOSE_PROJECT_NAME:-claude-code-hub}` to enable complete project isolation via environment variable. * feat(dashboard): improve user management, statistics reset, and i18n (#610) * fix(dashboard/logs): add reset options to filters and use short time format - Add "All keys" SelectItem to API Key filter dropdown - Add "All status codes" SelectItem to Status Code filter dropdown - Use __all__ value instead of empty string (Radix Select requirement) - Add formatDateDistanceShort() for compact time display (2h ago, 3d ago) - Update RelativeTime component to use short format Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(providers): add Auto Sort button to dashboard and fix i18n - Add AutoSortPriorityDialog to dashboard/providers page - EN: "Auto Sort Priority" -> "Auto Sort" - RU: "Авто сортировка приоритета" -> "Автосорт" - RU: "Добавить провайдера" -> "Добавить поставщика" Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(i18n): improve Russian localization and fix login errors Russian localization improvements: - Menu: "Управление поставщиками" -> "Поставщики" - Menu: "Доступность" -> "Мониторинг" - Filters: "Последние 7/30 дней" -> "7д/30д" - Dashboard: "Статистика использования" -> "Статистика" - Dashboard: "Показать статистику..." -> "Только ваши ключи" - Quota: add missing translations (manageNotice, withQuotas, etc.) Login error localization: - Fix issue where login errors displayed in Chinese ("无效或已过期") regardless of locale - Add locale detection from NEXT_LOCALE cookie and Accept-Language header - Add 3 new error keys: apiKeyRequired, apiKeyInvalidOrExpired, serverError - Support all 5 languages: EN, JA, RU, ZH-CN, ZH-TW - Remove product name from login privacyNote for all locales Files changed: - messages/*/auth.json: new error keys, update privacyNote - messages/ru/dashboard.json, messages/ru/quota.json: Russian improvements - src/app/api/auth/login/route.ts: add getLocaleFromRequest() Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * feat(dashboard/users): improve user management with key quotas and tokens - Add access/model restrictions support (allowedClients/allowedModels) - Add tokens column and refresh button to users table - Add todayTokens calculation in repository layer (sum all token types) - Add visual status indicators with color-coded icons (active/disabled/expiring/expired) - Allow users to view their own key quota (was admin-only) - Fix React Query cache invalidation on status toggle - Fix filter logic: change tag/keyGroup from OR to AND - Refactor time display: move formatDateDistanceShort to component with i18n - Add fixed header/footer to key dialogs for better UX Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * feat(dashboard/users): add reset statistics with optimized Redis pipeline - Implement reset all statistics functionality for admins - Optimize Redis operations: replace sequential redis.keys() with parallel SCAN - Add scanPattern() helper for production-safe key scanning - Comprehensive error handling and performance metrics logging - 50-100x performance improvement with no Redis blocking Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix(lint): apply biome formatting and fix React hooks dependencies - Fix useEffect dependencies in RelativeTime component (wrap formatShortDistance in useCallback) - Remove unused effectiveGroupText variable in key-row-item.tsx - Apply consistent LF line endings across modified files Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: address code review feedback from PR #610 - Remove duplicate max-h class in edit-key-dialog.tsx (keep max-h-[90dvh] only) - Add try-catch fallback for getTranslations in login route catch block Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: translate Russian comments to English for consistency Addresses Gemini Code Assist review feedback. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * test(users): add unit tests for resetUserAllStatistics function Cover all requirement scenarios: - Permission check (admin-only) - User not found handling - Success path with DB + Redis cleanup - Redis not ready graceful handling - Redis partial failure warning - scanPattern failure warning - pipeline.exec failure error logging - Unexpected error handling - Empty keys list handling 10 test cases with full mock coverage. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: John Doe <johndoe@example.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> * feat(my-usage): cache statistics and timezone fixes (#623) * feat(my-usage): add cache token statistics to model breakdown Add cacheCreationTokens and cacheReadTokens fields to ModelBreakdownItem interface and related DB queries for displaying cache statistics in the statistics summary card modal. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(my-usage): use configured timezone for date filtering Use server timezone (TZ config) instead of browser locale when: - Filtering statistics by date (getMyStatsSummary) - Calculating daily quota time ranges (getTimeRangeForPeriod) This ensures consistent date interpretation across different client timezones. Fixes discrepancy between Daily Quota and Statistics Summary when user is in a different timezone than the server. Added comprehensive unit tests covering: - Date parsing in configured timezone - Timezone offset calculations - Day boundary edge cases * refactor(my-usage): clean up expiration displays Remove duplicate expiration information: - Remove Key Expires chip from Welcome header (keep User Expires only) - Remove "Expiring Soon" warning block from Quota Usage cards Improve countdown display in ExpirationInfo component: - Add Clock icon - Increase font size to match date display - Use monospace font for better readability - Color-coded by status (emerald/amber/red) ExpirationInfo remains the single source of expiration data. * fix: correct cache hit rate formula to exclude output tokens Output tokens are never cached (per Anthropic docs), so they should not be included in cache hit rate calculation. Changed formula from: cacheReadTokens / (input + output + cacheCreate + cacheRead) to: cacheReadTokens / (input + cacheCreate + cacheRead) Affects: - /my-usage model breakdown cache hit rate display - /dashboard/leaderboard provider cache hit rate ranking Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix: address PR #623 review feedback - Fix outdated cache hit rate comment (leaderboard.ts:430) - Add keyboard accessibility to model breakdown rows (a11y) - Rename totalTokens -> totalInputTokens in ProviderCacheHitRateLeaderboardEntry - Extract parseDateRangeInServerTimezone helper to reduce duplication Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: add backward compatibility for totalTokens field Keep returning totalTokens (deprecated) alongside totalInputTokens for API consumers that haven't migrated yet. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: John Doe <johndoe@example.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> * fix: Set HOSTNAME environment variable in Dockerfile (#622) Add HOSTNAME environment variable for container * fix: inherit 1M flag from client Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * fix(my-usage): make date range DST-safe Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * test(api): cover leaderboard comma-list parsing Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * fix(auth): avoid hardcoded server error fallback Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * fix(actions): localize key quota permission errors Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * docs: clarify context 1M inherit behavior Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * test(settings): fix model multi-select messages loader Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * test(dashboard): provide QueryClientProvider in edit key form test Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * chore(scripts): avoid hardcoded docker container names Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * fix(i18n): replace fullwidth parentheses in ja dashboard Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * fix(ui): add a11y label and clean up dialog styles Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * chore: ignore tmp scratch directory Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * feat: Dashboard Logs:秒级时间筛选 + Session ID 精确筛选/联想/展示(含回归修复) (#611) * feat: add sessionId filter for usage logs * feat: add seconds-level time filters for logs * feat: wire sessionId into logs URL filters * chore: add i18n keys for logs sessionId * feat: add sessionId column to logs tables * feat: add sessionId suggestions for logs * docs: document dashboard logs call chain * test: add logs sessionId/time filter coverage config * fix: keep sessionId search input focused * fix: drop leaked page param on logs apply * fix: reload sessionId suggestions on scope change * fix: harden logs url params and time parsing * fix: avoid keys join for sessionId suggestions * test: strengthen empty sessionId filter assertions * chore: format logs sessionId suggestions test * Update src/actions/usage-logs.ts Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * feat: 补充公共静态变量 SESSION_ID_SUGGESTION_LIMIT * fix: use prefix LIKE for sessionId suggestions * refactor: centralize usage logs sessionId suggestion constants * refactor: simplify logs url filters parsing * refactor: reuse clipboard util for sessionId copy * chore(db): add sessionId prefix index * docs: clarify sessionId suggestion semantics * test: add escapeLike unit tests * chore: apply biome fixes for sessionId search * feat: include session id in error responses * test: add coverage suite for session id errors * docs: add guide for error session id * chore: format code (feat-logs-sessionid-time-filter-233f96a) * feat(dashboard/logs): add fullscreen mode (#632) * Fix provider exhaustion after model redirect (refs #629) (#633) * Fix provider fallback after model redirect Use original model for provider selection during failover and tolerate tool_result blocks in non-stream converters to prevent premature provider exhaustion. References #629. * Add regression test for model redirect provider selection Ensures provider selection uses original model even when current model is redirected (refs #629). * chore: format code (fix-629-provider-fallback-toolresult-b17f671) --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> --------- Co-authored-by: YangQing-Lin <56943790+YangQing-Lin@users.noreply.github.com> Co-authored-by: SaladDay <92240037+SaladDay@users.noreply.github.com> Co-authored-by: miraserver <20286838+miraserver@users.noreply.github.com> Co-authored-by: John Doe <johndoe@example.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: hwa <yll2002mail@gmail.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Ding <ding113@users.noreply.github.com> Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Summary
This PR improves the my-usage page with cache token statistics and timezone-related fixes:
Changes
Cache Token Statistics (
b0f6f896)Timezone Fixes (
3a66813c)getMyStatsSummaryandgetTimeRangeForPeriodUI Cleanup (
a6b972d1)Cache Hit Rate Formula Fix (
bc8f0d2f)cacheRead / (input + output + cacheCreate + cacheRead)tocacheRead / (input + cacheCreate + cacheRead)Testing
bun run lint- Biome formatting appliedbun run typecheck- Type checks passedbun run build- Production build successfulFiles Changed
All changes are scoped to the my-usage module.
🤖 Generated with Claude Code
Greptile Summary
This PR enhances the my-usage page with cache token statistics and fixes timezone-related issues for accurate date filtering.
Key Improvements:
Cache Statistics Feature: Added comprehensive cache token tracking (
cacheCreationTokensandcacheReadTokens) to model breakdowns with a detailed modal showing cache hit rates, color-coded performance indicators (green ≥85%, yellow ≥60%, orange <60%), and token breakdowns across 5 languages.Timezone Fix: Replaced browser-based date parsing with server timezone (
TZconfig) usingfromZonedTimeingetMyStatsSummaryandgetMyUsageLogs. This ensures consistent date filtering across different user timezones, fixing discrepancies between Daily Quota and Statistics Summary displays.Cache Hit Rate Formula Correction: Updated formula to exclude output tokens (which are never cached) from
cacheRead / (input + output + cacheCreate + cacheRead)tocacheRead / (input + cacheCreate + cacheRead). Applied consistently across my-usage and leaderboard modules.UI Cleanup: Removed duplicate expiration displays (chips and warnings) from quota cards. Enhanced
ExpirationInfocomponent with Clock icon, larger font, and status-based color coding for a single, improved source of truth.Testing: All changes passed linting, type checking, and production build validation. Changes are well-scoped to the my-usage module with proper i18n support.
Confidence Score: 4/5
Important Files Changed
Sequence Diagram
sequenceDiagram participant User participant StatsSummaryCard participant getMyStatsSummary participant DB participant ModelBreakdownRow User->>StatsSummaryCard: View statistics (select date) StatsSummaryCard->>StatsSummaryCard: Format date with date-fns StatsSummaryCard->>getMyStatsSummary: Request stats with date range Note over getMyStatsSummary: Use fromZonedTime to convert<br/>date string to timestamp<br/>in configured timezone (TZ) getMyStatsSummary->>DB: Query message_request table Note over DB: Aggregate by model:<br/>- inputTokens<br/>- outputTokens<br/>- cacheCreationTokens<br/>- cacheReadTokens<br/>- cost DB-->>getMyStatsSummary: Return aggregated data getMyStatsSummary-->>StatsSummaryCard: Return ModelBreakdownItem[] StatsSummaryCard->>ModelBreakdownRow: Display each model Note over ModelBreakdownRow: Calculate cache hit rate:<br/>cacheReadTokens / <br/>(inputTokens + cacheCreationTokens<br/>+ cacheReadTokens) ModelBreakdownRow->>ModelBreakdownRow: Apply color coding<br/>(green/yellow/orange) User->>ModelBreakdownRow: Click to view details ModelBreakdownRow->>ModelBreakdownRow: Open modal with detailed<br/>token breakdown and<br/>cache statistics