fix(dashboard): unify timezone handling#1231
Conversation
📝 Walkthrough概览该PR为整个仪表盘系统添加了全面的时区支持:Redis缓存键包含时区维度、统计查询按时区分桶和归一化、日期选择器按时区计算日期范围、组件props传递时区配置,以及系统时区变更时的级联缓存失效。修复了用户在非本地时区服务器上看到错位日期的问题。 变更时区感知仪表盘缓存和统计
代码评审工作量评估🎯 4 (复杂) | ⏱️ ~65 分钟 可能相关的PR
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
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) }; |
There was a problem hiding this comment.
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.
| return { startDate: "2020-01-01", endDate: formatDateInSystemTimeZone(now, timeZone) }; | |
| return { startDate: "2020-01-01", endDate: formatDate(baseDate) }; |
There was a problem hiding this comment.
🧹 Nitpick comments (3)
src/actions/system-config.ts (1)
159-169: ⚡ Quick win仅在时区实际发生变化时才失效全部仪表盘缓存。
当前条件只判断
validated.timezone !== undefined。若设置表单在每次保存时都携带timezone字段(部分更新表单通常会回传完整负载),那么即使时区未改变,每次保存都会清空全部 overview/statistics/leaderboard 缓存,导致随后大量请求回源数据库重算。建议改为比较保存前后的实际值。此处已有
before与updated,可直接对比:♻️ 建议改动
- 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.current为true。当前逻辑是正确的,但建议添加注释说明这一行为,以避免后续维护时产生困惑。🤖 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移除该测试中的无用
messageRequestmock 字段:getUserOverviewMetrics仅使用usageLedger(由LEDGER_BILLING_CONDITION引入的usageLedger.blockedBy/endpoint),未引用messageRequest.blockedBy/endpoint;当前用例断言也不涉及messageRequest,因此测试里这段messageRequestmock(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
📒 Files selected for processing (32)
src/actions/statistics.tssrc/actions/system-config.tssrc/app/[locale]/dashboard/leaderboard/_components/date-range-picker.tsxsrc/app/[locale]/dashboard/leaderboard/user/[userId]/_components/filters/types.tssrc/app/[locale]/dashboard/leaderboard/user/[userId]/_components/user-insights-view.tsxsrc/app/[locale]/dashboard/logs/_components/filters/active-filters-display.tsxsrc/app/[locale]/dashboard/logs/_components/logs-date-range-picker.tsxsrc/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsxsrc/app/[locale]/dashboard/logs/_components/usage-logs-view-virtualized.tsxsrc/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsxsrc/app/[locale]/dashboard/logs/_utils/time-range.tssrc/app/[locale]/my-usage/_components/statistics-summary-card.tsxsrc/app/[locale]/my-usage/_components/usage-logs-section.tsxsrc/app/[locale]/my-usage/_components/usage-logs-table.tsxsrc/app/api/admin/system-config/route.tssrc/components/ui/relative-time.tsxsrc/lib/redis/index.tssrc/lib/redis/leaderboard-cache.tssrc/lib/redis/overview-cache.tssrc/lib/redis/statistics-cache.tssrc/repository/admin-user-insights.tssrc/repository/statistics.tssrc/types/dashboard-cache.tstests/unit/dashboard-logs-time-range-utils.test.tstests/unit/dashboard/dashboard-cache-keys.test.tstests/unit/dashboard/leaderboard-view-user-cache-hit-rate.test.tsxtests/unit/redis/leaderboard-cache.test.tstests/unit/redis/overview-cache.test.tstests/unit/redis/statistics-cache.test.tstests/unit/repository/admin-user-insights-overview.test.tstests/unit/repository/statistics-timezone-buckets.test.tstests/unit/user-insights-filters.test.ts
There was a problem hiding this comment.
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:
- Core repository layer changes (statistics.ts, admin-user-insights.ts)
- Cache layer changes (redis/*.ts)
- UI components and utils
- 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)
-
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.warnrather than blocking the settings save operation. This is intentional - cache invalidation is a best-effort operation, and stale cache will self-heal via TTL. -
Function overload pattern (src/types/dashboard-cache.ts): The
buildStatisticsCacheKeyfunction uses function overloads to maintain backward compatibility while adding timezone support. TheuserIdOrTimezoneparameter interpretation is context-dependent but type-safe via the overload signatures. -
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
There was a problem hiding this comment.
💡 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); |
There was a problem hiding this comment.
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 }; |
There was a problem hiding this 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").
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.| 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(); |
There was a problem hiding this 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.
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!
Supplements PR ding113#1231 with the behavior from: - 88153045148b510c2d2a5c82ea73f58ace855a8b fix: restore dashboard this month statistics - 54d0dfbf1eaf3cc0a77a2d565844d32bb746973a fix: navigate leaderboard monthly ranges by calendar month
There was a problem hiding this comment.
🧹 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
📒 Files selected for processing (4)
src/app/[locale]/dashboard/leaderboard/_components/date-range-picker.tsxsrc/repository/statistics.tstests/unit/dashboard/leaderboard-date-range-picker.test.tsxtests/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
2072729 to
aeff2f3
Compare
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:
Dateparsing, causing UTC/local date drift and off-by-one day shifts in charts (e.g. April 28 data showing under April 27)Related Issues
Solution
resolveSystemTimezone()(DB config -> env TZ -> UTC fallback) anddate-fns-tzutilities (toZonedTime,formatInTimeZone,fromZonedTime) to compute dates in the correct timezonetz:<timezone>segment to prevent stale data when timezone changesinvalidateAllOverviewCaches,invalidateAllStatisticsCaches,invalidateAllLeaderboardCachesnormalizeBucketInstantreplacesnormalizeBucketDateto correctly parse bucket timestamps through the system timezone, preventing UTC/local driftserializeChartBucketDatereplaces resolution-dependent date formatting with consistent ISO serialization, ensuring stable date keys across all time rangesChanges
Core Changes
src/repository/statistics.ts(+52/-19) — ReplacenormalizeBucketDatewithnormalizeBucketInstantthat parses through system timezone; add timezone parameter to all DB query functionssrc/repository/admin-user-insights.ts(+36/-27) — AddbuildSystemTimezoneDateConditionshelper for all date range queries; useAT TIME ZONEin SQL conditionssrc/lib/redis/leaderboard-cache.ts(+23/-5) — Include timezone in all leaderboard cache keys; addinvalidateAllLeaderboardCachessrc/lib/redis/overview-cache.ts(+30/-6) — Include timezone in overview cache keys; addinvalidateAllOverviewCaches; handle legacy key cleanupsrc/lib/redis/statistics-cache.ts(+42/-14) — Include timezone in statistics cache keys; addinvalidateAllStatisticsCaches; handle legacy key cleanupsrc/actions/statistics.ts(+6/-10) — Replace resolution-dependent date formatting withserializeChartBucketDatefor stable chart bucketssrc/actions/system-config.ts(+17/-0) — Invalidate dashboard caches when timezone setting changessrc/app/api/admin/system-config/route.ts(+16/-0) — Same cache invalidation for REST API pathUI Changes
src/app/[locale]/dashboard/leaderboard/_components/date-range-picker.tsx(+17/-8) — UseformatInTimeZone/toZonedTimefor all date range computationsrc/app/[locale]/dashboard/leaderboard/user/[userId]/_components/filters/types.ts(+26/-18) — Add timezone parameter to time preset resolutionsrc/app/[locale]/dashboard/logs/_components/filters/active-filters-display.tsx(+10/-3) — Display active filter dates in system timezonesrc/app/[locale]/dashboard/logs/_components/logs-date-range-picker.tsx(+6/-2) — Use timezone-aware "today" for calendar disabled/ navigationsrc/app/[locale]/dashboard/logs/_utils/time-range.ts(+13/-18) — SimplifygetQuickDateRangeto usetoZonedTimethen plaindate-fnsformattingsrc/app/[locale]/my-usage/_components/statistics-summary-card.tsx(+22/-6) — UseformatInTimeZonefor default date range; auto-reset range on timezone changesrc/components/ui/relative-time.tsx(+4/-1) — Accept optionaltimeZoneoverride propsrc/types/dashboard-cache.ts(+30/-6) — Addtimezonefield to cache key types; add timezone-scoped cache key buildersTest Changes
tests/unit/dashboard-logs-time-range-utils.test.ts(+10/-0) — Add test for early-morning UTC in a different timezonetests/unit/dashboard/dashboard-cache-keys.test.ts(+21/-11) — Update cache key tests for timezone-scoped keystests/unit/redis/leaderboard-cache.test.ts(+29/-1) — Test timezone-scoped Redis keys and cross-timezone behaviortests/unit/redis/overview-cache.test.ts(+35/-14) — Test timezone-scoped keys and invalidationtests/unit/redis/statistics-cache.test.ts(+70/-23) — Test timezone-scoped keys and DB parameter passingtests/unit/repository/admin-user-insights-overview.test.ts(+12/-0) — Add timezone-specific testtests/unit/repository/statistics-timezone-buckets.test.ts(+38/-0) — New file testing bucket normalization with timezonetests/unit/user-insights-filters.test.ts(+17/-0) — Test timezone-aware filter presetsBreaking Changes
None — all changes are backward-compatible:
tz:<timezone>, while legacy keys are still cleaned up on invalidationTesting
Automated Tests
bunx vitest runspecific tests passedbun run typecheckpassedbun run lintpassedbun run buildpassedManual Testing
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(), addingtz:<timezone>segments to Redis cache keys, invalidating timezone-sensitive caches on settings change, and replacing naïveDateparsing in bucket normalization with a newnormalizeBucketInstant+fromZonedTimeapproach.normalizeBucketInstantstrips 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.tz:<timezone>so a timezone change automatically misses the old cache;invalidateAll*Cachesfunctions clean up both new and legacy keys atomically on settings save.RelativeTimecomponents 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
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]%%{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]Reviews (3): Last reviewed commit: "fix: navigate leaderboard monthly ranges..." | Re-trigger Greptile