Skip to content

release v0.5.2#675

Merged
ding113 merged 14 commits intomainfrom
dev
Jan 29, 2026
Merged

release v0.5.2#675
ding113 merged 14 commits intomainfrom
dev

Conversation

@ding113
Copy link
Owner

@ding113 ding113 commented Jan 29, 2026

Summary

Release v0.5.2 merges the dev branch into main, bringing significant improvements to rate limiting, timezone handling, provider management, billing accuracy, and dashboard UI.

Highlights

Rate Limiting & Quota Enforcement

  • Lease-based quota system: Unified all periodic cost limits (5h/daily/weekly/monthly) to use lease mechanism for atomic budget allocation
  • Rolling window semantics: 5h and daily quotas now use rolling windows calculated from first request in period
  • Data source consistency: All quota UIs now use direct DB queries for accurate display

Timezone Consistency

  • System-wide timezone setting: Configurable timezone stored in DB with IANA validation
  • Unified timezone resolution: resolveSystemTimezone() with fallback chain (DB -> env TZ -> UTC)
  • Server timezone support for logs: Date filtering uses server timezone for accurate "today"/"yesterday" calculations

Provider Management

  • Dynamic probe intervals: Three-tier priority system (10s for timeout recovery, 10min for single-endpoint vendors, 60s default)
  • Vendor reclustering: New host:port based vendor key when website_url is empty, with preview/apply UI

Billing Fixes

  • Gemini IMAGE token billing: Properly bills IMAGE modality tokens at 10x rate ($0.00012 vs $0.000012)

Dashboard UI Improvements

  • Provider chain popover: Enhanced with exclusion reason translations and deduped group badges
  • TagInput positioning: Fixed dropdown positioning inside Dialog components
  • Cache badge alignment: TTL badges moved to left, token numbers right-aligned
  • Endpoint inline toggle: Enable/disable endpoints directly in table rows

Related Issues

Included PRs

PR Title Type
#657 Dashboard UI improvements Enhancement
#664 Fix Gemini IMAGE modality token billing Bug Fix
#668 Timezone consistency & rate-limit lease-based quota Enhancement
#669 Dynamic endpoint probe intervals Enhancement
#670 Recluster vendors by host:port when website_url empty Enhancement
#674 Unify lease mechanism for all periodic cost limits Bug Fix

Database Migrations

Required migrations (auto-applied with AUTO_MIGRATE=true):

  • 0048_add_system_timezone.sql - Add timezone column to system_settings
  • 0057_conscious_quicksilver.sql - Convert token columns to bigint
  • 0058_silly_sleepwalker.sql - Add lease configuration columns
  • 0059_safe_xorn.sql - Change expires_at to timestamptz

Breaking Changes

Change Impact Migration
Default ENDPOINT_PROBE_INTERVAL_MS changed from 10s to 60s Existing deployments probe less frequently Set ENDPOINT_PROBE_INTERVAL_MS=10000 to restore old behavior

New Configuration Options

System Settings (via UI)

  • timezone: IANA timezone (e.g., 'Asia/Shanghai')
  • quota_db_refresh_interval_seconds: Lease refresh interval (default: 10s)
  • quota_lease_percent_*: Lease percentages for 5h/daily/weekly/monthly (default: 5%)
  • quota_lease_cap_usd: Optional cap in USD

Environment Variables

  • ENDPOINT_PROBE_INTERVAL_MS: Base probe interval (default: 60000ms)

i18n Updates

Added translations for all 5 supported languages (en, zh-CN, zh-TW, ja, ru):

  • Provider chain exclusion reasons
  • Timezone and lease configuration settings
  • Vendor reclustering UI strings
  • Probe interval documentation

Testing

  • All unit tests pass (1619+ tests)
  • TypeScript type check passes
  • Biome lint check passes

Checklist

  • All included PRs have been reviewed and merged to dev
  • Database migrations generated and reviewed
  • i18n translations complete for all languages
  • No breaking changes to external APIs

Description enhanced by Claude AI

Greptile Overview

Greptile Summary

This release merges dev into main, introducing a comprehensive lease-based rate limiting system, timezone consistency improvements, dynamic provider probe intervals, Gemini IMAGE token billing fixes, and various UI enhancements.

Key Changes

  • Lease-based quota enforcement: All periodic cost limits (5h/daily/weekly/monthly) now use atomic Redis lease mechanism with DB as authoritative source. Lease budgets are allocated as configurable percentages (default 5%) and decremented atomically using Lua scripts. Fire-and-forget decrements happen after each request.

  • Rolling window semantics: 5h quotas now use true rolling windows (past 5 hours from current time). Daily quotas support both fixed (custom reset time) and rolling (past 24 hours) modes. Error messages correctly indicate rolling windows have no fixed reset time.

  • Timezone consistency: System-wide timezone setting with fallback chain (DB → env TZ → UTC). All date calculations use resolveSystemTimezone() with IANA validation. Keys expires_at changed to timestamptz for timezone-aware expiration.

  • Dynamic probe intervals: Three-tier system - 10s for timeout recovery, 10min for single-endpoint vendors, 60s default (changed from 10s). Reduces unnecessary probing while maintaining fast recovery from timeouts.

  • Gemini IMAGE billing: Properly extracts IMAGE modality tokens from candidatesTokensDetails/promptTokensDetails and bills at 10x rate using output_cost_per_image_token field.

  • UI improvements: Fixed TagInput dropdown positioning in Dialogs, clickable provider chain items for navigation, deduped group badges, and various alignment fixes.

Database Migrations

  • Migration 0057: Token columns to bigint for overflow protection
  • Migration 0058: Adds lease configuration columns with defaults
  • Migration 0059: Changes expires_at to timestamptz, adds system timezone

Data Consistency

Provider/user/key quota displays now query DB directly instead of Redis for consistency with lease mechanism as authoritative source.

Confidence Score: 4/5

  • Safe to merge with minor considerations - well-tested complex rate limiting refactor with comprehensive test coverage
  • Score reflects the large scope (202 files) and complexity of rate limiting changes, but strong test coverage (1619+ tests including 577 lease tests, 668 rolling window tests, 1179 lease-service tests) and thorough documentation mitigate risks. The lease mechanism uses fail-open error handling and atomic Redis operations. Breaking change to default probe interval is well-documented.
  • Pay attention to src/lib/rate-limit/lease-service.ts and src/app/v1/_lib/proxy/rate-limit-guard.ts as they implement critical rate limiting logic. Monitor production for any lease budget exhaustion issues.

Important Files Changed

Filename Overview
src/lib/rate-limit/lease-service.ts Implements lease-based budget slicing with Redis atomic operations, DB authoritative usage queries, and fail-open error handling
src/lib/rate-limit/lease.ts Core lease data structures and calculation functions with proper validation and serialization
src/app/v1/_lib/proxy/rate-limit-guard.ts Unified rate limit checks using lease mechanism for 5h/daily/weekly/monthly windows with proper rolling window error messages
src/app/v1/_lib/proxy/response-handler.ts Adds Gemini IMAGE token extraction from candidatesTokensDetails/promptTokensDetails and fire-and-forget lease budget decrements
src/lib/utils/cost-calculation.ts Adds separate image token billing using output_cost_per_image_token with 10x rate for Gemini IMAGE modality
src/lib/utils/timezone.ts Implements timezone resolution fallback chain (DB → env TZ → UTC) with IANA validation
src/lib/rate-limit/time-utils.ts Adds rolling window support for 5h and daily quotas with timezone-aware calculations
src/lib/provider-endpoints/probe-scheduler.ts Implements dynamic probe intervals (10s timeout override, 10min single-vendor, 60s default) with vendor endpoint counting
drizzle/0058_silly_sleepwalker.sql Adds lease configuration columns with sensible defaults (5% lease, 10s refresh)
drizzle/0059_safe_xorn.sql Changes keys.expires_at to timestamptz for timezone-aware expiration and adds system timezone column

Sequence Diagram

sequenceDiagram
    participant Client
    participant RateLimitGuard
    participant LeaseService
    participant Redis
    participant DB
    participant ResponseHandler

    Client->>RateLimitGuard: Request
    
    Note over RateLimitGuard: Check order: Total → Concurrent → RPM → 5h → Daily → Weekly → Monthly
    
    RateLimitGuard->>LeaseService: getCostLease(entityId, window, limit)
    LeaseService->>Redis: GET lease key
    
    alt Cache Hit & Valid
        Redis-->>LeaseService: Cached lease
        LeaseService-->>RateLimitGuard: Return lease
    else Cache Miss or Expired
        LeaseService->>DB: Query current usage in window
        DB-->>LeaseService: currentUsage
        Note over LeaseService: Calculate slice = min(limit * 5%, remaining, cap)
        LeaseService->>Redis: SETEX lease with TTL
        LeaseService-->>RateLimitGuard: Return new lease
    end
    
    alt Lease budget sufficient
        RateLimitGuard-->>Client: Allow request
        Client->>ResponseHandler: Process response
        ResponseHandler->>ResponseHandler: Extract usage metrics & IMAGE tokens
        ResponseHandler->>LeaseService: decrementLeaseBudget() [fire-and-forget]
        LeaseService->>Redis: EVAL Lua script (atomic decrement)
        Redis-->>LeaseService: New remaining
    else Insufficient budget
        RateLimitGuard-->>Client: 429 Rate Limit Error (with rolling window message)
    end
Loading

ding113 and others added 13 commits January 27, 2026 20:57
* 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>
…674)

- Migrate User daily quota from checkUserDailyCost to checkCostLimitsWithLease
- Align key-quota.ts and keys.ts to use DB direct (sumKeyCostInTimeRange)
- All periodic limits (5h/daily/weekly/monthly) now use lease mechanism
- Total limits remain on 5-min Redis cache (no time window applicable)

Closes #673
- Add lease budget decrement after request completion (key/user/provider)
- Provider quota display uses DB direct sums instead of Redis
- User all-limit daily window respects dailyResetMode configuration
- Total usage unified to all-time semantics (36500 days)
- Cost-alert uses time-utils for natural windows and filters warmup/deleted

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The virtualized logs table was using server-passed searchParams props
to compute filters, which don't update on client-side navigation.
This caused the "show retries" filter (and other filters) to not work
after clicking Apply.

- Switch filters useMemo to use useSearchParams hook instead of props
- Remove unnecessary invalidateQueries useEffect (React Query handles
  automatic refetch when queryKey changes)
- Fix test mock to return proper URLSearchParams object

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Jan 29, 2026

Important

Review skipped

Too many files!

This PR contains 202 files, which is 102 over the limit of 100.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • 🔍 Trigger a full review
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot added enhancement New feature or request area:UI area:i18n size/XL Extra Large PR (> 1000 lines) labels Jan 29, 2026
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @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 delivers a comprehensive update focused on improving the platform's operational robustness, resource management, and data consistency. It refines how endpoint health is monitored, introduces an advanced quota management system, and provides new tools for administrative control over providers. The changes also ensure accurate and consistent time-based operations and cost calculations, particularly for advanced AI models.

Highlights

  • Dynamic Endpoint Probing Intervals: Endpoint probing now uses dynamic intervals based on endpoint status (e.g., faster probing for timeouts) and vendor configuration (e.g., slower for single-endpoint vendors), improving responsiveness and efficiency.
  • Quota Leasing Mechanism: Introduced a new quota leasing system to optimize rate limiting checks. This mechanism reduces database load by caching budget slices in Redis, while maintaining accuracy and allowing for configurable refresh intervals and lease percentages.
  • Enhanced Timezone Support: The system now supports configurable IANA timezones, ensuring consistent date and time calculations and displays across the backend and frontend, particularly for features like rate limit resets and usage logs.
  • Rolling Window Rate Limits: Implemented support for rolling window rate limits (e.g., 5-hour and 24-hour rolling windows), providing more flexible and responsive quota enforcement compared to fixed daily/weekly/monthly periods.
  • Provider Reclustering Feature: Added a new administrative tool to recluster providers based on updated grouping rules. This allows for better organization and management of providers, especially when website URLs are absent, by using host:port as a clustering key.
  • Improved Token Aggregation for Multi-modal Models: Enhanced token usage extraction for multi-modal models like Gemini, now accurately accounting for image tokens (input and output) alongside text tokens for more precise cost calculation.
  • Database Schema Updates: Multiple database migrations were applied to support new features, including adding a timezone column to system settings, changing token count columns to bigint for larger values, and introducing new columns for quota lease configurations.
