Skip to content

fix(dashboard): unify timezone handling#1231

Open
ROOOO wants to merge 3 commits into
ding113:devfrom
ROOOO:codex/dashboard-timezone-consistency
Open

fix(dashboard): unify timezone handling#1231
ROOOO wants to merge 3 commits into
ding113:devfrom
ROOOO:codex/dashboard-timezone-consistency

Conversation

@ROOOO

@ROOOO ROOOO commented May 30, 2026

Copy link
Copy Markdown

Summary

Resolve dashboard, logs, my-usage, and leaderboard date ranges against the configured system timezone. Include timezone in dashboard Redis cache keys and invalidate timezone-sensitive caches when timezone settings change. Normalize statistics bucket serialization to prevent UTC/local date drift.

Problem

Dashboard views (statistics, logs, leaderboard, my-usage) were using inconsistent timezone handling:

  • Date range queries were computed using browser local time or UTC rather than the configured system timezone
  • Redis cache keys did not incorporate the system timezone, causing stale data after timezone changes
  • Statistics bucket normalization used naive Date parsing, causing UTC/local date drift and off-by-one day shifts in charts (e.g. April 28 data showing under April 27)
  • Usage logs date range pickers and filters displayed dates incorrectly when the system timezone differed from the server timezone

Related Issues

Solution

  1. Timezone-aware date resolution — All dashboard date range queries now use resolveSystemTimezone() (DB config -> env TZ -> UTC fallback) and date-fns-tz utilities (toZonedTime, formatInTimeZone, fromZonedTime) to compute dates in the correct timezone
  2. Timezone-scoped Redis cache keys — Dashboard cache keys (overview, statistics, leaderboard) now include tz:<timezone> segment to prevent stale data when timezone changes
  3. Cache invalidation on timezone change — When system timezone is updated via settings, all dashboard caches are invalidated atomically via invalidateAllOverviewCaches, invalidateAllStatisticsCaches, invalidateAllLeaderboardCaches
  4. Bucket normalization fixnormalizeBucketInstant replaces normalizeBucketDate to correctly parse bucket timestamps through the system timezone, preventing UTC/local drift
  5. Chart serialization fixserializeChartBucketDate replaces resolution-dependent date formatting with consistent ISO serialization, ensuring stable date keys across all time ranges

Changes

Core Changes

  • src/repository/statistics.ts (+52/-19) — Replace normalizeBucketDate with normalizeBucketInstant that parses through system timezone; add timezone parameter to all DB query functions
  • src/repository/admin-user-insights.ts (+36/-27) — Add buildSystemTimezoneDateConditions helper for all date range queries; use AT TIME ZONE in SQL conditions
  • src/lib/redis/leaderboard-cache.ts (+23/-5) — Include timezone in all leaderboard cache keys; add invalidateAllLeaderboardCaches
  • src/lib/redis/overview-cache.ts (+30/-6) — Include timezone in overview cache keys; add invalidateAllOverviewCaches; handle legacy key cleanup
  • src/lib/redis/statistics-cache.ts (+42/-14) — Include timezone in statistics cache keys; add invalidateAllStatisticsCaches; handle legacy key cleanup
  • src/actions/statistics.ts (+6/-10) — Replace resolution-dependent date formatting with serializeChartBucketDate for stable chart buckets
  • src/actions/system-config.ts (+17/-0) — Invalidate dashboard caches when timezone setting changes
  • src/app/api/admin/system-config/route.ts (+16/-0) — Same cache invalidation for REST API path

UI Changes

  • src/app/[locale]/dashboard/leaderboard/_components/date-range-picker.tsx (+17/-8) — Use formatInTimeZone / toZonedTime for all date range computation
  • src/app/[locale]/dashboard/leaderboard/user/[userId]/_components/filters/types.ts (+26/-18) — Add timezone parameter to time preset resolution
  • src/app/[locale]/dashboard/logs/_components/filters/active-filters-display.tsx (+10/-3) — Display active filter dates in system timezone
  • src/app/[locale]/dashboard/logs/_components/logs-date-range-picker.tsx (+6/-2) — Use timezone-aware "today" for calendar disabled/ navigation
  • src/app/[locale]/dashboard/logs/_utils/time-range.ts (+13/-18) — Simplify getQuickDateRange to use toZonedTime then plain date-fns formatting
  • src/app/[locale]/my-usage/_components/statistics-summary-card.tsx (+22/-6) — Use formatInTimeZone for default date range; auto-reset range on timezone change
  • src/components/ui/relative-time.tsx (+4/-1) — Accept optional timeZone override prop
  • src/types/dashboard-cache.ts (+30/-6) — Add timezone field to cache key types; add timezone-scoped cache key builders

