Conversation
* feat(dashboard): enhance logs UI and provider circuit breaker display - Add time range filter improvements with preset options - Enhance provider chain popover with better error display - Improve provider vendor view circuit breaker UI - Add availability dashboard and provider endpoints display name tests - Update i18n messages for provider chain and settings Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore: format code (feat-dashboard-ui-improvements-601d7b0) * fix: make deriveDisplayNameFromDomain async for Server Actions compatibility The file has "use server" directive, which requires all exported functions to be async. This fixes the Next.js Turbopack build error: "Server Actions must be async functions." CI Run: https://github.com/ding113/claude-code-hub/actions/runs/21357337034 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix(ui): improve TagInput dropdown positioning inside Dialog - Detect when TagInput is inside a Dialog and portal suggestions to dialog-content - Use absolute positioning relative to dialog container instead of fixed viewport positioning - Fix scroll/resize event listeners to target the correct scroll container - Add unit tests for TagInput behavior inside Dialog Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore: format code (feat-dashboard-ui-improvements-65ea8e5) * fix(db): prevent token count overflow by using bigint columns Change token-related columns in message_request table from integer to bigint to prevent overflow when storing large token counts. Also update all SQL aggregation queries to use double precision instead of ::int casting to avoid overflow during SUM operations. Changes: - Schema: inputTokens, outputTokens, cacheCreationInputTokens, cacheReadInputTokens, cacheCreation5mInputTokens, cacheCreation1hInputTokens now use bigint - Schema: providerVendorId is now NOT NULL - Queries: All token SUM operations use ::double precision - Tests: Add unit tests to verify no ::int casting in token aggregations Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore: format code (feat-dashboard-ui-improvements-7e73f97) * fix(repository): handle multiple API prefixes in domain name derivation Extend deriveDisplayNameFromDomain to skip common API prefixes (api, v1, v2, v3, www) when extracting display names from domains. This fixes handling of multi-prefix domains like v1.api.anthropic.com. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(dashboard): improve home layout with sidebar and responsive grid - Refactor DashboardBento to use two-column layout with fixed-width sidebar (300px) for LiveSessionsPanel on admin view - Update DashboardMain to remove max-w-7xl constraint on dashboard home page for wider content area - Simplify ActiveSessionsSkeleton to match compact list style - Add showTokensCost prop to SessionListItem and ActiveSessionsList for conditional token/cost display - Fix router import to use i18n routing in ActiveSessionsList - Add unit tests for layout behavior and SessionListItem props Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(dashboard): prevent decision chain overflow with text wrapping Add overflow constraints to LogicTraceTab and StepCard components: - Add min-w-0 to grid containers to allow content shrinking - Add break-all to font-mono text for long IDs and URLs - Add flex-wrap and truncate for provider name badges - Add overflow-hidden to StepCard details container Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor(dashboard): improve home layout alignment and structure - Add max-w-7xl constraint to dashboard home container for consistent alignment with header - Restructure dashboard-bento into 3 independent sections: metrics, chart, leaderboards - Remove nested grid structure that caused misalignment on desktop - Change admin layout from 2-column (content + sidebar) to 4-column grid (3 leaderboards + live sessions) - Remove colSpan/rowSpan from StatisticsChartCard for full-width independent rendering - Add min-h-[280px] to LeaderboardCard for consistent card heights - Update tests to match new layout structure Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: address code review feedback - Remove redundant isDashboardHomePage conditional branch (now same as default) - Remove trailing period from vendorAggregationRule for style consistency Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
* feat(probe): implement dynamic endpoint probe intervals - Change probe scheduling from global interval to per-endpoint dynamic intervals: - Base interval: 60s (configurable via ENDPOINT_PROBE_INTERVAL_MS) - Single-endpoint vendor: 10min (reduces unnecessary probing) - Timeout override: 10s (faster recovery for timeout errors) - Filter probes to only "due" endpoints based on lastProbedAt + effectiveInterval - Remove type tabs from VendorEndpointsSection, show all endpoints in single list - Display type icon with tooltip for each endpoint row - Sort endpoints by type order (from getAllProviderTypes) then sortOrder - Add type selector in AddEndpointButton dialog - Update i18n strings for all 5 languages - Add comprehensive unit tests for dynamic interval rules Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: derive selectableTypes from getAllProviderTypes Address code review feedback: use getAllProviderTypes().filter() instead of hardcoded array to ensure automatic sync when new provider types are added. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
问题背景: - gemini-3-pro-image-preview 等图片生成模型返回的 usage 中包含 candidatesTokensDetails - 该字段按 modality 细分 token (IMAGE/TEXT) - IMAGE modality token 价格为 $0.00012/token,是普通 TEXT token 的 10 倍 - 原系统未解析此字段,导致 IMAGE token 按 TEXT 价格计费,计费偏低约 7.6 倍 类型扩展 (src/types/model-price.ts): - 新增 output_cost_per_image_token: 输出图片 token 单价 (按 token 计费) - 新增 input_cost_per_image_token: 输入图片 token 单价 (按 token 计费) - 保留 input_cost_per_image: 输入图片固定价格 (按张计费,$0.0011/张) - 保留 output_cost_per_image: 输出图片固定价格 (按张计费) Usage 提取逻辑 (src/app/v1/_lib/proxy/response-handler.ts): - 解析 candidatesTokensDetails 提取 output_image_tokens 和 output_tokens (TEXT) - 解析 promptTokensDetails 提取 input_image_tokens 和 input_tokens (TEXT) - 使用 toUpperCase() 进行大小写不敏感匹配 (IMAGE/image/Image) - 添加 hasValidToken 守卫,仅在解析到有效 token 时覆盖原始值 - 修复 promptTokensDetails 解析不完整导致 input IMAGE tokens 被重复计费的问题 - 计算 candidatesTokenCount 与 details 总和的差值作为未分类 TEXT tokens (这些是图片生成的内部开销,按 TEXT 价格计费) 计费逻辑 (src/lib/utils/cost-calculation.ts): - output_image_tokens 优先使用 output_cost_per_image_token 计费 - input_image_tokens 优先使用 input_cost_per_image_token 计费 - 若未配置 image token 价格,回退到普通 token 价格 (向后兼容) - 倍率 (multiplier) 同时作用于 image token 费用 测试覆盖: - 新增 cost-calculation-image-tokens.test.ts (10 个测试) - 扩展 extract-usage-metrics.test.ts (12 个 Gemini image 测试) - 覆盖场景: 纯 IMAGE、IMAGE+TEXT 混合、无效数据、大小写变体、向后兼容、 混合输入输出、candidatesTokenCount 差值计算 计费示例 (完整图片生成请求): - promptTokenCount=326, candidatesTokenCount=2340, thoughtsTokenCount=337 - candidatesTokensDetails: IMAGE=2000 (差值 340 为未分类 TEXT) - 输入 TEXT: 326 × $0.000002 = $0.000652 - 输出 TEXT: (340+337) × $0.000012 = $0.008124 - 输出 IMAGE: 2000 × $0.00012 = $0.240000 - 总计: $0.248776 (修复前 $0.244696,少收 $0.00408) Fixes #663
* feat(rate-limit): implement lease-based budget slicing for quota enforcement
Introduce a "DB authoritative + Redis lease deduction" mechanism for rate
limiting that ensures UI and decision chain use the same data source.
Key changes:
- Add BudgetLease model with snapshotAtMs anchoring for window calculation
- Implement LeaseService with getCostLease, refreshCostLeaseFromDb, and
decrementLeaseBudget (atomic Lua script) methods
- Add checkCostLimitsWithLease to RateLimitService for lease-based checks
- Add system settings for quota lease configuration:
- quotaDbRefreshIntervalSeconds (lease TTL)
- quotaLeasePercent5h/Daily/Weekly/Monthly (slice percentages)
- quotaLeaseCapUsd (maximum slice cap)
- Add UI form for quota lease settings with i18n support (5 languages)
- Add database migration for new system settings columns
- Implement fail-open behavior on Redis/DB errors
- Add comprehensive unit tests (108 quota tests passing)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(rate-limit): force lease refresh when quota limit changes
When admin modifies a user/key's quota limit, the cached lease in Redis
may have stale limitAmount. This causes:
- Users still blocked after limit increase
- Users still allowed after limit decrease
Add limitAmount comparison in getCostLease() to detect changes and
force DB refresh when the configured limit differs from cached value.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(rate-limit): use rolling window semantics for 5h/daily limits
Rolling windows (5h, daily rolling) now correctly indicate that there
is no fixed reset time - usage gradually expires over the window period.
Changes:
- Add RATE_LIMIT_5H_ROLLING_EXCEEDED and RATE_LIMIT_DAILY_ROLLING_EXCEEDED
error codes to distinguish rolling from fixed windows
- Update i18n messages in all 5 languages to explain rolling behavior
("usage gradually expires") instead of showing incorrect reset times
- Allow RateLimitError.resetTime to be null for rolling windows
- Skip X-RateLimit-Reset and Retry-After headers when resetTime is null
- Add comprehensive unit tests for 5h rolling window behavior (21 tests)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat(timezone): unify timezone handling across frontend and backend
- Add system timezone setting with IANA validation and UI configuration
- Create parseDateInputAsTimezone helper for date-only (end-of-day) and
datetime inputs parsed in system timezone
- Update key/user expiresAt parsing to use system timezone
- Replace hardcoded zh-CN locale in webhook date formatting with
formatInTimeZone for locale-independent output
- Add resolveSystemTimezone with fallback chain: DB -> env TZ -> UTC
- Configure next-intl with system timezone for consistent SSR/CSR display
- Add comprehensive tests for DST transitions and timezone boundaries
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* chore: format code (fix-quota-limit-refactor-edf12a8)
* fix: address bugbot review comments for timezone and rate-limit
- drizzle/0059: add IF NOT EXISTS to prevent migration conflict
- date-input: detect timezone designator (Z/+-HH:MM) in ISO strings
to avoid double conversion by fromZonedTime
- lease.ts: add boundary protection for calculateLeaseSlice
(clamp percent to [0,1], ensure non-negative capUsd and result)
- placeholders.ts: wrap formatLocalTimestamp in try-catch to handle
invalid IANA timezone gracefully
- availability-view: pass locale to formatBucketTime for i18n month names
- lease-service.test: use vi.hoisted() to fix TDZ issue with mock
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
…efault Add collapsible behavior to system settings form: - Quota Lease Settings section now collapsed by default - Response Fixer sub-options now collapsed by default - Both sections expand on click with chevron rotation animation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
#670) * feat(providers): recluster vendors by host:port when website_url empty - Add computeVendorKey helper with host:port support for IP-based providers - When website_url is empty, use host:port as vendor key (different ports = different vendors) - Support IPv6 addresses with [ipv6]:port format - Use protocol default ports (http=80, https=443) when port not specified - Add reclusterProviderVendors action with preview/apply mode - Add ReclusterVendorsDialog UI component - Add i18n support for 5 languages (zh-CN, zh-TW, en, ja, ru) - Existing behavior unchanged when website_url is present (hostname only) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * perf(providers): optimize recluster with batch vendor loading and Map lookup - Batch load all vendor data upfront with Promise.all to avoid N+1 queries - Use Map for O(1) provider lookup instead of O(N) find() in transaction loop - Addresses bugbot review comments from gemini-code-assist and greptile-apps Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The computeVendorKey function was exported from a file with "use server" directive but was not async, causing Next.js build to fail with: "Server Actions must be async functions." Changes: - Made computeVendorKey async and return Promise<string | null> - Added await to all call sites in provider-endpoints.ts - Added await to call site in providers.ts (reclusterProviderVendors) - Updated all test cases to use async/await CI Run: https://github.com/ding113/claude-code-hub/actions/runs/21436692308 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…e paths The revalidatePath calls in saveSystemSettings lacked locale prefixes, causing non-default locale pages (/en/*, /ja/*, etc.) to serve stale cached data after save. Users had to save twice for changes to appear. - Add locale loop for revalidatePath in system-config.ts - Add unit tests for saveSystemSettings revalidation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
Important Review skippedToo many files! This PR contains 182 files, which is 82 over the limit of 100. You can disable this status message by setting the
✨ 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 release significantly refines the platform's infrastructure by centralizing timezone configuration for consistent data handling and display. It introduces a robust, performant lease-based rate limiting system and intelligent dynamic probing for provider endpoints. Furthermore, it enhances provider management with a new re-clustering tool and expands cost calculation capabilities to include image tokens, ensuring more accurate usage tracking. 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
|
| * - IPv6 format: [ipv6]:port | ||
| * - Missing port: use protocol default (http=80, https=443) | ||
| * - No scheme: assume https | ||
| */ |
There was a problem hiding this comment.
computeVendorKey is declared as async but doesn't use any await statements internally - functions are synchronous
| */ | |
| export function computeVendorKey(input: { |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/repository/provider-endpoints.ts
Line: 111:111
Comment:
`computeVendorKey` is declared as async but doesn't use any await statements internally - functions are synchronous
```suggestion
export function computeVendorKey(input: {
```
How can I resolve this? If you propose a fix, please make it concise.| function deriveDisplayNameFromDomain(domain: string): string { | ||
| const parts = domain.split("."); | ||
| const name = parts[0] === "api" && parts[1] ? parts[1] : parts[0]; | ||
| export async function deriveDisplayNameFromDomain(domain: string): Promise<string> { |
There was a problem hiding this comment.
deriveDisplayNameFromDomain is declared as async but doesn't use await - synchronous function
| export async function deriveDisplayNameFromDomain(domain: string): Promise<string> { | |
| export function deriveDisplayNameFromDomain(domain: string): string { |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/repository/provider-endpoints.ts
Line: 311:311
Comment:
`deriveDisplayNameFromDomain` is declared as async but doesn't use await - synchronous function
```suggestion
export function deriveDisplayNameFromDomain(domain: string): string {
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Code Review
This is a substantial release that introduces several major improvements. The most significant is the comprehensive timezone support, which will ensure consistent date and time handling across the application. The introduction of a lease-based mechanism for rate limiting is a smart performance optimization that will reduce database load. The endpoint probing logic has also been made more intelligent with dynamic intervals. Additionally, there are numerous UI/UX enhancements, including a revamped dashboard layout and a new provider reclustering tool. The database schema has been updated to support these new features and to prevent data overflow for token counts. Overall, this is a very solid release with well-implemented features and improvements.
| @@ -0,0 +1,2 @@ | |||
| ALTER TABLE "keys" ALTER COLUMN "expires_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint | |||
| ALTER TABLE "system_settings" ADD COLUMN IF NOT EXISTS "timezone" varchar(64); No newline at end of file | |||
There was a problem hiding this comment.
This ADD COLUMN statement is redundant as it's already present in the 0048_add_system_timezone.sql migration. While IF NOT EXISTS prevents an error, it's best to remove duplicate statements from migrations to keep them clean and avoid potential confusion. This might indicate a small issue in the migration generation process.
There was a problem hiding this comment.
Code Review Summary
This is a large release PR (v0.5.2) with significant changes across 182 files. The PR introduces several important features including:
- System Timezone Configuration - New timezone settings with proper IANA validation
- Lease-based Rate Limiting - New budget slicing mechanism for rate limits
- Rolling Window Rate Limits - Proper handling of 5h and daily rolling windows
- Provider Vendor Reclustering - New admin tool for reorganizing provider vendors
- Gemini Image Token Support - Extraction of modality-specific tokens
PR Size: XL
- Lines changed: 19,797 (18,763 additions, 1,034 deletions)
- Files changed: 182
Split Suggestion: This XL PR could be split into:
- Timezone feature (timezone.ts, date-input.ts, time-utils changes)
- Rate limit lease system (lease.ts, lease-service.ts)
- Provider recluster feature (recluster dialog, computeVendorKey)
- UI improvements (logs components, settings form)
Issues Found
| Category | Critical | High | Medium | Low |
|---|---|---|---|---|
| Logic/Bugs | 0 | 0 | 0 | 0 |
| Security | 0 | 0 | 0 | 0 |
| Error Handling | 0 | 0 | 2 | 0 |
| Types | 0 | 0 | 0 | 0 |
| Comments/Docs | 0 | 0 | 0 | 0 |
| Tests | 0 | 0 | 0 | 0 |
| Simplification | 0 | 0 | 0 | 0 |
Medium Priority Issues (Should Consider)
1. src/actions/keys.ts:1072 - Missing specific error handling for invalid date input
The parseDateInputAsTimezone function throws an error when the date input is invalid. In renewKeyExpiresAt, while the outer try-catch will catch this error, it exposes the raw error message (e.g., "Invalid date input: xyz") to the user instead of returning a proper i18n error message with ERROR_CODES.INVALID_FORMAT.
Compare with editKey function (lines 507-519) which properly catches this specific error and returns an i18n message.
Suggested fix:
const timezone = await resolveSystemTimezone();
let expiresAt: Date;
try {
expiresAt = parseDateInputAsTimezone(data.expiresAt, timezone);
} catch {
return { ok: false, error: tError("INVALID_FORMAT"), errorCode: ERROR_CODES.INVALID_FORMAT };
}2. src/actions/users.ts:1345 - Same issue in renewUser function
The renewUser function has the same pattern where parseDateInputAsTimezone can throw but is not specifically caught to return a proper i18n error message.
Review Coverage
- Logic and correctness - Clean
- Security (OWASP Top 10) - Clean
- Error handling - 2 medium issues noted
- Type safety - Clean
- Documentation accuracy - Clean
- Test coverage - Adequate (new tests for lease, timezone, date-input)
- Code clarity - Good
Positive Observations (not reported per instructions, but noted)
- Comprehensive test coverage for new lease and timezone features
- Proper i18n support for new error messages
- Good use of fail-open pattern in rate limiting
- Clean separation of concerns in new modules
Automated review by Claude AI
There was a problem hiding this comment.
Actionable comments posted: 16
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (15)
src/app/[locale]/dashboard/availability/_components/endpoint/probe-terminal.tsx (1)
189-211: 修复未使用的Icon变量(流水线失败)
Icon变量已定义但从未使用,导致 lint 检查失败。应该在日志行中渲染图标,或者移除未使用的变量。建议方案:在状态标签前渲染图标
{/* Timestamp */} <span className="text-muted-foreground opacity-60 w-20 shrink-0"> [{formatTime(log.createdAt, timeZone)}] </span> + {/* Status Icon */} + <Icon className={cn("h-3.5 w-3.5 shrink-0", config.color)} /> + {/* Status */} <span className={cn("w-12 shrink-0 font-bold", config.color)}>{config.label}</span>或者,如果不需要图标,直接移除未使用的变量:
const level = getLogLevel(log); const config = levelConfig[level]; - const Icon = config.icon;src/components/customs/active-sessions-list.tsx (1)
14-20: 硬编码的错误消息违反 i18n 规范第 17 行的错误消息
"获取活跃 Session 失败"是硬编码的中文文本。根据编码规范,所有用户可见的字符串必须使用 i18n(支持 5 种语言:zh-CN、zh-TW、en、ja、ru)。🔧 建议的修复
async function fetchActiveSessions(): Promise<ActiveSessionInfo[]> { const result = await getActiveSessions(); if (!result.ok) { - throw new Error(result.error || "获取活跃 Session 失败"); + throw new Error(result.error || "Failed to fetch active sessions"); } return result.data; }或者,如果此错误消息需要对用户显示,应使用翻译函数。不过由于此函数在组件外部定义,可能需要重构为在组件内部处理错误,以便访问
useTranslationshook。As per coding guidelines: "All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text"
src/components/ui/data-table.tsx (1)
274-274: 硬编码的中文字符串违反 i18n 规范。默认参数
"操作"是硬编码的中文字符串,应使用 i18n 机制替代。根据编码规范,所有用户可见的文本必须支持国际化(支持 zh-CN、zh-TW、en、ja、ru 五种语言)。🌐 建议的修复
由于这是一个静态工厂函数而非组件,无法直接使用
useTranslations。建议移除默认值,由调用方传入已翻译的字符串:actions: <T extends TableData>( - title: string = "操作", + title: string, render: (value: any, record: T, index: number) => ReactNode, options?: Partial<TableColumn<T>> ): TableColumn<T> => ({As per coding guidelines: "All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text"
src/app/[locale]/dashboard/sessions/[sessionId]/messages/_components/request-list-sidebar.tsx (3)
127-127: 存在硬编码的用户可见字符串。
title属性中的"Unknown"应使用 i18n 翻译。- title={`#${req.sequence} - ${req.model || "Unknown"}`} + title={`#${req.sequence} - ${req.model || t("requestList.unknownModel")}`}Based on learnings: "All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text"
167-169: 搜索占位符文本未国际化。
placeholder="Search..."应使用翻译函数。<Input - placeholder="Search..." + placeholder={t("requestList.searchPlaceholder")} className="h-7 text-xs pl-7 bg-muted/20 border-muted-foreground/20"Based on learnings: "All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text"
244-246: "Unknown Model" 文本未国际化。- {request.model || "Unknown Model"} + {request.model || t("requestList.unknownModel")}Based on learnings: "All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text"
src/repository/statistics.ts (1)
471-479: 硬编码的中文字符串违反 i18n 规范。SQL 查询中直接硬编码了
'其他用户'作为user_name,这违反了项目的国际化要求(支持 5 种语言:zh-CN, zh-TW, en, ja, ru)。建议将此标识符改为一个语言无关的常量(如
'__others__'),然后在前端展示层进行国际化处理。建议的修复方案
SELECT -1 AS user_id, - '其他用户' AS user_name, + '__others__' AS user_name, hour AS date, api_calls::integer, total_cost::numeric同样的修改需要应用到以下位置:
- Line 545
- Line 617
- Line 689
As per coding guidelines: "All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text"
src/app/[locale]/settings/prices/_components/price-list.tsx (1)
23-75: 未使用的 locale 触发 lint 失败,需要移除。流水线已报错:
lint/correctness/noUnusedVariables。建议移除useLocale及locale变量。建议修复
-import { useLocale, useTimeZone, useTranslations } from "next-intl"; +import { useTimeZone, useTranslations } from "next-intl"; @@ - const locale = useLocale(); const timeZone = useTimeZone() ?? "UTC";src/lib/webhook/templates/test-messages.ts (1)
28-48: 时区参数未在所有消息类型中一致应用。
buildTestMessage接收timezone参数但仅传递给circuit-breaker类型。cost-alert和daily-leaderboard两个消息类型均使用new Date()创建时间戳,但不接收 timezone 参数,导致这些消息的日期/时间格式化无法进行时区转换。建议为buildCostAlertMessage和buildDailyLeaderboardMessage的函数签名添加 timezone 参数,保持实现的一致性。src/lib/webhook/templates/circuit-breaker.ts (1)
17-22: 代码中包含 emoji 字符,违反编码规范。Line 21 的
icon: "🔌"使用了 emoji 字符。根据编码规范,不应在代码、注释或字符串字面量中使用 emoji 字符。建议使用图标组件或图标名称字符串替代 emoji。
As per coding guidelines: "Never use emoji characters in any code, comments, or string literals"
src/app/[locale]/dashboard/_components/statistics/chart.tsx (1)
233-250: 处理日期字符串的时区解析问题,防止图表跨日偏移。当
dateStr为 "YYYY-MM-DD" 格式(日分辨率)时,new Date(dateStr)会按 UTC 解析为午夜,再用formatInTimeZone转换负时区时会显示前一天,导致图表 X 轴与 tooltip 日期不匹配。建议使用
zonedTimeToUtc按目标时区正确解析日期字符串:修正方案
- 更新导入:
import { formatInTimeZone, zonedTimeToUtc } from "date-fns-tz";
- 添加日期转换辅助函数(lines 233 前):
const toChartDate = (dateStr: string) => { const isDateOnly = /^\d{4}-\d{2}-\d{2}$/.test(dateStr); return isDateOnly ? zonedTimeToUtc(`${dateStr}T00:00:00`, timeZone) : new Date(dateStr); };
- 更新两个格式化函数:
const formatDate = (dateStr: string) => { const date = toChartDate(dateStr); // ... rest remains same }; const formatTooltipDate = (dateStr: string) => { const date = toChartDate(dateStr); // ... rest remains same };src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/LogicTraceTab.tsx (1)
51-57: 未使用的参数会导致 lint 失败。
statusCode当前未使用,CI 已报错;请移除或实际使用该参数以通过校验。修复建议
-export function LogicTraceTab({ - statusCode, - providerChain, +export function LogicTraceTab({ + providerChain, blockedBy, blockedReason, requestSequence, }: LogicTraceTabProps) {src/app/[locale]/dashboard/_components/bento/dashboard-bento.tsx (1)
260-309: data-testid 未透传到 LiveSessionsPanel 实际节点。
此处传入 data-testid,但 LiveSessionsPanel 未展开剩余 props,最终不会落到 BentoCard/DOM,测试选择器可能失效。建议在组件内接收并透传剩余属性。建议修复
-export function LiveSessionsPanel({ - sessions, - isLoading, - maxItems: maxItemsProp, - className, -}: LiveSessionsPanelProps) { +export function LiveSessionsPanel({ + sessions, + isLoading, + maxItems: maxItemsProp, + className, + ...rest +}: LiveSessionsPanelProps) {- <BentoCard - ref={containerRef} - colSpan={1} - rowSpan={2} - className={cn( - "flex flex-col overflow-hidden p-0", - "bg-slate-50 dark:bg-[`#0a0a0c`]", - "border-slate-200 dark:border-white/[0.06]", - className - )} - > + <BentoCard + ref={containerRef} + colSpan={1} + rowSpan={2} + className={cn( + "flex flex-col overflow-hidden p-0", + "bg-slate-50 dark:bg-[`#0a0a0c`]", + "border-slate-200 dark:border-white/[0.06]", + className + )} + {...rest} + >src/lib/webhook/templates/placeholders.ts (1)
25-30: 占位符描述新增文案需 i18n。
该描述为用户可见文案,请改为多语言资源。
As per coding guidelines: All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text.src/lib/rate-limit/time-utils.ts (1)
43-235: 修复硬编码的中文字符串,改为使用 i18ngetResetInfo 和 getResetInfoWithMode 返回的 period 字段含有硬编码中文字符串("5 小时"、"24 小时"),这些是用户界面展示的内容,必须使用 i18n 国际化而非硬编码。请将这些字符串改为从 i18n 翻译函数获取。
🤖 Fix all issues with AI agents
In `@drizzle/meta/0058_snapshot.json`:
- Around line 200-205: 快照显示 keys.expires_at 仍为
timestamp、notification_target_bindings.schedule_timezone 带默认值且 system_settings
缺少 timezone,与代码中的模式不一致;在模式定义文件中修正对应列(更新 keys.expires_at 的类型、移除或更新
notification_target_bindings.schedule_timezone 的默认值、为 system_settings 添加
timezone 列),然后运行生成命令 bun run db:generate 重新生成迁移与快照,确保不手工编辑 SQL 文件以使迁移与模式对齐。
In `@src/actions/users.ts`:
- Around line 1487-1490: The daily time range calculation currently calls
getTimeRangeForPeriod("daily", user.dailyResetTime || "00:00") which ignores the
user's rolling vs fixed reset setting; replace this with
getTimeRangeForPeriodWithMode("daily", user.dailyResetTime || "00:00",
user.dailyResetMode) (or the equivalent parameter order used by
getTimeRangeForPeriodWithMode) so rangeDaily is computed with the dailyResetMode
taken into account; update any references to rangeDaily accordingly.
In `@src/app/`[locale]/dashboard/logs/_components/usage-logs-filters.tsx:
- Around line 264-268: The quick-filter ("today", "this-week") range calculation
currently uses local timezone startOfDay/startOfWeek and thus diverges from
TimeFilters' serverTimeZone-aware parsing; update the quick-filter logic that
sets localFilters (used by TimeFilters via setLocalFilters) to compute ranges
using the existing getQuickDateRange(timeKey, serverTimeZone) utility from
time-range.ts so the generated start/end dates are timezone-aware and consistent
with TimeFilters' display and parsing. Ensure you replace any direct
startOfDay/startOfWeek calls with getQuickDateRange and pass serverTimeZone
through to the function when handling quick-filter selection.
In
`@src/app/`[locale]/settings/providers/_components/provider-list-item.legacy.tsx:
- Line 66: The code risks a RangeError when formatInTimeZone receives an invalid
date or timezone: update the timeZone assignment to use fallback with || (e.g.,
const timeZone = useTimeZone() || "UTC") and add a date validity check before
calling formatInTimeZone — compute const d = new Date(item.lastCallTime) and
ensure it's valid (e.g., !Number.isNaN(d.getTime()) or use isValid from
date-fns) and only call formatInTimeZone(d, timeZone, ...) when valid; otherwise
render a safe fallback (empty string or "—") to avoid rendering crashes.
In `@src/app/v1/_lib/proxy/rate-limit-guard.ts`:
- Around line 359-426: The current guard if (user.dailyQuota !== null)
incorrectly allows undefined to proceed into RateLimitService.checkUserDailyCost
and cause unintended throttling; update the logic to distinguish undefined
(inherit default) from null (unlimited) by only running the daily check when the
quota is a concrete number. Concretely, in the block around user.dailyQuota,
compute a local quota like const quota = user.dailyQuota; and change the
condition to typeof quota === "number" (or explicitly check !== null && quota
!== undefined), pass quota into RateLimitService.checkUserDailyCost and use
quota (not user.dailyQuota) when formatting limit and building RateLimitError
messages, keeping the existing branches for rolling vs fixed reset using
getResetInfoWithMode and getErrorMessageServer.
In `@src/components/ui/data-table.tsx`:
- Line 3: The date renderer is hardcoding the timezone "UTC" via
formatInTimeZone; update the date-column rendering to use a configurable
timezone (from system/user settings or React context) instead of "UTC". Locate
the date column renderer (the column type or renderCell function that calls
formatInTimeZone in src/components/ui/data-table.tsx) and replace the literal
"UTC" with a timezone value obtained from props/context/config (e.g., a
TimezoneContext, a global config hook, or a column-specific tz option) and
ensure a sensible default is used when no timezone is provided.
In `@src/components/ui/relative-time.tsx`:
- Line 3: Remove the unused import "format as formatDate" from date-fns in
src/components/ui/relative-time.tsx; the code now uses formatInTimeZone instead,
so delete the import statement referencing formatDate to resolve the pipeline
error and keep imports consistent.
In `@src/drizzle/schema.ts`:
- Around line 96-98: The migration for keys.expires_at in Migration 0059 changes
the column to timestamp with time zone but lacks an explicit timezone
conversion, which causes data drift; update the ALTER TABLE statement in
Migration 0059 to perform an explicit conversion using the USING clause (e.g.,
converting stored values from UTC) so existing timestamps are interpreted
correctly, and verify or document that the DB server timezone is UTC before
running the migration; look for the expiresAt column definition (expiresAt:
timestamp('expires_at', { withTimezone: true })) and the migration script that
alters "expires_at" to add the USING ... AT TIME ZONE 'UTC' conversion.
- Around line 155-159: The providerVendorId column in the schema currently has a
.notNull() constraint which breaks the backfillProviderVendorsFromProviders()
logic; remove the .notNull() call from the providerVendorId definition in
src/drizzle/schema.ts so the column is nullable while keeping the references(()
=> providerVendors.id, { onDelete: 'restrict' }) intact.
In `@src/lib/rate-limit/service.ts`:
- Around line 1299-1407: The failure reason is hardcoded in
checkCostLimitsWithLease; replace the literal English message with an
i18n-backed string (or the project's existing limit-error builder) so
user-facing text is localized and consistent. Locate the return in
checkCostLimitsWithLease where it constructs reason using typeName, check.name,
lease.currentUsage and check.limit, and call the i18n translation function (e.g.
i18n.t or the project's error formatter) with a key like
"rate_limit.cost_limit_reached" and interpolations { type: typeName, window:
check.name, usage: lease.currentUsage.toFixed(4), limit: check.limit.toFixed(4)
} (or reuse the project's existing helper that formats limit-exceeded messages)
so all five supported locales are covered and styling matches other limit
errors.
In `@src/lib/utils/date-input.ts`:
- Around line 54-62: The ISO datetime parsing branch currently uses new
Date(input) which interprets zone-less ISO strings in the runtime local zone
causing inconsistent results; update the logic in the date parsing function in
date-input.ts to use the same approach as the date-only branch by calling
parse(input, "yyyy-MM-dd'T'HH:mm:ss", new Date()) (or equivalent parse pattern)
then pass that parsed Date into fromZonedTime(timeParsed, timezone) and keep the
existing Number.isNaN check on the parsed Date; ensure you reference and reuse
the parse utility (instead of new Date(input)) and call
fromZonedTime(parsedDate, timezone) so zone-less ISO datetimes are interpreted
correctly for the target timezone.
In `@src/lib/validation/schemas.ts`:
- Around line 732-740: The timezone zod schema currently uses a hardcoded
Chinese error message; replace that with an i18n lookup so user-facing
validation messages are localized. Update the timezone schema (the timezone
field in src/lib/validation/schemas.ts) to call the project's i18n function or
error-mapping utility (e.g., t('validation.invalidTimezone') or the centralized
error mapper) instead of the literal string, ensuring keys exist for zh-CN,
zh-TW, en, ja, and ru; keep the refine predicate (isValidIANATimezone) unchanged
and return the i18n key/value for the message so the validation layer emits
localized text rather than a hardcoded string.
- Around line 786-813: The Zod validators for quotaDbRefreshIntervalSeconds,
quotaLeasePercent5h, quotaLeasePercentDaily, quotaLeasePercentWeekly,
quotaLeasePercentMonthly, and quotaLeaseCapUsd currently use hardcoded English
error messages; replace those literal strings with i18n lookups (use the
project's translation function and stable i18n keys) so validators call the i18n
resource instead of embedding text, and add corresponding keys/translations for
zh-CN, zh-TW, en, ja, ru; keep the same validation logic (.int, .min, .max,
.nullable, .optional) and ensure messages are passed the translated string
values.
In `@src/lib/webhook/templates/circuit-breaker.ts`:
- Around line 8-11: The fields array in circuit-breaker.ts contains hardcoded
user-facing labels ("失败次数", "预计恢复") which violate i18n rules; replace these
literals with calls to the project's translation function (e.g., t or i18n.t)
and add appropriate message keys (e.g., "circuitBreaker.failureCount",
"circuitBreaker.estimatedRecovery") in the i18n message files for all supported
locales (zh-CN, zh-TW, en, ja, ru); update the const fields initialization to
use the translation calls (passing any interpolations like `${data.failureCount}
次` as variables where appropriate) and ensure formatDateTime(data.retryAt,
timezone || "UTC") remains as the translated value for the "estimated recovery"
label.
In `@src/repository/_shared/transformers.ts`:
- Around line 198-211: The code incorrectly uses truthy checks so string "0"
becomes false and falls back to defaults; update each quota field check to test
for null/undefined (e.g., dbSettings?.quotaLeasePercent5h != null) then
parseFloat and if Number.isNaN(parsed) fall back to the intended default (0.05
for percent fields, null for quotaLeaseCapUsd). Apply this to
quotaLeasePercent5h, quotaLeasePercentDaily, quotaLeasePercentWeekly,
quotaLeasePercentMonthly and quotaLeaseCapUsd, keeping
quotaDbRefreshIntervalSeconds as-is (it already uses ??).
In `@src/repository/provider-endpoints.ts`:
- Around line 63-99: normalizeHostWithPort produces ambiguous IPv6 keys (e.g.,
"::1:443") because URL.hostname returns IPv6 without brackets; update the
function (normalizeHostWithPort) to wrap the normalizedHostname in square
brackets when it contains a colon and doesn't already start with "[" before
appending the port (i.e., detect hostname.includes(":") &&
!hostname.startsWith("["), then set normalizedHostname =
`[${normalizedHostname}]`) so returned strings follow the "[ipv6]:port" rule.
🟡 Minor comments (13)
src/app/[locale]/dashboard/availability/_components/provider/latency-chart.tsx-6-6 (1)
6-6: 移除未使用的导入ResponsiveContainer。管道检测到未使用的导入。经检查,
ResponsiveContainer在代码中确实未被使用,应将其移除。建议修复
-import { Area, AreaChart, CartesianGrid, ResponsiveContainer, XAxis, YAxis } from "recharts"; +import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";tests/unit/dashboard/availability/availability-dashboard-ui.test.tsx-62-75 (1)
62-75: 缺少异步操作等待机制,测试结果可能不稳定。组件在
useEffect中调用异步的fetchData,但测试在renderWithIntl后立即同步检查container.textContent,此时组件可能尚未完成数据加载和状态更新。建议使用异步等待机制确保组件渲染完成:
🔧 建议的修复方案
- test("does not render Probe All floating button", async () => { + test("does not render Probe All floating button", async () => { vi.stubGlobal( "fetch", vi.fn(async () => ({ ok: true, json: async () => ({ providers: [], systemAvailability: 0 }), })) ); const { container, unmount } = renderWithIntl(<AvailabilityDashboard />); + // 等待异步操作完成 + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 0)); + }); + expect(container.textContent).not.toContain("Probe All"); unmount(); });src/app/[locale]/dashboard/availability/_components/endpoint/latency-curve.tsx-6-6 (1)
6-6: Pipeline 失败:存在未使用的导入。根据 CI 报告,需要移除未使用的导入以修复 lint 错误:
修复建议
-import { CartesianGrid, Line, LineChart, ResponsiveContainer, XAxis, YAxis } from "recharts"; +import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";import { type ChartConfig, ChartContainer, ChartTooltip, - ChartTooltipContent, } from "@/components/ui/chart";Also applies to: 11-11
src/actions/keys.ts-1070-1073 (1)
1070-1073: 续期接口同样需要拦截无效日期,避免英文错误泄漏。
当前解析失败会被外层 catch 直接返回error.message;建议与 editKey 统一返回INVALID_FORMAT。Based on learnings: Do not modify hardcoded Chinese error messages in Server Actions under src/actions/*.ts as part of piecemeal changes. This is a repo-wide architectural decision that requires a coordinated i18n refactor across all Server Action files (e.g., model-prices.ts, users.ts, system-config.ts). Treat i18n refactor as a separate unified task rather than per-PR changes, and plan a project-wide approach for replacing hardcoded strings with localized resources.可选的修正方式
- const timezone = await resolveSystemTimezone(); - const expiresAt = parseDateInputAsTimezone(data.expiresAt, timezone); + let expiresAt: Date; + try { + const timezone = await resolveSystemTimezone(); + expiresAt = parseDateInputAsTimezone(data.expiresAt, timezone); + } catch { + return { + ok: false, + error: tError("INVALID_FORMAT"), + errorCode: ERROR_CODES.INVALID_FORMAT, + }; + }src/actions/keys.ts-285-291 (1)
285-291: 添加密钥时缺少对无效日期的专门处理,可能回传英文错误。
parseDateInputAsTimezone抛错会被外层 catch 直接透传error.message,导致用户看到英文且缺少INVALID_FORMAT的错误码。建议在此处捕获并返回统一的错误。Based on learnings: Do not modify hardcoded Chinese error messages in Server Actions under src/actions/*.ts as part of piecemeal changes. This is a repo-wide architectural decision that requires a coordinated i18n refactor across all Server Action files (e.g., model-prices.ts, users.ts, system-config.ts). Treat i18n refactor as a separate unified task rather than per-PR changes, and plan a project-wide approach for replacing hardcoded strings with localized resources.可选的修正方式
- const timezone = await resolveSystemTimezone(); - const expiresAt = - validatedData.expiresAt === undefined - ? null - : parseDateInputAsTimezone(validatedData.expiresAt, timezone); + const timezone = await resolveSystemTimezone(); + let expiresAt: Date | null; + if (validatedData.expiresAt === undefined) { + expiresAt = null; + } else { + try { + expiresAt = parseDateInputAsTimezone(validatedData.expiresAt, timezone); + } catch { + return { + ok: false, + error: tError("INVALID_FORMAT"), + errorCode: ERROR_CODES.INVALID_FORMAT, + }; + } + }src/actions/providers.ts-3693-3711 (1)
3693-3711: 事务内调用外部函数可能导致一致性问题。
getOrCreateProviderVendorIdFromUrls在事务回调内部调用,但该函数使用独立的数据库连接执行操作,而非事务上下文 (tx)。如果事务回滚,新创建的 vendor 记录不会被撤销。建议将 vendor 创建逻辑也纳入事务,或者接受这种部分一致性(vendor 是可复用的实体,孤立的 vendor 记录影响较小)。
src/lib/utils/timezone.ts-19-54 (1)
19-54: 修复无效的时区标识符列表中的
"Pacific/Sydney"不是有效的 IANA 时区标识符。根据 IANA 时区数据库,Sydney 应该使用"Australia/Sydney"。这会导致该选项在通过isValidIANATimezone()验证时失败。建议修复
- "Pacific/Sydney", + "Australia/Sydney",src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx-154-168 (1)
154-168: 改用locale占位符以支持locale特定的日期顺序当前代码使用固定的日期格式字符串("M/d"、"yyyy MMMM d"等),这可能不遵循特定locale的日期排列规则。建议改用date-fns的locale占位符("p"、"P"、"PPP"等),以让formatInTimeZone正确地根据locale调整日期组件的顺序。
建议修复
const formatDate = (dateStr: string) => { const date = new Date(dateStr); if (data.resolution === "hour") { - return formatInTimeZone(date, timeZone, "HH:mm", { locale: dateFnsLocale }); + return formatInTimeZone(date, timeZone, "p", { locale: dateFnsLocale }); } - return formatInTimeZone(date, timeZone, "M/d", { locale: dateFnsLocale }); + return formatInTimeZone(date, timeZone, "P", { locale: dateFnsLocale }); }; const formatTooltipDate = (dateStr: string) => { const date = new New(dateStr); if (data.resolution === "hour") { - return formatInTimeZone(date, timeZone, "MMMM d HH:mm", { locale: dateFnsLocale }); + return formatInTimeZone(date, timeZone, "PPpp", { locale: dateFnsLocale }); } - return formatInTimeZone(date, timeZone, "yyyy MMMM d", { locale: dateFnsLocale }); + return formatInTimeZone(date, timeZone, "PPP", { locale: dateFnsLocale }); };src/lib/rate-limit/lease.ts-136-160 (1)
136-160:deserializeLease未验证枚举值的有效性。当前验证仅检查字段类型,但未验证
entityType、window、resetMode是否为有效的枚举值。如果存储了损坏的数据,可能导致下游逻辑出错。🔧 建议添加枚举值验证
+import { LeaseEntityType, LeaseWindow } from "./lease"; +import { DailyResetModes } from "./time-utils"; // 假设导出了有效值数组 export function deserializeLease(json: string): BudgetLease | null { try { const parsed = JSON.parse(json); // Validate required fields if ( typeof parsed.entityType !== "string" || typeof parsed.entityId !== "number" || typeof parsed.window !== "string" || typeof parsed.resetMode !== "string" || typeof parsed.resetTime !== "string" || typeof parsed.snapshotAtMs !== "number" || typeof parsed.currentUsage !== "number" || typeof parsed.limitAmount !== "number" || typeof parsed.remainingBudget !== "number" || typeof parsed.ttlSeconds !== "number" ) { return null; } + // Validate enum values + if ( + !LeaseEntityType.includes(parsed.entityType) || + !LeaseWindow.includes(parsed.window) + ) { + return null; + } return parsed as BudgetLease; } catch { return null; } }src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx-439-452 (1)
439-452: 类型图标仅图形展示,建议补充可访问性标签当前单元格只显示图标,建议添加 aria-label(或 sr-only 文本)避免无文本可读。
建议修改
- <span - className={`inline-flex h-6 w-6 items-center justify-center rounded ${typeConfig.bgColor}`} - > + <span + role="img" + aria-label={typeLabel} + className={`inline-flex h-6 w-6 items-center justify-center rounded ${typeConfig.bgColor}`} + >src/app/[locale]/settings/config/_components/system-settings-form.tsx-435-445 (1)
435-445: 折叠触发按钮缺少可访问性标签该触发器只有图标,建议补充 aria-label,避免无文本可读。
建议修改
- <button - type="button" - className="flex items-center gap-1.5 mt-3 ml-11 text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer" - > + <button + type="button" + aria-label={t("enableResponseFixer")} + className="flex items-center gap-1.5 mt-3 ml-11 text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer" + >src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx-223-235 (1)
223-235: 图标按钮缺少可访问性标签该按钮只有图标,建议补充 aria-label 以便屏幕阅读器识别。
建议修改
- <button - type="button" - className="text-muted-foreground hover:text-foreground" - > + <button + type="button" + aria-label={t("vendorAggregationRule")} + className="text-muted-foreground hover:text-foreground" + >src/lib/rate-limit/lease-service.ts-306-312 (1)
306-312: Lua 脚本在 TTL <= 0 时不会持久化更新当
TTL返回 -1(key 存在但无过期时间)或 0 时,更新后的 lease 不会被保存回 Redis。虽然正常流程中setex总是设置 TTL,但边缘情况(如外部修改、竞态条件)可能导致 key 无 TTL,此时扣减成功返回但实际未持久化。建议处理 TTL <= 0 的情况
-- Get TTL and update lease local ttl = redis.call('TTL', key) if ttl > 0 then redis.call('SETEX', key, ttl, cjson.encode(lease)) + else + -- Key has no TTL or is expiring, use original ttlSeconds from lease + local leaseTtl = tonumber(lease.ttlSeconds) or 10 + redis.call('SETEX', key, leaseTtl, cjson.encode(lease)) end或者在 TTL 无效时返回失败,由调用方触发刷新:
local ttl = redis.call('TTL', key) - if ttl > 0 then - redis.call('SETEX', key, ttl, cjson.encode(lease)) - end + if ttl <= 0 then + return {remaining, 0} -- 返回失败,触发调用方刷新 + end + redis.call('SETEX', key, ttl, cjson.encode(lease))
🧹 Nitpick comments (24)
src/app/[locale]/dashboard/logs/_components/active-sessions-skeleton.tsx (1)
15-15: 建议:使用 Tailwind 类替代内联样式。为了与项目中其他样式保持一致,建议将内联
style替换为 Tailwind 工具类。♻️ 建议的修改
- <div style={{ maxHeight: "200px" }} className="overflow-y-auto"> + <div className="max-h-[200px] overflow-y-auto">src/app/[locale]/dashboard/logs/_components/provider-chain-popover.tsx (1)
335-350: 建议:将 TooltipProvider 提升到外层,避免为每个徽章创建独立实例。当前实现为每个 group badge 创建独立的
TooltipProvider,这会导致不必要的上下文开销。建议将TooltipProvider提升到外层,包裹所有 group badges。♻️ 建议的优化
)} - {/* Group tag badges (if present) */} - {finalGroupTags.map((group) => ( - <TooltipProvider key={group}> - <Tooltip delayDuration={200}> - <TooltipTrigger asChild> - <Badge - variant="outline" - className="text-[10px] px-1 py-0 shrink-0 bg-slate-50 text-slate-600 border-slate-200 dark:bg-slate-900/30 dark:text-slate-400 dark:border-slate-700 max-w-[120px] truncate" - > - {group} - </Badge> - </TooltipTrigger> - <TooltipContent>{group}</TooltipContent> - </Tooltip> - </TooltipProvider> - ))} + {/* Group tag badges (if present) */} + {finalGroupTags.length > 0 && ( + <TooltipProvider> + {finalGroupTags.map((group) => ( + <Tooltip key={group} delayDuration={200}> + <TooltipTrigger asChild> + <Badge + variant="outline" + className="text-[10px] px-1 py-0 shrink-0 bg-slate-50 text-slate-600 border-slate-200 dark:bg-slate-900/30 dark:text-slate-400 dark:border-slate-700 max-w-[120px] truncate" + > + {group} + </Badge> + </TooltipTrigger> + <TooltipContent>{group}</TooltipContent> + </Tooltip> + ))} + </TooltipProvider> + )}src/app/[locale]/settings/providers/_components/forms/test-result-card.tsx (1)
137-137: 考虑提取日期格式常量。日期格式字符串
"yyyy-MM-dd HH:mm:ss"在第 137 行和第 381 行重复使用。建议提取为常量以便统一维护。♻️ 建议的重构
在文件顶部添加常量:
const DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss";然后在两处使用:
- formatInTimeZone(new Date(result.testedAt), timeZone, "yyyy-MM-dd HH:mm:ss") + formatInTimeZone(new Date(result.testedAt), timeZone, DATETIME_FORMAT)tests/unit/components/session-list-item.test.tsx (2)
41-49: 冗余的类型定义和不必要的类型转换
SessionListItemProps已从@/components/customs/session-list-item导出,此处重复定义是多余的。类型转换也可以简化。♻️ 建议的改进
-import { SessionListItem } from "@/components/customs/session-list-item"; +import { SessionListItem, type SessionListItemProps } from "@/components/customs/session-list-item"; import type { CurrencyCode } from "@/lib/utils/currency"; import type { ActiveSessionInfo } from "@/types/session"; // ... mocks ... const COST_SENTINEL = "__COST__"; -type SessionListItemProps = { - session: ActiveSessionInfo; - currencyCode?: CurrencyCode; - showTokensCost?: boolean; -}; - -const SessionListItemTest = SessionListItem as unknown as ( - props: SessionListItemProps -) => JSX.Element;然后在
renderTextContent中直接使用SessionListItem。
82-103: 测试覆盖率有限当前仅测试了
showTokensCost的开关行为。根据编码规范,新功能需要至少 80% 的测试覆盖率。建议补充以下测试场景:
inputTokens或outputTokens为undefined时的显示costUsd为undefined时的显示- 不同
status状态(in_progress、error)的图标渲染durationMs不同区间(毫秒、秒、分钟)的格式化tests/unit/dashboard/availability/availability-dashboard-ui.test.tsx (2)
61-77: 测试覆盖率不足,建议补充更多测试用例。根据
availability-dashboard.tsx的实现,组件包含多个功能点:状态管理、数据获取、自动刷新、计算指标、标签页切换等。当前仅有一个检查 "Probe All" 按钮不存在的负向断言测试,覆盖率远低于编码规范要求的 80%。建议补充以下测试场景:
- 初始加载状态的渲染
- 数据获取成功后的渲染
- 数据获取失败时的错误处理
- 标签页切换行为
- 时间范围变更
基于 learnings:
All new features must have unit test coverage of at least 80%
63-69: 全局 fetch 存根未清理,可能影响其他测试。
vi.stubGlobal("fetch", ...)在测试结束后未恢复,可能导致其他测试受到污染。建议在测试后或使用afterEach钩子进行清理。🔧 建议的修复方案
describe("AvailabilityDashboard UI", () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + test("does not render Probe All floating button", async () => {src/components/ui/__tests__/tag-input-dialog.test.tsx (2)
67-68: 存在未使用的变量container。两个测试用例中都解构了
container但未使用。建议移除或使用下划线前缀标记为有意忽略。建议修复
- const { container, unmount } = render(<DialogTagInput />); + const { unmount } = render(<DialogTagInput />);Also applies to: 97-98
73-76: 异步等待使用setTimeout可能导致测试不稳定。使用固定延迟(50ms)等待状态更新可能在不同环境下不够可靠。考虑使用更健壮的等待策略,如轮询断言或
waitFor工具。不过,对于当前的 focus 触发 suggestions 显示的场景,这种方式在 happy-dom 环境下是可以接受的。
Also applies to: 103-106
tests/unit/dashboard-logs-virtualized-special-settings-ui.test.tsx (1)
164-177: 测试断言可能过于依赖实现细节。直接断言
ml-autoCSS 类名是脆弱的测试方式——如果将来重构样式(例如改用justify-end或其他布局方案),测试会失败但功能可能仍然正确。建议考虑:
- 仅保留
"1h"的存在性检查(核心功能)- 或使用视觉回归测试来验证布局
当前实现可以接受,但标记为潜在的维护风险。
src/lib/webhook/utils/date.ts (1)
19-24:formatTimestamp的 "backward compatibility" 注释具有误导性。
formatTimestamp函数同样需要timezone参数,并非真正的向后兼容。如果旧代码使用单参数调用,仍会报错。建议修改注释以更准确地描述其用途(例如"别名函数"或"语义化命名")。📝 建议的注释修改
/** - * Alias for formatDateTime for backward compatibility + * Alias for formatDateTime with semantic naming for timestamp formatting */ export function formatTimestamp(date: Date, timezone: string): string { return formatDateTime(date, timezone); }tests/unit/dashboard/dashboard-home-layout.test.tsx (1)
138-146:findClosestWithClasses辅助函数未被使用。该函数已定义但在测试中未被调用。建议移除未使用的代码,或者如果计划在后续测试中使用,可以添加相关测试用例。
🧹 建议移除未使用的辅助函数
-function findClosestWithClasses(element: Element | null, classes: string[]) { - let current = element?.parentElement ?? null; - while (current) { - const hasAll = classes.every((cls) => current.classList.contains(cls)); - if (hasAll) return current; - current = current.parentElement; - } - return null; -} -src/lib/webhook/types.ts (1)
102-107: 明确 timezone 的 null/undefined 语义,避免调用侧语义歧义若调用侧需要显式表达“清空/不覆盖”,建议允许
null并在注释中写清楚语义区分(undefined=继承默认,null=显式不指定)。这有助于和其他配置字段语义保持一致。Proposed change
- /** IANA timezone identifier for date/time formatting */ - timezone?: string; + /** IANA timezone identifier for date/time formatting. undefined=inherit, null=explicit default */ + timezone?: string | null;Based on learnings: 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.
tests/unit/lib/date-format-timezone.test.ts (1)
1-60: 测试覆盖全面且实现正确。测试套件覆盖了关键场景:
- UTC 和多个 IANA 时区(Asia/Shanghai、America/New_York、Asia/Tokyo)
- 跨午夜的日期格式化
- 字符串和数值类型的日期输入
- 未定义时区时的降级行为
- 带时区的 locale 格式化
使用固定的 UTC 时间戳确保了测试的确定性。
可考虑添加一个 DST(夏令时)边界的测试用例,例如使用 6 月的日期测试
America/New_York(EDT,UTC-4)以验证夏令时处理。当前 Line 25-29 的测试使用 1 月(EST,UTC-5),Line 44-48 使用 6 月但针对Asia/Tokyo(无 DST)。src/app/[locale]/dashboard/_components/user/key-list-header.tsx (1)
41-75: 避免硬编码日期格式,改为本地化格式化超过 7 天时输出固定
yyyy-MM-dd,这是用户可见的日期格式,建议基于 locale 做本地化格式化以避免不同语言环境显示不一致。建议修改示例
-import { formatInTimeZone } from "date-fns-tz"; +// 若改用 Intl.DateTimeFormat,可移除此导入 -function createFormatRelativeTime( - t: (key: string, params?: Record<string, number>) => string, - timeZone: string -) { +function createFormatRelativeTime( + t: (key: string, params?: Record<string, number>) => string, + locale: string, + timeZone: string +) { return (timestamp: number): string => { const diff = Date.now() - timestamp; @@ - return formatInTimeZone(new Date(timestamp), timeZone, "yyyy-MM-dd"); + return new Intl.DateTimeFormat(locale, { timeZone, dateStyle: "medium" }).format( + new Date(timestamp) + ); }; } @@ -const formatRelativeTime = useMemo(() => createFormatRelativeTime(t, timeZone), [t, timeZone]); +const formatRelativeTime = useMemo( + () => createFormatRelativeTime(t, locale, timeZone), + [t, locale, timeZone] +);As per coding guidelines: All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text.
src/lib/utils/date.ts (1)
42-60: 为formatInTimeZone添加错误处理以防止无效时区导致崩溃
formatInTimeZone在收到无效 IANA 时区字符串时会抛出异常,可能中断渲染链路。建议添加 try/catch 后退到本地格式化,保证应用稳定性。修改建议
export function formatDate(date: Date, locale = "zh-CN", timezone?: string): string { if (timezone) { - return formatInTimeZone(date, timezone, "yyyy-MM-dd"); + try { + return formatInTimeZone(date, timezone, "yyyy-MM-dd"); + } catch { + // fall through to locale formatting + } } return date.toLocaleDateString(locale, { year: "numeric", month: "2-digit", day: "2-digit", }); } @@ export function formatDateTime(date: Date, locale = "zh-CN", timezone?: string): string { if (timezone) { - return formatInTimeZone(date, timezone, "yyyy-MM-dd HH:mm:ss"); + try { + return formatInTimeZone(date, timezone, "yyyy-MM-dd HH:mm:ss"); + } catch { + // fall through to locale formatting + } } return date.toLocaleString(locale, { year: "numeric", month: "2-digit", day: "2-digit",src/lib/webhook/renderers/dingtalk.ts (1)
1-10: 导入路径建议改为@/别名以符合仓库规范。
当前仍使用相对路径导入../types/../utils/date/./index,建议统一为@/lib/webhook/...,避免路径风格混用。As per coding guidelines: Use path alias `@/` to reference files in `./src/` directory可选的调整示例
-import type { - ListItem, - Section, - SectionContent, - StructuredMessage, - WebhookPayload, - WebhookSendOptions, -} from "../types"; -import { formatTimestamp } from "../utils/date"; -import type { Renderer } from "./index"; +import type { + ListItem, + Section, + SectionContent, + StructuredMessage, + WebhookPayload, + WebhookSendOptions, +} from "@/lib/webhook/types"; +import { formatTimestamp } from "@/lib/webhook/utils/date"; +import type { Renderer } from "@/lib/webhook/renderers";src/app/[locale]/dashboard/logs/_components/logs-date-range-picker.tsx (2)
21-21:PickerQuickPeriod中的"custom"值未被使用。
detectQuickPeriod函数的返回类型声明为PickerQuickPeriod | null,但实际上当日期范围不匹配任何快捷周期时返回null而非"custom"。如果"custom"是设计上需要的返回值,建议修改函数逻辑;否则可简化类型定义。如果需要返回 "custom" 而非 null
function detectQuickPeriod( startDate?: string, endDate?: string, serverTimeZone?: string -): PickerQuickPeriod | null { +): PickerQuickPeriod { if (!startDate || !endDate) return null; for (const period of QUICK_PERIODS) { const range = getDateRangeForPeriod(period, serverTimeZone); if (range.startDate === startDate && range.endDate === endDate) { return period; } } - return null; + return "custom"; }Also applies to: 47-61
40-45: 考虑内联getDateRangeForPeriod函数。此函数仅是
getQuickDateRange的简单包装,可考虑直接在调用处使用getQuickDateRange,减少间接层。src/lib/notification/notification-queue.ts (1)
141-151: 重复调用getBindingById可优化。当
bindingId存在时,getBindingById被调用两次:
- 第 145-147 行:获取
scheduleTimezone- 第 228-230 行:获取
templateOverride建议合并为一次调用并复用结果。
♻️ 建议重构
+ // Fetch binding once if bindingId is provided + let binding: Awaited<ReturnType<typeof getBindingById>> | null = null; + if (bindingId) { + const { getBindingById } = await import("@/repository/notification-bindings"); + binding = await getBindingById(bindingId); + } + // Resolve timezone for formatting // Priority: binding's scheduleTimezone > system timezone let timezone: string | undefined; - if (bindingId) { - const { getBindingById } = await import("@/repository/notification-bindings"); - const binding = await getBindingById(bindingId); - timezone = binding?.scheduleTimezone ?? undefined; - } + timezone = binding?.scheduleTimezone ?? undefined; if (!timezone) { timezone = await resolveSystemTimezone(); } // ... later in the code ... - let templateOverride: Record<string, unknown> | null = null; - if (bindingId) { - const { getBindingById } = await import("@/repository/notification-bindings"); - const binding = await getBindingById(bindingId); - templateOverride = binding?.templateOverride ?? null; - } + const templateOverride = binding?.templateOverride ?? null;Also applies to: 226-231
src/lib/rate-limit/lease.ts (2)
24-25: 类型命名建议:LeaseEntityTypeType命名冗余。
LeaseEntityTypeType包含两个 "Type" 后缀,建议简化为LeaseEntityType并将 const 数组重命名为LeaseEntityTypes或LEASE_ENTITY_TYPES。♻️ 建议重构
-export const LeaseEntityType = ["key", "user", "provider"] as const; -export type LeaseEntityTypeType = (typeof LeaseEntityType)[number]; +export const LEASE_ENTITY_TYPES = ["key", "user", "provider"] as const; +export type LeaseEntityType = (typeof LEASE_ENTITY_TYPES)[number];
66-84: 类型转换可简化:LeaseWindowType与TimePeriod定义相同。
LeaseWindowType和TimePeriod都是"5h" | "daily" | "weekly" | "monthly",建议直接复用TimePeriod类型以避免类型断言和潜在的不同步风险。♻️ 建议重构
+import { TimePeriod } from "./time-utils"; + -export const LeaseWindow = ["5h", "daily", "weekly", "monthly"] as const; -export type LeaseWindowType = (typeof LeaseWindow)[number]; +export type LeaseWindowType = TimePeriod; +export const LeaseWindow: readonly LeaseWindowType[] = ["5h", "daily", "weekly", "monthly"];这样
getLeaseTimeRange和getLeaseTtlSeconds中就不需要as TimePeriod转换。src/app/[locale]/settings/providers/_components/recluster-vendors-dialog.tsx (1)
29-46: 导出ReclusterChange和ReclusterResult类型供组件复用。这两个类型在
src/actions/providers.ts(第 3565-3581 行)和当前组件中分别定义,造成了不必要的类型重复。建议在providers.ts中导出这些类型,然后在组件中通过@/actions/providers导入,以保持类型定义的单一性。建议的改动
在
src/actions/providers.ts中:- type ReclusterChange = { + export type ReclusterChange = {- type ReclusterResult = { + export type ReclusterResult = {在组件中移除本地类型定义,改为导入:
+ import type { ReclusterChange, ReclusterResult } from "@/actions/providers";src/lib/rate-limit/lease-service.ts (1)
153-154: 冗余的空值合并操作符
settings.quotaLeaseCapUsd ?? undefined是冗余的——如果quotaLeaseCapUsd为null或undefined,结果仍然是undefined。建议简化
const ttlSeconds = settings.quotaDbRefreshIntervalSeconds ?? 10; - const capUsd = settings.quotaLeaseCapUsd ?? undefined; + const capUsd = settings.quotaLeaseCapUsd ?? undefined; // 如果确需将 null 转为 undefined,保留此写法并添加注释说明意图如果这是为了显式将
null转换为undefined(根据项目约定null表示"显式无限制",undefined表示"继承默认"),建议添加注释说明意图。否则可直接使用settings.quotaLeaseCapUsd。
| "expires_at": { | ||
| "name": "expires_at", | ||
| "type": "timestamp", | ||
| "primaryKey": false, | ||
| "notNull": false | ||
| }, |
There was a problem hiding this comment.
快照与 schema 不一致,需重新生成
快照中 keys.expires_at 仍是 timestamp、notification_target_bindings.schedule_timezone 仍带默认值,且 system_settings 中缺少 timezone 列,与 src/drizzle/schema.ts 不一致。请重新运行生成流程并确保迁移对齐。Based on learnings: Modify database schema in src/drizzle/schema.ts, then run bun run db:generate to generate migrations. Never create SQL migration files manually
Also applies to: 1062-1068, 2354-2390
🤖 Prompt for AI Agents
In `@drizzle/meta/0058_snapshot.json` around lines 200 - 205, 快照显示 keys.expires_at
仍为 timestamp、notification_target_bindings.schedule_timezone 带默认值且
system_settings 缺少 timezone,与代码中的模式不一致;在模式定义文件中修正对应列(更新 keys.expires_at
的类型、移除或更新 notification_target_bindings.schedule_timezone 的默认值、为 system_settings
添加 timezone 列),然后运行生成命令 bun run db:generate 重新生成迁移与快照,确保不手工编辑 SQL 文件以使迁移与模式对齐。
| const range5h = await getTimeRangeForPeriod("5h"); | ||
| const rangeDaily = await getTimeRangeForPeriod("daily", user.dailyResetTime || "00:00"); | ||
| const rangeWeekly = await getTimeRangeForPeriod("weekly"); | ||
| const rangeMonthly = await getTimeRangeForPeriod("monthly"); |
There was a problem hiding this comment.
日限额统计需考虑 rolling 模式
当前 daily 使用 getTimeRangeForPeriod,会忽略 rolling 配置,导致日限额展示偏差。建议改为 getTimeRangeForPeriodWithMode 并传入 dailyResetMode。
建议修复
- const { getTimeRangeForPeriod } = await import("@/lib/rate-limit/time-utils");
+ const { getTimeRangeForPeriod, getTimeRangeForPeriodWithMode } = await import(
+ "@/lib/rate-limit/time-utils"
+ );
@@
- const rangeDaily = await getTimeRangeForPeriod("daily", user.dailyResetTime || "00:00");
+ const rangeDaily = await getTimeRangeForPeriodWithMode(
+ "daily",
+ user.dailyResetTime || "00:00",
+ user.dailyResetMode ?? "fixed"
+ );🤖 Prompt for AI Agents
In `@src/actions/users.ts` around lines 1487 - 1490, The daily time range
calculation currently calls getTimeRangeForPeriod("daily", user.dailyResetTime
|| "00:00") which ignores the user's rolling vs fixed reset setting; replace
this with getTimeRangeForPeriodWithMode("daily", user.dailyResetTime || "00:00",
user.dailyResetMode) (or the equivalent parameter order used by
getTimeRangeForPeriodWithMode) so rangeDaily is computed with the dailyResetMode
taken into account; update any references to rangeDaily accordingly.
| <TimeFilters | ||
| filters={localFilters} | ||
| onFiltersChange={setLocalFilters} | ||
| serverTimeZone={serverTimeZone} | ||
| /> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's find and examine the TimeFilters component
fd "TimeFilters" --type fRepository: ding113/claude-code-hub
Length of output: 49
🏁 Script executed:
# Also, let's look at the file structure around the mentioned file
find src/app/\[locale\]/dashboard/logs/_components -type f -name "*.tsx" -o -name "*.ts" | head -20Repository: ding113/claude-code-hub
Length of output: 1653
🏁 Script executed:
# Let's read the usage-logs-filters.tsx file to understand the context
cat -n src/app/\[locale\]/dashboard/logs/_components/usage-logs-filters.tsx | head -100Repository: ding113/claude-code-hub
Length of output: 3953
🏁 Script executed:
# Continue reading the usage-logs-filters.tsx file to see the TimeFilters usage
cat -n src/app/\[locale\]/dashboard/logs/_components/usage-logs-filters.tsx | sed -n '250,280p'Repository: ding113/claude-code-hub
Length of output: 1255
🏁 Script executed:
# Let's examine the TimeFilters component itself
cat -n src/app/\[locale\]/dashboard/logs/_components/filters/time-filters.tsxRepository: ding113/claude-code-hub
Length of output: 7379
🏁 Script executed:
# Let's also check the quick-filters-bar to see how presets are calculated
cat -n src/app/\[locale\]/dashboard/logs/_components/filters/quick-filters-bar.tsxRepository: ding113/claude-code-hub
Length of output: 2827
🏁 Script executed:
# Let's search for where the preset logic is applied (the onPresetToggle implementation)
cat -n src/app/\[locale\]/dashboard/logs/_components/usage-logs-filters.tsx | sed -n '100,250p'Repository: ding113/claude-code-hub
Length of output: 5804
🏁 Script executed:
# Let's check what imports are used at the top of usage-logs-filters.tsx for date operations
cat -n src/app/\[locale\]/dashboard/logs/_components/usage-logs-filters.tsx | sed -n '1,50p'Repository: ding113/claude-code-hub
Length of output: 2128
🏁 Script executed:
# Let's look for the time-range utility functions
cat -n src/app/\[locale\]/dashboard/logs/_utils/time-range.tsRepository: ding113/claude-code-hub
Length of output: 4140
快速筛选的时区计算与 TimeFilters 不一致。
TimeFilters 已正确按 serverTimeZone 展示和解析日期,但快速筛选("today" 和 "this-week")仍使用本地时区的 startOfDay() 和 startOfWeek() 计算。这导致用户选择快速筛选时的时间范围与显示时区产生偏差。
建议将快速筛选的时间计算改为基于 serverTimeZone,可参考已存在的 getQuickDateRange() 工具函数(time-range.ts)进行改造,该函数已支持时区感知的日期范围计算。
🤖 Prompt for AI Agents
In `@src/app/`[locale]/dashboard/logs/_components/usage-logs-filters.tsx around
lines 264 - 268, The quick-filter ("today", "this-week") range calculation
currently uses local timezone startOfDay/startOfWeek and thus diverges from
TimeFilters' serverTimeZone-aware parsing; update the quick-filter logic that
sets localFilters (used by TimeFilters via setLocalFilters) to compute ranges
using the existing getQuickDateRange(timeKey, serverTimeZone) utility from
time-range.ts so the generated start/end dates are timezone-aware and consistent
with TimeFilters' display and parsing. Ensure you replace any direct
startOfDay/startOfWeek calls with getQuickDateRange and pass serverTimeZone
through to the function when handling quick-filter selection.
| enableMultiProviderTypes, | ||
| }: ProviderListItemProps) { | ||
| const router = useRouter(); | ||
| const timeZone = useTimeZone() ?? "UTC"; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n src/app/[locale]/settings/providers/_components/provider-list-item.legacy.tsx | head -80Repository: ding113/claude-code-hub
Length of output: 3682
🌐 Web query:
date-fns-tz formatInTimeZone RangeError invalid date timezone
💡 Result:
date-fns-tz’s formatInTimeZone(date, timeZone, fmt) ultimately calls date-fns/format. If either the date or the time zone can’t be resolved, it becomes an Invalid Date, and format throws a RangeError (often shown as “Invalid time value” / “invalid date”). [1][2]
Common causes + fixes:
- Your
dateis invalid
- Happens when parsing a non-ISO string, a “time-only” string,
undefined, or an out-of-bounds timestamp; operations like ISO conversion/formatting then throwRangeError. [3] - Fix: ensure you pass a real
Date(or a valid ISO string), and validate before formatting.import { isValid } from 'date-fns' import { formatInTimeZone } from 'date-fns-tz' if (!isValid(date)) throw new Error('Invalid date input')
- Your
timeZoneis invalid in that runtime
formatInTimeZoneexpects an IANA zone likeAmerica/New_York(or an offset like-05:00). If the zone string is wrong/empty, it will throw. [1][2]- Also, some environments/users can hit
RangeError: Invalid time zone specified: <IANA zone>even when the zone looks valid (because time zone support comes from the platformIntlimplementation). This has been reported in the wild. [4]
- Missing / limited
Intltime zone support (notably some React Native / Android setups)
date-fns-tzrelies onIntl. On Android in React Native,Intlisn’t available by default in some configurations; you may need anIntl-enabled JS engine build or a polyfill. [5]- Fix: add the recommended
Intlsupport/polyfill for your target (RN/Android, Hermes vs non-Hermes). [5]
If you paste:
- the exact
datevalue you’re passing (and how you create it), - the exact
timeZonestring, - and your runtime (browser + version / Node version / React Native + Hermes?)
…I can tell you which of the above is triggering your specificRangeError.
Sources: [1] [2] [3] [4] [5]
🌐 Web query:
next-intl useTimeZone hook return value behavior empty string
💡 Result:
useTimeZone() just returns the timeZone value from the active next-intl config/context as-is (it doesn’t normalize, validate, or “fall back” if it’s empty). In the compiled hook, it effectively does return useConfig("useTimeZone").timeZone; [1].
So if you observe "" (empty string), that means your resolved next-intl config’s timeZone is actually "" (e.g., you passed timeZone="" to NextIntlClientProvider, or returned timeZone: '' from getRequestConfig). next-intl won’t replace that with a default [1].
When you don’t configure timeZone, next-intl’s server-side default is taken from the runtime via Intl.DateTimeFormat().resolvedOptions().timeZone [2], and the docs describe this as “the time zone of the server runtime will be used” [3].
Practical takeaway: don’t use "" to mean “unset”—omit timeZone (or use undefined) or provide a valid IANA zone like "America/New_York" [3]. If you need the user’s browser zone, compute it separately with Intl.DateTimeFormat().resolvedOptions().timeZone and pass that.
Sources: [1] [2] [3]
🏁 Script executed:
cat -n src/app/[locale]/settings/providers/_components/provider-list-item.legacy.tsx | sed -n '350,365p'Repository: ding113/claude-code-hub
Length of output: 965
防止 formatInTimeZone 因无效日期抛 RangeError
date-fns-tz 在接收无效日期或时区时会抛 RangeError。当前代码仅对 lastCallTime 做 truthy 判断,若其值为非空但无效的日期字符串(如格式错误的字符串),new Date(item.lastCallTime) 将创建 Invalid Date,formatInTimeZone 随后会抛错导致渲染崩溃。建议添加日期有效性检查。另外,使用 || 替代 ?? 做 timeZone 的回退更为稳妥。
建议修改
- const timeZone = useTimeZone() ?? "UTC";
+ const timeZone = useTimeZone() || "UTC";
...
- {item.lastCallTime
- ? formatInTimeZone(new Date(item.lastCallTime), timeZone, "yyyy-MM-dd HH:mm")
- : "-"}
+ {item.lastCallTime
+ ? (() => {
+ const lastCallDate = new Date(item.lastCallTime);
+ if (Number.isNaN(lastCallDate.getTime())) {
+ return "-";
+ }
+ return formatInTimeZone(lastCallDate, timeZone, "yyyy-MM-dd HH:mm");
+ })()
+ : "-"}🤖 Prompt for AI Agents
In
`@src/app/`[locale]/settings/providers/_components/provider-list-item.legacy.tsx
at line 66, The code risks a RangeError when formatInTimeZone receives an
invalid date or timezone: update the timeZone assignment to use fallback with ||
(e.g., const timeZone = useTimeZone() || "UTC") and add a date validity check
before calling formatInTimeZone — compute const d = new Date(item.lastCallTime)
and ensure it's valid (e.g., !Number.isNaN(d.getTime()) or use isValid from
date-fns) and only call formatInTimeZone(d, timeZone, ...) when valid; otherwise
render a safe fallback (empty string or "—") to avoid rendering crashes.
| // 8. User 每日额度(User 独有的常用预算)- null 表示无限制 | ||
| if (user.dailyQuota !== null) { | ||
| const dailyCheck = await RateLimitService.checkUserDailyCost( | ||
| user.id, | ||
| user.dailyQuota, | ||
| user.dailyResetTime, | ||
| user.dailyResetMode | ||
| ); | ||
|
|
||
| if (!dailyCheck.allowed) { | ||
| logger.warn(`[RateLimit] User daily limit exceeded: user=${user.id}, ${dailyCheck.reason}`); | ||
|
|
||
| const { getLocale } = await import("next-intl/server"); | ||
| const locale = await getLocale(); | ||
|
|
||
| // 根据模式选择不同的错误消息 | ||
| if (user.dailyResetMode === "rolling") { | ||
| // rolling 模式:使用滚动窗口专用消息(无固定重置时间) | ||
| const message = await getErrorMessageServer( | ||
| locale, | ||
| ERROR_CODES.RATE_LIMIT_DAILY_ROLLING_EXCEEDED, | ||
| { | ||
| current: (dailyCheck.current || 0).toFixed(4), | ||
| limit: user.dailyQuota.toFixed(4), | ||
| } | ||
| ); | ||
|
|
||
| throw new RateLimitError( | ||
| "rate_limit_error", | ||
| message, | ||
| "daily_quota", | ||
| dailyCheck.current || 0, | ||
| user.dailyQuota, | ||
| null, // 滚动窗口没有固定重置时间 | ||
| null | ||
| ); | ||
| } else { | ||
| // fixed 模式:有固定重置时间 | ||
| const resetInfo = await getResetInfoWithMode( | ||
| "daily", | ||
| user.dailyResetTime, | ||
| user.dailyResetMode | ||
| ); | ||
| const resetTime = | ||
| resetInfo.resetAt?.toISOString() ?? | ||
| new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(); | ||
|
|
||
| const message = await getErrorMessageServer( | ||
| locale, | ||
| ERROR_CODES.RATE_LIMIT_DAILY_QUOTA_EXCEEDED, | ||
| { | ||
| current: (dailyCheck.current || 0).toFixed(4), | ||
| limit: user.dailyQuota.toFixed(4), | ||
| resetTime, | ||
| } | ||
| ); | ||
|
|
||
| throw new RateLimitError( | ||
| "rate_limit_error", | ||
| message, | ||
| "daily_quota", | ||
| dailyCheck.current || 0, | ||
| user.dailyQuota, | ||
| resetTime, | ||
| null | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
User 日额度判断需区分 undefined 与 null
当前条件只排除了 null,若 dailyQuota 为 undefined 仍会进入校验,可能导致意外限流。建议显式区分 undefined(继承默认)与 null(无限制),或在此处先解析默认值后再判断。
建议修改
- if (user.dailyQuota !== null) {
+ if (user.dailyQuota !== null && user.dailyQuota !== undefined) {🤖 Prompt for AI Agents
In `@src/app/v1/_lib/proxy/rate-limit-guard.ts` around lines 359 - 426, The
current guard if (user.dailyQuota !== null) incorrectly allows undefined to
proceed into RateLimitService.checkUserDailyCost and cause unintended
throttling; update the logic to distinguish undefined (inherit default) from
null (unlimited) by only running the daily check when the quota is a concrete
number. Concretely, in the block around user.dailyQuota, compute a local quota
like const quota = user.dailyQuota; and change the condition to typeof quota ===
"number" (or explicitly check !== null && quota !== undefined), pass quota into
RateLimitService.checkUserDailyCost and use quota (not user.dailyQuota) when
formatting limit and building RateLimitError messages, keeping the existing
branches for rolling vs fixed reset using getResetInfoWithMode and
getErrorMessageServer.
| // 系统时区配置(可选) | ||
| // 必须是有效的 IANA 时区标识符(如 "Asia/Shanghai", "America/New_York") | ||
| timezone: z | ||
| .string() | ||
| .refine((val) => isValidIANATimezone(val), { | ||
| message: "无效的时区标识符,请使用 IANA 时区格式(如 Asia/Shanghai)", | ||
| }) | ||
| .nullable() | ||
| .optional(), |
There was a problem hiding this comment.
时区校验提示需走 i18n。
新增错误提示为硬编码字符串,属于用户可见文案;请改为多语言资源或统一错误映射。
As per coding guidelines: All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text.
🤖 Prompt for AI Agents
In `@src/lib/validation/schemas.ts` around lines 732 - 740, The timezone zod
schema currently uses a hardcoded Chinese error message; replace that with an
i18n lookup so user-facing validation messages are localized. Update the
timezone schema (the timezone field in src/lib/validation/schemas.ts) to call
the project's i18n function or error-mapping utility (e.g.,
t('validation.invalidTimezone') or the centralized error mapper) instead of the
literal string, ensuring keys exist for zh-CN, zh-TW, en, ja, and ru; keep the
refine predicate (isValidIANATimezone) unchanged and return the i18n key/value
for the message so the validation layer emits localized text rather than a
hardcoded string.
| // Quota lease settings | ||
| quotaDbRefreshIntervalSeconds: z.coerce | ||
| .number() | ||
| .int("DB refresh interval must be an integer") | ||
| .min(1, "DB refresh interval cannot be less than 1 second") | ||
| .max(300, "DB refresh interval cannot exceed 300 seconds") | ||
| .optional(), | ||
| quotaLeasePercent5h: z.coerce | ||
| .number() | ||
| .min(0, "Lease percent cannot be negative") | ||
| .max(1, "Lease percent cannot exceed 1") | ||
| .optional(), | ||
| quotaLeasePercentDaily: z.coerce | ||
| .number() | ||
| .min(0, "Lease percent cannot be negative") | ||
| .max(1, "Lease percent cannot exceed 1") | ||
| .optional(), | ||
| quotaLeasePercentWeekly: z.coerce | ||
| .number() | ||
| .min(0, "Lease percent cannot be negative") | ||
| .max(1, "Lease percent cannot exceed 1") | ||
| .optional(), | ||
| quotaLeasePercentMonthly: z.coerce | ||
| .number() | ||
| .min(0, "Lease percent cannot be negative") | ||
| .max(1, "Lease percent cannot exceed 1") | ||
| .optional(), | ||
| quotaLeaseCapUsd: z.coerce.number().min(0, "Lease cap cannot be negative").nullable().optional(), |
There was a problem hiding this comment.
租赁配额校验文案需统一多语言。
新增英文提示会直接返回给前端,和规范不一致;建议使用 i18n 资源。
As per coding guidelines: All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text.
🤖 Prompt for AI Agents
In `@src/lib/validation/schemas.ts` around lines 786 - 813, The Zod validators for
quotaDbRefreshIntervalSeconds, quotaLeasePercent5h, quotaLeasePercentDaily,
quotaLeasePercentWeekly, quotaLeasePercentMonthly, and quotaLeaseCapUsd
currently use hardcoded English error messages; replace those literal strings
with i18n lookups (use the project's translation function and stable i18n keys)
so validators call the i18n resource instead of embedding text, and add
corresponding keys/translations for zh-CN, zh-TW, en, ja, ru; keep the same
validation logic (.int, .min, .max, .nullable, .optional) and ensure messages
are passed the translated string values.
| const fields = [ | ||
| { label: "失败次数", value: `${data.failureCount} 次` }, | ||
| { label: "预计恢复", value: formatDateTime(data.retryAt) }, | ||
| { label: "预计恢复", value: formatDateTime(data.retryAt, timezone || "UTC") }, | ||
| ]; |
There was a problem hiding this comment.
硬编码的中文字符串违反 i18n 规范。
根据编码规范,所有用户可见的字符串必须使用 i18n(支持 5 种语言:zh-CN、zh-TW、en、ja、ru)。当前 "失败次数"、"预计恢复" 等字符串是硬编码的。
建议将这些字符串提取到 i18n 消息文件中,并通过翻译函数获取。
As per coding guidelines: "All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text"
🤖 Prompt for AI Agents
In `@src/lib/webhook/templates/circuit-breaker.ts` around lines 8 - 11, The fields
array in circuit-breaker.ts contains hardcoded user-facing labels ("失败次数",
"预计恢复") which violate i18n rules; replace these literals with calls to the
project's translation function (e.g., t or i18n.t) and add appropriate message
keys (e.g., "circuitBreaker.failureCount", "circuitBreaker.estimatedRecovery")
in the i18n message files for all supported locales (zh-CN, zh-TW, en, ja, ru);
update the const fields initialization to use the translation calls (passing any
interpolations like `${data.failureCount} 次` as variables where appropriate) and
ensure formatDateTime(data.retryAt, timezone || "UTC") remains as the translated
value for the "estimated recovery" label.
| quotaDbRefreshIntervalSeconds: dbSettings?.quotaDbRefreshIntervalSeconds ?? 10, | ||
| quotaLeasePercent5h: dbSettings?.quotaLeasePercent5h | ||
| ? parseFloat(dbSettings.quotaLeasePercent5h) | ||
| : 0.05, | ||
| quotaLeasePercentDaily: dbSettings?.quotaLeasePercentDaily | ||
| ? parseFloat(dbSettings.quotaLeasePercentDaily) | ||
| : 0.05, | ||
| quotaLeasePercentWeekly: dbSettings?.quotaLeasePercentWeekly | ||
| ? parseFloat(dbSettings.quotaLeasePercentWeekly) | ||
| : 0.05, | ||
| quotaLeasePercentMonthly: dbSettings?.quotaLeasePercentMonthly | ||
| ? parseFloat(dbSettings.quotaLeasePercentMonthly) | ||
| : 0.05, | ||
| quotaLeaseCapUsd: dbSettings?.quotaLeaseCapUsd ? parseFloat(dbSettings.quotaLeaseCapUsd) : null, |
There was a problem hiding this comment.
避免把 0 误判为未配置
当前使用 truthy 判断时,"0" 会被回退到默认值或 null,导致无法显式关闭/清零配额租赁。建议改为 null/undefined 判断并对 NaN 回退。
建议修复
export function toSystemSettings(dbSettings: any): SystemSettings {
const defaultResponseFixerConfig: ResponseFixerConfig = {
fixTruncatedJson: true,
fixSseFormat: true,
fixEncoding: true,
maxJsonDepth: 200,
maxFixSize: 1024 * 1024,
};
+
+ const parseOptionalFloatOrDefault = (value: unknown, fallback: number): number => {
+ if (value === null || value === undefined) return fallback;
+ const parsed = Number.parseFloat(String(value));
+ return Number.isNaN(parsed) ? fallback : parsed;
+ };
+
+ const parseOptionalFloatOrNull = (value: unknown): number | null => {
+ if (value === null || value === undefined) return null;
+ const parsed = Number.parseFloat(String(value));
+ return Number.isNaN(parsed) ? null : parsed;
+ };
@@
- quotaLeasePercent5h: dbSettings?.quotaLeasePercent5h
- ? parseFloat(dbSettings.quotaLeasePercent5h)
- : 0.05,
- quotaLeasePercentDaily: dbSettings?.quotaLeasePercentDaily
- ? parseFloat(dbSettings.quotaLeasePercentDaily)
- : 0.05,
- quotaLeasePercentWeekly: dbSettings?.quotaLeasePercentWeekly
- ? parseFloat(dbSettings.quotaLeasePercentWeekly)
- : 0.05,
- quotaLeasePercentMonthly: dbSettings?.quotaLeasePercentMonthly
- ? parseFloat(dbSettings.quotaLeasePercentMonthly)
- : 0.05,
- quotaLeaseCapUsd: dbSettings?.quotaLeaseCapUsd ? parseFloat(dbSettings.quotaLeaseCapUsd) : null,
+ quotaLeasePercent5h: parseOptionalFloatOrDefault(dbSettings?.quotaLeasePercent5h, 0.05),
+ quotaLeasePercentDaily: parseOptionalFloatOrDefault(
+ dbSettings?.quotaLeasePercentDaily,
+ 0.05
+ ),
+ quotaLeasePercentWeekly: parseOptionalFloatOrDefault(
+ dbSettings?.quotaLeasePercentWeekly,
+ 0.05
+ ),
+ quotaLeasePercentMonthly: parseOptionalFloatOrDefault(
+ dbSettings?.quotaLeasePercentMonthly,
+ 0.05
+ ),
+ quotaLeaseCapUsd: parseOptionalFloatOrNull(dbSettings?.quotaLeaseCapUsd),📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| quotaDbRefreshIntervalSeconds: dbSettings?.quotaDbRefreshIntervalSeconds ?? 10, | |
| quotaLeasePercent5h: dbSettings?.quotaLeasePercent5h | |
| ? parseFloat(dbSettings.quotaLeasePercent5h) | |
| : 0.05, | |
| quotaLeasePercentDaily: dbSettings?.quotaLeasePercentDaily | |
| ? parseFloat(dbSettings.quotaLeasePercentDaily) | |
| : 0.05, | |
| quotaLeasePercentWeekly: dbSettings?.quotaLeasePercentWeekly | |
| ? parseFloat(dbSettings.quotaLeasePercentWeekly) | |
| : 0.05, | |
| quotaLeasePercentMonthly: dbSettings?.quotaLeasePercentMonthly | |
| ? parseFloat(dbSettings.quotaLeasePercentMonthly) | |
| : 0.05, | |
| quotaLeaseCapUsd: dbSettings?.quotaLeaseCapUsd ? parseFloat(dbSettings.quotaLeaseCapUsd) : null, | |
| const parseOptionalFloatOrDefault = (value: unknown, fallback: number): number => { | |
| if (value === null || value === undefined) return fallback; | |
| const parsed = Number.parseFloat(String(value)); | |
| return Number.isNaN(parsed) ? fallback : parsed; | |
| }; | |
| const parseOptionalFloatOrNull = (value: unknown): number | null => { | |
| if (value === null || value === undefined) return null; | |
| const parsed = Number.parseFloat(String(value)); | |
| return Number.isNaN(parsed) ? null : parsed; | |
| }; | |
| quotaDbRefreshIntervalSeconds: dbSettings?.quotaDbRefreshIntervalSeconds ?? 10, | |
| quotaLeasePercent5h: parseOptionalFloatOrDefault(dbSettings?.quotaLeasePercent5h, 0.05), | |
| quotaLeasePercentDaily: parseOptionalFloatOrDefault( | |
| dbSettings?.quotaLeasePercentDaily, | |
| 0.05 | |
| ), | |
| quotaLeasePercentWeekly: parseOptionalFloatOrDefault( | |
| dbSettings?.quotaLeasePercentWeekly, | |
| 0.05 | |
| ), | |
| quotaLeasePercentMonthly: parseOptionalFloatOrDefault( | |
| dbSettings?.quotaLeasePercentMonthly, | |
| 0.05 | |
| ), | |
| quotaLeaseCapUsd: parseOptionalFloatOrNull(dbSettings?.quotaLeaseCapUsd), |
🤖 Prompt for AI Agents
In `@src/repository/_shared/transformers.ts` around lines 198 - 211, The code
incorrectly uses truthy checks so string "0" becomes false and falls back to
defaults; update each quota field check to test for null/undefined (e.g.,
dbSettings?.quotaLeasePercent5h != null) then parseFloat and if
Number.isNaN(parsed) fall back to the intended default (0.05 for percent fields,
null for quotaLeaseCapUsd). Apply this to quotaLeasePercent5h,
quotaLeasePercentDaily, quotaLeasePercentWeekly, quotaLeasePercentMonthly and
quotaLeaseCapUsd, keeping quotaDbRefreshIntervalSeconds as-is (it already uses
??).
| function normalizeHostWithPort(rawUrl: string): string | null { | ||
| const trimmed = rawUrl.trim(); | ||
| if (!trimmed) return null; | ||
|
|
||
| // Add https:// if no scheme present | ||
| let urlString = trimmed; | ||
| if (!/^[a-zA-Z][a-zA-Z\d+.-]*:\/\//.test(trimmed)) { | ||
| urlString = `https://${trimmed}`; | ||
| } | ||
|
|
||
| try { | ||
| const parsed = new URL(urlString); | ||
| const hostname = parsed.hostname?.toLowerCase(); | ||
| if (!hostname) return null; | ||
|
|
||
| // Strip www. prefix | ||
| const normalizedHostname = hostname.startsWith("www.") ? hostname.slice(4) : hostname; | ||
|
|
||
| // Determine port | ||
| let port: string; | ||
| if (parsed.port) { | ||
| port = parsed.port; | ||
| } else { | ||
| // Use protocol default port | ||
| port = parsed.protocol === "http:" ? "80" : "443"; | ||
| } | ||
|
|
||
| // IPv6 addresses already have brackets from URL parser (e.g., "[::1]") | ||
| // Just append the port directly | ||
| return `${normalizedHostname}:${port}`; | ||
| } catch (error) { | ||
| logger.debug("[ProviderVendor] Failed to parse URL for host:port", { | ||
| urlLength: rawUrl.length, | ||
| error: error instanceof Error ? error.message : String(error), | ||
| }); | ||
| return null; | ||
| } |
There was a problem hiding this comment.
IPv6 host:port 归一化未加方括号会产生歧义
URL.hostname 对 IPv6 会返回不带方括号的地址,当前实现会生成 ::1:443 这类不可解析的 key,与注释中的 [ipv6]:port 规则不一致。建议检测包含冒号的主机名并补齐方括号。
建议修改
- // IPv6 addresses already have brackets from URL parser (e.g., "[::1]")
- // Just append the port directly
- return `${normalizedHostname}:${port}`;
+ const hostForKey =
+ normalizedHostname.includes(":") && !normalizedHostname.startsWith("[")
+ ? `[${normalizedHostname}]`
+ : normalizedHostname;
+ return `${hostForKey}:${port}`;🤖 Prompt for AI Agents
In `@src/repository/provider-endpoints.ts` around lines 63 - 99,
normalizeHostWithPort produces ambiguous IPv6 keys (e.g., "::1:443") because
URL.hostname returns IPv6 without brackets; update the function
(normalizeHostWithPort) to wrap the normalizedHostname in square brackets when
it contains a colon and doesn't already start with "[" before appending the port
(i.e., detect hostname.includes(":") && !hostname.startsWith("["), then set
normalizedHostname = `[${normalizedHostname}]`) so returned strings follow the
"[ipv6]:port" rule.
Summary
Release v0.5.2 consolidates multiple feature enhancements and bug fixes from the dev branch, including timezone consistency improvements, lease-based rate limiting, dynamic probe intervals, Gemini image billing fixes, vendor reclustering, and dashboard UI improvements.
Included Changes
Features
Timezone Consistency & Lease-Based Quota (#668)
resolveSystemTimezone()with fallback chain: DB -> env TZ -> UTCDynamic Endpoint Probe Intervals (#669)
ENDPOINT_PROBE_INTERVAL_MS)Vendor Reclustering by Host:Port (#670)
computeVendorKeyhelper with host:port support for IP-based providerswebsite_urlis empty, use host:port as vendor key[ipv6]:portformatReclusterVendorsDialogUI component with preview/apply modeDashboard UI Improvements (#657)
Bug Fixes
Gemini Image Billing Fix (#664)
UsageMetricstype to includeoutput_image_tokensandinput_image_tokensSettings Persistence Fix
Server Actions Compliance
computeVendorKeyasync for Server Actions complianceRelated Issues
Database Migrations
4 new migrations included:
0048_add_system_timezone.sql- Add timezone column to system_settings0057_conscious_quicksilver.sql- Token columns changed to bigint0058_silly_sleepwalker.sql- Add lease configuration columns0059_safe_xorn.sql- Change expires_at to timestamptzBreaking Changes
ENDPOINT_PROBE_INTERVAL_MSchanged from 10s to 60sENDPOINT_PROBE_INTERVAL_MS=10000to restore old behaviorConfiguration
New Environment Variables
ENDPOINT_PROBE_INTERVAL_MS- Base probe interval (default: 60000ms)New System Settings
timezone- IANA timezone (e.g., 'Asia/Shanghai')quota_db_refresh_interval_seconds- Lease refresh interval (default: 10s)quota_lease_percent_5h/daily/weekly/monthly- Lease percentages (default: 5%)quota_lease_cap_usd- Optional cap in USDFiles Changed
Checklist
Description enhanced by Claude AI
Greptile Overview
Greptile Summary
This release (v0.5.2) introduces several major features and improvements:
Key Features:
host:portas vendor key whenwebsiteUrlis empty (previously used only hostname). Includes admin UI with preview/confirm workflow.Bug Fixes:
getResetInfoandgetResetInfoWithMode(were missing await)Improvements:
Issues Found:
computeVendorKey,deriveDisplayNameFromDomain) are declared async but don't use await statements - should be synchronousConfidence Score: 4/5
Important Files Changed
Sequence Diagram
sequenceDiagram participant Client participant RateLimitGuard as Rate Limit Guard participant RateLimitService as Rate Limit Service participant LeaseService as Lease Service participant Redis participant DB as Database Client->>RateLimitGuard: API Request Note over RateLimitGuard: 13-layer check order<br/>Total limits, Resource protection<br/>Short-term and Long-term windows RateLimitGuard->>RateLimitService: checkCostLimits(entityId, window) RateLimitService->>LeaseService: getCostLease(entityId, window, limitAmount) alt Cache Hit in Redis LeaseService->>Redis: GET lease key Redis-->>LeaseService: Cached lease alt Lease expired or limit changed LeaseService->>LeaseService: refreshCostLeaseFromDb() LeaseService->>DB: Query usage in time window DB-->>LeaseService: Current usage LeaseService->>LeaseService: calculateLeaseSlice(limit, usage, percent, cap) LeaseService->>Redis: Store lease with TTL end else Cache Miss LeaseService->>DB: Query usage in time window DB-->>LeaseService: Current usage LeaseService->>LeaseService: calculateLeaseSlice(limit, usage, percent, cap) Note over LeaseService: Lease equals min of limit times percent<br/>remaining budget and cap USD LeaseService->>Redis: Store lease with TTL end LeaseService-->>RateLimitService: BudgetLease alt Budget sufficient RateLimitService-->>RateLimitGuard: allowed equals true RateLimitGuard-->>Client: Process request Note over Client,DB: After request completes Client->>LeaseService: decrementLeaseBudget(cost) LeaseService->>Redis: EVAL Lua script atomically Note over Redis: Atomic decrement with<br/>budget check and TTL preservation Redis-->>LeaseService: newRemaining else Budget insufficient RateLimitService-->>RateLimitGuard: allowed equals false with reason RateLimitGuard-->>Client: RateLimitError end