Using Gemini Code Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request is a significant release, merging numerous features and fixes from the dev branch. The changes are extensive and well-executed, focusing on major improvements in rate limiting, timezone handling, provider management, and UI consistency. Key highlights include the introduction of a robust lease-based quota system, dynamic endpoint probing intervals, and consistent timezone-aware date handling across the backend and frontend. The data source for quota displays has been unified to use direct database queries, resolving potential inconsistencies. I have one minor suggestion regarding a redundant schema modification in one of the database migration files, but overall, this is an excellent and high-quality release.

@@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This ADD COLUMN statement for the timezone column appears to be redundant. The same column is also added in migration 0048_add_system_timezone.sql. While IF NOT EXISTS prevents an error during execution, having duplicate schema modifications across different migration files can lead to confusion and potential issues with manual rollbacks or out-of-order execution. It would be cleaner to remove this line to ensure each schema change is declared only once.

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

This is a large release PR (v0.5.2) merging dev into main with significant improvements to rate limiting, timezone handling, provider management, and billing accuracy. The changes are well-structured with comprehensive i18n support and test coverage.

PR Size: XL

  • Lines changed: 22,769 (21,515 additions, 1,254 deletions)
  • Files changed: 195

Note: For XL PRs of this size, consider splitting future releases into smaller, focused PRs when possible (e.g., separate PRs for rate-limit changes, timezone changes, UI improvements).

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
Performance 0 0 1 0

Medium Priority Issues (Should Consider)

1. [PERFORMANCE-ISSUE] N+1 query pattern in getProviderLimitUsageBatch (src/actions/providers.ts:1332-1346)

The batch function now executes 4 database queries per provider inside a loop. For N providers, this results in 4*N database queries. The original implementation used RateLimitService.getCurrentCostBatch which was a single Redis pipeline call.

While the change to use DB as the source of truth for quota display is intentional (to ensure consistency with the lease-based system), consider adding a batch version of sumProviderCostInTimeRange that uses a single query with GROUP BY:

export async function sumProviderCostInTimeRangeBatch(
  providerIds: number[],
  startTime: Date,
  endTime: Date
): Promise<Map<number, number>> {
  const result = await db
    .select({
      providerId: messageRequest.providerId,
      total: sql<number>\`COALESCE(SUM(${messageRequest.costUsd}), 0)\`,
    })
    .from(messageRequest)
    .where(
      and(
        inArray(messageRequest.providerId, providerIds),
        gte(messageRequest.createdAt, startTime),
        lt(messageRequest.createdAt, endTime),
        isNull(messageRequest.deletedAt),
        EXCLUDE_WARMUP_CONDITION
      )
    )
    .groupBy(messageRequest.providerId);

  return new Map(result.map((r) => [r.providerId, Number(r.total)]));
}

Impact: Dashboard pages listing many providers will experience increased latency proportional to the number of providers.

Review Coverage

  • Logic and correctness - Clean
  • Security (OWASP Top 10) - Clean
  • Error handling - Clean (proper logging, user feedback)
  • Type safety - Clean
  • Documentation accuracy - Clean
  • Test coverage - Adequate (new tests added for group badges)
  • Code clarity - Good

Positive Observations (not reported per instructions, but noted)

  • Comprehensive i18n support across all 5 languages
  • Good test coverage for new functionality (provider-chain-popover group badges)
  • Proper error handling with logging in new recluster function
  • Clean separation of preview/apply modes in recluster dialog

Automated review by Claude AI

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 20

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (16)
src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/LogicTraceTab.tsx (1)

51-57: 修复未使用的参数 statusCode

Pipeline 检测到 statusCode 参数在函数体中从未使用。这会导致 lint 错误并阻止 CI 通过。

如果该参数确实不需要,应将其移除或使用下划线前缀 _statusCode 标记为有意忽略。

🛠️ 建议的修复方案

方案 1:移除未使用的参数(如果确实不需要)

 export function LogicTraceTab({
-  statusCode,
   providerChain,
   blockedBy,
   blockedReason,
   requestSequence,
 }: LogicTraceTabProps) {

方案 2:使用下划线前缀标记为有意忽略

 export function LogicTraceTab({
-  statusCode,
+  statusCode: _statusCode,
   providerChain,
   blockedBy,
   blockedReason,
   requestSequence,
 }: LogicTraceTabProps) {
src/components/customs/active-sessions-list.tsx (1)

14-20: 硬编码的中文错误消息应使用 i18n。

第 17 行的 fallback 错误消息 "获取活跃 Session 失败" 是硬编码的中文文本。根据编码规范,所有用户可见的字符串都应使用 i18n(支持 5 种语言:zh-CN、zh-TW、en、ja、ru)。

建议在组件中使用翻译函数,或者将此错误处理移至组件内部以便访问 useTranslations

建议的修复方案

由于 fetchActiveSessions 是在组件外定义的异步函数,无法直接使用 hooks。可以考虑:

  1. 将错误消息作为 key 抛出,在组件内翻译:
 async function fetchActiveSessions(): Promise<ActiveSessionInfo[]> {
   const result = await getActiveSessions();
   if (!result.ok) {
-    throw new Error(result.error || "获取活跃 Session 失败");
+    throw new Error(result.error || "activeSessions.fetchError");
   }
   return result.data;
 }
  1. 或在组件的 error handling 中处理翻译。

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/_components/dashboard-main.tsx (1)

1-1: PR 目标分支不符合仓库约定。

该仓库要求所有 PR 以 dev 为目标分支,但当前 PR 目标为 main。请改为面向 dev
Based on learnings: All pull requests must target the dev branch (https://github.com/ding113/claude-code-hub)

src/app/[locale]/dashboard/availability/_components/endpoint/probe-terminal.tsx (2)

81-85: 修复 useEffect 依赖 lint 失败

Biome 报 useExhaustiveDependencies,因为 logs 未在 effect 内使用。建议改为使用 logs.length 并显式引用,以保留“新日志触发滚动”的语义并通过 lint。

建议修改
-  // Auto-scroll to bottom when new logs arrive
-  useEffect(() => {
-    if (autoScroll && !userScrolled && containerRef.current) {
-      containerRef.current.scrollTop = containerRef.current.scrollHeight;
-    }
-  }, [logs, autoScroll, userScrolled]);
+  const logCount = logs.length;
+  // Auto-scroll to bottom when new logs arrive
+  useEffect(() => {
+    if (logCount === 0) return;
+    if (autoScroll && !userScrolled && containerRef.current) {
+      containerRef.current.scrollTop = containerRef.current.scrollHeight;
+    }
+  }, [logCount, autoScroll, userScrolled]);

189-191: 删除未使用的 Icon 变量

lint 报 noUnusedVariables;Icon 未使用,需移除或实际渲染图标。

建议修改
-            const Icon = config.icon;
src/app/[locale]/dashboard/availability/_components/endpoint/latency-curve.tsx (1)

3-12: 删除未使用的导入,避免 lint 失败。

ResponsiveContainerChartTooltipContent 未被使用,会触发 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";
src/app/v1/_lib/proxy/response-handler.ts (1)

1563-1579: Claude SSE 合并逻辑未合并 image token 字段

新增 input_image_tokens/output_image_tokens 后,mergeUsageMetrics 仍只合并旧字段,message_start 与 message_delta 混合时会丢失图片 token 信息。建议一并合并新字段。

调整建议
       return {
         input_tokens: patch.input_tokens ?? base.input_tokens,
         output_tokens: patch.output_tokens ?? base.output_tokens,
+        input_image_tokens: patch.input_image_tokens ?? base.input_image_tokens,
+        output_image_tokens: patch.output_image_tokens ?? base.output_image_tokens,
         cache_creation_input_tokens:
           patch.cache_creation_input_tokens ?? base.cache_creation_input_tokens,
src/components/ui/data-table.tsx (1)

233-243: 验证日期有效性,防止 formatInTimeZone 抛错

Line 242 对于非法日期字符串(如 "invalid-date"),new Date(value) 会创建 Invalid Date 对象,随后 formatInTimeZone 会抛出错误,导致表格渲染崩溃。建议先校验日期有效性。

建议修复
    render: (value) => {
      if (!value) return "-";
-      return formatInTimeZone(new Date(value), "UTC", "yyyy-MM-dd");
+      const date = new Date(value);
+      if (Number.isNaN(date.getTime())) return "-";
+      return formatInTimeZone(date, "UTC", "yyyy-MM-dd");
    },
src/lib/webhook/templates/circuit-breaker.ts (1)

8-41: 通知文案需 i18n 且 icon 字段不得使用 emoji 字符。

当前字段标签、标题、正文与脚注均为硬编码显示文本,且 icon 使用了 emoji 字符;请改为 i18n 资源并用非 emoji 的图标标识(如图标 key)。

As per coding guidelines: **/*.{js,ts,tsx,jsx}: Never use emoji characters in any code, comments, or string literals; **/*.{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.

src/lib/webhook/templates/test-messages.ts (1)

14-48: 测试消息中的展示文案需走 i18n。

这些测试通知会直接展示给用户,当前硬编码的名称与描述需要改为 i18n 资源或通过翻译表注入。

As per coding guidelines: **/*.{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.

src/app/v1/_lib/proxy/error-handler.ts (1)

259-313: 建议在设置重置头前校验 resetTime 是否为有效日期

当前仅判断 resetTime !== null。若 resetTime 为无效字符串,Date 会返回 NaN,导致 X-RateLimit-Reset/Retry-After 变成 "NaN"。建议在设置头前先做有效性校验。

修改建议
-    if (error.resetTime !== null) {
-      const resetTimestamp = Math.floor(new Date(error.resetTime).getTime() / 1000);
-      headers.set("X-RateLimit-Reset", resetTimestamp.toString());
-      headers.set("Retry-After", ProxyErrorHandler.calculateRetryAfter(error.resetTime));
-    }
+    if (error.resetTime !== null) {
+      const resetMs = new Date(error.resetTime).getTime();
+      if (Number.isFinite(resetMs)) {
+        const resetTimestamp = Math.floor(resetMs / 1000);
+        headers.set("X-RateLimit-Reset", resetTimestamp.toString());
+        headers.set("Retry-After", ProxyErrorHandler.calculateRetryAfter(error.resetTime));
+      }
+    }
src/app/[locale]/settings/prices/_components/price-list.tsx (1)

23-75: 移除未使用的 locale 以修复 lint 失败
useLocale() 的结果未被使用,已触发 lint/correctness/noUnusedVariables。请移除导入与变量或实际使用。

修复示例
-import { useLocale, useTimeZone, useTranslations } from "next-intl";
+import { useTimeZone, useTranslations } from "next-intl";
@@
-  const locale = useLocale();
   const timeZone = useTimeZone() ?? "UTC";
src/lib/redis/leaderboard-cache.ts (1)

80-95: last24h 周期添加独立的缓存键生成逻辑。

last24h 是 LeaderboardPeriod 类型定义中的有效周期(见 src/repository/leaderboard.ts 第 129 行),并在数据库查询层有专门处理(createdAt >= (CURRENT_TIMESTAMP - INTERVAL '24 hours'))。但当前的 getLakeboardCacheKey 函数对 last24h 没有明确的 case 分支,导致它被当作 allTime 处理,使用相同的缓存键格式。

由于 last24h(过去 24 小时)与 allTime(全部历史数据)返回的数据集完全不同,它们应该有独立的缓存键。建议为 last24h 添加明确的 case 分支,类似于 daily/weekly/monthly,使用时间戳或日期组件来确保缓存键的唯一性。

src/app/[locale]/dashboard/logs/_components/logs-date-range-picker.tsx (1)

230-235: “下一周期”禁用判断应使用 serverTimeZone

当 serverTimeZone 与本地时区不同时,当前判断可能允许跳到服务器“未来日期”。建议用 serverTimeZone 计算“今天”再比较。

修改建议
-        <Button
+        <Button
           variant="outline"
           size="icon-sm"
           onClick={() => handleNavigate("next")}
-          disabled={!hasDateRange || (endDate !== undefined && endDate >= formatDate(new Date()))}
+          disabled={
+            !hasDateRange ||
+            (endDate !== undefined &&
+              endDate >=
+                (serverTimeZone
+                  ? getDateRangeForPeriod("today", serverTimeZone).endDate
+                  : formatDate(new Date())))
+          }
           title={t("leaderboard.dateRange.nextPeriod")}
         >
src/repository/_shared/transformers.ts (1)

182-211: 修复配额字段的真值判断逻辑缺陷

确认代码存在缺陷:quotaLeasePercent5hquotaLeasePercentDailyquotaLeasePercentWeeklyquotaLeasePercentMonthlyquotaLeaseCapUsd 字段使用真值判断,导致显式设置为 0 或 "0" 时会被误判为 false,从而回退到默认值。应改用 != null 检查以正确区分 null/undefined 与 0。

建议修复
-    quotaLeasePercent5h: dbSettings?.quotaLeasePercent5h
-      ? parseFloat(dbSettings.quotaLeasePercent5h)
-      : 0.05,
+    quotaLeasePercent5h:
+      dbSettings?.quotaLeasePercent5h != null
+        ? parseFloat(dbSettings.quotaLeasePercent5h)
+        : 0.05,
-    quotaLeasePercentDaily: dbSettings?.quotaLeasePercentDaily
-      ? parseFloat(dbSettings.quotaLeasePercentDaily)
-      : 0.05,
+    quotaLeasePercentDaily:
+      dbSettings?.quotaLeasePercentDaily != null
+        ? parseFloat(dbSettings.quotaLeasePercentDaily)
+        : 0.05,
-    quotaLeasePercentWeekly: dbSettings?.quotaLeasePercentWeekly
-      ? parseFloat(dbSettings.quotaLeasePercentWeekly)
-      : 0.05,
+    quotaLeasePercentWeekly:
+      dbSettings?.quotaLeasePercentWeekly != null
+        ? parseFloat(dbSettings.quotaLeasePercentWeekly)
+        : 0.05,
-    quotaLeasePercentMonthly: dbSettings?.quotaLeasePercentMonthly
-      ? parseFloat(dbSettings.quotaLeasePercentMonthly)
-      : 0.05,
+    quotaLeasePercentMonthly:
+      dbSettings?.quotaLeasePercentMonthly != null
+        ? parseFloat(dbSettings.quotaLeasePercentMonthly)
+        : 0.05,
-    quotaLeaseCapUsd: dbSettings?.quotaLeaseCapUsd ? parseFloat(dbSettings.quotaLeaseCapUsd) : null,
+    quotaLeaseCapUsd:
+      dbSettings?.quotaLeaseCapUsd != null ? parseFloat(dbSettings.quotaLeaseCapUsd) : null,
src/lib/rate-limit/time-utils.ts (1)

178-183: 硬编码的中文字符串违反 i18n 规范

period 字段中的 "5 小时""24 小时" 是用户可见的字符串,根据编码规范应使用 i18n 国际化处理,支持 5 种语言(zh-CN、zh-TW、en、ja、ru)。

建议修复方案

需要将这些字符串替换为 i18n key,并在各语言文件中添加对应翻译。例如:

 case "5h":
   return {
     type: "rolling",
-    period: "5 小时",
+    period: "5h", // 或使用 i18n key,在前端展示层进行国际化处理
   };

或者在调用处使用 i18n 函数进行翻译。

根据编码规范:All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text

Also applies to: 227-231

🤖 Fix all issues with AI agents
In `@drizzle/0059_safe_xorn.sql`:
- Around line 1-2: 在将 keys 表的 expires_at 列从 timestamp 转为 timestamp with time
zone 时,明确在 ALTER TABLE ... ALTER COLUMN "expires_at" SET DATA TYPE 子句中添加 USING
子句,把现有的 timestamp 值按 UTC 解释后再转为带时区的时间(即使用 expires_at AT TIME ZONE 'UTC'
之类的表达式),以避免会话时区导致的时间偏移;更新相应的 ALTER TABLE "keys" ALTER COLUMN "expires_at" 语句并保留对
system_settings 表新增 timezone 列的变更。

In `@messages/en/errors.json`:
- Around line 55-61: The new error keys RATE_LIMIT_5H_ROLLING_EXCEEDED and
RATE_LIMIT_DAILY_ROLLING_EXCEEDED are only present in messages/en/errors.json
and must be added to all nested language error files; update each
messages/<lang>/settings/errors.json and
messages/<lang>/settings/providers/form/errors.json (for all 5 language packs)
by adding the two keys with the translated/localized strings (or placeholder
copies) matching the primary messages file; ensure the JSON keys and
interpolation placeholders (${current}, ${limit}, {resetTime}) match the
originals and that files remain valid JSON after insertion.

In `@src/actions/keys.ts`:
- Around line 1080-1083: The renewKeyExpiresAt flow currently calls
resolveSystemTimezone() and parseDateInputAsTimezone(...) without handling parse
failures; update the renewKeyExpiresAt handler to catch date parsing errors like
editKey does: wrap the parseDateInputAsTimezone call (and any timezone
resolution if needed) in a try/catch, detect invalid parse and return the
INVALID_FORMAT error code using the i18n error message lookup (not a hardcoded
string) for all supported locales; reference functions/idents:
renewKeyExpiresAt, editKey, resolveSystemTimezone, parseDateInputAsTimezone, and
INVALID_FORMAT to locate where to add the try/catch and i18n error response.
- Around line 285-290: In addKey, handle parseDateInputAsTimezone failures
locally instead of letting the error bubble up: wrap the call to
resolveSystemTimezone() and parseDateInputAsTimezone(validatedData.expiresAt,
timezone) in a try/catch inside the addKey flow and on parse failure
throw/return the same INVALID_FORMAT error used by editKey (use the same error
identifier/message shape) and produce the message via i18n (supporting zh-CN,
zh-TW, en, ja, ru) rather than hardcoding text; reference resolved symbols
validatedData.expiresAt, resolveSystemTimezone, parseDateInputAsTimezone, and
addKey to locate where to add the try/catch and i18n error mapping.

In `@src/app/`[locale]/dashboard/_components/user/key-list-header.tsx:
- Around line 3-5: The component uses hardcoded date format "yyyy-MM-dd" in
calls that render to users (via formatInTimeZone and another formatDate usage),
which violates i18n guidance; replace those hardcoded formats by using the
existing formatDate utility with the current locale (call formatDate(new
Date(timestamp), "yyyy-MM-dd", locale) and pass locale obtained from
useLocale()) or, if you prefer translation-driven formats, add the
proxyStatus.timeAgo.dateFormat key to every messages/*/dashboard.json and use
translations; update both the formatInTimeZone usage and the other formatDate
call (in the KeyListHeader component where formatInTimeZone and formatDate are
used) to use the locale-aware approach and ensure locale is threaded into
formatDate.

In
`@src/app/`[locale]/dashboard/availability/_components/endpoint/probe-terminal.tsx:
- Line 4: The import list currently includes an unused symbol Trash2 causing a
lint error; remove Trash2 from the import statement that imports lucide-react
icons (the line importing AlertCircle, CheckCircle2, Download, Trash2, XCircle)
so only the used icons remain (e.g., AlertCircle, CheckCircle2, Download,
XCircle).
- Line 1: The PR is targeting main but the repository policy requires all PRs to
target the dev branch; update the pull request's target branch from "main" to
"dev" (or recreate the PR with base branch set to dev) before merging so the
changes in this file (probe-terminal.tsx) are merged into dev instead of main.

In
`@src/app/`[locale]/dashboard/sessions/[sessionId]/messages/_components/session-stats.tsx:
- Around line 222-238: The TimeRow component currently hardcodes date/time
formats using formatInTimeZone and literal patterns; replace that with
next-intl's useFormatter so formatting follows i18n rules: inside TimeRow import
and call useFormatter(), then use formatter.dateTime({ dateStyle: "medium",
timeStyle: "medium", timeZone }) (or separate dateStyle/timeStyle calls if you
need two spans) to format the Date d instead of formatInTimeZone; keep the
null-check and the TimeRow signature intact and ensure the timeZone prop is
forwarded to the formatter.

In `@src/app/`[locale]/settings/config/_components/system-settings-form.tsx:
- Around line 434-444: The expand/collapse button inside CollapsibleTrigger in
system-settings-form.tsx (rendered when enableResponseFixer is true) lacks an
accessible name; update the button element used by CollapsibleTrigger to include
an aria-label prop (use the existing i18n string for the response fixer toggle)
so screen readers can announce its purpose—ensure the label reflects the
open/closed state if needed (using responseFixerOpen) and keep the visual icon
unchanged.
- Around line 133-147: The submission currently uses
parseFloat(quotaLeaseCapUsd) which can produce NaN for invalid input; update the
submit logic around the object construction that references quotaLeaseCapUsd so
that you parse and validate first (e.g. const parsed =
parseFloat(quotaLeaseCapUsd)); then if quotaLeaseCapUsd.trim() === "" set null,
else if Number.isFinite(parsed) use parsed, otherwise set null (or mark the form
field invalid and prevent submit) before assigning quotaLeaseCapUsd: in the
payload; adjust any validation/error-feedback flow in the same form submit
handler so invalid numeric strings are not sent to the backend.

In `@src/app/v1/_lib/proxy/response-handler.ts`:
- Around line 1331-1356: The promptTokensDetails handling assigns
result.input_tokens = textTokens without subtracting cachedContentTokenCount,
which can double-count cached tokens; update the block that processes
promptTokensDetails (variables: promptDetails, textTokens, imageTokens) to
subtract cachedContentTokenCount from textTokens before setting
result.input_tokens (use the same logic as used when handling promptTokenCount),
ensure the subtraction clamps to zero (no negative tokens), and only apply when
cachedContentTokenCount is a positive number so the cached deduction mirrors the
promptTokenCount path and avoids duplicate billing.

In `@src/components/customs/version-checker.tsx`:
- Around line 95-99: The rendered publishedAt block can throw if
versionInfo.publishedAt is not a valid date string; before calling
formatInTimeZone (used with versionInfo.publishedAt and timeZone in the
VersionChecker component), parse the value into a Date and validate it (e.g.,
const d = new Date(versionInfo.publishedAt); if (!isNaN(d.getTime())) ...) or
wrap the formatting call in a safe try/catch; only render the formatted date
when the Date is valid (otherwise skip or render a fallback). Ensure you
reference versionInfo.publishedAt, formatInTimeZone, and timeZone when locating
and updating the code.

In `@src/components/ui/relative-time.tsx`:
- Around line 3-6: 在 src/components/ui/relative-time.tsx 中移除未使用的 import 标识符
formatDate(来自 "date-fns"),因为该导入没有在组件中被引用导致 lint 报错;打开文件,定位 import 行包含 formatDate
并删除该命名导入或整行 import(保留仍被使用的其他导入如 formatInTimeZone、useLocale 等),保存后运行 lint/ts
检查以确认警告已消失。

In `@src/drizzle/schema.ts`:
- Around line 155-159: The initialization order is wrong: call
backfillProviderVendorsFromProviders() before applying the NOT NULL migration so
existing NULLs are fixed prior to runMigrations(); update the startup sequence
in instrumentation initialization to invoke
backfillProviderVendorsFromProviders() (the function referenced) prior to
calling runMigrations() (and keep runMigrations()/applyMigrations code paths
unchanged), ensuring any NULL or 0 providerVendorId rows are backfilled before
the NOT NULL constraint migration runs.

In `@src/lib/rate-limit/lease-service.ts`:
- Around line 283-313: The Lua script DECREMENT_LUA_SCRIPT and its caller
decrementLeaseBudget lack validation for the cost parameter allowing negative or
NaN values to corrupt remainingBudget; add defensive validation inside
decrementLeaseBudget to ensure cost is a finite number and >= 0 (or > 0 if you
require positive consumption), coerce to an integer if appropriate, and
reject/throw an error (or return a clear failure) before invoking the Lua script
when the check fails; this prevents negative/NaN values from being passed into
DECREMENT_LUA_SCRIPT and mutating lease.remainingBudget.

In `@src/lib/utils/date-format.ts`:
- Around line 54-58: The call to formatInTimeZone can throw (RangeError) for
invalid IANA timezones, so wrap the formatInTimeZone(dateObj, timezone,
formatString, { locale: dateFnsLocale }) invocation in a try-catch in the same
function; on success return the formatted string, on error log/handle the
exception (or swallow) and fall back to formatting using format(dateObj,
formatString, { locale: dateFnsLocale }) so invalid timezone input doesn't crash
the app. Ensure you reference formatInTimeZone, dateObj, timezone, formatString,
and dateFnsLocale when locating the code to change.

In `@src/lib/utils/date-input.ts`:
- Around line 25-59: In parseDateInputAsTimezone replace all hardcoded Error
messages (e.g., "Invalid date input: empty string" and `Invalid date input:
${input}`) with i18n keys or standardized error codes so callers can
map/translate them; update every throw in this function (the initial empty
check, the invalid parse branches for date-only, timezone-designated, and
ISO-without-timezone) to throw a consistent error identifier (or a small Error
subclass containing a code property) instead of user-facing text, e.g., throw {
code: "ERR_INVALID_DATE_INPUT", details: input } or new
ErrorWithCode("ERR_INVALID_DATE_INPUT", input) so upper layers can perform
localization.

In `@src/lib/validation/schemas.ts`:
- Around line 786-813: The validation schemas (quotaDbRefreshIntervalSeconds,
quotaLeasePercent5h, quotaLeasePercentDaily, quotaLeasePercentWeekly,
quotaLeasePercentMonthly, quotaLeaseCapUsd) currently hardcode English error
messages; replace these literal strings with calls to the project's
i18n/error-mapping utility (e.g., use i18n.t('key') or
mapValidationError('KEY')) so all user-facing messages come from the translation
resources (zh-CN, zh-TW, en, ja, ru); update each .int/.min/.max/.nullable
message argument to reference the appropriate i18n key or mapped constant and
ensure optional nullables still produce translated messages via the same
mapping.
- Line 9: The timezone validator currently uses a hardcoded error string;
replace that with an i18n key by importing and using the app's i18n translator
(e.g., the project's i18n.t or equivalent) inside the schema so the validation
message becomes something like t('validation.timezone.invalid') or the project’s
standard error-mapping key instead of a literal string, update the import block
to include the i18n helper alongside isValidIANATimezone, and apply the same
change to nearby validation messages in this file that are also hardcoded so all
user-facing validation text uses i18n keys.

In `@tests/unit/actions/system-config-save.test.ts`:
- Around line 1-3: The pull request is targeting the main branch but the repo
policy requires all PRs target dev; change the PR base branch from main to dev
in the Git hosting UI (use the "Edit" button next to the PR title or the branch
selector) so the PR targets dev instead of main, then update any branch-specific
references in the PR description if present.
🧹 Nitpick comments (35)
src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/LogicTraceTab.tsx (1)

601-601: 考虑将中文注释改为英文。

代码中的注释使用了中文。虽然这不违反 i18n 规则(注释不是用户可见的字符串),但为了代码库的一致性和国际团队协作,建议使用英文注释。

♻️ 建议的修改
-                    {/* 后端 buildRequestDetails 已根据 STORE_SESSION_MESSAGES 配置进行脱敏 */}
+                    {/* Backend buildRequestDetails sanitizes data based on STORE_SESSION_MESSAGES config */}
src/app/[locale]/dashboard/_components/rate-limit-events-chart.tsx (1)

38-47: 日期格式顺序未适配不同地区惯例

dateFnsLocale 只影响文本内容(如月份名称 "MMMM"),但格式化字符串中的标记顺序(如 M/d vs d/M)是固定的。

对于支持多语言的应用(zh-CN, zh-TW, en, ja, ru),不同地区有不同的日期顺序习惯:

  • 美式:M/d(月/日)
  • 欧洲/俄罗斯:d/M(日/月)
  • 日本/中国:yyyy/M/d

当前 "M/d HH:mm" 格式可能让非美式地区用户感到困惑。

建议:使用 date-fns 的本地化格式模式

可以考虑使用 Pp 等本地化格式标记,或根据 locale 动态选择格式:

// 方案1: 使用本地化短日期格式
const formatHour = (hourStr: string) => {
  const date = new Date(hourStr);
  return formatInTimeZone(date, timeZone, "P HH:mm", { locale: dateFnsLocale });
};

// 方案2: 根据 locale 动态选择格式
const dateFormatByLocale: Record<string, string> = {
  "en": "M/d HH:mm",
  "zh-CN": "M/d HH:mm",
  "zh-TW": "M/d HH:mm",
  "ja": "M/d HH:mm",
  "ru": "d/M HH:mm",
};
src/app/[locale]/dashboard/_components/statistics/chart.tsx (1)

243-251: 注释语言不一致。

第 243 行的注释使用英文,而文件中其他注释使用中文(如第 15、43、52-54 行等)。建议保持注释语言一致性。

建议修改
-  // Format tooltip date with timezone
+  // 格式化 tooltip 日期(带时区)
   const formatTooltipDate = (dateStr: string) => {
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (3)

308-321: 变量遮蔽问题:t 已被翻译函数占用

第 311 行的 map 回调参数 t 遮蔽了第 297 行定义的翻译函数 t。虽然当前代码在排序逻辑中没有调用翻译函数,但这种遮蔽会导致代码可读性下降,且未来如果需要在该作用域内使用翻译函数将会出错。

♻️ 建议修复
   const endpoints = useMemo(() => {
     const typeOrder = getAllProviderTypes();
-    const typeIndexMap = new Map(typeOrder.map((t, i) => [t, i]));
+    const typeIndexMap = new Map(typeOrder.map((type, i) => [type, i]));

     return [...rawEndpoints].sort((a, b) => {

376-435: Query Key 不一致可能导致缓存失效问题

EndpointsTable 使用 ["provider-endpoints", vendorId] 作为查询键(第 301 行),AddEndpointButton 在 invalidate 时也使用了 ["provider-endpoints", vendorId](第 573 行)。但此处的 probeMutationdeleteMutationtoggleMutation 都只使用 ["provider-endpoints"] 进行 invalidate。

虽然 React Query 的部分匹配机制可能使其正常工作,但这种不一致性可能在某些场景下导致意外行为。建议统一使用完整的 query key。

♻️ 建议修复:使用 endpoint.vendorId 进行精确 invalidate
   const probeMutation = useMutation({
     // ... mutationFn
     onSuccess: (data) => {
-      queryClient.invalidateQueries({ queryKey: ["provider-endpoints"] });
+      queryClient.invalidateQueries({ queryKey: ["provider-endpoints", endpoint.vendorId] });
       // ... rest
     },
   });

   const deleteMutation = useMutation({
     // ... mutationFn
     onSuccess: () => {
-      queryClient.invalidateQueries({ queryKey: ["provider-endpoints"] });
+      queryClient.invalidateQueries({ queryKey: ["provider-endpoints", endpoint.vendorId] });
       queryClient.invalidateQueries({ queryKey: ["provider-vendors"] });
       // ... rest
     },
   });

   const toggleMutation = useMutation({
     // ... mutationFn
     onSuccess: () => {
-      queryClient.invalidateQueries({ queryKey: ["provider-endpoints"] });
+      queryClient.invalidateQueries({ queryKey: ["provider-endpoints", endpoint.vendorId] });
       // ... rest
     },
   });

679-682: Query Key 不一致

EndpointRow 中的 mutation 类似,这里的 invalidate 使用了 ["provider-endpoints"] 而非 ["provider-endpoints", endpoint.vendorId],与 AddEndpointButton(第 573 行)的用法不一致。

♻️ 建议修复
       if (res.ok) {
         toast.success(t("endpointUpdateSuccess"));
         setOpen(false);
-        queryClient.invalidateQueries({ queryKey: ["provider-endpoints"] });
+        queryClient.invalidateQueries({ queryKey: ["provider-endpoints", endpoint.vendorId] });
       }
src/components/ui/__tests__/tag-input-dialog.test.tsx (3)

1-64: 测试文件位置与编码规范不一致

根据编码规范,源代码相邻的测试文件应放置在 src/**/*.test.ts 路径下,与源文件并列。当前文件位于 __tests__ 子目录中。

建议将文件移动到: src/components/ui/tag-input-dialog.test.tsx

As per coding guidelines: "Source-adjacent tests should be placed in src/**/*.test.ts alongside source files"


67-95: container 变量已解构但未使用

在第 68 行解构了 container,但测试中使用的是 document.querySelector 而非 container.querySelector。建议移除未使用的变量,或改用 container 进行更精确的作用域查询。

♻️ 建议的修复方案
-    const { container, unmount } = render(<DialogTagInput />);
+    const { unmount } = render(<DialogTagInput />);

97-122: 同样存在未使用的 container 变量

第 98 行也解构了 container 但未使用。

♻️ 建议的修复方案
-    const { container, unmount } = render(<DialogTagInput />);
+    const { unmount } = render(<DialogTagInput />);
src/app/[locale]/dashboard/sessions/[sessionId]/messages/_components/request-list-sidebar.tsx (2)

168-170: 用户界面字符串应使用 i18n。

placeholder="Search..." 是硬编码的英文文本,应使用翻译函数。

建议修复
             <Input
-              placeholder="Search..."
+              placeholder={t("requestList.search")}
               className="h-7 text-xs pl-7 bg-muted/20 border-muted-foreground/20"
               disabled
             />

需要在相应的 i18n JSON 文件中添加 requestList.search 键。


245-245: 硬编码的 "Unknown Model" 应使用 i18n。

此用户可见文本应通过翻译函数获取。

建议修复
-                      {request.model || "Unknown Model"}
+                      {request.model || t("requestList.unknownModel")}
src/app/[locale]/settings/providers/_components/forms/test-result-card.tsx (1)

291-299: 可选优化:避免重复调用 useTimeZone()

TestResultDetails 内部再次调用了 useTimeZone(),而父组件 TestResultCard 已经获取了 timeZone。可以考虑将 timeZone 作为 prop 传递,减少 hook 调用。

建议修改
 function TestResultDetails({
   result,
   onCopy,
+  timeZone,
 }: {
   result: UnifiedTestResultData;
   onCopy: () => void;
+  timeZone: string;
 }) {
   const t = useTranslations("settings.providers.form.apiTest");
-  const timeZone = useTimeZone() ?? "UTC";

然后在调用处传递:

-              <TestResultDetails result={result} onCopy={handleCopyResult} />
+              <TestResultDetails result={result} onCopy={handleCopyResult} timeZone={timeZone} />
src/app/[locale]/settings/providers/_components/provider-list-item.legacy.tsx (2)

130-145: 硬编码的中文字符串违反了 i18n 规范。

此文件包含大量硬编码的中文字符串(如 "熔断器已重置"、"供应商"、"重置熔断器失败" 等),违反了编码规范中"所有用户可见字符串必须使用 i18n"的要求。

虽然这不是本次变更引入的问题,但作为 legacy 文件,建议在后续版本中迁移这些字符串到 i18n 资源文件。


212-264: 代码中使用了 emoji 字符,违反编码规范。

根据编码指南,不应在代码、注释或字符串字面量中使用 emoji 字符。此文件中存在以下 emoji 使用:

  • Line 213: 🔴 熔断中
  • Line 263: 🟡 恢复中
  • Line 371: ✓ 允许所有模型

建议移除这些 emoji,或使用图标组件替代。基于编码指南中 **/*.{js,ts,tsx,jsx}: Never use emoji characters in any code, comments, or string literals。

src/lib/notification/tasks/cost-alert.ts (1)

104-104: 考虑将 period 标签国际化。

period 字段使用了硬编码的中文字符串("5小时"、"本周"、"本月"),这些值会传递到 CostAlertData 并最终用于 webhook 通知。如果 webhook 接收方可能是非中文用户,建议考虑使用 i18n 或 locale-neutral 标识符(如 "5h"、"weekly"、"monthly")。

src/repository/provider-endpoints.ts (1)

311-331: deriveDisplayNameFromDomain 声明为 async 但未使用 await

该函数被标记为 async 并返回 Promise<string>,但函数体内没有任何异步操作。这可能是为未来扩展预留的,但如果当前不需要异步行为,建议移除 async 关键字以减少不必要的 Promise 包装开销。

可选重构:移除不必要的 async
-export async function deriveDisplayNameFromDomain(domain: string): Promise<string> {
+export function deriveDisplayNameFromDomain(domain: string): string {
   const parts = domain
     .split(".")
     .map((part) => part.trim())
     .filter(Boolean);
   // ... rest of the function
 }

注意:如果此更改会影响调用方,需要同步更新调用处(如 Line 400)。

src/lib/provider-endpoints/probe-scheduler.ts (1)

26-31: 考虑将硬编码的间隔时间设为可配置。

SINGLE_VENDOR_INTERVAL_MSTIMEOUT_OVERRIDE_INTERVAL_MS 是硬编码的,而 BASE_INTERVAL_MS 可通过环境变量配置。为了保持一致性并便于运维调优,可以考虑为这些值也添加环境变量支持。

♻️ 建议的改动
-// Single-vendor interval (10 minutes)
-const SINGLE_VENDOR_INTERVAL_MS = 600_000;
-// Timeout override interval (10 seconds)
-const TIMEOUT_OVERRIDE_INTERVAL_MS = 10_000;
+// Single-vendor interval (default 10 minutes)
+const SINGLE_VENDOR_INTERVAL_MS = Math.max(
+  1,
+  parseIntWithDefault(process.env.ENDPOINT_PROBE_SINGLE_VENDOR_INTERVAL_MS, 600_000)
+);
+// Timeout override interval (default 10 seconds)
+const TIMEOUT_OVERRIDE_INTERVAL_MS = Math.max(
+  1,
+  parseIntWithDefault(process.env.ENDPOINT_PROBE_TIMEOUT_OVERRIDE_INTERVAL_MS, 10_000)
+);
src/lib/webhook/templates/circuit-breaker.ts (1)

1-2: 相对导入请改为 @/ 路径别名。

当前使用相对路径引用 src 下模块,建议统一为别名导入以符合约定。

Proposed change
-import type { CircuitBreakerAlertData, StructuredMessage } from "../types";
-import { formatDateTime } from "../utils/date";
+import type { CircuitBreakerAlertData, StructuredMessage } from "@/lib/webhook/types";
+import { formatDateTime } from "@/lib/webhook/utils/date";

As per coding guidelines: Use path alias @/ to reference files in ./src/ directory.

src/lib/webhook/templates/test-messages.ts (1)

1-5: 相对导入请改为 @/ 路径别名。

建议统一使用 @/ 引用 src 下模块以符合项目约定。

Proposed change
-import type { StructuredMessage } from "../types";
-import { buildCircuitBreakerMessage } from "./circuit-breaker";
-import { buildCostAlertMessage } from "./cost-alert";
-import { buildDailyLeaderboardMessage } from "./daily-leaderboard";
+import type { StructuredMessage } from "@/lib/webhook/types";
+import { buildCircuitBreakerMessage } from "@/lib/webhook/templates/circuit-breaker";
+import { buildCostAlertMessage } from "@/lib/webhook/templates/cost-alert";
+import { buildDailyLeaderboardMessage } from "@/lib/webhook/templates/daily-leaderboard";

As per coding guidelines: Use path alias @/ to reference files in ./src/ directory.

src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx (1)

437-457: 建议提取 isActualRequest 逻辑避免重复
这里与 ProviderChainPopover 的判定逻辑同步实现,建议抽成共享工具函数,避免后续漂移。

tests/unit/actions/providers-usage.test.ts (2)

100-105: vi.doMock 在测试用例内部可能不会按预期工作。

vi.doMock 用于在测试用例内部设置 mock,但由于模块顶部已经使用 vi.mock("@/repository/statistics", ...) 设置了同一模块的 mock(Lines 37-40),此处的 vi.doMock 可能会被忽略。

这个测试用例的断言(Lines 108-109)仅验证 mock 函数类型,实际上并未验证任何有意义的行为。考虑移除此 mock 或使用 vi.mocked() 来验证实际调用。

♻️ 建议的改进
  it("should use sumKeyCostInTimeRange for Key quota (not RateLimitService.getCurrentCost)", async () => {
    // This test documents the expected behavior:
    // Key quota should use direct DB query (sumKeyCostInTimeRange) instead of Redis-first (getCurrentCost)
-
-   // Mock the statistics module
-   const sumKeyCostInTimeRangeMock = vi.fn(async () => 10.5);
-   const sumKeyTotalCostByIdMock = vi.fn(async () => 100.25);
-   const sumUserCostInTimeRangeMock = vi.fn(async () => 10.5);
-   const sumUserTotalCostMock = vi.fn(async () => 100.25);
-
-   vi.doMock("@/repository/statistics", () => ({
-     sumKeyCostInTimeRange: sumKeyCostInTimeRangeMock,
-     sumKeyTotalCostById: sumKeyTotalCostByIdMock,
-     sumUserCostInTimeRange: sumUserCostInTimeRangeMock,
-     sumUserTotalCost: sumUserTotalCostMock,
-   }));
-
-   // Verify the function signatures match
-   expect(typeof sumKeyCostInTimeRangeMock).toBe("function");
-   expect(typeof sumKeyTotalCostByIdMock).toBe("function");
-
+   // Documentation test: verifies the expected data source pattern
    // The test validates that:
    // 1. Key 5h/daily/weekly/monthly uses sumKeyCostInTimeRange (DB direct)
    // 2. Key total uses sumKeyTotalCostById (DB direct)
    // 3. User 5h/weekly/monthly uses sumUserCost (which calls sumUserCostInTimeRange)
    // 4. User daily uses sumUserCostInTimeRange
    // 5. User total uses sumUserTotalCost
    //
    // Both Key and User now use the same data source (database), ensuring consistency
+   expect(true).toBe(true); // Documentation test
  });

167-169: 缺少 afterEach 中的 mock 清理调用。

afterEach 只调用了 vi.useRealTimers(),但没有调用 vi.clearAllMocks()。虽然 beforeEach 在每个测试前会调用 vi.clearAllMocks(),但在 afterEach 中也清理是更安全的做法,可以防止测试间状态泄漏。

♻️ 建议的改进
  afterEach(() => {
    vi.useRealTimers();
+   vi.clearAllMocks();
  });
src/lib/notification/notification-queue.ts (1)

144-148: 考虑添加错误处理。

如果 getBindingById(bindingId) 调用失败(如数据库连接问题),当前代码会抛出异常并导致整个任务处理失败。考虑添加 try-catch 来优雅处理这种情况,使其回退到系统时区而不是失败。

♻️ 建议的改进
      let timezone: string | undefined;
      if (bindingId) {
+       try {
        const { getBindingById } = await import("@/repository/notification-bindings");
        const binding = await getBindingById(bindingId);
        timezone = binding?.scheduleTimezone ?? undefined;
+       } catch (error) {
+         logger.warn({
+           action: "notification_binding_lookup_failed",
+           jobId: job.id,
+           bindingId,
+           error: error instanceof Error ? error.message : String(error),
+         });
+       }
      }
tests/unit/actions/my-usage-consistency.test.ts (2)

90-119: Mock 设置未被使用。

此测试用例使用 vi.doMock 设置了多个函数的 mock,但从未导入或调用被 mock 的模块。这使得 mock 设置完全无效。如果目的是文档记录,建议移除无用的 mock 代码;如果需要实际测试,应该导入并调用相关函数。

♻️ 建议的改进 - 简化为纯文档测试
  it("should use sumKeyCostInTimeRange for Key quota (not RateLimitService.getCurrentCost)", async () => {
    // This test documents the expected behavior:
    // Key quota should use direct DB query (sumKeyCostInTimeRange) instead of Redis-first (getCurrentCost)

-   // Mock the statistics module
-   const sumKeyCostInTimeRangeMock = vi.fn(async () => 10.5);
-   const sumKeyTotalCostByIdMock = vi.fn(async () => 100.25);
-   const sumUserCostInTimeRangeMock = vi.fn(async () => 10.5);
-   const sumUserTotalCostMock = vi.fn(async () => 100.25);
-
-   vi.doMock("@/repository/statistics", () => ({
-     sumKeyCostInTimeRange: sumKeyCostInTimeRangeMock,
-     sumKeyTotalCostById: sumKeyTotalCostByIdMock,
-     sumUserCostInTimeRange: sumUserCostInTimeRangeMock,
-     sumUserTotalCost: sumUserTotalCostMock,
-   }));
-
-   // Verify the function signatures match
-   expect(typeof sumKeyCostInTimeRangeMock).toBe("function");
-   expect(typeof sumKeyTotalCostByIdMock).toBe("function");
-
    // The test validates that:
    // 1. Key 5h/daily/weekly/monthly uses sumKeyCostInTimeRange (DB direct)
    // 2. Key total uses sumKeyTotalCostById (DB direct)
    // 3. User 5h/weekly/monthly uses sumUserCost (which calls sumUserCostInTimeRange)
    // 4. User daily uses sumUserCostInTimeRange
    // 5. User total uses sumUserTotalCost
    //
    // Both Key and User now use the same data source (database), ensuring consistency
+
+   expect(true).toBe(true); // Documentation test
  });

121-134: 文档测试模式的可维护性考虑。

这些测试用例使用 expect(true).toBe(true) 作为占位符,仅用于文档目的。虽然这种模式可以在测试运行中保持文档可见性,但存在以下问题:

  1. 会虚假提高测试覆盖率统计
  2. 可能被误认为是真正的测试
  3. 文档与代码分离时难以同步更新

建议考虑:

  • 使用 it.skip()it.todo() 明确标记为非执行测试
  • 或将这些文档移至专门的 ADR/设计文档

Also applies to: 136-146, 148-184, 186-221

src/actions/users.ts (1)

1460-1461: 建议将 ALL_TIME_MAX_AGE_DAYS 常量提升到模块级别。

该常量在 src/actions/my-usage.ts 中也有定义。考虑将其提取到共享模块(如 @/lib/constants)中以避免重复定义和潜在的不一致。

src/app/[locale]/my-usage/page.tsx (1)

43-46: 考虑添加错误日志记录。

getServerTimeZone 失败时,错误被静默忽略。虽然 UI 可以在没有时区的情况下继续工作,但记录错误有助于调试。

♻️ 建议的改进
     void getServerTimeZone().then((tzResult) => {
       if (tzResult.ok) setServerTimeZone(tzResult.data.timeZone);
+    }).catch((error) => {
+      console.error("Failed to fetch server timezone:", error);
     });
src/app/[locale]/dashboard/logs/_components/logs-date-range-picker.tsx (1)

12-12: 建议用路径别名替代相对路径

请改用 @/ 引用 src/ 下的模块,保持导入风格一致。

修改建议
-import { getQuickDateRange, type QuickPeriod } from "../_utils/time-range";
+import { getQuickDateRange, type QuickPeriod } from "@/app/[locale]/dashboard/logs/_utils/time-range";
As per coding guidelines: "Use path alias `@/` to reference files in `./src/` directory".
src/lib/utils/timezone.ts (1)

125-135: 避免用 toLocaleString 的字符串解析计算偏移

new Date(now.toLocaleString(...)) 依赖非标准解析,跨运行时可能不一致。建议改用 Intl.DateTimeFormat().formatToParts 解析 offset,或使用 date-fns-tz 提供的偏移计算工具(若已引入)。请在目标运行时验证该实现的可靠性。

src/lib/webhook/renderers/dingtalk.ts (1)

1-8: 类型导入建议使用路径别名

统一使用 @/ 引用 src/ 下模块,避免深层相对路径。

修改建议
-import type {
-  ListItem,
-  Section,
-  SectionContent,
-  StructuredMessage,
-  WebhookPayload,
-  WebhookSendOptions,
-} from "../types";
+import type {
+  ListItem,
+  Section,
+  SectionContent,
+  StructuredMessage,
+  WebhookPayload,
+  WebhookSendOptions,
+} from "@/lib/webhook/types";
As per coding guidelines: "Use path alias `@/` to reference files in `./src/` directory".
src/actions/providers.ts (2)

3640-3650: Vendor 数据预加载方式可以优化。

当前实现使用 Promise.all 逐个加载 vendor:

const vendors = await Promise.all(uniqueVendorIds.map((id) => findProviderVendorById(id)));

虽然 Promise.all 可以并行执行,但每个 findProviderVendorById 仍会产生独立的数据库查询。建议考虑在 provider-endpoints 模块中添加批量查询方法(如 findProviderVendorsByIds)以减少数据库往返次数。

不过考虑到这是一个低频的管理员操作,当前实现的性能影响有限。


3709-3728: 事务内的循环更新可能影响性能。

db.transaction 内部,代码对每个变更的 provider 执行单独的 getOrCreateProviderVendorIdFromUrlstx.update。对于大量 provider 的情况,这可能导致:

  1. 多次 getOrCreateProviderVendorIdFromUrls 调用(可能包含 INSERT/SELECT)
  2. 多次独立的 UPDATE 语句

建议考虑:

  • 预先批量创建所有需要的 vendor
  • 使用批量 UPDATE 语句

当前实现对于正常规模的 provider 数量是可接受的。

src/types/system-config.ts (1)

60-66: 建议明确 quota 字段的 null/undefined 语义

SystemSettings 中这些字段是可选,但实际返回通常已填默认值;UpdateSystemSettingsInput 也未说明 null 与 undefined 的差异。建议补充注释并考虑使用 number | null | undefined(或将 SystemSettings 设为必填)以保持语义一致。
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.

Also applies to: 114-120

src/app/[locale]/settings/config/_components/system-settings-form.tsx (1)

254-280: 建议按当前 locale 生成时区标签

getTimezoneLabel 默认使用 "en",多语言界面下建议传入当前 locale,以保持 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.

建议修复
-import { useTranslations } from "next-intl";
+import { useLocale, useTranslations } from "next-intl";
@@
   const t = useTranslations("settings.config.form");
+  const locale = useLocale();
@@
-                {getTimezoneLabel(tz)}
+                {getTimezoneLabel(tz, locale)}
src/lib/rate-limit/lease.ts (1)

125-169: 建议反序列化时校验枚举取值

目前仅校验 string 类型,若 Redis 数据被污染可能接受未知 entityType/window。建议额外校验枚举成员。

建议修复
-      typeof parsed.ttlSeconds !== "number"
+      typeof parsed.ttlSeconds !== "number" ||
+      !LeaseEntityType.includes(parsed.entityType) ||
+      !LeaseWindow.includes(parsed.window)

Comment on lines +1 to +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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd /root && find . -name "0059_safe_xorn.sql" -type f 2>/dev/null | head -5

Repository: ding113/claude-code-hub

Length of output: 122


🏁 Script executed:

find . -path "*/drizzle/*" -name "*.sql" -type f 2>/dev/null | head -20

Repository: ding113/claude-code-hub

Length of output: 802


🏁 Script executed:

find . -name "*.sql" -path "*drizzle*" 2>/dev/null | sort

Repository: ding113/claude-code-hub

Length of output: 2353


🏁 Script executed:

cat -n ./drizzle/0059_safe_xorn.sql

Repository: ding113/claude-code-hub

Length of output: 268


🏁 Script executed:

cat -n ./drizzle/0048_add_system_timezone.sql

Repository: ding113/claude-code-hub

Length of output: 377


🌐 Web query:

PostgreSQL timestamp to timestamptz type conversion behavior timezone

💡 Result:

In PostgreSQL:

  • timestamp (without time zone) is just a calendar date+time with no time zone context.
  • timestamptz (timestamp with time zone) represents a single instant in time; PostgreSQL stores it in UTC internally and displays it in your session TimeZone. [1], [3]

What happens when you cast?

timestamptimestamptz

When you do:

ts::timestamptz

PostgreSQL assumes ts is in the session TimeZone, then converts that local time to UTC for storage as a timestamptz. [1]

So the numeric instant you get depends on TimeZone.

timestamptztimestamp

When you do:

tstz::timestamp

PostgreSQL converts the stored UTC instant into the session TimeZone for display, then drops the time zone, yielding a “local wall clock” timestamp. [1]

How to control the time zone used in conversion

Use AT TIME ZONE:

  • timestamp AT TIME ZONE zonetimestamptz (interprets the timestamp as if it occurred in zone) [2]
  • timestamptz AT TIME ZONE zonetimestamp (shows what the local wall time was in zone at that instant) [2]

Example:

SELECT
  timestamp '2026-01-29 12:00:00' AT TIME ZONE 'America/New_York'  AS as_in_ny,   -- timestamptz
  (timestamptz '2026-01-29 12:00:00+00' AT TIME ZONE 'America/New_York') AS ny_wall_time; -- timestamp

These operators are the explicit, predictable alternative to relying on the session TimeZone during casts. [1], [2]


🌐 Web query:

PostgreSQL ALTER COLUMN timestamp with time zone USING clause conversion

💡 Result:

To convert an existing column to timestamp with time zone (timestamptz) in PostgreSQL, use ALTER ... TYPE ... USING <expression>, where the USING expression computes the new value from the old one. The USING clause is required when PostgreSQL can’t apply an implicit/assignment cast, and it’s also how you control timezone semantics during conversion. [1]

1) timestamp (no time zone) → timestamptz (interpret old values in a specific zone)

If your existing timestamp values represent local wall-clock time in a known zone (common case), convert with AT TIME ZONE:

ALTER TABLE my_table
  ALTER COLUMN ts TYPE timestamptz
  USING ts AT TIME ZONE 'America/New_York';

AT TIME ZONE lets you specify a zone different from the session TimeZone; otherwise, conversions between timestamp and timestamptz normally assume the session TimeZone. [2]

If your timestamp values are actually UTC wall-clock times:

ALTER TABLE my_table
  ALTER COLUMN ts TYPE timestamptz
  USING ts AT TIME ZONE 'UTC';

2) integer/Bigint Unix epoch → timestamptz

PostgreSQL’s docs show converting Unix seconds since epoch like this: [1]

ALTER TABLE foo
  ALTER COLUMN foo_timestamp TYPE timestamptz
  USING timestamptz 'epoch' + foo_timestamp * interval '1 second';

3) If the column has a default that won’t cast cleanly

Because the USING expression is not applied to the existing default, you may need to drop/reset it during the change: [1]

ALTER TABLE foo
  ALTER COLUMN foo_timestamp DROP DEFAULT,
  ALTER COLUMN foo_timestamp TYPE timestamptz
  USING timestamptz 'epoch' + foo_timestamp * interval '1 second',
  ALTER COLUMN foo_timestamp SET DEFAULT now();

Tip: choosing the correct USING ... AT TIME ZONE '...' depends on what your existing data means (which zone it was intended to be in), not where the server runs. Conversions between timestamp and timestamptz otherwise follow the session TimeZone. [2]

Sources: PostgreSQL ALTER TABLE ... ALTER COLUMN ... TYPE ... USING documentation [1]; PostgreSQL date/time and timezone conversion behavior (timestamp vs timestamptz, AT TIME ZONE) [2].

References:
[1] PostgreSQL Docs: ALTER TABLE (current) https://www.postgresql.org/docs/current/sql-altertable.html
[2] PostgreSQL Docs: Date/Time Types (current) https://www.postgresql.org/docs/current/datatype-datetime.html


在不改变数据的前提下,类型转换需显式指定时区

timestamp 转换为 timestamptz 时,PostgreSQL 会根据数据库会话的 TimeZone 来解释原有的值。如果历史数据以 UTC 存储,但转换时会话的时区设置不同,会导致存储的 UTC 时刻被错误地偏移。建议添加 USING 子句明确指定源数据的时区。

修改建议
-ALTER TABLE "keys" ALTER COLUMN "expires_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
+ALTER TABLE "keys" ALTER COLUMN "expires_at" SET DATA TYPE timestamp with time zone
+  USING "expires_at" AT TIME ZONE 'UTC';--> statement-breakpoint
📝 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.

Suggested change
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);
ALTER TABLE "keys" ALTER COLUMN "expires_at" SET DATA TYPE timestamp with time zone
USING "expires_at" AT TIME ZONE 'UTC';--> statement-breakpoint
ALTER TABLE "system_settings" ADD COLUMN IF NOT EXISTS "timezone" varchar(64);
🤖 Prompt for AI Agents
In `@drizzle/0059_safe_xorn.sql` around lines 1 - 2, 在将 keys 表的 expires_at 列从
timestamp 转为 timestamp with time zone 时,明确在 ALTER TABLE ... ALTER COLUMN
"expires_at" SET DATA TYPE 子句中添加 USING 子句,把现有的 timestamp 值按 UTC 解释后再转为带时区的时间(即使用
expires_at AT TIME ZONE 'UTC' 之类的表达式),以避免会话时区导致的时间偏移;更新相应的 ALTER TABLE "keys"
ALTER COLUMN "expires_at" 语句并保留对 system_settings 表新增 timezone 列的变更。

Comment on lines +55 to +61
"RATE_LIMIT_5H_ROLLING_EXCEEDED": "5-hour rolling window cost limit exceeded: ${current} USD (limit: ${limit} USD). Usage gradually expires over the past 5 hours",
"RATE_LIMIT_WEEKLY_EXCEEDED": "Weekly cost limit exceeded: ${current} USD (limit: ${limit} USD). Resets at {resetTime}",
"RATE_LIMIT_MONTHLY_EXCEEDED": "Monthly cost limit exceeded: ${current} USD (limit: ${limit} USD). Resets at {resetTime}",
"RATE_LIMIT_TOTAL_EXCEEDED": "Total spending limit exceeded: ${current} USD (limit: ${limit} USD)",
"RATE_LIMIT_CONCURRENT_SESSIONS_EXCEEDED": "Concurrent sessions limit exceeded: {current} sessions (limit: {limit}). Please wait for active sessions to complete",
"RATE_LIMIT_DAILY_QUOTA_EXCEEDED": "Daily quota exceeded: ${current} USD (limit: ${limit} USD). Resets at {resetTime}",
"RATE_LIMIT_DAILY_ROLLING_EXCEEDED": "24-hour rolling window cost limit exceeded: ${current} USD (limit: ${limit} USD). Usage gradually expires over the past 24 hours",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

python3 - <<'PY'
import json
import glob
import os

keys = ["RATE_LIMIT_5H_ROLLING_EXCEEDED", "RATE_LIMIT_DAILY_ROLLING_EXCEEDED"]
files = glob.glob("messages/**/errors.json", recursive=True)

print(f"Found {len(files)} error files")
print(f"Checking for keys: {keys}\n")

missing = {}
for path in sorted(files):
    try:
        with open(path, "r", encoding="utf-8") as f:
            data = json.load(f)
        miss = [k for k in keys if k not in data]
        if miss:
            missing[path] = miss
            print(f"❌ {path}: missing {', '.join(miss)}")
        else:
            print(f"✓ {path}: all keys present")
    except Exception as e:
        print(f"⚠ {path}: error reading file - {e}")

if missing:
    print(f"\n⚠️ Summary: Keys missing in {len(missing)} file(s)")
else:
    print(f"\n✓ All locale error files contain the new keys.")
PY

Repository: ding113/claude-code-hub

Length of output: 1632


补充新增错误码到所有嵌套的错误文件。

新增的 RATE_LIMIT_5H_ROLLING_EXCEEDED 和 RATE_LIMIT_DAILY_ROLLING_EXCEEDED 键缺失在以下文件中:

  • messages/*/settings/errors.json(所有 5 个语言包)
  • messages/*/settings/providers/form/errors.json(所有 5 个语言包)

这些嵌套文件中的缺失可能导致回退显示或文案丢失。请在所有语言文件的对应嵌套位置同步添加这两个键。

🤖 Prompt for AI Agents
In `@messages/en/errors.json` around lines 55 - 61, The new error keys
RATE_LIMIT_5H_ROLLING_EXCEEDED and RATE_LIMIT_DAILY_ROLLING_EXCEEDED are only
present in messages/en/errors.json and must be added to all nested language
error files; update each messages/<lang>/settings/errors.json and
messages/<lang>/settings/providers/form/errors.json (for all 5 language packs)
by adding the two keys with the translated/localized strings (or placeholder
copies) matching the primary messages file; ensure the JSON keys and
interpolation placeholders (${current}, ${limit}, {resetTime}) match the
originals and that files remain valid JSON after insertion.

Comment on lines +285 to +290
// 转换 expiresAt: undefined → null(永不过期),string → Date(按系统时区解析)
const timezone = await resolveSystemTimezone();
const expiresAt =
validatedData.expiresAt === undefined ? null : new Date(validatedData.expiresAt);
validatedData.expiresAt === undefined
? null
: parseDateInputAsTimezone(validatedData.expiresAt, timezone);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

addKey 需显式处理 expiresAt 解析失败

parseDateInputAsTimezone 抛错会落到外层 catch,可能把英文错误直接返回给用户,也与 editKey 的 INVALID_FORMAT 行为不一致。建议本段就返回统一的格式错误。

修改建议
-    const timezone = await resolveSystemTimezone();
-    const expiresAt =
-      validatedData.expiresAt === undefined
-        ? null
-        : parseDateInputAsTimezone(validatedData.expiresAt, timezone);
+    let expiresAt: Date | null;
+    if (validatedData.expiresAt === undefined) {
+      expiresAt = null;
+    } else {
+      try {
+        const timezone = await resolveSystemTimezone();
+        expiresAt = parseDateInputAsTimezone(validatedData.expiresAt, timezone);
+      } catch {
+        return {
+          ok: false,
+          error: tError("INVALID_FORMAT"),
+          errorCode: ERROR_CODES.INVALID_FORMAT,
+        };
+      }
+    }
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/actions/keys.ts` around lines 285 - 290, In addKey, handle
parseDateInputAsTimezone failures locally instead of letting the error bubble
up: wrap the call to resolveSystemTimezone() and
parseDateInputAsTimezone(validatedData.expiresAt, timezone) in a try/catch
inside the addKey flow and on parse failure throw/return the same INVALID_FORMAT
error used by editKey (use the same error identifier/message shape) and produce
the message via i18n (supporting zh-CN, zh-TW, en, ja, ru) rather than
hardcoding text; reference resolved symbols validatedData.expiresAt,
resolveSystemTimezone, parseDateInputAsTimezone, and addKey to locate where to
add the try/catch and i18n error mapping.

Comment on lines +1080 to 1083
// 按系统时区解析过期日期
const timezone = await resolveSystemTimezone();
const expiresAt = parseDateInputAsTimezone(data.expiresAt, timezone);

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

renewKeyExpiresAt 同样需要捕获日期解析错误

当前解析失败会直接走外层 catch 并可能回传英文错误。建议与 editKey 保持一致返回 INVALID_FORMAT。

修改建议
-    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,
+      };
+    }
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".
📝 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.

Suggested change
// 按系统时区解析过期日期
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,
};
}
🤖 Prompt for AI Agents
In `@src/actions/keys.ts` around lines 1080 - 1083, The renewKeyExpiresAt flow
currently calls resolveSystemTimezone() and parseDateInputAsTimezone(...)
without handling parse failures; update the renewKeyExpiresAt handler to catch
date parsing errors like editKey does: wrap the parseDateInputAsTimezone call
(and any timezone resolution if needed) in a try/catch, detect invalid parse and
return the INVALID_FORMAT error code using the i18n error message lookup (not a
hardcoded string) for all supported locales; reference functions/idents:
renewKeyExpiresAt, editKey, resolveSystemTimezone, parseDateInputAsTimezone, and
INVALID_FORMAT to locate where to add the try/catch and i18n error response.