Test Changes

  • tests/unit/dashboard-logs-time-range-utils.test.ts (+10/-0) — Add test for early-morning UTC in a different timezone
  • tests/unit/dashboard/dashboard-cache-keys.test.ts (+21/-11) — Update cache key tests for timezone-scoped keys
  • tests/unit/redis/leaderboard-cache.test.ts (+29/-1) — Test timezone-scoped Redis keys and cross-timezone behavior
  • tests/unit/redis/overview-cache.test.ts (+35/-14) — Test timezone-scoped keys and invalidation
  • tests/unit/redis/statistics-cache.test.ts (+70/-23) — Test timezone-scoped keys and DB parameter passing
  • tests/unit/repository/admin-user-insights-overview.test.ts (+12/-0) — Add timezone-specific test
  • tests/unit/repository/statistics-timezone-buckets.test.ts (+38/-0) — New file testing bucket normalization with timezone
  • tests/unit/user-insights-filters.test.ts (+17/-0) — Test timezone-aware filter presets

Breaking Changes

None — all changes are backward-compatible:

  • New function parameters are optional with sensible defaults
  • Redis cache keys are extended (not renamed) to include tz:<timezone>, while legacy keys are still cleaned up on invalidation
  • No API surface changes or schema migrations

Testing

Automated Tests

  • ✅ 9 test files updated/added with timezone-specific coverage
  • bunx vitest run specific tests passed
  • bun run typecheck passed
  • bun run lint passed
  • bun run build passed

Manual Testing

  1. Set system timezone to a non-UTC timezone (e.g., Asia/Shanghai UTC+8)
  2. Open dashboard and verify daily statistics chart shows correct date labels
  3. Verify usage logs date range picker shows today in the correct timezone
  4. Verify leaderboard date range picker shows correct date ranges
  5. Verify my-usage statistics card shows correct date range
  6. Change system timezone in settings and verify caches are invalidated (dashboard data refreshes)

Validation

  • bunx vitest run tests/unit/dashboard-logs-time-range-utils.test.ts tests/unit/dashboard/dashboard-cache-keys.test.ts tests/unit/dashboard/leaderboard-view-user-cache-hit-rate.test.tsx tests/unit/redis/leaderboard-cache.test.ts tests/unit/redis/overview-cache.test.ts tests/unit/redis/statistics-cache.test.ts tests/unit/repository/admin-user-insights-overview.test.ts tests/unit/repository/statistics-timezone-buckets.test.ts tests/unit/user-insights-filters.test.ts (passed)
  • bun run typecheck (passed)
  • bun run lint (passed; Biome schema version info only)
  • bun run build (passed; existing Edge Runtime warnings in instrumentation dependencies)

Description enhanced by Claude AI PR Agent

Greptile Summary

This PR unifies timezone handling across dashboard views (statistics, logs, leaderboard, my-usage) by routing all date-range computations through resolveSystemTimezone(), adding tz:<timezone> segments to Redis cache keys, invalidating timezone-sensitive caches on settings change, and replacing naïve Date parsing in bucket normalization with a new normalizeBucketInstant + fromZonedTime approach.

  • Core correctness fix: normalizeBucketInstant strips timezone offsets from PostgreSQL bucket timestamps and reinterprets them as instants in the configured timezone, resolving the UTC/local day-alignment drift that caused April 28 data to appear under April 27 in UTC+8.
  • Cache-key scoping: All three Redis cache families now embed tz:<timezone> so a timezone change automatically misses the old cache; invalidateAll*Caches functions clean up both new and legacy keys atomically on settings save.
  • UI consistency: Date range pickers, active-filter display, and RelativeTime components accept or derive the system timezone directly, eliminating browser-local vs server-timezone discrepancies.

Confidence Score: 4/5

Safe to merge; the core timezone-alignment fix is correct and well-tested, with two rough edges worth addressing in a follow-up.

The fundamental approach is sound: timezone is resolved once per cache request, propagated to DB queries, and embedded in cache keys. All scan-based invalidation patterns also match active lock keys, meaning a concurrent cache-population attempt can have its distributed lock silently deleted during invalidation. Additionally, normalizeBucketInstant's Date-object path uses getDate()/getHours() (server-local time) rather than getUTCDate()/getUTCHours(), so on a non-UTC Node server bucket timestamps would be misinterpreted. Both are performance/edge-case issues rather than data-loss bugs on UTC servers, but they narrow the robustness margin of an otherwise thorough change.

src/repository/statistics.ts (formatLocalDateTime uses local-time getters), src/lib/redis/overview-cache.ts, src/lib/redis/statistics-cache.ts, src/lib/redis/leaderboard-cache.ts (scan patterns include lock keys)

Important Files Changed

