-
Notifications
You must be signed in to change notification settings - Fork 142
release v0.4.3 #628
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
base: main
Are you sure you want to change the base?
release v0.4.3 #628
Conversation
) * 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
- 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
* 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.
…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): 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>
Add HOSTNAME environment variable for container
|
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本次变更:新增日志会话 ID 筛选与建议、排行榜按用户标签/分组过滤、重置用户统计操作、我的使用统计模态与缓存令牌计数、Redis SCAN 工具、短格式相对时间、广泛 i18n 扩展、多处前端布局/类型调整,以及 Docker/Docker Compose 可配置化与 HOSTNAME 环境变量。 Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Summary of ChangesHello @ding113, 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 introduces significant enhancements to user management, usage tracking, and internationalization. It empowers administrators with a powerful tool to reset user statistics, provides users with more granular insights into their API usage, and refines the overall user experience through improved filtering, detailed metrics, and consistent localization across the application. The changes also include minor infrastructure updates for Docker deployments. 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request introduces a significant number of features and improvements for version 0.4.3. Key additions include a user statistics reset function, advanced filtering on the leaderboard, and more detailed statistics modals. The codebase has been improved through refactoring, such as centralizing date handling to be timezone-aware and enhancing UI components like dialogs and relative time displays. I've also noticed several important correctness fixes, including cache invalidation and bug fixes in filter logic. The addition of new tests for these features is commendable. I have one major concern regarding a logic change in the 1M context window feature that appears to be a regression, which I've detailed in a specific comment. Overall, this is a high-quality release with substantial enhancements across the board.
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
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.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
messages/ru/settings/providers/strings.json (1)
4-13: 术语不一致:建议统一“поставщик/провайдер”用法。同一界面同时出现“поставщика”和“провайдера”(如 Line 10–12、Line 22 仍是“провайдер”)。为避免用户困惑,建议在该文件内统一术语。
src/lib/special-attributes/index.ts (1)
74-91: 默认情况的逻辑与 JSDoc 文档不一致。函数在
preference为"inherit"时返回false,但 JSDoc 注释第 89 行声明"Default (inherit): passthrough client headers without modification"。这意味着在继承模式下应该返回客户端的实际请求状态(即返回clientRequestedContext1m),而不是硬编码false。建议修改默认分支,使其与文档描述保持一致:
修复逻辑不一致
// Default (inherit): passthrough client headers without modification - return false; + return clientRequestedContext1m;
🤖 Fix all issues with AI agents
In `@docker-compose.yaml`:
- Line 1: The top-level compose `name` removal broke hardcoded container names
used by scripts; update either the compose services or the scripts: add explicit
`container_name: claude-code-hub-{service}-dev` entries to the postgres/redis
services in docker-compose.yaml to restore the old names, or modify scripts
`scripts/run-e2e-tests.sh` (checks for claude-code-hub-db-dev) and
`scripts/cleanup-test-users.sh` (multiple `docker exec claude-code-hub-db-dev`
calls) to explicitly use `-f docker-compose.dev.yaml` when bringing up services
or to detect containers dynamically (e.g., use `docker compose ps`/labels to
find the running postgres container) so they no longer rely on a fixed container
name.
In `@src/actions/my-usage.ts`:
- Around line 28-46: The parseDateRangeInServerTimezone function can throw if
getEnvConfig().TZ is empty/invalid; before calling fromZonedTime validate the
timezone (from getEnvConfig().TZ) or use a safe default (e.g., 'UTC') and handle
errors: compute a validatedTimezone variable, guard against empty/invalid values
(use Intl.supportedValuesOf('timeZone') or try/catch a test conversion), then
use validatedTimezone in both fromZonedTime calls and wrap the conversions in
try/catch to convert thrown RangeError into returning undefined times (or a
clear fallback). Ensure you update references to timezone in
parseDateRangeInServerTimezone and keep endTime logic (add 24h) intact when
fallback is applied.
In `@src/app/`[locale]/dashboard/_components/user/add-key-dialog.tsx:
- Line 73: The DialogContent element in add-key-dialog.tsx currently has
duplicate max-h classes ("max-h-[90vh]" and "max-h-[90dvh]") where the latter
overrides the former; update the class list on the DialogContent in the
AddKeyDialog component to either remove the redundant rule or reorder so the dvh
variant comes first and the vh fallback comes last (e.g., place "max-h-[90dvh]"
before "max-h-[90vh]") to ensure the intended fallback behavior in browsers that
don't support dvh.
In `@src/app/`[locale]/dashboard/_components/user/user-key-table-row.tsx:
- Around line 221-222: 摘要:注释语言与代码库不一致,当前使用俄语,应改为中文。 在 user-key-table-row.tsx
中定位到紧邻 queryClient.invalidateQueries({ queryKey: ["users"] }); 的注释(当前为
"Инвалидировать кэш React Query для всех фильтров"),将其替换为中文等价描述,例如“使 React Query
针对所有筛选器的用户缓存失效”,并检查该文件内其他注释以确保与仓库的中文注释风格保持一致。
In `@src/app/`[locale]/dashboard/_components/user/user-management-table.tsx:
- Around line 430-440: The refresh Button (component Button rendering RefreshCw
icon) is an icon-only control so add an accessible name: update the Button
rendered in the onRefresh branch to include an aria-label (e.g.,
aria-label={tUserMgmt("table.refresh")}) or include visually hidden text
(sr-only) alongside the RefreshCw icon; ensure the label uses the existing
translation via tUserMgmt and preserve disabled/isRefreshing behavior and the
animate-spin class on RefreshCw so screen readers can identify the control while
visual behavior remains unchanged.
In `@src/app/api/auth/login/route.ts`:
- Around line 67-72: The POST handler returns a hard-coded "Server error" when
getTranslations fails; pre-load translations at the start of the POST function
using getTranslations({ locale, namespace: "auth.errors" }) with a fallback to
defaultLocale (e.g., call getTranslations({ locale: locale || defaultLocale, ...
}) or catch and re-call with defaultLocale) and replace the hard-coded string in
the catch/response with the i18n key result (t("serverError")), ensuring all
NextResponse.json error responses use the translated message rather than a
literal string; update references in the POST function and related error paths
(getTranslations, locale, defaultLocale, NextResponse.json) accordingly.
In `@tests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsx`:
- Around line 28-36: 测试仍在调用 loadTestMessages 导致 ReferenceError;在测试中把对
loadTestMessages 的引用替换为新导出的 loadMessages(或如果想保留旧名则恢复导入 loadTestMessages),具体修改
locate 并更新所有使用 loadTestMessages 的地方(如在 setup/fixtures 或 beforeEach 中的调用)改为调用
loadMessages,并确保相应的 import 声明也指向提供 loadMessages 的模块。
🧹 Nitpick comments (13)
src/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsx (1)
421-450: keyId 过滤器使用"__all__"哨兵值的逻辑正确。使用
"__all__"作为哨兵值来表示"全部"是一个合理的选择,可以避免空字符串在某些 UI 库中可能引发的问题。逻辑正确处理了选择和取消选择的场景。然而,注意到代码一致性问题:
keyId和statusCode过滤器使用"__all__"作为哨兵值,而model(第 539、553 行)和endpoint(第 572、586 行)过滤器仍然使用"all"。建议统一所有过滤器的哨兵值命名,以提高代码可维护性。src/app/[locale]/my-usage/_components/expiration-info.tsx (2)
59-65: 可考虑复用 statusStyles 以避免样式漂移countdownStyles 与 statusStyles 大部分重复,后续维护容易不一致。可以基于 statusStyles 覆盖 normal 的颜色。
建议修改
- const countdownStyles: Record<ExpireStatus, string> = { - none: "text-muted-foreground", - normal: "text-emerald-600 dark:text-emerald-400", - warning: "text-amber-600 dark:text-amber-400", - danger: "text-red-600 dark:text-red-400", - expired: "text-destructive", - }; + const countdownStyles: Record<ExpireStatus, string> = { + ...statusStyles, + normal: "text-emerald-600 dark:text-emerald-400", + };
3-3: 确认 Clock 图标的无障碍属性若 lucide-react 默认未设置 aria-hidden,屏幕阅读器可能会读出图标。若仅作装饰用途,建议显式隐藏。请确认组件默认行为。
建议修改
- <Clock className="h-3.5 w-3.5" /> + <Clock className="h-3.5 w-3.5" aria-hidden="true" />Also applies to: 90-94
src/lib/redis/scan-helper.ts (1)
17-31: 考虑使用 Set 进行去重处理Redis SCAN 命令可能会返回重复的键(这是 SCAN 游标迭代设计的结果),需要客户端负责去重。当前实现直接累积 batch 中的所有键,可能导致重复键被返回。建议使用 Set 进行去重以确保结果中的键唯一。
建议的修复方案
export async function scanPattern(redis: Redis, pattern: string, count = 100): Promise<string[]> { - const keys: string[] = []; + const keys = new Set<string>(); let cursor = "0"; do { const [nextCursor, batch] = (await redis.scan(cursor, "MATCH", pattern, "COUNT", count)) as [ string, string[], ]; cursor = nextCursor; - keys.push(...batch); + for (const key of batch) { + keys.add(key); + } } while (cursor !== "0"); - return keys; + return Array.from(keys); }src/actions/users.ts (1)
1512-1622: 请确认 Redis 扫描的并发规模是否可控。当前对每个 keyId 并行执行 scanPattern,若用户 key 数量较多,可能造成 Redis 扫描压力与响应时间波动。建议评估生产规模下的表现,必要时做并发限制或分批处理。
src/app/[locale]/dashboard/_components/user/batch-edit/batch-edit-dialog.tsx (1)
377-381: 缓存失效逻辑正确,可考虑并行化以提升性能。新增的
userKeyGroups和userTags缓存失效与其他表单组件(如edit-key-form.tsx)保持一致,确保 UI 数据同步。可选优化:三个
invalidateQueries调用可以并行执行:♻️ 可选的并行化重构
if (anySuccess) { - await queryClient.invalidateQueries({ queryKey: ["users"] }); - await queryClient.invalidateQueries({ queryKey: ["userKeyGroups"] }); - await queryClient.invalidateQueries({ queryKey: ["userTags"] }); + await Promise.all([ + queryClient.invalidateQueries({ queryKey: ["users"] }), + queryClient.invalidateQueries({ queryKey: ["userKeyGroups"] }), + queryClient.invalidateQueries({ queryKey: ["userTags"] }), + ]); }tests/unit/i18n/big-screen-metadata-keys.test.ts (1)
9-17: 测试覆盖正确,建议增加失败时的 locale 信息以便调试。测试逻辑正确,验证了所有 5 种语言的
pageTitle和pageDescription键。若断言失败,当前无法直接确定是哪个 locale 出错。♻️ 建议:在断言中添加 locale 信息
test("provides pageTitle/pageDescription", () => { - const all = [enBigScreen, jaBigScreen, ruBigScreen, zhCNBigScreen, zhTWBigScreen]; + const all = [ + { locale: "en", data: enBigScreen }, + { locale: "ja", data: jaBigScreen }, + { locale: "ru", data: ruBigScreen }, + { locale: "zh-CN", data: zhCNBigScreen }, + { locale: "zh-TW", data: zhTWBigScreen }, + ]; - for (const bigScreen of all) { - expect(bigScreen).toHaveProperty("pageTitle"); - expect(bigScreen).toHaveProperty("pageDescription"); + for (const { locale, data } of all) { + expect(data, `Missing pageTitle in ${locale}`).toHaveProperty("pageTitle"); + expect(data, `Missing pageDescription in ${locale}`).toHaveProperty("pageDescription"); } });src/app/api/leaderboard/route.ts (1)
78-79: 建议对 userTags/userGroups 做去重与单项长度限制。
Line 78-79、130-145 目前仅限制数量,超长值会放大缓存 key/日志体积;可在解析时去重并裁剪单项长度。Proposed fix
- const items = param - .split(",") - .map((s) => s.trim()) - .filter((s) => s.length > 0) - .slice(0, 20); - return items.length > 0 ? items : undefined; + const items = param + .split(",") + .map((s) => s.trim().slice(0, 64)) + .filter((s) => s.length > 0); + const unique = Array.from(new Set(items)).slice(0, 20); + return unique.length > 0 ? unique : undefined;Also applies to: 130-145
src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx (1)
56-77: 建议为管理员筛选建议请求补充异常处理与卸载保护Promise.all 失败会导致未处理异常,且组件卸载后可能触发 setState。可加 try/catch 与取消标记。
建议修改
useEffect(() => { if (!isAdmin) return; + let cancelled = false; const fetchSuggestions = async () => { - const [tagsResult, groupsResult] = await Promise.all([ - getAllUserTags(), - getAllUserKeyGroups(), - ]); - if (tagsResult.ok) setTagSuggestions(tagsResult.data); - if (groupsResult.ok) setGroupSuggestions(groupsResult.data); + try { + const [tagsResult, groupsResult] = await Promise.all([ + getAllUserTags(), + getAllUserKeyGroups(), + ]); + if (cancelled) return; + if (tagsResult.ok) setTagSuggestions(tagsResult.data); + if (groupsResult.ok) setGroupSuggestions(groupsResult.data); + } catch { + if (!cancelled) { + setTagSuggestions([]); + setGroupSuggestions([]); + } + } }; fetchSuggestions(); + return () => { + cancelled = true; + }; }, [isAdmin]);src/lib/redis/leaderboard-cache.ts (1)
299-317:invalidateLeaderboardCache无法清除带用户过滤器的缓存。当前
invalidateLeaderboardCache函数不接受filters参数,因此无法清除包含userTags或userGroups后缀的缓存条目。如果需要手动清除特定用户过滤条件的缓存,当前实现无法支持。考虑是否需要扩展此函数以支持过滤器参数:
建议的改进方案
export async function invalidateLeaderboardCache( period: LeaderboardPeriod, currencyDisplay: string, - scope: LeaderboardScope = "user" + scope: LeaderboardScope = "user", + filters?: LeaderboardFilters ): Promise<void> { const redis = getRedisClient(); if (!redis) { return; } - const cacheKey = buildCacheKey(period, currencyDisplay, scope); + const cacheKey = buildCacheKey(period, currencyDisplay, scope, undefined, filters); try { await redis.del(cacheKey); logger.info("[LeaderboardCache] Cache invalidated", { period, scope, cacheKey }); } catch (error) { logger.error("[LeaderboardCache] Failed to invalidate cache", { period, scope, error }); } }src/app/[locale]/dashboard/_components/user/edit-user-dialog.tsx (1)
289-340: 建议:考虑基于用户角色条件渲染重置数据区域。虽然
resetUserAllStatisticsaction 在服务端验证了管理员权限,但对非管理员用户显示此区域可能造成困惑(点击后会收到权限错误)。可选:条件渲染示例
如果有可用的用户角色信息(如通过 session 或 props 传递),可以考虑:
{isAdmin && ( <section className="rounded-lg border border-destructive/30 bg-destructive/5 p-4"> {/* Reset Data UI */} </section> )}当前实现在功能上是正确的,这只是一个用户体验优化建议。
src/repository/leaderboard.ts (1)
191-199: 分组筛选的正则分割可能存在性能隐患。使用
regexp_split_to_array对每行数据进行字符串分割会阻止索引利用。当数据量较大时,可能影响查询性能。当前实现可接受,但建议后续优化方向:
- 考虑为
providerGroup字段建立 GIN 索引(如果改为数组类型存储)- 或在应用层预处理筛选逻辑
src/app/[locale]/usage-doc/page.tsx (1)
451-468: 建议合并 shellConfig 计算以避免重复。
shellConfig 与 shellConfigFile 逻辑一致,直接复用可减少后续分叉风险。Proposed change
- const shellConfig = - os === "linux" - ? t("placeholders.shellConfig.linux") - : os === "macos" - ? t("placeholders.shellConfig.macos") - : ""; + const shellConfig = shellConfigFile;Also applies to: 551-553
| /** | ||
| * 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
cat -n src/actions/my-usage.ts | head -50Repository: ding113/claude-code-hub
Length of output: 2313
🏁 Script executed:
grep -n "fromZonedTime\|getEnvConfig\|export.*parseDateRangeInServerTimezone" src/actions/my-usage.ts | head -20Repository: ding113/claude-code-hub
Length of output: 373
🌐 Web query:
date-fns-tz v3 fromZonedTime empty timezone string behavior
💡 Result:
Short answer: fromZonedTime expects a valid IANA time‑zone (or offset) and an empty string is treated as invalid — date-fns-tz will reject it (via tzParseTimezone/Intl) and you’ll get an error/Invalid Date (e.g. RangeError "Invalid time zone specified: "). [1][2][3]
Sources:
- GitHub issue discussing fromZonedTime behaviour. [1]
- GitHub issue about "Invalid time zone specified" from tz parsing. [2]
- date-fns-tz README describing expected timeZone input and Invalid Date on invalid zone. [3]
服务器时区解析逻辑正确,但缺少时区验证。
日期字符串转换和 endTime 加 24 小时创建排他性上边界的实现是正确的。
需要修复: 当 getEnvConfig().TZ 返回空字符串、undefined 或无效时区值时,fromZonedTime 会抛出 RangeError: Invalid time zone specified。当前代码缺少错误处理和时区验证。建议在调用 fromZonedTime 前验证时区有效性,或提供默认时区作为降级方案。
🤖 Prompt for AI Agents
In `@src/actions/my-usage.ts` around lines 28 - 46, The
parseDateRangeInServerTimezone function can throw if getEnvConfig().TZ is
empty/invalid; before calling fromZonedTime validate the timezone (from
getEnvConfig().TZ) or use a safe default (e.g., 'UTC') and handle errors:
compute a validatedTimezone variable, guard against empty/invalid values (use
Intl.supportedValuesOf('timeZone') or try/catch a test conversion), then use
validatedTimezone in both fromZonedTime calls and wrap the conversions in
try/catch to convert thrown RangeError into returning undefined times (or a
clear fallback). Ensure you update references to timezone in
parseDateRangeInServerTimezone and keep endTime logic (add 24h) intact when
fallback is applied.
src/app/[locale]/dashboard/_components/user/user-key-table-row.tsx
Outdated
Show resolved
Hide resolved
tests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsx
Show resolved
Hide resolved
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.
Code Review Summary
This release PR (v0.4.3) introduces multiple well-implemented features including leaderboard filters, user statistics reset functionality, timezone-aware date parsing, and comprehensive i18n improvements. The code quality is good with appropriate error handling and test coverage.
PR Size: XL
- Lines changed: 2,785 (2,315 additions + 470 deletions)
- Files changed: 90
Note: For PRs of this size, consider splitting into smaller feature-specific PRs in the future for easier review and rollback isolation.
Issues Found
| Category | Critical | High | Medium | Low |
|---|---|---|---|---|
| Logic/Bugs | 0 | 0 | 0 | 0 |
| Security | 0 | 0 | 0 | 0 |
| Error Handling | 0 | 0 | 0 | 0 |
| Types | 0 | 0 | 0 | 0 |
| Comments/Docs | 0 | 0 | 0 | 0 |
| Tests | 0 | 0 | 0 | 0 |
| Simplification | 0 | 0 | 0 | 0 |
Review Coverage
- Logic and correctness - Clean
- Security (OWASP Top 10) - Clean
- SQL queries use parameterized Drizzle ORM queries
- User input is properly sanitized in leaderboard filters
- Auth checks present on sensitive operations
- Error handling - Clean
resetUserAllStatisticshas proper error boundaries with logging- Redis failures don't block DB operations (intentional design)
- Login route properly handles i18n for error messages
- Type safety - Clean
- New
todayTokensfield properly typed inUserKeyDisplay totalInputTokensadded with appropriate deprecation oftotalTokens
- New
- Documentation accuracy - Clean
- Comment update for 'inherit' mode correctly reflects passthrough behavior
- Test coverage - Adequate
- New tests:
users-reset-all-statistics.test.ts(246 lines) - New tests:
scan-helper.test.ts(39 lines) - New tests:
async-params-layouts.test.tsx(101 lines) - New tests:
big-screen-metadata-keys.test.ts(18 lines)
- New tests:
- Code clarity - Good
Highlights
Well-implemented features:
- User Statistics Reset: Proper admin-only permission check, comprehensive Redis cleanup with scan-based pattern matching, graceful degradation when Redis unavailable
- Leaderboard Filters: Clean SQL construction with OR logic for tags/groups, proper cache key generation for filtered results
- Timezone-aware Statistics: Correct use of
fromZonedTimefor server timezone parsing - i18n Improvements: Template variable syntax fixed (
${var}->{var}), new translations for all 5 languages
Code quality observations:
- Error handling follows consistent patterns across new code
- Dynamic imports used appropriately for code splitting
- Test coverage includes edge cases (Redis unavailable, partial failures)
Automated review by Claude AI
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.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/lib/special-attributes/index.ts`:
- Line 29: 更新文档对 'inherit' 行的描述以反映实际逻辑:说明“Passthrough client headers without
modification”仅在模型支持 1M 上下文时生效(参见 shouldApplyContext1m 的检查),并明确对于不支持 1M
的模型仍会触发相应的校验/行为;在 special-attributes 文本中替换该句为包含“仅对支持 1M 模型生效”或等效表述,以避免误导。
src/actions/my-usage.ts
Outdated
|
|
||
| return { | ||
| startTime: Number.isFinite(parsedStart) ? parsedStart : undefined, | ||
| endTime: Number.isFinite(parsedEnd) ? parsedEnd + 24 * 60 * 60 * 1000 : undefined, |
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.
[HIGH] [LOGIC-BUG] endTime is not “next day midnight” in DST timezones
Evidence (src/actions/my-usage.ts:44):
endTime: Number.isFinite(parsedEnd) ? parsedEnd + 24 * 60 * 60 * 1000 : undefined,This contradicts the stated contract (src/actions/my-usage.ts:29-30) that it returns “endTime as next day midnight (exclusive upper bound)”.
Why this is a problem: For TZ values with DST transitions, “next day midnight” is not always +24h after midnight. This can shift the exclusive upper bound and produce incorrect log/stat ranges around DST boundaries.
Suggested fix (advance the calendar date, then re-parse in the target timezone):
const nextDay = (dateStr: string): string | undefined => {
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dateStr);
if (!m) return undefined;
const d = new Date(Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3])));
d.setUTCDate(d.getUTCDate() + 1);
return d.toISOString().slice(0, 10);
};
const endExclusive = endDate ? nextDay(endDate) : undefined;
const parsedEndExclusive = endExclusive
? fromZonedTime(`${endExclusive}T00:00:00`, timezone).getTime()
: Number.NaN;
return {
startTime: Number.isFinite(parsedStart) ? parsedStart : undefined,
endTime: Number.isFinite(parsedEndExclusive) ? parsedEndExclusive : undefined,
};
src/app/api/auth/login/route.ts
Outdated
| const t = await getTranslations({ locale, namespace: "auth.errors" }); | ||
| return NextResponse.json({ error: t("serverError") }, { status: 500 }); | ||
| } catch { | ||
| return NextResponse.json({ error: "Server error" }, { status: 500 }); |
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.
[HIGH] [ERROR-SILENT] Bare catch {} hides translation failures + hardcodes a user-facing error
Evidence (src/app/api/auth/login/route.ts:70-72):
} catch {
return NextResponse.json({ error: "Server error" }, { status: 500 });
}Why this is a problem:
- The bare
catch {}drops the underlyinggetTranslations(...)failure on the floor (no log entry), which makes production debugging harder. "Server error"is a hardcoded user-facing string, while the rest of this handler is localized viaauth.errors.
Suggested fix (capture + log, and fall back to defaultLocale translations instead of hardcoded text):
} catch (translationError) {
logger.warn("Login error: failed to load auth.errors translations", {
locale,
error: translationError instanceof Error ? translationError.message : String(translationError),
});
const tDefault = await getTranslations({ locale: defaultLocale, namespace: "auth.errors" });
return NextResponse.json({ error: tDefault("serverError") }, { status: 500 });
}
src/actions/key-quota.ts
Outdated
|
|
||
| // Allow admin to view any key, users can only view their own keys | ||
| if (session.user.role !== "admin" && keyRow.userId !== session.user.id) { | ||
| return { ok: false, error: "Access denied" }; |
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.
[MEDIUM] [STANDARD-VIOLATION] User-facing permission error is hardcoded (not i18n)
Evidence (src/actions/key-quota.ts:46):
return { ok: false, error: "Access denied" };This error is displayed directly in the UI (the client uses toast.error(res.error || ...)).
Suggested fix (return an i18n-able error code + localized message):
import { getTranslations } from "next-intl/server";
import { ERROR_CODES } from "@/lib/utils/error-messages";
// ...
const tError = await getTranslations("errors");
if (session.user.role !== "admin" && keyRow.userId !== session.user.id) {
return {
ok: false,
error: tError("PERMISSION_DENIED"),
errorCode: ERROR_CODES.PERMISSION_DENIED,
};
}| providerType = providerTypeParam; | ||
| } | ||
|
|
||
| const parseListParam = (param: string | null): string[] | undefined => { |
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.
[HIGH] [TEST-MISSING-CRITICAL] New userTags/userGroups filtering lacks unit coverage
Evidence (src/app/api/leaderboard/route.ts:130-154):
const parseListParam = (param: string | null): string[] | undefined => {
// split/trim/filter/slice(0, 20)
};
if (scope === "user") {
userTags = parseListParam(userTagsParam);
userGroups = parseListParam(userGroupsParam);
}
// ...
{ providerType, userTags, userGroups }Why this is a problem: This introduces new request surface area (parsing + trimming + max=20 behavior + scope gating). A small regression here can silently change cache keys and/or leaderboard results.
Suggested fix (extract a pure helper + add unit tests for trimming/empty handling/max cap):
// src/lib/utils/parse-comma-list.ts
export function parseCommaList(param: string | null, max = 20): string[] | undefined {
const items = (param ?? "")
.split(",")
.map((s) => s.trim())
.filter(Boolean)
.slice(0, max);
return items.length ? items : undefined;
}// tests/unit/utils/parse-comma-list.test.ts
import { describe, expect, it } from "vitest";
import { parseCommaList } from "@/lib/utils/parse-comma-list";
describe("parseCommaList", () => {
it("trims, drops empties, caps at max", () => {
expect(parseCommaList(" a, ,b ,c,")) .toEqual(["a", "b", "c"]);
expect(
parseCommaList(Array.from({ length: 25 }, (_, i) => String(i)).join(","))?.length
).toBe(20);
});
});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.
Code Review Summary
This is an XL release PR that mixes UI, API, repository, Redis-cache, and deployment/doc changes in a single diff. The breadth makes it hard to validate behavior changes (especially around date/time handling and caching) and increases rollback risk if any part regresses.
PR Size: XL
- Lines changed: 2785
- Files changed: 90
- Split suggestions (recommended):
- Leaderboard filtering (API
userTags/userGroups+ Redis cache key changes + UI filters) - My Usage stats changes (token breakdown + timezone parsing)
- Admin/user management changes (reset user statistics + dialogs + related tests)
- Docs + i18n message updates + Docker/deploy changes
- Leaderboard filtering (API
Issues Found
| Category | Critical | High | Medium | Low |
|---|---|---|---|---|
| Logic/Bugs | 0 | 1 | 1 | 0 |
| Security | 0 | 0 | 0 | 0 |
| Error Handling | 0 | 1 | 0 | 0 |
| Types | 0 | 0 | 0 | 0 |
| Comments/Docs | 0 | 0 | 0 | 0 |
| Tests | 0 | 1 | 0 | 0 |
| Simplification | 0 | 0 | 0 | 0 |
Critical Issues (Must Fix)
src/actions/my-usage.ts:44— DST-unsafeendTimecalculation (+24h) breaks “next day midnight” contract (Confidence: 95)
High Priority Issues (Should Fix)
src/app/api/auth/login/route.ts:70-72— barecatch {}hides translation failures and falls back to hardcoded user-facing text (Confidence: 90)src/actions/key-quota.ts:46— user-facing permission error is hardcoded (not i18n), shown directly via toast (Confidence: 80)src/app/api/leaderboard/route.ts:130— newuserTags/userGroupsparsing/filtering behavior lacks unit coverage (Confidence: 85)
Review Coverage
- Logic and correctness
- Security (OWASP Top 10)
- Error handling
- Type safety
- Documentation accuracy
- Test coverage
- Code clarity
Automated review by Codex AI
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.
PR identified and reviewed: #628 “release v0.4.3” (base: main, head: dev)
- Applied PR size label:
size/XL(additions: 2315, deletions: 470, files changed: 90; total lines changed: 2785) - Posted 4 inline review comments (all anchored to changed lines)
- Submitted the required summary review comment via
gh pr review
Inline issues filed (with concrete fix suggestions in the PR comments):
src/actions/my-usage.ts:44— DST-unsafeendTimecalculation (+ 24h) can produce incorrect date-range filtering around DST boundariessrc/app/api/auth/login/route.ts:71— barecatch {}swallows i18n load failures and falls back to hardcoded user-facing"Server error"src/actions/key-quota.ts:46— user-facing permission error"Access denied"is hardcoded (not i18n) and is shown directly via toastsrc/app/api/leaderboard/route.ts:130— newuserTags/userGroupsparsing/filtering behavior lacks unit coverage; suggested extracting a pure helper + adding unit tests
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
🧪 测试结果
总体结果: ✅ 所有测试通过 |
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.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
scripts/run-e2e-tests.sh (1)
72-82:set -e会导致测试失败时跳过清理
bun run test非零会直接退出脚本,导致服务器不被停止。建议捕获退出码后继续清理。🧯 建议修改
# 运行 E2E 测试 -bun run test tests/e2e/ - -TEST_EXIT_CODE=$? +TEST_EXIT_CODE=0 +bun run test tests/e2e/ || TEST_EXIT_CODE=$?
🤖 Fix all issues with AI agents
In `@scripts/run-e2e-tests.sh`:
- Around line 21-30: The script currently checks only PostgreSQL using
COMPOSE_FILE and the docker compose ps command; update the health-check logic to
also verify Redis (e.g., test both "postgres" and "redis" containers via docker
compose -f "$COMPOSE_FILE" ps -q --status running) and if either is not running,
start the missing services with docker compose -f "$COMPOSE_FILE" up -d postgres
redis and wait (sleep) until both are up; ensure the check uses the same
COMPOSE_FILE variable and the same commands invoked around the existing echo
"等待数据库启动..." and sleep 5 so Redis is started or detected alongside Postgres.
In `@src/app/api/auth/login/route.ts`:
- Around line 34-53: getAuthErrorTranslations currently returns null if both
getTranslations calls fail, which leads callers using t?.("key") to emit
undefined; change the final fallback so that getAuthErrorTranslations (not
callers) returns a minimal hard-coded translations object (e.g., keys your code
expects such as "invalid_credentials" and a generic "unknown_error" or a default
function that always returns a generic message) instead of null; keep the
existing warning/error logs but replace the final return null with this fallback
object (references: getAuthErrorTranslations, getTranslations, defaultLocale and
callers that use t?.("key")) so consumers always receive a string for expected
keys.
♻️ Duplicate comments (2)
src/app/[locale]/dashboard/_components/user/user-key-table-row.tsx (1)
221-222: 注释语言已修正。俄语注释已替换为中文,与代码库风格保持一致。
src/actions/my-usage.ts (1)
32-72: 需要校验 TZ,否则 fromZonedTime 可能抛异常。
Line 36 若 TZ 为空或非法,会导致 RangeError 并使接口失败。建议提供有效时区兜底或 try/catch。修改建议
- const timezone = getEnvConfig().TZ; + const rawTimezone = getEnvConfig().TZ; + const fallbackTimezone = rawTimezone && rawTimezone.trim() ? rawTimezone : "UTC"; + let timezone = fallbackTimezone; + try { + new Intl.DateTimeFormat("en-US", { timeZone: fallbackTimezone }); + } catch { + timezone = "UTC"; + }
🧹 Nitpick comments (3)
scripts/cleanup-test-users.sh (1)
1-4: 建议添加错误处理以提高脚本健壮性。脚本缺少错误处理机制。如果
docker compose命令失败(例如容器未运行或 compose 文件不存在),脚本会静默继续执行,可能导致误导性的输出。♻️ 建议的改进
#!/bin/bash # 清理测试用户脚本 +set -e +set -o pipefail + +# 检查 compose 文件是否存在 +if [ ! -f "docker-compose.dev.yaml" ]; then + echo "错误: docker-compose.dev.yaml 文件不存在" + exit 1 +fi echo "检查测试用户数量..."tests/unit/dashboard/edit-key-form-expiry-clear-ui.test.tsx (1)
53-59: 建议在 unmount 时清理 QueryClientReact Query 可能保留缓存或定时器;在测试释放时清理可降低句柄泄漏或跨用例污染风险。
可选修复示例
return { container, unmount: () => { act(() => root.unmount()); + queryClient.clear(); container.remove(); }, };src/actions/key-quota.ts (1)
31-49: 确认 Server Action 错误信息是否要在此 PR 引入 i18n。
当前 src/actions 里仍有统一错误文案策略,单点切换可能造成错误提示风格不一致。Based on learnings, 建议确认是否要统一推进或在本 PR 暂缓。
| COMPOSE_FILE="docker-compose.dev.yaml" | ||
|
|
||
| echo "检查数据库连接..." | ||
| if [ -n "$(docker compose -f "$COMPOSE_FILE" ps -q --status running postgres 2>/dev/null)" ]; then | ||
| echo "PostgreSQL 已运行" | ||
| else | ||
| echo "❌ PostgreSQL 未运行,正在启动..." | ||
| docker compose up -d postgres redis | ||
| echo "⏳ 等待数据库启动..." | ||
| echo "PostgreSQL 未运行,正在启动..." | ||
| docker compose -f "$COMPOSE_FILE" up -d postgres redis | ||
| echo "等待数据库启动..." | ||
| sleep 5 |
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.
需要同时检查 Redis 是否运行
当前只判断 postgres,若 Redis 未运行会导致 E2E 失败且难以定位。建议同时检查两者或直接补启动。
🔧 建议修改
-COMPOSE_FILE="docker-compose.dev.yaml"
+COMPOSE_FILE="docker-compose.dev.yaml"
echo "检查数据库连接..."
-if [ -n "$(docker compose -f "$COMPOSE_FILE" ps -q --status running postgres 2>/dev/null)" ]; then
- echo "PostgreSQL 已运行"
+POSTGRES_RUNNING=$(docker compose -f "$COMPOSE_FILE" ps -q --status running postgres 2>/dev/null)
+REDIS_RUNNING=$(docker compose -f "$COMPOSE_FILE" ps -q --status running redis 2>/dev/null)
+if [ -n "$POSTGRES_RUNNING" ] && [ -n "$REDIS_RUNNING" ]; then
+ echo "PostgreSQL/Redis 已运行"
else
- echo "PostgreSQL 未运行,正在启动..."
+ echo "数据库未完全运行,正在启动..."
docker compose -f "$COMPOSE_FILE" up -d postgres redis
echo "等待数据库启动..."
sleep 5
fi🤖 Prompt for AI Agents
In `@scripts/run-e2e-tests.sh` around lines 21 - 30, The script currently checks
only PostgreSQL using COMPOSE_FILE and the docker compose ps command; update the
health-check logic to also verify Redis (e.g., test both "postgres" and "redis"
containers via docker compose -f "$COMPOSE_FILE" ps -q --status running) and if
either is not running, start the missing services with docker compose -f
"$COMPOSE_FILE" up -d postgres redis and wait (sleep) until both are up; ensure
the check uses the same COMPOSE_FILE variable and the same commands invoked
around the existing echo "等待数据库启动..." and sleep 5 so Redis is started or
detected alongside Postgres.
| async function getAuthErrorTranslations(locale: Locale) { | ||
| try { | ||
| return await getTranslations({ locale, namespace: "auth.errors" }); | ||
| } catch (error) { | ||
| logger.warn("Login route: failed to load auth.errors translations", { | ||
| locale, | ||
| error: error instanceof Error ? error.message : String(error), | ||
| }); | ||
|
|
||
| try { | ||
| return await getTranslations({ locale: defaultLocale, namespace: "auth.errors" }); | ||
| } catch (fallbackError) { | ||
| logger.error("Login route: failed to load default auth.errors translations", { | ||
| locale: defaultLocale, | ||
| error: fallbackError instanceof Error ? fallbackError.message : String(fallbackError), | ||
| }); | ||
| return null; | ||
| } | ||
| } | ||
| } |
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.
翻译加载与兜底逻辑完善,但需处理极端失败场景。
函数实现了合理的二级兜底(先尝试用户 locale,失败后回退到 defaultLocale),并正确记录了警告和错误日志。
但当 getAuthErrorTranslations 返回 null 时(两次翻译加载均失败),后续使用 t?.("key") 会返回 undefined,导致错误响应为 { error: undefined }。虽然这是极端边缘情况,但会导致客户端无法获取有意义的错误信息。
建议在函数内返回一个硬编码的最终兜底对象,或在调用处检查 t 是否为 null 并返回通用 HTTP 状态码。
建议的修复方案
async function getAuthErrorTranslations(locale: Locale) {
try {
return await getTranslations({ locale, namespace: "auth.errors" });
} catch (error) {
logger.warn("Login route: failed to load auth.errors translations", {
locale,
error: error instanceof Error ? error.message : String(error),
});
try {
return await getTranslations({ locale: defaultLocale, namespace: "auth.errors" });
} catch (fallbackError) {
logger.error("Login route: failed to load default auth.errors translations", {
locale: defaultLocale,
error: fallbackError instanceof Error ? fallbackError.message : String(fallbackError),
});
- return null;
+ // 最终兜底:返回一个简单的翻译函数,避免 undefined 错误消息
+ return (key: string) => {
+ const fallbacks: Record<string, string> = {
+ apiKeyRequired: "API key is required",
+ apiKeyInvalidOrExpired: "Invalid or expired API key",
+ serverError: "Server error",
+ };
+ return fallbacks[key] ?? "Unknown error";
+ };
}
}
}🤖 Prompt for AI Agents
In `@src/app/api/auth/login/route.ts` around lines 34 - 53,
getAuthErrorTranslations currently returns null if both getTranslations calls
fail, which leads callers using t?.("key") to emit undefined; change the final
fallback so that getAuthErrorTranslations (not callers) returns a minimal
hard-coded translations object (e.g., keys your code expects such as
"invalid_credentials" and a generic "unknown_error" or a default function that
always returns a generic message) instead of null; keep the existing
warning/error logs but replace the final return null with this fallback object
(references: getAuthErrorTranslations, getTranslations, defaultLocale and
callers that use t?.("key")) so consumers always receive a string for expected
keys.
* 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)
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.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/app/v1/_lib/codex/chat-completions-handler.ts (1)
49-78: OpenAI 缺少 model 与转换失败分支仍未附加 sessionId这两个错误路径直接返回 Response,导致 header 或 message 未包含 sessionId,与其他错误分支不一致。建议也走统一包装。
建议修复
if (!openAIRequest.model) { - return new Response( + const response = new Response( JSON.stringify({ error: { message: "Invalid request: model is required", type: "invalid_request_error", code: "missing_required_fields", }, }), { status: 400, headers: { "Content-Type": "application/json" } } ); + return await attachSessionIdToErrorResponse(session.sessionId, response); } @@ - return new Response( + const response = new Response( JSON.stringify({ error: { message: "Failed to transform request format", type: "invalid_request_error", code: "transformation_error", }, }), { status: 400, headers: { "Content-Type": "application/json" } } ); + return await attachSessionIdToErrorResponse(session.sessionId, response);Also applies to: 125-138
messages/ru/dashboard.json (1)
63-103: 统一 session 术语以避免混淆Line 63 和 Line 102 使用了“ID сессии”,但本文件中其他位置(如 session 详情)多使用“сеанс/сеанса”。建议统一为同一术语,避免界面用词不一致。
修改建议
- "sessionId": "ID сессии", + "sessionId": "ID сеанса", ... - "sessionId": "ID сессии", + "sessionId": "ID сеанса",
🤖 Fix all issues with AI agents
In `@src/app/`[locale]/dashboard/logs/_components/usage-logs-table.test.tsx:
- Around line 194-238: The test "copies sessionId on click and shows toast"
mutates global descriptors (navigator.clipboard and window.isSecureContext) and
leaves toastMocks state modified; capture the original descriptors for
navigator.clipboard and window.isSecureContext before overriding, then restore
them after the test (use a try/finally or afterEach) so other tests aren't
affected, and also reset toastMocks (e.g., clear mock call history) before/after
the test; reference the test name, navigator.clipboard, window.isSecureContext,
writeText, and toastMocks to locate where to save/restore and clear.
🧹 Nitpick comments (8)
tests/unit/repository/escape-like.test.ts (1)
5-22: 测试覆盖充分,结构清晰。测试用例覆盖了普通字符串、单独特殊字符、组合输入和空字符串,符合 Vitest 测试规范并使用了
@/路径别名。可选:考虑补充连续特殊字符的边界测试(如
"%%"→"\\%\\%"),进一步提升健壮性。src/lib/utils/clipboard.ts (1)
14-33: 建议:确保 textarea 在异常时也能被移除。如果
textarea.select()或后续操作抛出异常,已添加到 DOM 的 textarea 可能不会被移除。建议使用try-finally确保清理:♻️ 建议的修复
function tryCopyViaExecCommand(text: string): boolean { if (typeof document === "undefined" || !document.body) return false; + const textarea = document.createElement("textarea"); try { - const textarea = document.createElement("textarea"); textarea.value = text; textarea.setAttribute("readonly", ""); textarea.style.position = "absolute"; textarea.style.left = "-9999px"; document.body.appendChild(textarea); textarea.select(); - const ok = document.execCommand?.("copy") ?? false; - document.body.removeChild(textarea); - return ok; + return document.execCommand?.("copy") ?? false; } catch { return false; + } finally { + if (textarea.parentNode) { + document.body.removeChild(textarea); + } } }src/app/v1/_lib/proxy/error-session-id.ts (1)
10-56: 实现完整,但建议处理 Content-Length 头。整体逻辑正确:
- 正确使用
response.clone().text()保留原始响应体- 类型守卫充分确保 JSON 结构安全
- 对
text/event-stream和非 JSON 内容类型有适当处理潜在问题:在第 49 行修改 JSON 后重建响应时,如果原响应包含
Content-Length头,该值可能与新的 body 长度不一致。建议移除或重新计算该头:♻️ 建议的修复
try { const parsed = JSON.parse(text) as unknown; if ( parsed && typeof parsed === "object" && "error" in parsed && parsed.error && typeof parsed.error === "object" && "message" in parsed.error && typeof (parsed.error as { message?: unknown }).message === "string" ) { const p = parsed as { error: { message: string } } & Record<string, unknown>; p.error.message = attachSessionIdToErrorMessage(sessionId, p.error.message); - return new Response(JSON.stringify(p), { status: response.status, headers }); + const newBody = JSON.stringify(p); + headers.delete("content-length"); // 让浏览器/fetch 自动计算 + return new Response(newBody, { status: response.status, headers }); } } catch { // best-effort: keep original response body }src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx (1)
67-77: 复制失败时缺少用户反馈。当
copyTextToClipboard返回false时(例如浏览器不支持或用户拒绝权限),用户没有收到任何反馈。建议添加失败提示以改善用户体验。建议的修复
const handleCopySessionIdClick = useCallback( (event: MouseEvent<HTMLButtonElement>) => { const sessionId = event.currentTarget.dataset.sessionId; if (!sessionId) return; void copyTextToClipboard(sessionId).then((ok) => { - if (ok) toast.success(t("actions.copied")); + if (ok) { + toast.success(t("actions.copied")); + } else { + toast.error(t("actions.copyFailed")); + } }); }, [t] );tests/unit/dashboard-logs-filters-time-range.test.tsx (1)
82-85: 硬编码的按钮文本可能导致测试脆弱。测试使用硬编码的
"Apply Filter"来查找按钮,如果 i18n 翻译变化或在非英语环境下运行测试,可能会导致测试失败。建议使用data-testid属性来定位按钮,使测试更健壮。建议的修复
在
UsageLogsFilters组件的 Apply 按钮上添加data-testid:<Button data-testid="apply-filter-btn" ...>然后在测试中:
- const applyBtn = Array.from(container.querySelectorAll("button")).find( - (b) => (b.textContent || "").trim() === "Apply Filter" - ); + const applyBtn = container.querySelector("[data-testid='apply-filter-btn']");src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx (1)
72-82: 与usage-logs-table.tsx存在重复代码。
handleCopySessionIdClick的实现与usage-logs-table.tsx中的完全相同。建议提取为共享的 hook 或工具函数,以减少代码重复并统一行为。同时,与另一个文件一样,复制失败时缺少用户反馈。
建议提取共享 hook
创建
src/hooks/use-copy-session-id.ts:import { type MouseEvent, useCallback } from "react"; import { toast } from "sonner"; import { useTranslations } from "next-intl"; import { copyTextToClipboard } from "@/lib/utils/clipboard"; export function useCopySessionId() { const t = useTranslations("dashboard"); return useCallback( (event: MouseEvent<HTMLButtonElement>) => { const sessionId = event.currentTarget.dataset.sessionId; if (!sessionId) return; void copyTextToClipboard(sessionId).then((ok) => { if (ok) { toast.success(t("actions.copied")); } else { toast.error(t("actions.copyFailed")); } }); }, [t] ); }然后在两个表格组件中使用:
const handleCopySessionIdClick = useCopySessionId();tests/unit/dashboard-logs-sessionid-suggestions-ui.test.tsx (1)
256-321: Provider 作用域变化测试有效。验证了当 provider 选择变化时,建议列表会使用新的 providerId 重新加载,确保数据范围正确。
建议在
describe块中添加afterEach清理,避免测试间状态污染:♻️ 可选改进
describe("UsageLogsFilters sessionId suggestions", () => { afterEach(() => { vi.clearAllMocks(); vi.useRealTimers(); document.body.innerHTML = ""; }); // ... tests });src/app/[locale]/dashboard/logs/_utils/logs-query.ts (1)
62-93: sessionId 的重复 trim 可以移除第 69 行对
sessionId再次调用trim(),但如果输入来自parseLogsUrlFilters,该值已经过修剪。可考虑移除冗余操作以保持一致性,但这不影响正确性。可选优化
- const sessionId = filters.sessionId?.trim(); - if (sessionId) query.set("sessionId", sessionId); + if (filters.sessionId) query.set("sessionId", filters.sessionId);
| test("copies sessionId on click and shows toast", async () => { | ||
| const writeText = vi.fn(async () => {}); | ||
| Object.defineProperty(navigator, "clipboard", { | ||
| value: { writeText }, | ||
| configurable: true, | ||
| }); | ||
| Object.defineProperty(window, "isSecureContext", { | ||
| value: true, | ||
| configurable: true, | ||
| }); | ||
|
|
||
| const container = document.createElement("div"); | ||
| document.body.appendChild(container); | ||
|
|
||
| const root = createRoot(container); | ||
| await act(async () => { | ||
| root.render( | ||
| <UsageLogsTable | ||
| logs={[makeLog({ id: 1, sessionId: "session_test" })]} | ||
| total={1} | ||
| page={1} | ||
| pageSize={50} | ||
| onPageChange={() => {}} | ||
| isPending={false} | ||
| /> | ||
| ); | ||
| }); | ||
|
|
||
| const sessionBtn = Array.from(container.querySelectorAll("button")).find((b) => | ||
| (b.textContent ?? "").includes("session_test") | ||
| ); | ||
| expect(sessionBtn).not.toBeUndefined(); | ||
|
|
||
| await act(async () => { | ||
| sessionBtn?.dispatchEvent(new MouseEvent("click", { bubbles: true })); | ||
| await Promise.resolve(); | ||
| }); | ||
|
|
||
| expect(writeText).toHaveBeenCalledWith("session_test"); | ||
| expect(toastMocks.success).toHaveBeenCalledWith("actions.copied"); | ||
|
|
||
| await act(async () => { | ||
| root.unmount(); | ||
| }); | ||
| container.remove(); |
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.
测试内覆盖全局对象建议清理以防串扰
Line 196-203 覆盖了 navigator.clipboard 和 window.isSecureContext,若不恢复可能影响同文件或后续测试。建议在测试中恢复原始描述符,并清理 toastMocks 调用记录。
修改建议
+import { beforeEach, afterEach } from "vitest";
+
+const clipboardDescriptor = Object.getOwnPropertyDescriptor(navigator, "clipboard");
+const secureContextDescriptor = Object.getOwnPropertyDescriptor(window, "isSecureContext");
+
+beforeEach(() => {
+ toastMocks.success.mockClear();
+ toastMocks.error.mockClear();
+});
+
+afterEach(() => {
+ if (clipboardDescriptor) {
+ Object.defineProperty(navigator, "clipboard", clipboardDescriptor);
+ } else {
+ // `@ts-expect-error` cleanup for test environment
+ delete (navigator as any).clipboard;
+ }
+ if (secureContextDescriptor) {
+ Object.defineProperty(window, "isSecureContext", secureContextDescriptor);
+ } else {
+ // `@ts-expect-error` cleanup for test environment
+ delete (window as any).isSecureContext;
+ }
+});🤖 Prompt for AI Agents
In `@src/app/`[locale]/dashboard/logs/_components/usage-logs-table.test.tsx around
lines 194 - 238, The test "copies sessionId on click and shows toast" mutates
global descriptors (navigator.clipboard and window.isSecureContext) and leaves
toastMocks state modified; capture the original descriptors for
navigator.clipboard and window.isSecureContext before overriding, then restore
them after the test (use a try/finally or afterEach) so other tests aren't
affected, and also reset toastMocks (e.g., clear mock call history) before/after
the test; reference the test name, navigator.clipboard, window.isSecureContext,
writeText, and toastMocks to locate where to save/restore and clear.
Summary
Release v0.4.3 containing multiple feature enhancements, bug fixes, and i18n improvements across the Claude Code Hub platform.
Highlights
Features
Bug Fixes
HOSTNAME=0.0.0.0in Dockerfile to fix container accessibility on WindowsCOMPOSE_PROJECT_NAMEfor multi-user isolationi18n
Related Issues
Changes
Core Changes
src/repository/leaderboard.ts- Added user tag/group filter support with OR logicsrc/actions/users.ts- AddedresetUserAllStatisticsaction for clearing user datasrc/actions/my-usage.ts- Added timezone-aware date parsing, cache token fieldssrc/lib/redis/scan-helper.ts- New utility for cursor-based Redis key scanningsrc/lib/redis/leaderboard-cache.ts- Extended cache keys for filter parametersDeployment
deploy/Dockerfile- AddedENV HOSTNAME=0.0.0.0docker-compose.yaml- Removed hardcoded container_name, addedname:fieldUI/Dashboard
src/app/[locale]/dashboard/leaderboard/- Added tag/group filter UIsrc/app/[locale]/dashboard/users/- Added statistics reset button, token displaysrc/components/ui/relative-time.tsx- Added "short" format with i18ni18n (all 5 languages)
relativeTimeShorttranslations in common.jsonpageTitle/pageDescriptionin bigScreen.json${var}->{var}) in usage.jsonTesting
Automated Tests
users-reset-all-statistics.test.ts(246 lines)scan-helper.test.ts(39 lines)async-params-layouts.test.tsx(101 lines)big-screen-metadata-keys.test.ts(18 lines)Included PRs
Checklist
Description enhanced by Claude AI