Comment on lines +3 to +5
import { formatInTimeZone } from "date-fns-tz";
import { CheckCircle, Copy, Eye, EyeOff, ListPlus } from "lucide-react";
import { useLocale, useTranslations } from "next-intl";
import { useLocale, useTimeZone, useTranslations } from "next-intl";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n src/app/[locale]/dashboard/_components/user/key-list-header.tsx

Repository: ding113/claude-code-hub

Length of output: 18580


🏁 Script executed:

# Search for the translation key mentioned in the review
find . -name "*.json" -type f | grep -E "messages|locales|i18n|translations" | head -20

Repository: ding113/claude-code-hub

Length of output: 681


🏁 Script executed:

# Look for date format related keys in translation files
rg "dateFormat|timeAgo" --type json

Repository: ding113/claude-code-hub

Length of output: 302


🏁 Script executed:

# Get the timeAgo section from dashboard.json files
rg -A 15 '"timeAgo"' messages/en/dashboard.json messages/zh-CN/dashboard.json

Repository: ding113/claude-code-hub

Length of output: 2291


🏁 Script executed:

# Check if dateFormat key exists in any dashboard.json
rg '"dateFormat"' messages/*/dashboard.json

Repository: ding113/claude-code-hub

Length of output: 49


🏁 Script executed:

# Check how date-fns-tz formatInTimeZone handles format strings
# Also check line 142 which also uses "yyyy-MM-dd"
sed -n '140,145p' src/app/[locale]/dashboard/_components/user/key-list-header.tsx

Repository: ding113/claude-code-hub

Length of output: 334


🏁 Script executed:

# Check line 142 context more clearly and verify other date format usages
sed -n '138,147p' src/app/[locale]/dashboard/_components/user/key-list-header.tsx

Repository: ding113/claude-code-hub

Length of output: 446


🏁 Script executed:

# Search for other uses of "yyyy-MM-dd" or similar hardcoded date formats in the file
rg '"yyyy-MM-dd"' src/app/[locale]/dashboard/_components/user/key-list-header.tsx

Repository: ding113/claude-code-hub

Length of output: 295


🏁 Script executed:

# Check the formatDate function to understand how it handles localization
rg -A 10 'export.*formatDate' src/lib/utils/date-format.ts

Repository: ding113/claude-code-hub

Length of output: 1215


硬编码日期格式需要本地化处理

第 74 行使用硬编码的 "yyyy-MM-dd" 格式字符串违反 i18n 指南,此格式在显示时直接呈现给用户(第 211 行)。但建议的解决方案存在问题:

  1. 建议的翻译键 proxyStatus.timeAgo.dateFormat 在任何语言文件中都不存在
  2. 若要使用翻译方式,需在 5 种语言的 messages/*/dashboard.json 中都添加该键
  3. 另一个选项是改用已有的 formatDate() 工具函数(如第 142 行所示),该函数已支持 locale 参数