Filename Overview
src/repository/statistics.ts Replaces normalizeBucketDate with normalizeBucketInstant for timezone-aware bucket parsing; adds timezoneOverride to all three DB query functions; the string path is correct, but the Date object path uses system-local getDate()/getHours() which could drift on non-UTC servers
src/lib/redis/overview-cache.ts Adds timezone-scoped cache keys and invalidateAllOverviewCaches; scanPattern("overview:*") will also match lock keys like "overview:global:tz:UTC:lock", causing lock deletion during invalidation
src/lib/redis/statistics-cache.ts Adds timezone-scoped cache keys and legacy key cleanup; same lock-key scan issue as overview-cache; resolveSystemTimezone resolved once per cache request and correctly passed down to DB queries
src/lib/redis/leaderboard-cache.ts Adds tz: segment to all leaderboard cache key variants and adds invalidateAllLeaderboardCaches with scanPattern("leaderboard:*") which may also match lock keys
src/types/dashboard-cache.ts Overloaded cache key builders correctly handle both global and user-scoped keys with timezone; TypeScript overloads prevent misuse of the implementation's wider signature
src/repository/admin-user-insights.ts Centralizes date condition building in buildSystemTimezoneDateConditions using AT TIME ZONE in parameterized SQL; consistent refactor across getUserOverviewMetrics, getUserModelBreakdown, getUserProviderBreakdown
src/actions/statistics.ts serializeChartBucketDate unifies hourly and daily bucket keys as ISO timestamps; day-resolution keys changed from YYYY-MM-DD to full ISO format — downstream chart rendering code may need updates
src/actions/system-config.ts Invalidates all three dashboard caches when timezone changes; uses Promise.all with .catch so cache failures don't surface to users
src/app/[locale]/my-usage/_components/statistics-summary-card.tsx Adds auto-reset of date range when timezone changes only if user hasn't manually set a range; autoDateRangeRef correctly tracks user intent
src/app/[locale]/dashboard/leaderboard/_components/date-range-picker.tsx Monthly navigation now correctly advances by calendar month; today computed in system timezone for disabled-date check

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Dashboard request] --> B{Redis client available?}
    B -- No --> C[Direct DB query with timezone]
    B -- Yes --> D[resolveSystemTimezone]
    D --> E[buildCacheKey with tz: segment]
    E --> F{Cache hit?}
    F -- Yes --> G[Return cached data]
    F -- No --> H{Lock acquired?}
    H -- No --> I[Wait 100ms, Retry cache]
    I --> J{Retry hit?}
    J -- Yes --> G
    J -- No --> C
    H -- Yes --> K[Query DB with timezone param]
    K --> L[normalizeBucketInstant strips TZ, fromZonedTime]
    L --> M[Write to Redis with TTL]
    M --> N[Release lock]
    N --> G
    P[Admin saves timezone] --> Q[updateSystemSettings]
    Q --> R[invalidateAll*Caches]
    R --> S[Scan patterns also match lock keys]
    S --> T[redis.del matched + legacy keys]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[Dashboard request] --> B{Redis client available?}
    B -- No --> C[Direct DB query with timezone]
    B -- Yes --> D[resolveSystemTimezone]
    D --> E[buildCacheKey with tz: segment]
    E --> F{Cache hit?}
    F -- Yes --> G[Return cached data]
    F -- No --> H{Lock acquired?}
    H -- No --> I[Wait 100ms, Retry cache]
    I --> J{Retry hit?}
    J -- Yes --> G
    J -- No --> C
    H -- Yes --> K[Query DB with timezone param]
    K --> L[normalizeBucketInstant strips TZ, fromZonedTime]
    L --> M[Write to Redis with TTL]
    M --> N[Release lock]
    N --> G
    P[Admin saves timezone] --> Q[updateSystemSettings]
    Q --> R[invalidateAll*Caches]
    R --> S[Scan patterns also match lock keys]
    S --> T[redis.del matched + legacy keys]
Loading

Reviews (3): Last reviewed commit: "fix: navigate leaderboard monthly ranges..." | Re-trigger Greptile

@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

概览

该PR为整个仪表盘系统添加了全面的时区支持:Redis缓存键包含时区维度、统计查询按时区分桶和归一化、日期选择器按时区计算日期范围、组件props传递时区配置,以及系统时区变更时的级联缓存失效。修复了用户在非本地时区服务器上看到错位日期的问题。

变更

时区感知仪表盘缓存和统计

