feat(leaderboard): add user tag and group filters for user ranking#607
feat(leaderboard): add user tag and group filters for user ranking#607
Conversation
) 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
📝 WalkthroughWalkthrough在排行榜功能中增加了按用户标签与用户组的筛选:更新了多语言本地化占位符、前端 UI(TagInput 与状态)、API 路由解析新查询参数、Redis 缓存键与查询分支以包含用户过滤,以及仓储层 SQL 查询以按标签/分组过滤用户排行数据。 Changes
Estimated code review effort🎯 3 (中等) | ⏱️ ~40 分钟 功能概览该变更为排行榜添加了基于用户标签和用户组的筛选功能。涵盖五种语言的本地化字符串、前端UI组件增强、API参数解析、缓存层更新及数据库查询逻辑扩展,实现完整的端到端筛选功能链路。 变更内容
代码审查工作量评估🎯 3 (中等) | ⏱️ ~40 分钟 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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 significantly enhances the leaderboard functionality by introducing advanced filtering options for user rankings. Administrators can now precisely narrow down user data based on custom tags and groups, providing a more powerful tool for analysis and management. This change improves the utility and flexibility of the leaderboard without affecting general user experience, as the feature is restricted to admin users. 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 filtering capabilities for the user leaderboard by tags and groups, a valuable enhancement for administrators. The changes span the full stack from the database repository to the frontend UI, including caching and API layers. The implementation is well-structured. My review focuses on improving database query performance for the new filters and enhancing code maintainability by refactoring duplicated logic in the API route and frontend components.
| <div className="flex flex-wrap gap-4 mb-4"> | ||
| <div className="flex-1 min-w-[200px] max-w-[300px]"> | ||
| <TagInput | ||
| value={userTagFilters} | ||
| onChange={setUserTagFilters} | ||
| placeholder={t("filters.userTagsPlaceholder")} | ||
| disabled={loading} | ||
| maxTags={20} | ||
| clearable | ||
| /> | ||
| </div> | ||
| <div className="flex-1 min-w-[200px] max-w-[300px]"> | ||
| <TagInput | ||
| value={userGroupFilters} | ||
| onChange={setUserGroupFilters} | ||
| placeholder={t("filters.userGroupsPlaceholder")} | ||
| disabled={loading} | ||
| maxTags={20} | ||
| clearable | ||
| /> | ||
| </div> | ||
| </div> |
There was a problem hiding this comment.
The two TagInput components for user tags and user groups are nearly identical. To reduce duplication and improve maintainability, you can define the filter configurations in an array and render the components in a loop. This makes the code cleaner and easier to extend with more filters in the future.
<div className="flex flex-wrap gap-4 mb-4">
{[
{
value: userTagFilters,
onChange: setUserTagFilters,
placeholder: t("filters.userTagsPlaceholder"),
},
{
value: userGroupFilters,
onChange: setUserGroupFilters,
placeholder: t("filters.userGroupsPlaceholder"),
},
].map((config, index) => (
<div key={index} className="flex-1 min-w-[200px] max-w-[300px]">
<TagInput
value={config.value}
onChange={config.onChange}
placeholder={config.placeholder}
disabled={loading}
maxTags={20}
clearable
/>
</div>
))}
</div>
| let userTags: string[] | undefined; | ||
| let userGroups: string[] | undefined; | ||
| if (scope === "user") { | ||
| if (userTagsParam) { | ||
| userTags = userTagsParam | ||
| .split(",") | ||
| .map((t) => t.trim()) | ||
| .filter((t) => t.length > 0) | ||
| .slice(0, 20); | ||
| } | ||
| if (userGroupsParam) { | ||
| userGroups = userGroupsParam | ||
| .split(",") | ||
| .map((g) => g.trim()) | ||
| .filter((g) => g.length > 0) | ||
| .slice(0, 20); | ||
| } | ||
| } |
There was a problem hiding this comment.
The logic for parsing userTagsParam and userGroupsParam is duplicated. To improve maintainability and reduce code duplication, this logic can be extracted into a reusable helper function.
const parseListParams = (param: string | null): string[] | undefined => {
if (param === null) {
return undefined;
}
return param
.split(",")
.map((item) => item.trim())
.filter((item) => item.length > 0)
.slice(0, 20);
};
let userTags: string[] | undefined;
let userGroups: string[] | undefined;
if (scope === "user") {
userTags = parseListParams(userTagsParam);
userGroups = parseListParams(userGroupsParam);
}
src/repository/leaderboard.ts
Outdated
| const tagConditions = normalizedTags.map( | ||
| (tag) => sql`${users.tags} @> ${JSON.stringify([tag])}::jsonb` | ||
| ); |
There was a problem hiding this comment.
For checking the existence of a top-level element in a JSONB array, using the ? operator is more direct and generally more performant than the @> containment operator. This change simplifies the query and better expresses the intent of checking for a tag's presence in the array.
To ensure this query is efficient, a GIN index should be present on the users.tags column.
| const tagConditions = normalizedTags.map( | |
| (tag) => sql`${users.tags} @> ${JSON.stringify([tag])}::jsonb` | |
| ); | |
| const tagConditions = normalizedTags.map( | |
| (tag) => sql`${users.tags} ? ${tag}` | |
| ); |
| const groupConditions = normalizedGroups.map( | ||
| (group) => | ||
| sql`${group} = ANY(regexp_split_to_array(coalesce(${users.providerGroup}, ''), '\\s*,\\s*'))` | ||
| ); |
There was a problem hiding this comment.
The use of regexp_split_to_array on the users.providerGroup column for every row can cause significant performance issues on a large users table. This operation is not SARGable, meaning it prevents the database from using an index efficiently and forces a full table scan with a CPU-intensive operation per row.
While functionally correct for the current schema, this approach may not scale well. For a long-term solution, consider normalizing the schema by introducing a dedicated user_groups table and a many-to-many join table between users and groups. This would allow for much more performant indexed lookups.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In @messages/zh-CN/dashboard.json:
- Around line 325-329: The translation for the key "userGroupsPlaceholder" may
be inconsistent—confirm whether the standard term for "group" in the project is
"分组" or "组/用户群组" and update the value of "userGroupsPlaceholder" in
messages/zh-CN/dashboard.json accordingly; locate the object "filters" and
change "userGroupsPlaceholder": "按用户分组筛选..." to the agreed canonical translation
(e.g., "按用户组筛选..." or "按用户群组筛选...") so it matches other occurrences of
group-related keys across the codebase and avoids confusion with other
domain-specific "分组" usages like supplier grouping.
🧹 Nitpick comments (1)
src/lib/redis/leaderboard-cache.ts (1)
299-316: 缓存失效函数未支持用户筛选参数
invalidateLeaderboardCache函数没有接受filters参数,这意味着当使用用户筛选时,无法精确失效特定筛选条件的缓存。目前可能不是问题(因为缓存 TTL 只有 60 秒),但如果将来需要主动失效带筛选条件的缓存,需要更新此函数。
可选的改进(如需要精确失效带筛选条件的缓存)
export async function invalidateLeaderboardCache( period: LeaderboardPeriod, currencyDisplay: string, - scope: LeaderboardScope = "user" + scope: LeaderboardScope = "user", + dateRange?: DateRangeParams, + filters?: LeaderboardFilters ): Promise<void> { const redis = getRedisClient(); if (!redis) { return; } - const cacheKey = buildCacheKey(period, currencyDisplay, scope); + const cacheKey = buildCacheKey(period, currencyDisplay, scope, dateRange, filters);
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to Reviews > Disable Cache setting
📒 Files selected for processing (9)
messages/en/dashboard.jsonmessages/ja/dashboard.jsonmessages/ru/dashboard.jsonmessages/zh-CN/dashboard.jsonmessages/zh-TW/dashboard.jsonsrc/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsxsrc/app/api/leaderboard/route.tssrc/lib/redis/leaderboard-cache.tssrc/repository/leaderboard.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{js,ts,tsx,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Never use emoji characters in any code, comments, or string literals
Files:
src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsxsrc/repository/leaderboard.tssrc/app/api/leaderboard/route.tssrc/lib/redis/leaderboard-cache.ts
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx,js,jsx}: All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text
Use path alias@/to reference files in./src/directory
Format code with Biome using: double quotes, trailing commas, 2-space indent, 100 character line width
Files:
src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsxsrc/repository/leaderboard.tssrc/app/api/leaderboard/route.tssrc/lib/redis/leaderboard-cache.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Prefer named exports over default exports
Files:
src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsxsrc/repository/leaderboard.tssrc/app/api/leaderboard/route.tssrc/lib/redis/leaderboard-cache.ts
src/repository/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use Drizzle ORM for data access in the repository layer
Files:
src/repository/leaderboard.ts
src/app/api/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
API routes should use Hono framework and follow Next.js 16 App Router conventions
Files:
src/app/api/leaderboard/route.ts
🧠 Learnings (2)
📚 Learning: 2026-01-05T03:02:14.502Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 539
File: src/app/[locale]/dashboard/_components/user/user-key-table-row.tsx:66-66
Timestamp: 2026-01-05T03:02:14.502Z
Learning: In the claude-code-hub project, the translations.actions.addKey field in UserKeyTableRowProps is defined as optional for backward compatibility, but all actual callers in the codebase provide the complete translations object. The field has been added to all 5 locale files (messages/{locale}/dashboard.json).
Applied to files:
messages/zh-CN/dashboard.json
📚 Learning: 2026-01-05T03:01:39.354Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 539
File: src/types/user.ts:158-170
Timestamp: 2026-01-05T03:01:39.354Z
Learning: In TypeScript interfaces, explicitly document and enforce distinct meanings for null and undefined. Example: for numeric limits like limitTotalUsd, use 'number | null | undefined' when null signifies explicitly unlimited (e.g., matches DB schema or special UI logic) and undefined signifies 'inherit default'. This pattern should be consistently reflected in type definitions across related fields to preserve semantic clarity between database constraints and UI behavior.
Applied to files:
src/repository/leaderboard.tssrc/app/api/leaderboard/route.tssrc/lib/redis/leaderboard-cache.ts
🧬 Code graph analysis (3)
src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx (1)
src/components/ui/tag-input.tsx (1)
TagInput(45-430)
src/app/api/leaderboard/route.ts (1)
src/lib/redis/leaderboard-cache.ts (1)
getLeaderboardWithCache(204-291)
src/lib/redis/leaderboard-cache.ts (1)
src/repository/leaderboard.ts (6)
UserLeaderboardFilters(25-30)findCustomRangeLeaderboard(242-248)findDailyLeaderboard(75-80)findWeeklyLeaderboard(97-102)findMonthlyLeaderboard(86-91)findAllTimeLeaderboard(107-112)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
- GitHub Check: pr-review
- GitHub Check: pr-label
- GitHub Check: pr-description
- GitHub Check: 🌐 API Tests
- GitHub Check: Docker Build Test
- GitHub Check: dev-build-deploy
🔇 Additional comments (20)
messages/en/dashboard.json (1)
325-329: 新增 i18n 键路径与 PR 目标一致,LGTM。
leaderboard.filters.*放在leaderboard下(而非leaderboard.permission下)更契合“filters”语义,也更便于 UI 侧按 scope 展示。messages/ja/dashboard.json (1)
324-328: 结构对齐其它 locale,LGTM。
建议仅确认一下产品侧是否更偏好「フィルタ」还是「絞り込み」用词(非阻塞)。messages/ru/dashboard.json (1)
324-328: 新增占位符键与其它语言版本保持一致,LGTM。messages/zh-TW/dashboard.json (1)
325-329: 与其它 locale 对齐,文案清晰,LGTM。src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx (4)
10-10: LGTM!新增的
TagInput导入和userTagFilters/userGroupFilters状态变量定义正确,遵循了文件中的现有模式。Also applies to: 55-56
102-109: LGTM!URL 构建逻辑正确:
- 仅当
scope === "user"时才添加用户筛选参数- 使用
encodeURIComponent正确编码逗号分隔的值- 空数组时不添加参数,避免不必要的 URL 参数
134-134: LGTM!Effect 依赖项正确更新,包含了新的
userTagFilters和userGroupFilters状态,确保筛选条件变化时触发数据重新加载。
383-406: LGTM!TagInput 组件配置合理:
- 仅对管理员 (
isAdmin) 且scope === "user"时显示maxTags={20}与后端限制保持一致- 使用 i18n 获取 placeholder 文本
disabled={loading}在加载时禁用输入clearable属性允许用户清空筛选条件src/app/api/leaderboard/route.ts (3)
78-79: 考虑是否需要在 API 层强制执行管理员权限检查根据 PR 描述,用户标签和用户分组筛选是"admin-only feature"。当前实现仅在前端隐藏了这些筛选控件,但 API 层允许任何有权限访问排行榜的用户使用这些筛选参数。
如果这是预期行为(即筛选参数本身不泄露敏感信息,只是缩小结果范围),当前实现是可以接受的。但如果希望严格限制为管理员专用,建议添加权限检查:
可选的权限检查实现
let userTags: string[] | undefined; let userGroups: string[] | undefined; if (scope === "user") { + // 用户筛选参数仅限管理员使用 + if ((userTagsParam || userGroupsParam) && !isAdmin) { + return NextResponse.json( + { error: "用户标签和分组筛选仅限管理员使用" }, + { status: 403 } + ); + } if (userTagsParam) {请确认是否需要在 API 层限制这些筛选参数仅限管理员使用。
Also applies to: 130-147
155-155: LGTM!筛选参数正确传递给
getLeaderboardWithCache,与src/lib/redis/leaderboard-cache.ts中的LeaderboardFilters接口定义一致。
186-187: LGTM!日志记录正确包含了新的筛选参数,便于调试和审计。
src/lib/redis/leaderboard-cache.ts (4)
47-48: LGTM!
LeaderboardFilters接口正确扩展,包含了userTags和userGroups可选字段,与 API 路由层传入的参数结构保持一致。
65-74: LGTM!缓存键构建逻辑正确:
- 仅当
scope === "user"时才添加用户筛选后缀- 使用
[...filters.userTags].sort()创建副本后排序,避免修改原数组- 排序确保相同筛选条件(不同输入顺序)生成相同的缓存键
106-109: LGTM!
userFilters对象仅在存在有效筛选条件时构建,避免不必要的对象创建和传递。
125-137: LGTM!
userFilters正确传递给所有用户范围的排行榜查询函数(daily/weekly/monthly/allTime),与src/repository/leaderboard.ts中更新的函数签名一致。src/repository/leaderboard.ts (5)
22-30: LGTM!
UserLeaderboardFilters接口定义清晰,JSDoc 注释明确说明了 OR 逻辑的语义。基于 learnings,此接口正确使用了undefined表示"不筛选"的语义。
190-198: 分组筛选逻辑正确但可能存在性能考量使用
regexp_split_to_array对providerGroup字段进行运行时拆分是正确的实现方式,能够处理逗号分隔的多分组值。需要注意:
- 该操作会对每行执行正则表达式拆分,在大数据量下可能影响查询性能
- 如果
providerGroup字段的使用频率较高,未来可考虑将其规范化为独立的关联表当前实现对于预期的数据规模应该是可接受的。
200-206: LGTM!筛选条件的组合逻辑正确实现了 PR 需求中的 OR 语义:
- 当同时提供标签和分组时,用 OR 连接两个条件
- 最终与其他 WHERE 条件(deletedAt、日期等)用 AND 连接
这意味着:匹配任一指定标签 或 匹配任一指定分组的用户都会被包含在结果中。
Also applies to: 226-226
75-79: LGTM!所有公开的排行榜查询函数签名已正确更新,接受可选的
userFilters参数并传递给内部的findLeaderboardWithTimezone函数。保持了向后兼容性。Also applies to: 86-90, 97-101, 107-111, 242-247
175-206: 当前实现已遵循 Drizzle ORM 的参数化安全机制Drizzle ORM 0.44 版本的
sql模板标签已内置参数化处理,能够防止 SQL 注入。Web 搜索确认该版本无已知的 SQL 注入漏洞。具体分析:
- 时区参数(Line 176):来自
getEnvConfig().TZ的环境配置,非用户输入- 标签筛选(Line 185):
JSON.stringify([tag])为标准做法,用户输入经.trim().filter(Boolean)规范化,通过参数化查询保护- 分组筛选(Line 195):用户输入同样经规范化处理,
sql模板会自动参数化代码在当前形式下是安全的,无需修改。
| }, | ||
| "filters": { | ||
| "userTagsPlaceholder": "按用户标签筛选...", | ||
| "userGroupsPlaceholder": "按用户分组筛选..." | ||
| } |
There was a problem hiding this comment.
建议确认「userGroups」中文术语是否应为「用户组/用户群组」而非「用户分组」。
如果系统内其它地方把 group 统一翻译成「分组」,那现在的文案也没问题;主要是避免与「供应商分组」等概念混淆。
🤖 Prompt for AI Agents
In @messages/zh-CN/dashboard.json around lines 325 - 329, The translation for
the key "userGroupsPlaceholder" may be inconsistent—confirm whether the standard
term for "group" in the project is "分组" or "组/用户群组" and update the value of
"userGroupsPlaceholder" in messages/zh-CN/dashboard.json accordingly; locate the
object "filters" and change "userGroupsPlaceholder": "按用户分组筛选..." to the agreed
canonical translation (e.g., "按用户组筛选..." or "按用户群组筛选...") so it matches other
occurrences of group-related keys across the codebase and avoids confusion with
other domain-specific "分组" usages like supplier grouping.
There was a problem hiding this comment.
Code Review Summary
This PR adds user tag and group filtering capability to the leaderboard user ranking feature. The implementation is clean, follows existing patterns in the codebase, and properly handles the new filter parameters across all layers (API, cache, repository).
PR Size: S
- Lines changed: 207 (176 additions, 31 deletions)
- Files changed: 9
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
- Filter logic correctly uses OR semantics for tags and groups
- Cache key generation properly sorts filters for consistency
- SQL queries use proper JSONB containment operators for tags
- Security (OWASP Top 10) - Clean
- User input is properly sanitized (trim, filter empty, slice to limit)
- SQL parameters are properly escaped via Drizzle ORM
- Admin-only UI restriction is properly enforced
- Error handling - Clean
- Existing error handling patterns are maintained
- API route has proper try/catch with logging
- Type safety - Clean
- New
UserLeaderboardFiltersinterface is well-defined - Type flow is consistent across all layers
- New
- Documentation accuracy - Clean
- JSDoc comments accurately describe OR logic for filters
- Test coverage - Adequate
- PR body indicates 870 tests passed
- Note: No dedicated unit tests for the new filter logic were added, but this is consistent with existing leaderboard code which also lacks unit tests
- Code clarity - Good
- Implementation follows existing patterns in the codebase
- i18n strings added for all 5 supported languages
Notes
- The filter feature is admin-only and properly gated in the UI
- Cache invalidation function doesn't include filter parameters, but this is acceptable since filtered queries will have different cache keys and will naturally expire
Automated review by Claude AI
- Use JSONB ? operator instead of @> for better performance - Extract parseListParam helper to reduce code duplication
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (1)
src/repository/leaderboard.ts (1)
181-204: 筛选逻辑实现正确,OR 语义符合需求。JSONB
?操作符和regexp_split_to_array的使用是正确的。不过有一点性能上的考量:
providerGroup字段当前没有索引,且regexp_split_to_array在每一行上执行正则分割,当数据量较大时可能影响查询性能。如果providerGroup字段经常用于筛选,可以考虑:
- 将其改为 JSONB 数组类型(与
tags字段一致),以便利用 GIN 索引- 或者在应用层预先获取匹配的用户 ID 列表
当前实现在中小规模数据集上应该没有问题,建议后续监控此查询的执行时间。
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to Reviews > Disable Cache setting
📒 Files selected for processing (2)
src/app/api/leaderboard/route.tssrc/repository/leaderboard.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{js,ts,tsx,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Never use emoji characters in any code, comments, or string literals
Files:
src/repository/leaderboard.tssrc/app/api/leaderboard/route.ts
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx,js,jsx}: All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text
Use path alias@/to reference files in./src/directory
Format code with Biome using: double quotes, trailing commas, 2-space indent, 100 character line width
Files:
src/repository/leaderboard.tssrc/app/api/leaderboard/route.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Prefer named exports over default exports
Files:
src/repository/leaderboard.tssrc/app/api/leaderboard/route.ts
src/repository/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use Drizzle ORM for data access in the repository layer
Files:
src/repository/leaderboard.ts
src/app/api/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
API routes should use Hono framework and follow Next.js 16 App Router conventions
Files:
src/app/api/leaderboard/route.ts
🧠 Learnings (1)
📚 Learning: 2026-01-05T03:01:39.354Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 539
File: src/types/user.ts:158-170
Timestamp: 2026-01-05T03:01:39.354Z
Learning: In TypeScript interfaces, explicitly document and enforce distinct meanings for null and undefined. Example: for numeric limits like limitTotalUsd, use 'number | null | undefined' when null signifies explicitly unlimited (e.g., matches DB schema or special UI logic) and undefined signifies 'inherit default'. This pattern should be consistently reflected in type definitions across related fields to preserve semantic clarity between database constraints and UI behavior.
Applied to files:
src/repository/leaderboard.tssrc/app/api/leaderboard/route.ts
🧬 Code graph analysis (2)
src/repository/leaderboard.ts (2)
src/drizzle/schema.ts (2)
messageRequest(276-362)users(35-86)src/repository/_shared/message-request-conditions.ts (1)
EXCLUDE_WARMUP_CONDITION(9-9)
src/app/api/leaderboard/route.ts (1)
src/lib/redis/leaderboard-cache.ts (1)
getLeaderboardWithCache(204-291)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Docker Build Test
- GitHub Check: 🌐 API Tests
- GitHub Check: dev-build-deploy
🔇 Additional comments (8)
src/repository/leaderboard.ts (3)
22-30: 接口定义清晰,文档完善。接口
UserLeaderboardFilters的定义准确反映了 PR 需求,注释明确说明了 OR 逻辑语义。可选字段使用undefined表示"不筛选",符合 TypeScript 惯例。
75-112: 函数签名更新一致,保持向后兼容。所有用户排行榜查询函数的签名更新统一,
userFilters作为可选参数不会破坏现有调用。
240-246: LGTM!自定义日期范围查询函数的签名更新与其他周期函数保持一致。
src/app/api/leaderboard/route.ts (5)
78-79: 参数提取正确。新增的查询参数提取逻辑清晰。
130-138:parseListParam辅助函数实现简洁有效。正确处理了空值、空白字符串和最大 20 项的限制。返回
undefined而非空数组的语义更清晰。
140-145: 确认:用户筛选参数是否应在 API 层限制为仅管理员可用?根据 PR 描述,用户标签和用户组筛选器是"admin-only"功能。当前代码仅在
scope="user"时解析这些参数,但未检查用户是否为管理员。如果设计意图是:
- 仅前端隐藏(非管理员看不到 UI):当前实现没问题
- API 层也要限制:需要添加权限检查
建议明确意图。如果需要在 API 层限制,可添加如下检查:
🔧 可选:添加管理员权限检查
let userTags: string[] | undefined; let userGroups: string[] | undefined; if (scope === "user") { - userTags = parseListParam(userTagsParam); - userGroups = parseListParam(userGroupsParam); + // 用户筛选仅管理员可用 + if (isAdmin) { + userTags = parseListParam(userTagsParam); + userGroups = parseListParam(userGroupsParam); + } }
147-154: 与缓存层的集成正确。筛选参数以对象形式传递给
getLeaderboardWithCache,符合LeaderboardFilters接口定义。
176-187: 日志记录完整,便于调试和审计。新增的筛选参数已添加到日志上下文中,与现有日志模式保持一致。
* 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
Changes
Repository Layer (
src/repository/leaderboard.ts)UserLeaderboardFiltersinterface withuserTags?: string[]anduserGroups?: string[]Cache Layer (
src/lib/redis/leaderboard-cache.ts)LeaderboardFiltersto includeuserTagsanduserGroupsbuildCacheKey()to include sorted user filters in cache keyqueryDatabase()to pass filters when scope=userAPI Route (
src/app/api/leaderboard/route.ts)userTagsanduserGroupsquery params (CSV format)Frontend (
src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx)userTagFiltersanduserGroupFiltersTagInputcomponents (admin-only, only when scope=user)i18n
leaderboard.filters.userTagsPlaceholderanduserGroupsPlaceholderfor 5 languagesVerification
Closes #606
Greptile Overview
Greptile Summary
Adds comprehensive filtering capability to the user leaderboard by user tags and groups, enabling admins to drill down into specific user segments.
Key Changes:
@>) for tags andregexp_split_to_arrayfor comma-separated groupsTagInputcomponents that appear only whenscope=user, with proper state management and URL parameter encodingImplementation Quality:
Confidence Score: 5/5
Important Files Changed
File Analysis
UserLeaderboardFiltersinterface and filtering logic using JSONB containment for tags and regex split for groups with OR logic between filtersqueryDatabaseto pass filters whenscope=useruserTagsanduserGroupsquery params (CSV format, trimmed, max 20 items), passes filters to cache layerTagInputcomponents (admin-only whenscope=user), appends filters to fetch URLSequence Diagram
sequenceDiagram participant User as Admin User participant UI as LeaderboardView participant API as /api/leaderboard participant Cache as Redis Cache participant Repo as Repository Layer participant DB as PostgreSQL User->>UI: Select user tags/groups filters UI->>UI: Update state (userTagFilters, userGroupFilters) UI->>API: GET /api/leaderboard?scope=user&userTags=tag1,tag2&userGroups=group1 API->>API: Parse & validate filters (trim, max 20) API->>Cache: getLeaderboardWithCache(period, scope, filters) Cache->>Cache: buildCacheKey(scope, filters) with sorted tags/groups Cache->>Cache: Check Redis for cached data alt Cache Hit Cache-->>API: Return cached data else Cache Miss Cache->>Repo: queryDatabase(period, scope, filters) Repo->>Repo: Build WHERE conditions Note over Repo: Tags: JSONB @> operator (OR logic)<br/>Groups: regexp_split_to_array (OR logic)<br/>Combined: (tags OR groups) Repo->>DB: SELECT with INNER JOIN users<br/>WHERE filters applied DB-->>Repo: Aggregated user rankings Repo-->>Cache: Return rankings Cache->>Cache: Store in Redis (60s TTL) Cache-->>API: Return rankings end API->>API: Format currency fields API-->>UI: Return filtered leaderboard data UI->>UI: Render filtered results