若要修复此问题,建议:

  • 为所有 5 种语言添加 proxyStatus.timeAgo.dateFormat 键,或
  • 改用 formatDate(new Date(timestamp), "yyyy-MM-dd", locale) 方式处理(需将 locale 传入该函数)

另外注意第 142 行也存在类似的硬编码格式字符串,应一并处理。

🤖 Prompt for AI Agents
In `@src/app/`[locale]/dashboard/_components/user/key-list-header.tsx around lines
3 - 5, The component uses hardcoded date format "yyyy-MM-dd" in calls that
render to users (via formatInTimeZone and another formatDate usage), which
violates i18n guidance; replace those hardcoded formats by using the existing
formatDate utility with the current locale (call formatDate(new Date(timestamp),
"yyyy-MM-dd", locale) and pass locale obtained from useLocale()) or, if you
prefer translation-driven formats, add the proxyStatus.timeAgo.dateFormat key to
every messages/*/dashboard.json and use translations; update both the
formatInTimeZone usage and the other formatDate call (in the KeyListHeader
component where formatInTimeZone and formatDate are used) to use the
locale-aware approach and ensure locale is threaded into formatDate.

Comment on lines +54 to +58
if (timezone) {
return formatInTimeZone(dateObj, timezone, formatString, {
locale: dateFnsLocale,
});
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n src/lib/utils/date-format.ts

Repository: ding113/claude-code-hub

Length of output: 5919


🌐 Web query:

date-fns-tz formatInTimeZone invalid IANA timezone RangeError behavior

💡 Result:

date-fns-tz’s formatInTimeZone(date, timeZone, formatStr, options) expects timeZone to be a valid IANA zone name (e.g. America/New_York) or an offset. If the date or time zone input is invalid, date-fns-tz ends up passing an Invalid Date into date-fns/format, and date-fns/format then throws a RangeError. [1]

Two details that matter in practice:

  • It may only throw when the time zone is actually used in formatting. The project README notes that when an invalid time zone is provided and included in the output (i.e., you use time zone tokens like z..zzzz, X, x, O in the format string), you’ll get a RangeError. [2]
  • “Invalid time zone specified” can happen even for “valid” IANA names on some user environments. date-fns-tz validates IANA strings by attempting new Intl.DateTimeFormat(undefined, { timeZone }); if that throws, date-fns-tz treats the zone as invalid. There’s an open issue reporting RangeError: Invalid time zone specified: Asia/Manila (and others like America/New_York) for a small subset of users due to their runtime’s Intl behavior/time-zone data. [3]

So the observed RangeError behavior is ultimately driven by the platform Intl.DateTimeFormat time zone support/validation plus date-fns/format’s “Invalid Date => RangeError” behavior. [1][3][4]

Sources: date-fns-tz README / docs on RangeError behavior [1][2], date-fns-tz issue about Intl.DateTimeFormat rejecting real IANA zones for some users [3], MDN on Intl.DateTimeFormat timeZone expecting IANA/offset identifiers [4].

References:
[1] https://github.com/marnusw/date-fns-tz
[2] https://github.com/marnusw/date-fns-tz (README section describing when invalid time zones throw)
[3] marnusw/date-fns-tz#264
[4] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat


🏁 Script executed:

rg "formatInTimeZone|timezone" src/lib/utils/date-format.ts -A 2 -B 2

Repository: ding113/claude-code-hub

Length of output: 1338


🏁 Script executed:

rg "formatDate" --type ts --type tsx -B 2 -A 2 | head -100

Repository: ding113/claude-code-hub

Length of output: 94


🏁 Script executed:

rg "formatDate" -A 1 -B 1 | head -100

Repository: ding113/claude-code-hub

Length of output: 6949


🏁 Script executed:

cat -n tests/unit/lib/date-format-timezone.test.ts

Repository: ding113/claude-code-hub

Length of output: 2922


应在 formatInTimeZone 调用处添加错误处理防止异常

date-fns-tz 在接收到无效 IANA 时区时会抛出 RangeError,特别是当格式字符串包含时区令牌时。由于 timezone 参数可能来自用户输入或配置,建议用 try-catch 包装调用并回退到本地时区格式化。

建议的改动
  if (timezone) {
-    return formatInTimeZone(dateObj, timezone, formatString, {
-      locale: dateFnsLocale,
-    });
+    try {
+      return formatInTimeZone(dateObj, timezone, formatString, {
+        locale: dateFnsLocale,
+      });
+    } catch {
+      return format(dateObj, formatString, { locale: dateFnsLocale });
+    }
  }
📝 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.

Suggested change
if (timezone) {
return formatInTimeZone(dateObj, timezone, formatString, {
locale: dateFnsLocale,
});
}
if (timezone) {
try {
return formatInTimeZone(dateObj, timezone, formatString, {
locale: dateFnsLocale,
});
} catch {
return format(dateObj, formatString, { locale: dateFnsLocale });
}
}
🤖 Prompt for AI Agents
In `@src/lib/utils/date-format.ts` around lines 54 - 58, The call to
formatInTimeZone can throw (RangeError) for invalid IANA timezones, so wrap the
formatInTimeZone(dateObj, timezone, formatString, { locale: dateFnsLocale })
invocation in a try-catch in the same function; on success return the formatted
string, on error log/handle the exception (or swallow) and fall back to
formatting using format(dateObj, formatString, { locale: dateFnsLocale }) so
invalid timezone input doesn't crash the app. Ensure you reference
formatInTimeZone, dateObj, timezone, formatString, and dateFnsLocale when
locating the code to change.

Comment on lines +25 to +59
export function parseDateInputAsTimezone(input: string, timezone: string): Date {
if (!input) {
throw new Error("Invalid date input: empty string");
}

// Date-only format: YYYY-MM-DD
if (/^\d{4}-\d{2}-\d{2}$/.test(input)) {
// Parse as end-of-day (23:59:59) in the given timezone
const localDateTime = parse(`${input} 23:59:59`, "yyyy-MM-dd HH:mm:ss", new Date());

if (Number.isNaN(localDateTime.getTime())) {
throw new Error(`Invalid date input: ${input}`);
}

// Convert from timezone local time to UTC
return fromZonedTime(localDateTime, timezone);
}

// Check if input has timezone designator (Z or +-HH:MM offset)
// If so, parse directly as it already represents an absolute instant
const hasTimezoneDesignator = /([zZ]|[+-]\d{2}:?\d{2})$/.test(input);
if (hasTimezoneDesignator) {
const directDate = new Date(input);
if (Number.isNaN(directDate.getTime())) {
throw new Error(`Invalid date input: ${input}`);
}
return directDate;
}

// ISO datetime without timezone: parse and treat as timezone local time
const localDate = new Date(input);

if (Number.isNaN(localDate.getTime())) {
throw new Error(`Invalid date input: ${input}`);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

错误文案避免硬编码
这些 Error 文案若会直接返回给用户或 API 响应,建议改为 i18n key 或统一错误码,由上层翻译/映射。

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/utils/date-input.ts` around lines 25 - 59, In
parseDateInputAsTimezone replace all hardcoded Error messages (e.g., "Invalid
date input: empty string" and `Invalid date input: ${input}`) with i18n keys or
standardized error codes so callers can map/translate them; update every throw
in this function (the initial empty check, the invalid parse branches for
date-only, timezone-designated, and ISO-without-timezone) to throw a consistent
error identifier (or a small Error subclass containing a code property) instead
of user-facing text, e.g., throw { code: "ERR_INVALID_DATE_INPUT", details:
input } or new ErrorWithCode("ERR_INVALID_DATE_INPUT", input) so upper layers
can perform localization.

} from "@/lib/constants/provider.constants";
import { USER_LIMITS } from "@/lib/constants/user.constants";
import { CURRENCY_CONFIG } from "@/lib/utils/currency";
import { isValidIANATimezone } from "@/lib/utils/timezone";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

校验提示语需要接入 i18n

timezone 的校验 message 仍是硬编码字符串。请改为 i18n key 或统一的错误映射,保持多语言一致性。

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".

Also applies to: 732-740

🤖 Prompt for AI Agents
In `@src/lib/validation/schemas.ts` at line 9, The timezone validator currently
uses a hardcoded error string; replace that with an i18n key by importing and
using the app's i18n translator (e.g., the project's i18n.t or equivalent)
inside the schema so the validation message becomes something like
t('validation.timezone.invalid') or the project’s standard error-mapping key
instead of a literal string, update the import block to include the i18n helper
alongside isValidIANATimezone, and apply the same change to nearby validation
messages in this file that are also hardcoded so all user-facing validation text
uses i18n keys.

Comment on lines +786 to +813
// 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(),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

新增配额校验文案不要硬编码英文

这些校验提示会直接面向用户,建议改为 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 validation schemas
(quotaDbRefreshIntervalSeconds, quotaLeasePercent5h, quotaLeasePercentDaily,
quotaLeasePercentWeekly, quotaLeasePercentMonthly, quotaLeaseCapUsd) currently
hardcode English error messages; replace these literal strings with calls to the
project's i18n/error-mapping utility (e.g., use i18n.t('key') or
mapValidationError('KEY')) so all user-facing messages come from the translation
resources (zh-CN, zh-TW, en, ja, ru); update each .int/.min/.max/.nullable
message argument to reference the appropriate i18n key or mapped constant and
ensure optional nullables still produce translated messages via the same
mapping.

Comment on lines +1 to +3
import { beforeEach, describe, expect, it, vi } from "vitest";
import { locales } from "@/i18n/config";

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

PR 目标分支不符合仓库规范
当前 PR 目标分支为 main,但规范要求所有 PR 目标为 dev。请先调整目标分支再继续评审。

Based on learnings: All pull requests must target the dev branch (https://github.com/ding113/claude-code-hub).

🤖 Prompt for AI Agents
In `@tests/unit/actions/system-config-save.test.ts` around lines 1 - 3, The pull
request is targeting the main branch but the repo policy requires all PRs target
dev; change the PR base branch from main to dev in the Git hosting UI (use the
"Edit" button next to the PR title or the branch selector) so the PR targets dev
instead of main, then update any branch-specific references in the PR
description if present.

"America/Mexico_City",
// Pacific
"Pacific/Auckland",
"Pacific/Sydney",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[High] [LOGIC-BUG] Invalid IANA timezone in COMMON_TIMEZONES

Why this is a problem: "Pacific/Sydney" is not a valid IANA timezone (Intl rejects it), but it is rendered as an option in the system timezone dropdown. Selecting it will always fail validation and prevents users from setting Sydney time.

Suggested fix:

// Pacific
"Pacific/Auckland",
"Australia/Sydney", // valid IANA timezone

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

This PR is XL in both files and churn, which makes it hard to validate behavior and isolate regressions.
One high-confidence functional bug was found in newly-added timezone utilities that will block selecting Sydney time in the settings UI.

PR Size: XL

  • Lines changed: 22769
  • Files changed: 195
  • Split suggestions:
    • PR 1: Timezone utilities + system settings form/schema/messages
    • PR 2: Quota lease (rate-limit) feature + related tests
    • PR 3: Provider endpoints + probe scheduler behavior changes + tests
    • PR 4: Dashboard/logs/my-usage UI refactors
    • PR 5: Drizzle migrations + snapshots

Issues Found

Category Critical High Medium Low
Logic/Bugs 0 1 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

Critical Issues (Must Fix)

  • (Confidence 95) src/lib/utils/timezone.ts:51 COMMON_TIMEZONES contains "Pacific/Sydney", which is not a valid IANA timezone and is rendered as a selectable option.

High Priority Issues (Should Fix)

  • None.

Review Coverage

  • Logic and correctness
  • Security (OWASP Top 10)
  • Error handling
  • Type safety
  • Documentation accuracy
  • Test coverage
  • Code clarity

Automated review by Codex AI

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Identified open PR #675 (“release v0.5.2”): 21515 additions, 1254 deletions, 195 files → XL; applied label size/XL.
  • GitHub API diff was too large (HTTP 406: diff exceeded maximum number of lines (20000)), so I reviewed via local git diff + per-file patches.
  • Posted 1 inline review comment (validated bug): src/lib/utils/timezone.ts:51 includes invalid IANA timezone "Pacific/Sydney" in COMMON_TIMEZONES (should be "Australia/Sydney").
  • Submitted the required PR review summary via gh pr review --comment.

- Add defaultExpanded prop to StepCard for external control
- Add initialTab and initialExpandedChainIndex to ErrorDetailsDialog
- Add onChainItemClick callback to ProviderChainPopover
- Integrate click-to-navigate in VirtualizedLogsTable and UsageLogsTable
- Add i18n translations for new hint text (en, zh-CN, zh-TW, ja, ru)

Closes #676

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

10 files reviewed, 5 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +307 to +310
local ttl = redis.call('TTL', key)
if ttl > 0 then
redis.call('SETEX', key, ttl, cjson.encode(lease))
end
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TTL edge case: if key expires between GET and TTL commands, ttl will be -2 and lease won't be updated

Suggested change
local ttl = redis.call('TTL', key)
if ttl > 0 then
redis.call('SETEX', key, ttl, cjson.encode(lease))
end
-- Get TTL and update lease
local ttl = redis.call('TTL', key)
if ttl > 0 then
redis.call('SETEX', key, ttl, cjson.encode(lease))
elseif ttl == -1 then
-- Key exists but has no expiration (shouldn't happen, but handle gracefully)
redis.call('SET', key, cjson.encode(lease))
end
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/rate-limit/lease-service.ts
Line: 307:310

Comment:
TTL edge case: if key expires between `GET` and `TTL` commands, `ttl` will be -2 and lease won't be updated

```suggestion
    -- Get TTL and update lease
    local ttl = redis.call('TTL', key)
    if ttl > 0 then
      redis.call('SETEX', key, ttl, cjson.encode(lease))
    elseif ttl == -1 then
      -- Key exists but has no expiration (shouldn't happen, but handle gracefully)
      redis.call('SET', key, cjson.encode(lease))
    end
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +2022 to +2034
// Decrement lease budgets for all windows (fire-and-forget)
const windows: LeaseWindowType[] = ["5h", "daily", "weekly", "monthly"];
void Promise.all([
...windows.map((w) => RateLimitService.decrementLeaseBudget(key.id, "key", w, costFloat)),
...windows.map((w) => RateLimitService.decrementLeaseBudget(user.id, "user", w, costFloat)),
...windows.map((w) =>
RateLimitService.decrementLeaseBudget(provider.id, "provider", w, costFloat)
),
]).catch((error) => {
logger.warn("[ResponseHandler] Failed to decrement lease budgets:", {
error: error instanceof Error ? error.message : String(error),
});
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fire-and-forget lease decrements could silently fail if all 12 operations (3 entities × 4 windows) fail. Consider adding metrics or more visibility for lease decrement failures in production monitoring

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/response-handler.ts
Line: 2022:2034

Comment:
Fire-and-forget lease decrements could silently fail if all 12 operations (3 entities × 4 windows) fail. Consider adding metrics or more visibility for lease decrement failures in production monitoring

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +1 to +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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verify existing expires_at timestamps convert correctly to timestamptz - PostgreSQL will interpret them as being in server timezone. Test with: SELECT timezone('UTC', expires_at) FROM keys WHERE expires_at IS NOT NULL LIMIT 10;

Prompt To Fix With AI
This is a comment left during a code review.
Path: drizzle/0059_safe_xorn.sql
Line: 1:2

Comment:
Verify existing `expires_at` timestamps convert correctly to `timestamptz` - PostgreSQL will interpret them as being in server timezone. Test with: `SELECT timezone('UTC', expires_at) FROM keys WHERE expires_at IS NOT NULL LIMIT 10;`

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +22 to +24
const BASE_INTERVAL_MS = Math.max(
1,
parseIntWithDefault(process.env.ENDPOINT_PROBE_INTERVAL_MS, 10_000)
parseIntWithDefault(process.env.ENDPOINT_PROBE_INTERVAL_MS, 60_000)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default probe interval changed from 10s to 60s - existing deployments will probe 6x less frequently. Ensure monitoring can detect endpoint failures within the new 60s window

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/provider-endpoints/probe-scheduler.ts
Line: 22:24

Comment:
Default probe interval changed from 10s to 60s - existing deployments will probe 6x less frequently. Ensure monitoring can detect endpoint failures within the new 60s window

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +109 to +111
// Clamp percent to valid range [0, 1]
const safePercent = Math.min(1, Math.max(0, percent));
let slice = limitAmount * safePercent;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider logging warning if percent is outside [0, 1] range before clamping, to detect configuration issues

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/rate-limit/lease.ts
Line: 109:111

Comment:
Consider logging warning if `percent` is outside [0, 1] range before clamping, to detect configuration issues

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

@ding113 ding113 merged commit 7309bad into main Jan 29, 2026
17 of 21 checks passed
@github-project-automation github-project-automation bot moved this from Backlog to Done in Claude Code Hub Roadmap Jan 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:i18n area:UI enhancement New feature or request size/XL Extra Large PR (> 1000 lines)

Projects

Status: Done

2 participants