层级 / 文件(s) 摘要
Redis缓存键中的时区维度
src/types/dashboard-cache.ts, src/lib/redis/index.ts
StatisticsCacheKey 加入必填 timezone: string 字段;buildOverviewCacheKey 和 buildStatisticsCacheKey 的重载签名新增 timezone 参数,生成的 key 包含 tz:{timezone} 段。
支持时区的排行榜缓存
src/lib/redis/leaderboard-cache.ts
buildCacheKey 在各周期(daily/weekly/monthly/custom/allTime)的 key 中插入 :tz:${timezone}: 段;新增 invalidateAllLeaderboardCaches 函数使用 scanPattern 批量删除 leaderboard:* 命名空间下所有缓存。
使用时区解析的概览缓存
src/lib/redis/overview-cache.ts
getOverviewWithCache 调用 resolveSystemTimezone 并将时区纳入 cache key 生成;invalidateOverviewCache 改为按作用域扫描时区相关键并批量删除;新增 invalidateAllOverviewCaches 删除 overview:* 命名空间内的所有键。
支持时区的统计缓存和DB回退
src/lib/redis/statistics-cache.ts
getStatisticsWithCache 解析系统时区、生成带时区的缓存键和锁键;queryDatabase 新增 timezone 参数并在 users/keys/mixed 模式下传递给仓储查询函数;Redis 不可用或锁等待失败时回退到带时区的数据库查询。
支持时区的统计仓储归一化
src/repository/statistics.ts
新增 formatLocalDateTime 和 normalizeBucketInstant 函数按指定时区解析和归一化分桶时间;getTimeBuckets、zeroFillUserStats、zeroFillKeyStats、zeroFillMixedOthersStats 均增加 timezone 参数;公开查询函数 getUserStatisticsFromDB、getKeyStatisticsFromDB、getMixedStatisticsFromDB 新增可选 timezoneOverride 参数。
使用时区过滤的管理员概览指标
src/repository/admin-user-insights.ts
新增 buildSystemTimezoneDateConditions 助手函数,生成带 AT TIME ZONE 换算的 SQL 日期条件;getUserOverviewMetrics、getUserModelBreakdown、getUserProviderBreakdown 使用该助手生成时区感知的日期过滤条件。
服务端时区触发的缓存失效
src/actions/system-config.ts, src/app/api/admin/system-config/route.ts
系统设置更新时,若 timezone 被显式修改则使用 Promise.all 并行失效 overview、statistics、leaderboard 等所有时区敏感缓存;失效失败仅记录警告日志,不阻断设置更新流程。
图表统计日期序列化
src/actions/statistics.ts
新增 serializeChartBucketDate 助手函数将日期值统一序列化为 ISO 字符串或原始字符串;替代原先按 resolution(hour/day)分支的格式化逻辑。
支持时区的排行榜日期选择器
src/app/[locale]/dashboard/leaderboard/_components/date-range-picker.tsx
新增 formatDateInSystemTimeZone 工具函数;getDateRangeForPeriod 增加 timeZone 和 now 参数,使用 toZonedTime 进行基准日期转换;DateRangePicker 通过 useTimeZone 获取时区、计算 today 值,用于日历 disabled.after 规则和下一周期按钮禁用条件同步;调整 shiftDateRange 使用 differenceInCalendarDays 计算日历天差,月度区间时按月平移。
用户洞察预设日期过滤时区支持
src/app/[locale]/dashboard/leaderboard/user/[userId]/_components/filters/types.ts, src/app/[locale]/dashboard/leaderboard/user/[userId]/_components/user-insights-view.tsx
resolveTimePresetDates 增加可选 timeZone 和 now 参数,使用 toZonedTime、subDays 和 format 重写日期计算逻辑;UserInsightsView 通过 useTimeZone 获取时区并传入该函数。
支持时区的日志日期范围和选择器
src/app/[locale]/dashboard/logs/_utils/time-range.ts, src/app/[locale]/dashboard/logs/_components/logs-date-range-picker.tsx, src/app/[locale]/dashboard/logs/_components/filters/active-filters-display.tsx
getQuickDateRange 改为先用 toZonedTime 将 now 转为目标时区的 baseDate 再进行日期计算;LogsDateRangePicker 新增时区推导的 today 值来自 quick period "today" 结果,同步日历禁用和导航按钮禁用规则;ActiveFiltersDisplay 新增可选 serverTimeZone 属性,按时区使用 formatInTimeZone 格式化日期范围展示。
通过组件props传播时区
src/app/[locale]/dashboard/logs/_components/usage-logs-view-virtualized.tsx, src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx, src/app/[locale]/my-usage/_components/usage-logs-section.tsx, src/app/[locale]/my-usage/_components/usage-logs-table.tsx, src/components/ui/relative-time.tsx
VirtualizedLogsTable、UsageLogsTable、RelativeTime 组件均新增可选 serverTimeZone/timeZone 属性;RelativeTime 优先使用 timeZone 覆盖值(timeZoneOverride ?? useTimeZone() ?? "UTC"),回退到环境时区;父组件通过 props 将 serverTimeZone 传递给子组件用于统一时区显示。
具有自动时区感知更新的统计卡
src/app/[locale]/my-usage/_components/statistics-summary-card.tsx
新增 getDefaultDateRange 按指定时区计算同一天的起止日期;引入 effectiveTimeZone(来自 serverTimeZone 或 useTimeZone)、autoDateRangeRef 和 previousTimeZoneRef 控制自动日期更新行为,仅在用户未手动选择日期时才随 effectiveTimeZone 变化自动重置 dateRange。
时区功能的全面测试覆盖
tests/unit/dashboard-logs-time-range-utils.test.ts, tests/unit/dashboard/dashboard-cache-keys.test.ts, tests/unit/dashboard/leaderboard-view-user-cache-hit-rate.test.tsx, tests/unit/redis/leaderboard-cache.test.ts, tests/unit/redis/overview-cache.test.ts, tests/unit/redis/statistics-cache.test.ts, tests/unit/repository/admin-user-insights-overview.test.ts, tests/unit/repository/statistics-timezone-buckets.test.ts, tests/unit/user-insights-filters.test.ts, tests/unit/dashboard/leaderboard-date-range-picker.test.tsx
添加对缓存 key 包含 tz: 段的断言;验证时区下统计分桶的序列化和日期转换;测试不同时区下日期范围计算的差异;新增 DateRangePicker 月度和自定义区间导航测试;在所有模块中 mock resolveSystemTimezone 以使用固定时区;验证 Redis scan/del 按时区模式匹配的行为以及 legacy key 兼容性。

代码评审工作量评估

🎯 4 (复杂) | ⏱️ ~65 分钟


可能相关的PR

  • ding113/claude-code-hub#657:该PR同样修改了仪表盘日志的日期范围和时区感知日期计算逻辑(如getQuickDateRange、LogsDateRangePicker),代码层面存在重叠。
  • ding113/claude-code-hub#808:两者都在统计看板/统计缓存链路修改相同代码(statistics-cache.ts、dashboard-cache.ts、statistics.ts),本PR进一步将缓存键和分桶日期按时区维度改造。
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 标题准确概括了 PR 的主要目的:统一时区处理,涵盖了整个变更集的核心改进方向。
Linked Issues check ✅ Passed PR 通过 normalizeBucketInstant 替代 normalizeBucketDate、使用 AT TIME ZONE SQL 子句、timezone 作用域的 Redis 缓存键等改动,完整解决了 #1135 反映的日期错位问题。
Out of Scope Changes check ✅ Passed 所有代码变更均围绕时区统一处理展开,包括日期计算、缓存键、数据库查询、UI 组件和测试,没有检测到范围外的改动。
Description check ✅ Passed 拉取请求描述详细说明了问题、解决方案、涉及的文件变更和测试验证,与代码变更集高度相关且信息完整。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 bug Something isn't working area:UI area:statistics labels May 30, 2026

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces comprehensive timezone support across the dashboard, logs, and statistics views, ensuring that date ranges, relative times, and database queries are resolved using the configured system timezone. Additionally, it updates Redis cache keys to include the timezone context and automatically invalidates these caches when the system timezone is updated. The review feedback suggests a minor optimization in the leaderboard's DateRangePicker to reuse the already-zoned baseDate variable instead of redundantly recalculating the timezone formatting.

}
default:
return { startDate: "2020-01-01", endDate: formatDate(new Date()) };
return { startDate: "2020-01-01", endDate: formatDateInSystemTimeZone(now, timeZone) };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Since baseDate is already computed as the zoned time in the target timezone (toZonedTime(now, timeZone)), we can directly use formatDate(baseDate) instead of calling formatDateInSystemTimeZone(now, timeZone). This improves consistency with the other cases in the switch statement and avoids redundant timezone calculations.

Suggested change
return { startDate: "2020-01-01", endDate: formatDateInSystemTimeZone(now, timeZone) };
return { startDate: "2020-01-01", endDate: formatDate(baseDate) };

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
src/actions/system-config.ts (1)

159-169: ⚡ Quick win

仅在时区实际发生变化时才失效全部仪表盘缓存。

当前条件只判断 validated.timezone !== undefined。若设置表单在每次保存时都携带 timezone 字段(部分更新表单通常会回传完整负载),那么即使时区未改变,每次保存都会清空全部 overview/statistics/leaderboard 缓存,导致随后大量请求回源数据库重算。建议改为比较保存前后的实际值。

此处已有 beforeupdated,可直接对比:

♻️ 建议改动
-    if (validated.timezone !== undefined) {
+    if (validated.timezone !== undefined && before?.timezone !== updated.timezone) {
       await Promise.all([
         invalidateAllOverviewCaches(),
         invalidateAllStatisticsCaches(),
         invalidateAllLeaderboardCaches(),
       ]).catch((error) => {
         logger.warn("[SystemSettings] Failed to invalidate timezone-sensitive dashboard caches", {
           error,
         });
       });
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/actions/system-config.ts` around lines 159 - 169, 当前逻辑只判断
validated.timezone !== undefined 导致每次保存都会失效缓存;请改为比较保存前后实际时区值(使用 before.timezone
和 updated.timezone)并且仅当二者不相等时才调用 invalidateAllOverviewCaches,
invalidateAllStatisticsCaches, invalidateAllLeaderboardCaches,保留原有的
Promise.all(...).catch(...) 错误捕获与 logger.warn;确保对 undefined/null
情况也能正确判断变化(例如使用严格不等 !== 或显式比较字符串化后的值)。
src/app/[locale]/my-usage/_components/statistics-summary-card.tsx (1)

39-44: 💤 Low value

初始状态可能使用不正确的时区计算

useState 的初始化函数在组件首次挂载时执行,此时 serverTimeZone 可能尚未从父组件加载完成(如上下文代码片段所示,serverTimeZone 是异步获取的)。这意味着初始 dateRange 可能使用 providerTimeZone(或 "UTC")计算,而非 serverTimeZone

虽然 Lines 62-67 的 effect 会在 effectiveTimeZone 变化时重置 dateRange,但这依赖于 autoDateRangeRef.currenttrue。当前逻辑是正确的,但建议添加注释说明这一行为,以避免后续维护时产生困惑。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`[locale]/my-usage/_components/statistics-summary-card.tsx around
lines 39 - 44, The initial dateRange state (created via useState calling
getDefaultDateRange(effectiveTimeZone)) can be computed before serverTimeZone is
available, so document this behavior: add a clear comment next to the dateRange
state initialization explaining that the first render may use
providerTimeZone/UTC, and that the effect which watches effectiveTimeZone (the
logic that compares effectiveTimeZone with previousTimeZoneRef and resets
dateRange when autoDateRangeRef.current is true) will update the dateRange once
the correct serverTimeZone arrives; reference getDefaultDateRange,
dateRange/setDateRange, effectiveTimeZone, autoDateRangeRef and
previousTimeZoneRef in the comment so maintainers understand why the lazy init
is safe.
tests/unit/repository/admin-user-insights-overview.test.ts (1)

71-74: ⚡ Quick win

移除该测试中的无用 messageRequest mock 字段getUserOverviewMetrics 仅使用 usageLedger(由 LEDGER_BILLING_CONDITION 引入的 usageLedger.blockedBy/endpoint),未引用 messageRequest.blockedBy/endpoint;当前用例断言也不涉及 messageRequest,因此测试里这段 messageRequest mock(lines 71-74)可删以避免误导。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/repository/admin-user-insights-overview.test.ts` around lines 71 -
74, Remove the unused messageRequest mock fields from the test: delete the
messageRequest object (the blockedBy and endpoint properties) since
getUserOverviewMetrics only reads usageLedger via LEDGER_BILLING_CONDITION
(usageLedger.blockedBy / usageLedger.endpoint) and the assertions in this test
do not reference messageRequest; update the test setup to rely solely on
usageLedger/LEDGER_BILLING_CONDITION values and remove any references to
messageRequest in the test fixture.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/actions/system-config.ts`:
- Around line 159-169: 当前逻辑只判断 validated.timezone !== undefined
导致每次保存都会失效缓存;请改为比较保存前后实际时区值(使用 before.timezone 和 updated.timezone)并且仅当二者不相等时才调用
invalidateAllOverviewCaches, invalidateAllStatisticsCaches,
invalidateAllLeaderboardCaches,保留原有的 Promise.all(...).catch(...) 错误捕获与
logger.warn;确保对 undefined/null 情况也能正确判断变化(例如使用严格不等 !== 或显式比较字符串化后的值)。

In `@src/app/`[locale]/my-usage/_components/statistics-summary-card.tsx:
- Around line 39-44: The initial dateRange state (created via useState calling
getDefaultDateRange(effectiveTimeZone)) can be computed before serverTimeZone is
available, so document this behavior: add a clear comment next to the dateRange
state initialization explaining that the first render may use
providerTimeZone/UTC, and that the effect which watches effectiveTimeZone (the
logic that compares effectiveTimeZone with previousTimeZoneRef and resets
dateRange when autoDateRangeRef.current is true) will update the dateRange once
the correct serverTimeZone arrives; reference getDefaultDateRange,
dateRange/setDateRange, effectiveTimeZone, autoDateRangeRef and
previousTimeZoneRef in the comment so maintainers understand why the lazy init
is safe.

In `@tests/unit/repository/admin-user-insights-overview.test.ts`:
- Around line 71-74: Remove the unused messageRequest mock fields from the test:
delete the messageRequest object (the blockedBy and endpoint properties) since
getUserOverviewMetrics only reads usageLedger via LEDGER_BILLING_CONDITION
(usageLedger.blockedBy / usageLedger.endpoint) and the assertions in this test
do not reference messageRequest; update the test setup to rely solely on
usageLedger/LEDGER_BILLING_CONDITION values and remove any references to
messageRequest in the test fixture.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c95f11f7-ddd3-4de3-9789-bc92681d0320

📥 Commits

Reviewing files that changed from the base of the PR and between ed95b48 and 98bcbc5.

📒 Files selected for processing (32)
  • src/actions/statistics.ts
  • src/actions/system-config.ts
  • src/app/[locale]/dashboard/leaderboard/_components/date-range-picker.tsx
  • src/app/[locale]/dashboard/leaderboard/user/[userId]/_components/filters/types.ts
  • src/app/[locale]/dashboard/leaderboard/user/[userId]/_components/user-insights-view.tsx
  • src/app/[locale]/dashboard/logs/_components/filters/active-filters-display.tsx
  • src/app/[locale]/dashboard/logs/_components/logs-date-range-picker.tsx
  • src/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsx
  • src/app/[locale]/dashboard/logs/_components/usage-logs-view-virtualized.tsx
  • src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx
  • src/app/[locale]/dashboard/logs/_utils/time-range.ts
  • src/app/[locale]/my-usage/_components/statistics-summary-card.tsx
  • src/app/[locale]/my-usage/_components/usage-logs-section.tsx
  • src/app/[locale]/my-usage/_components/usage-logs-table.tsx
  • src/app/api/admin/system-config/route.ts
  • src/components/ui/relative-time.tsx
  • src/lib/redis/index.ts
  • src/lib/redis/leaderboard-cache.ts
  • src/lib/redis/overview-cache.ts
  • src/lib/redis/statistics-cache.ts
  • src/repository/admin-user-insights.ts
  • src/repository/statistics.ts
  • src/types/dashboard-cache.ts
  • tests/unit/dashboard-logs-time-range-utils.test.ts
  • tests/unit/dashboard/dashboard-cache-keys.test.ts
  • tests/unit/dashboard/leaderboard-view-user-cache-hit-rate.test.tsx
  • tests/unit/redis/leaderboard-cache.test.ts
  • tests/unit/redis/overview-cache.test.ts
  • tests/unit/redis/statistics-cache.test.ts
  • tests/unit/repository/admin-user-insights-overview.test.ts
  • tests/unit/repository/statistics-timezone-buckets.test.ts
  • tests/unit/user-insights-filters.test.ts

@ROOOO ROOOO marked this pull request as ready for review June 21, 2026 03:19
@github-actions github-actions Bot added the size/XL Extra Large PR (> 1000 lines) label Jun 21, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

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 well-executed timezone consistency fix that addresses dashboard date handling across 32 files. The implementation demonstrates solid understanding of timezone-aware programming with proper cache invalidation and backward compatibility.

PR Size: XL

  • Lines changed: 816 (+616/-200)
  • Files changed: 32

Recommendation: Consider splitting this PR into smaller parts for easier review. Potential splits:

  1. Core repository layer changes (statistics.ts, admin-user-insights.ts)
  2. Cache layer changes (redis/*.ts)
  3. UI components and utils
  4. Test updates

Issues Found

Category Critical High Medium Low
Logic/Bugs 0 0 0 0
Security 0 0 0 0
Error Handling 0 0 0 0
Types 0 0 0 0
Comments/Docs 0 0 0 0
Tests 0 0 0 0
Simplification 0 0 0 0

Critical Issues (Must Fix)

None

High Priority Issues (Should Fix)

None

Review Coverage

  • Logic and correctness - Clean
  • Security (OWASP Top 10) - Clean (parameterized SQL, no injection risks)
  • Error handling - Clean (all error paths logged, cache invalidation failures caught and warned)
  • Type safety - Clean (function overloads used correctly for backward compatibility)
  • Documentation accuracy - Clean (comments accurate where present)
  • Test coverage - Good (9 test files updated/added with timezone-specific coverage)
  • Code clarity - Good (naming appropriate, complexity managed)

Notable Design Decisions (Verified as Intentional)

  1. Cache invalidation error swallowing (src/actions/system-config.ts:60-64, src/app/api/admin/system-config/route.ts:595-604): Cache invalidation failures are caught and logged with logger.warn rather than blocking the settings save operation. This is intentional - cache invalidation is a best-effort operation, and stale cache will self-heal via TTL.

  2. Function overload pattern (src/types/dashboard-cache.ts): The buildStatisticsCacheKey function uses function overloads to maintain backward compatibility while adding timezone support. The userIdOrTimezone parameter interpretation is context-dependent but type-safe via the overload signatures.

  3. Legacy cache key cleanup (src/lib/redis/overview-cache.ts, statistics-cache.ts): When invalidating caches, both new timezone-scoped keys and legacy non-timezone keys are deleted. This ensures smooth migration path.


Automated review by Claude AI

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 98bcbc5068

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

localDateTime = `${match[1]}T${match[2]}`;
}

const parsed = fromZonedTime(localDateTime, timezone);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Format key-trend bucket dates in the system timezone

For day-resolution ranges in time zones east of UTC, this now converts a local bucket such as 2026-05-30 00:00:00 in Asia/Shanghai to the instant 2026-05-29T16:00:00.000Z. The admin user key trend still groups non-today rows with row.date.split("T")[0] in user-key-trend-chart.tsx, so those buckets render under the previous UTC date even though the DB query selected the correct local day. Please either keep day buckets as local date strings for that consumer or format/group these instants with the configured timezone.

Useful? React with 👍 / 👎.

}
case "thisMonth":
return { startDate: `${yyyy}-${mm}-01`, endDate: today };
return { startDate: format(baseDate, "yyyy-MM-01"), endDate: today };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 thisMonth start-date format string uses unescaped literals

format(baseDate, "yyyy-MM-01") relies on 0 and 1 not being recognized date-fns format tokens so they pass through as literals — producing "01" as the day. This coincidentally gives the correct first-of-month date today, but any future date-fns version that interprets single-digit characters differently would silently produce wrong output. The idiomatic alternative is format(startOfMonth(baseDate), "yyyy-MM-dd").

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/leaderboard/user/[userId]/_components/filters/types.ts
Line: 55

Comment:
**`thisMonth` start-date format string uses unescaped literals**

`format(baseDate, "yyyy-MM-01")` relies on `0` and `1` not being recognized date-fns format tokens so they pass through as literals — producing "01" as the day. This coincidentally gives the correct first-of-month date today, but any future date-fns version that interprets single-digit characters differently would silently produce wrong output. The idiomatic alternative is `format(startOfMonth(baseDate), "yyyy-MM-dd")`.

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

Comment thread src/actions/statistics.ts
Comment on lines 25 to +29
const createDataKey = (prefix: string, id: number): string => `${prefix}-${id}`;

function serializeChartBucketDate(value: string | Date): string {
const date = value instanceof Date ? value : new Date(value);
return Number.isNaN(date.getTime()) ? String(value) : date.toISOString();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 serializeChartBucketDate changes day-resolution chart keys from date strings to ISO timestamps

Previously, day-resolution keys were YYYY-MM-DD strings; they are now full ISO-8601 timestamps (e.g. "2025-01-14T16:00:00.000Z" for midnight Asia/Shanghai). Any client-side chart code that compares or formats item.date against a YYYY-MM-DD pattern (e.g. item.date.slice(0, 10) or label formatters that branch on string length) will need updating. The serialized value is correct for the fix, but it's worth verifying the chart rendering components handle ISO timestamps uniformly for both hourly and daily resolutions.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/actions/statistics.ts
Line: 25-29

Comment:
**`serializeChartBucketDate` changes day-resolution chart keys from date strings to ISO timestamps**

Previously, day-resolution keys were `YYYY-MM-DD` strings; they are now full ISO-8601 timestamps (e.g. `"2025-01-14T16:00:00.000Z"` for midnight Asia/Shanghai). Any client-side chart code that compares or formats `item.date` against a `YYYY-MM-DD` pattern (e.g. `item.date.slice(0, 10)` or label formatters that branch on string length) will need updating. The serialized value is correct for the fix, but it's worth verifying the chart rendering components handle ISO timestamps uniformly for both hourly and daily resolutions.

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

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!

ROOOO added a commit to ROOOO/claude-code-hub that referenced this pull request Jun 21, 2026
Supplements PR ding113#1231 with the behavior from:

- 88153045148b510c2d2a5c82ea73f58ace855a8b fix: restore dashboard this month statistics

- 54d0dfbf1eaf3cc0a77a2d565844d32bb746973a fix: navigate leaderboard monthly ranges by calendar month

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
tests/unit/dashboard/leaderboard-date-range-picker.test.tsx (1)

1-128: ⚡ Quick win

建议使用 React Testing Library 替代手动 createRoot 方式。

当前测试使用手动 createRoot/unmount 和 DOM 查询编写。虽然功能正确,但 React 19 官方文档明确推荐使用 @testing-library/react 进行组件测试。React Testing Library 提供:

  • 更好的错误信息和调试体验
  • 更符合用户行为的查询方式(按角色、标签等)
  • 对实现细节变更更具弹性的测试
  • 行业标准测试模式
📚 建议的重构方向

使用 React Testing Library 重写测试:

-import { act, useState } from "react";
-import { createRoot } from "react-dom/client";
+import { render, screen } from "`@testing-library/react`";
+import userEvent from "`@testing-library/user-event`";

-  beforeEach(() => {
-    vi.useFakeTimers();
-    vi.setSystemTime(new Date("2026-06-01T12:00:00Z"));
-    container = document.createElement("div");
-    document.body.appendChild(container);
-    root = createRoot(container);
-  });

+  beforeEach(() => {
+    vi.useFakeTimers();
+    vi.setSystemTime(new Date("2026-06-01T12:00:00Z"));
+  });

   it("navigates monthly ranges by calendar month", async () => {
-    await act(async () => {
-      root!.render(<TestHarness initialPeriod="monthly" />);
-    });
-    expectDisplayedRange(container!, "2026-06-01", "2026-06-30");
-    const previousButton = getButtonByTitle(container!, "Previous period");
-    await act(async () => {
-      previousButton.dispatchEvent(new MouseEvent("click", { bubbles: true }));
-    });

+    const user = userEvent.setup({ delay: null });
+    render(<TestHarness initialPeriod="monthly" />);
+    expect(screen.getByText(/2026-06-01 to 2026-06-30/)).toBeInTheDocument();
+    await user.click(screen.getByTitle("Previous period"));
+    expect(screen.getByText(/2026-05-01 to 2026-05-31/)).toBeInTheDocument();
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/dashboard/leaderboard-date-range-picker.test.tsx` around lines 1 -
128, Refactor the test file to use React Testing Library instead of manual
createRoot setup. Replace the beforeEach/afterEach blocks that manually manage
container and root with React Testing Library's render function. Replace the
getButtonByTitle function that uses querySelector with React Testing Library's
getByRole query to find buttons by their accessible role and title. Replace
manual event dispatching using dispatchEvent with React Testing Library's
userEvent or fireEvent utilities. Update the test cases to remove the act
wrapper around render calls since React Testing Library handles that
automatically.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/unit/dashboard/leaderboard-date-range-picker.test.tsx`:
- Around line 1-128: Refactor the test file to use React Testing Library instead
of manual createRoot setup. Replace the beforeEach/afterEach blocks that
manually manage container and root with React Testing Library's render function.
Replace the getButtonByTitle function that uses querySelector with React Testing
Library's getByRole query to find buttons by their accessible role and title.
Replace manual event dispatching using dispatchEvent with React Testing
Library's userEvent or fireEvent utilities. Update the test cases to remove the
act wrapper around render calls since React Testing Library handles that
automatically.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6eb7c11e-6ef4-4274-acd2-c16fcae9682e

📥 Commits

Reviewing files that changed from the base of the PR and between 98bcbc5 and 2072729.

📒 Files selected for processing (4)
  • src/app/[locale]/dashboard/leaderboard/_components/date-range-picker.tsx
  • src/repository/statistics.ts
  • tests/unit/dashboard/leaderboard-date-range-picker.test.tsx
  • tests/unit/repository/statistics-timezone-buckets.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/app/[locale]/dashboard/leaderboard/_components/date-range-picker.tsx

@ROOOO ROOOO force-pushed the codex/dashboard-timezone-consistency branch from 2072729 to aeff2f3 Compare June 21, 2026 17:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:statistics area:UI bug Something isn't working size/XL Extra Large PR (> 1000 lines)

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

1 participant