Skip to content

fix: make display timezone configurable via TIMEZONE env var#34

Merged
sxjeru merged 3 commits intosxjeru:mainfrom
etnperlong:fix/timezone
Feb 14, 2026
Merged

fix: make display timezone configurable via TIMEZONE env var#34
sxjeru merged 3 commits intosxjeru:mainfrom
etnperlong:fix/timezone

Conversation

@etnperlong
Copy link
Contributor

@etnperlong etnperlong commented Feb 13, 2026

The dashboard hardcoded Asia/Shanghai in two places: the PostgreSQL date_trunc expressions that determine day/hour bucket boundaries, and the Intl.DateTimeFormat calls that format timestamps on screen. For users outside China, this means day boundaries are wrong—a request at 5 PM GMT lands in the next day's bucket because it's past midnight in Shanghai.

Changes

lib/config.ts — adds normalizeTimezone(), which validates the TIMEZONE env var via Intl.DateTimeFormat and falls back to Asia/Shanghai if the value is missing or unrecognized.

lib/queries/overview.ts — the timezone passed to AT TIME ZONE in the date_trunc expressions now comes from config. The value is embedded with sql.raw() rather than a query parameter. Passing it as $1 caused PostgreSQL error 42803—the planner sees AT TIME ZONE $1 in SELECT and AT TIME ZONE $3 in GROUP BY as different expressions, even when both parameters hold the same string.

Frontend (four pages) — removed the hardcoded timeZone: "Asia/Shanghai" option from Intl.DateTimeFormat and toLocaleString calls. Without an explicit timezone, they fall back to the browser's local timezone.

Configuration

Add to .env.local:

TIMEZONE=Europe/London

Defaults to Asia/Shanghai when unset, so existing deployments keep their current behavior.

Database

No migration needed. All timestamps are stored as timestamptz (UTC internally). TIMEZONE only changes how records are grouped into day/hour buckets—the underlying data is unchanged.


仪表盘原来在两处写死了 Asia/Shanghai:PostgreSQL date_trunc 表达式(决定"今天"从哪个时刻算起)和前端的格式化调用。对中国以外的用户来说,这会导致统计错位——英国用户下午 5 点的请求,会被计入上海时间凌晨 1 点之后的"明天"。

改动

lib/config.ts — 新增 normalizeTimezone(),用 Intl.DateTimeFormat 校验 TIMEZONE 环境变量的值,无效或未设置时回退到 Asia/Shanghai

lib/queries/overview.tsdate_truncAT TIME ZONE 的时区值现在从配置读取。这里用了 sql.raw() 将时区嵌入为 SQL 字面量,而不是查询参数。最初用参数传递时,PostgreSQL 报错 42803——SELECT 里的 AT TIME ZONE $1 和 GROUP BY 里的 AT TIME ZONE $3 参数索引不同,规划器无法确认两者是同一个表达式,就报了"must appear in GROUP BY"。

前端(四个页面) — 删除了 Intl.DateTimeFormattoLocaleString 调用中硬编码的 timeZone: "Asia/Shanghai",让浏览器自动使用本地时区。

配置方式

.env.local 中添加:

TIMEZONE=Europe/London

不设置时默认 Asia/Shanghai,已有部署无须任何改动。

数据库

不需要迁移数据。所有时间戳以 timestamptz 存储,PostgreSQL 内部始终用 UTC 保存。TIMEZONE 只影响分组时用的边界,不修改原始数据。

Summary by Sourcery

使仪表盘的时区可配置,并移除在后端聚合和前端展示中对 Asia/Shanghai 的硬编码使用。

新功能:

  • 在配置中新增带验证与规范化的 TIMEZONE 环境变量支持,用于控制仪表盘的时区。

改进:

  • 在概览聚合查询中使用配置或传入的时区,而不是硬编码的值。
  • 通过从日期格式化器中移除对 Asia/Shanghai 的硬编码选项,使前端时间戳格式化使用浏览器的本地时区。
Original summary in English

Summary by Sourcery

Make dashboard time zone configurable and remove hardcoded Asia/Shanghai usage across backend aggregation and frontend display.

New Features:

  • Add TIMEZONE environment variable support with validation and normalization in config to control dashboard time zone.

Enhancements:

  • Use configured or provided time zone in overview aggregation queries instead of a hardcoded value.
  • Let frontend timestamp formatting use the browser's local time zone by removing hardcoded Asia/Shanghai options from date formatters.

Add a TIMEZONE environment variable (defaults to Asia/Shanghai for
backward compatibility) that controls how dates are grouped in PostgreSQL
queries and displayed across the dashboard.

- lib/config.ts: add normalizeTimezone() + config.timezone field
- lib/queries/overview.ts: pass timezone into date_trunc / AT TIME ZONE
- app/api/overview/route.ts: forward config.timezone to getOverview()
- app/page/records/explore/logs: remove hardcoded timeZone option so
  Intl formatters use the browser's local timezone automatically
- .env.example: document the new TIMEZONE variable
When the timezone is passed as a query parameter (e.g. $1, $3, $4),
PostgreSQL treats each occurrence as a potentially distinct value and
cannot match the SELECT expression to the GROUP BY clause, resulting in
error 42803 (column must appear in GROUP BY or aggregate function).

Switch to sql.raw() so the timezone is inlined as a literal string
(e.g. AT TIME ZONE 'Europe/London'). The value is safe to embed because
it has already been validated as a valid IANA timezone by normalizeTimezone()
in lib/config.ts.
@vercel
Copy link

vercel bot commented Feb 13, 2026

@etnperlong is attempting to deploy a commit to the sxjeru's projects Team on Vercel.

A member of the Team first needs to authorize it.

@sourcery-ai
Copy link

sourcery-ai bot commented Feb 13, 2026

审阅者指南

通过 TIMEZONE 环境变量让仪表盘的时区处理可配置,将其串联到后端配置和 overview 查询分桶逻辑中,同时让前端格式化时间时回退到浏览器本地时间,而不是将时区硬编码为 Asia/Shanghai。

overview API 中可配置时区的时序图

sequenceDiagram
  actor User
  participant Browser
  participant NextApi as NextJS_API_route_overview
  participant Config as Config_module
  participant Query as Overview_query
  participant DB as Postgres

  User->>Browser: Open dashboard overview
  Browser->>NextApi: GET /api/overview

  NextApi->>Config: assertEnv()
  Config-->>NextApi: config.timezone

  NextApi->>Query: getOverview(daysInput, opts_with_timezone)
  activate Query
  Query->>Query: tz = opts.timezone || Asia_Shanghai
  Query->>Query: tzLiteral = sql.raw(tz)

  Query->>DB: SELECT with date_trunc(... at time zone tzLiteral)
  DB-->>Query: Aggregated usage rows
  deactivate Query

  NextApi-->>Browser: JSON overview (bucketed in tz)
  Browser->>Browser: Render timestamps using local timezone
Loading

更新后的 config 和 overview 查询时区处理的类图

classDiagram
  class ConfigModule {
    +string cliproxy_baseUrl
    +string postgresUrl
    +string password
    +string cronSecret
    +string timezone
    +assertEnv() void
  }

  class normalizeTimezoneFunction {
    +normalizeTimezone(raw string) string
  }

  class OverviewQueryModule {
    +getOverview(daysInput number, opts GetOverviewOpts) Promise_OverviewResult
  }

  class GetOverviewOpts {
    +string model
    +string route
    +number page
    +number pageSize
    +string start
    +string end
    +string timezone
  }

  class OverviewApiRoute {
    +GET(request Request) Promise_Response
  }

  class PostgresUsageRecords {
    +Date occurredAt
  }

  ConfigModule --> normalizeTimezoneFunction : uses
  OverviewApiRoute --> ConfigModule : reads_timezone
  OverviewApiRoute --> OverviewQueryModule : passes_timezone
  OverviewQueryModule --> PostgresUsageRecords : buckets_by_timezone
Loading

文件级变更

变更 详情 文件
在后端配置中引入规范化、可配置的时区,并通过共享的配置对象暴露出来。
  • 新增 normalizeTimezone() helper:对 TIMEZONE 环境变量进行 trim,使用 Intl.DateTimeFormat 校验其合法性,在缺失或无效时回退到默认值。
  • TIMEZONE 无效时打印 console 警告日志,并使用安全的回退时区值。
  • 扩展导出的 config 对象,包含一个由 normalizeTimezone() 提供的 timezone 字段。
lib/config.ts
在 overview 查询中聚合使用数据时使用配置的时区,并从 API 层传递该时区。
  • 扩展 getOverview() 的 options,使其接受一个可选的 timezone 参数。
  • 为 overview 查询推导一个时区值,当未传入时默认使用 Asia/Shanghai
  • 通过 sql.raw() 将时区作为 SQL literal 嵌入,用于 SELECTGROUP BY 里的 date_trunc ... AT TIME ZONE 表达式,以避免 PostgreSQL 在 GROUP BY 上的执行计划问题。
  • 将动态的时区 literal 应用于按天和按小时的截断表达式,以及 hourStart AT TIME ZONE 表达式。
  • 从 API 路由中将 config.timezone 传递给 getOverview(),从而使服务端分桶遵守配置的时区。
lib/queries/overview.ts
app/api/overview/route.ts
从客户端时间戳格式化逻辑中移除硬编码的 Asia/Shanghai 时区,让浏览器本地时区生效。
  • 更新日志页面和记录页面,在使用 Intl.DateTimeFormat 格式化时间戳时不再指定 timeZone,同时保持 24 小时制。
  • 更新 explore 页面和主仪表盘页面,在构造 Intl.DateTimeFormat 实例时不再传入固定的 timeZone 选项,使图表/刻度以用户本地时区渲染。
app/logs/page.tsx
app/records/page.tsx
app/explore/page.tsx
app/page.tsx

提示与命令

与 Sourcery 交互

  • 触发新的代码审阅: 在 Pull Request 上评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的审阅评论即可继续讨论。
  • 从审阅评论生成 GitHub issue: 在某条审阅评论下回复,要求 Sourcery 根据该评论创建一个 issue。你也可以直接回复 @sourcery-ai issue 从该评论生成一个 issue。
  • 生成 Pull Request 标题: 在 Pull Request 标题的任意位置写上 @sourcery-ai,即可在任意时间生成标题。你也可以在 Pull Request 中评论 @sourcery-ai title 来(重新)生成标题。
  • 生成 Pull Request 摘要: 在 Pull Request 内容正文的任意位置写上 @sourcery-ai summary,即可在该位置生成 PR 摘要。你也可以评论 @sourcery-ai summary 来在任意时间(重新)生成摘要。
  • 生成审阅者指南: 在 Pull Request 上评论 @sourcery-ai guide,即可在任意时间(重新)生成审阅者指南。
  • 解决所有 Sourcery 评论: 在 Pull Request 上评论 @sourcery-ai resolve,即可将所有 Sourcery 评论标记为已解决。如果你已经处理完所有评论且不想再看到它们,这会很有用。
  • 撤销所有 Sourcery 审阅: 在 Pull Request 上评论 @sourcery-ai dismiss,即可撤销所有现有的 Sourcery 审阅。特别适合你想从一个全新的审阅开始的情况——别忘了随后评论 @sourcery-ai review 来触发新的审阅!

自定义你的使用体验

访问你的 dashboard 以:

  • 启用或禁用审阅功能,例如 Sourcery 自动生成的 Pull Request 摘要、审阅者指南等。
  • 修改审阅语言。
  • 添加、移除或编辑自定义审阅说明。
  • 调整其他审阅设置。

获取帮助

Original review guide in English

Reviewer's Guide

Makes dashboard time zone handling configurable via a TIMEZONE env var, wiring it through backend config and overview query bucketing while letting frontend formatting fall back to the browser’s local time instead of hardcoding Asia/Shanghai.

Sequence diagram for configurable timezone in overview API

sequenceDiagram
  actor User
  participant Browser
  participant NextApi as NextJS_API_route_overview
  participant Config as Config_module
  participant Query as Overview_query
  participant DB as Postgres

  User->>Browser: Open dashboard overview
  Browser->>NextApi: GET /api/overview

  NextApi->>Config: assertEnv()
  Config-->>NextApi: config.timezone

  NextApi->>Query: getOverview(daysInput, opts_with_timezone)
  activate Query
  Query->>Query: tz = opts.timezone || Asia_Shanghai
  Query->>Query: tzLiteral = sql.raw(tz)

  Query->>DB: SELECT with date_trunc(... at time zone tzLiteral)
  DB-->>Query: Aggregated usage rows
  deactivate Query

  NextApi-->>Browser: JSON overview (bucketed in tz)
  Browser->>Browser: Render timestamps using local timezone
Loading

Class diagram for updated config and overview query timezone handling

classDiagram
  class ConfigModule {
    +string cliproxy_baseUrl
    +string postgresUrl
    +string password
    +string cronSecret
    +string timezone
    +assertEnv() void
  }

  class normalizeTimezoneFunction {
    +normalizeTimezone(raw string) string
  }

  class OverviewQueryModule {
    +getOverview(daysInput number, opts GetOverviewOpts) Promise_OverviewResult
  }

  class GetOverviewOpts {
    +string model
    +string route
    +number page
    +number pageSize
    +string start
    +string end
    +string timezone
  }

  class OverviewApiRoute {
    +GET(request Request) Promise_Response
  }

  class PostgresUsageRecords {
    +Date occurredAt
  }

  ConfigModule --> normalizeTimezoneFunction : uses
  OverviewApiRoute --> ConfigModule : reads_timezone
  OverviewApiRoute --> OverviewQueryModule : passes_timezone
  OverviewQueryModule --> PostgresUsageRecords : buckets_by_timezone
Loading

File-Level Changes

Change Details Files
Introduce a normalized, configurable timezone in backend config and expose it via the shared config object.
  • Add normalizeTimezone() helper that trims TIMEZONE env var, validates it with Intl.DateTimeFormat, and falls back to a default when missing or invalid.
  • Log a console warning when TIMEZONE is invalid and use a safe fallback timezone value.
  • Extend the exported config object to include a timezone field sourced from normalizeTimezone().
lib/config.ts
Use the configured timezone when aggregating usage data in the overview query, and thread it from the API layer.
  • Extend getOverview() options to accept an optional timezone argument.
  • Derive a timezone value for the overview query, defaulting to Asia/Shanghai when none is passed.
  • Embed the timezone as a SQL literal via sql.raw() for the date_trunc AT TIME ZONE expressions used in SELECT and GROUP BY to avoid PostgreSQL GROUP BY planner issues.
  • Apply the dynamic timezone literal to both day and hour truncation expressions and to the hourStart AT TIME ZONE expression.
  • Pass config.timezone from the API route into getOverview() so server-side bucketing respects the configured timezone.
lib/queries/overview.ts
app/api/overview/route.ts
Remove hardcoded Asia/Shanghai timezone from client-side timestamp formatting so the browser’s local timezone is used.
  • Update logs and records pages to format timestamps with Intl.DateTimeFormat without specifying timeZone, preserving 24-hour format.
  • Update explore and main dashboard pages to construct Intl.DateTimeFormat instances without a fixed timeZone option so charts/ticks render in the user’s local timezone.
app/logs/page.tsx
app/records/page.tsx
app/explore/page.tsx
app/page.tsx

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@gemini-code-assist
Copy link

Summary of Changes

Hello @etnperlong, 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 resolves an issue where dashboard data grouping and timestamp display were rigidly set to Asia/Shanghai, causing inaccuracies for users in different timezones. By introducing a configurable TIMEZONE environment variable, the system now dynamically adjusts both backend data aggregation and frontend timestamp formatting. This enhancement ensures that time-based metrics are accurately presented according to the user's specified or local timezone, significantly improving the dashboard's global usability and data relevance.

Highlights

  • Configurable Timezone: Introduced a TIMEZONE environment variable to allow users to configure the display timezone for date grouping in charts, defaulting to Asia/Shanghai if unset.
  • Timezone Normalization and Validation: Added a normalizeTimezone() utility in lib/config.ts to validate the TIMEZONE environment variable using Intl.DateTimeFormat and fall back to Asia/Shanghai (or UTC for invalid values) if it's missing or unrecognized.
  • Dynamic Database Query Timezone: Modified PostgreSQL date_trunc expressions in lib/queries/overview.ts to use the configurable timezone, employing sql.raw() to embed the timezone as a SQL literal and avoid PostgreSQL errors related to parameter indexing.
  • Frontend Timezone Localization: Removed hardcoded timeZone: "Asia/Shanghai" from Intl.DateTimeFormat and toLocaleString calls across several frontend pages, allowing them to default to the browser's local timezone for display.
Changelog
  • .env.example
    • Added TIMEZONE environment variable with example usage and fallback information.
  • app/api/overview/route.ts
    • Imported config from lib/config.
    • Passed config.timezone to the getOverview function.
  • app/explore/page.tsx
    • Removed the hardcoded timeZone: "Asia/Shanghai" option from Intl.DateTimeFormat.
  • app/logs/page.tsx
    • Removed the hardcoded timeZone: "Asia/Shanghai" option from Intl.DateTimeFormat.
  • app/page.tsx
    • Removed the hardcoded timeZone: "Asia/Shanghai" option from Intl.DateTimeFormat.
  • app/records/page.tsx
    • Removed the hardcoded timeZone: "Asia/Shanghai" option from Intl.DateTimeFormat.
  • lib/config.ts
    • Implemented normalizeTimezone function to validate and provide a fallback for the TIMEZONE environment variable.
    • Added the timezone property, derived from normalizeTimezone, to the exported config object.
  • lib/queries/overview.ts
    • Updated the getOverview function signature to accept a timezone option.
    • Introduced a tzLiteral variable using sql.raw() to embed the timezone directly into SQL queries.
    • Modified date_trunc and at time zone expressions to use the configurable tzLiteral instead of the hardcoded Asia/Shanghai.
Activity
  • No specific activity (comments, reviews, progress) has been recorded for this pull request yet.
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

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - 我发现了两个问题,并给出了一些总体反馈:

  • normalizeTimezone 中,catch 块里日志写的是将回退到 Asia/Shanghai,但实际返回的是 "UTC",这既与描述不符,也与空输入时的行为不一致;请根据预期默认值对齐回退值和日志信息。
  • opts?.timezone 未设置时,getOverview 依然将 "Asia/Shanghai" 硬编码为默认值,而不是使用 config.timezone。如果其他调用方也不传入 timezone,就可能导致行为不一致;建议在函数内部默认使用 config.timezone
供 AI 代理使用的提示词
Please address the comments from this code review:

## Overall Comments
- In `normalizeTimezone`, the catch block logs that it is falling back to `Asia/Shanghai` but actually returns `"UTC"`, which contradicts both the description and the empty-input behavior; align the fallback value and log message with the intended default.
- `getOverview` still hardcodes `"Asia/Shanghai"` as the default when `opts?.timezone` is unset, instead of using `config.timezone`, which can lead to inconsistent behavior if other callers don’t pass `timezone`; consider defaulting to `config.timezone` inside the function.

## Individual Comments

### Comment 1
<location> `lib/config.ts:16-20` </location>
<code_context>
+function normalizeTimezone(raw: string | undefined): string {
+  const value = (raw || "").trim();
+  if (!value) return "Asia/Shanghai";
+  try {
+    Intl.DateTimeFormat(undefined, { timeZone: value });
+    return value;
+  } catch {
+    console.warn(`TIMEZONE env var "${value}" is not a valid IANA timezone. Falling back to Asia/Shanghai.`);
+    return "UTC";
+  }
</code_context>

<issue_to_address>
**issue (bug_risk):** The fallback log message and the actual fallback timezone are inconsistent.

The warning mentions falling back to Asia/Shanghai, but the catch branch actually returns UTC, which can mislead anyone debugging invalid TIMEZONE values. Please update either the log message or the fallback value so they match.
</issue_to_address>

### Comment 2
<location> `app/api/overview/route.ts:88-89` </location>
<code_context>
       pageSize,
       start,
-      end
+      end,
+      timezone: config.timezone
     });

</code_context>

<issue_to_address>
**question (bug_risk):** Overview aggregation now uses a fixed server timezone while UI date formatting uses the browser’s local timezone, which may confuse users.

With `config.timezone` now passed into `getOverview`, aggregation and bucketing run in that server timezone, while the updated pages (`logs`, `records`, `explore`, root) format dates in the browser’s local timezone. This mismatch means users outside `config.timezone` may see buckets that appear shifted by several hours.

If this is unintentional, either:
- Use `config.timezone` (or a user-selected timezone) for client-side formatting as well, or
- Have the API return UTC timestamps plus the bucketing timezone so the client can format consistently.
</issue_to_address>

Sourcery 对开源项目免费使用——如果你觉得我们的代码评审有帮助,欢迎分享 ✨
帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈持续改进代码评审质量。
Original comment in English

Hey - I've found 2 issues, and left some high level feedback:

  • In normalizeTimezone, the catch block logs that it is falling back to Asia/Shanghai but actually returns "UTC", which contradicts both the description and the empty-input behavior; align the fallback value and log message with the intended default.
  • getOverview still hardcodes "Asia/Shanghai" as the default when opts?.timezone is unset, instead of using config.timezone, which can lead to inconsistent behavior if other callers don’t pass timezone; consider defaulting to config.timezone inside the function.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `normalizeTimezone`, the catch block logs that it is falling back to `Asia/Shanghai` but actually returns `"UTC"`, which contradicts both the description and the empty-input behavior; align the fallback value and log message with the intended default.
- `getOverview` still hardcodes `"Asia/Shanghai"` as the default when `opts?.timezone` is unset, instead of using `config.timezone`, which can lead to inconsistent behavior if other callers don’t pass `timezone`; consider defaulting to `config.timezone` inside the function.

## Individual Comments

### Comment 1
<location> `lib/config.ts:16-20` </location>
<code_context>
+function normalizeTimezone(raw: string | undefined): string {
+  const value = (raw || "").trim();
+  if (!value) return "Asia/Shanghai";
+  try {
+    Intl.DateTimeFormat(undefined, { timeZone: value });
+    return value;
+  } catch {
+    console.warn(`TIMEZONE env var "${value}" is not a valid IANA timezone. Falling back to Asia/Shanghai.`);
+    return "UTC";
+  }
</code_context>

<issue_to_address>
**issue (bug_risk):** The fallback log message and the actual fallback timezone are inconsistent.

The warning mentions falling back to Asia/Shanghai, but the catch branch actually returns UTC, which can mislead anyone debugging invalid TIMEZONE values. Please update either the log message or the fallback value so they match.
</issue_to_address>

### Comment 2
<location> `app/api/overview/route.ts:88-89` </location>
<code_context>
       pageSize,
       start,
-      end
+      end,
+      timezone: config.timezone
     });

</code_context>

<issue_to_address>
**question (bug_risk):** Overview aggregation now uses a fixed server timezone while UI date formatting uses the browser’s local timezone, which may confuse users.

With `config.timezone` now passed into `getOverview`, aggregation and bucketing run in that server timezone, while the updated pages (`logs`, `records`, `explore`, root) format dates in the browser’s local timezone. This mismatch means users outside `config.timezone` may see buckets that appear shifted by several hours.

If this is unintentional, either:
- Use `config.timezone` (or a user-selected timezone) for client-side formatting as well, or
- Have the API return UTC timestamps plus the bucketing timezone so the client can format consistently.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +88 to +89
end,
timezone: config.timezone
Copy link

Choose a reason for hiding this comment

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

question (bug_risk): 概览聚合现在使用固定的服务器时区,而 UI 日期格式化使用浏览器本地时区,可能会让用户感到困惑。

随着 config.timezone 传入 getOverview,聚合和分桶逻辑会在该服务器时区下运行,而更新后的页面(logsrecordsexplore、根路径)会按浏览器本地时区格式化日期。这种不匹配会导致位于 config.timezone 之外的用户看到的分桶时间看起来偏移了好几个小时。

如果这不是有意为之,建议:

  • 客户端格式化也使用 config.timezone(或用户选择的时区),或者
  • 让 API 返回 UTC 时间戳以及用于分桶的时区,以便客户端能保持一致地格式化。
Original comment in English

question (bug_risk): Overview aggregation now uses a fixed server timezone while UI date formatting uses the browser’s local timezone, which may confuse users.

With config.timezone now passed into getOverview, aggregation and bucketing run in that server timezone, while the updated pages (logs, records, explore, root) format dates in the browser’s local timezone. This mismatch means users outside config.timezone may see buckets that appear shifted by several hours.

If this is unintentional, either:

  • Use config.timezone (or a user-selected timezone) for client-side formatting as well, or
  • Have the API return UTC timestamps plus the bucketing timezone so the client can format consistently.

Copy link

@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 effectively addresses the issue of a hardcoded timezone by making it configurable through the TIMEZONE environment variable. The changes are well-structured, with backend queries now respecting the configured timezone and frontend components using the browser's local timezone for display. The implementation includes validation for the timezone value, which is a good practice. I have one suggestion for improvement in lib/config.ts to ensure the fallback behavior for an invalid timezone is consistent with the documentation and warning messages. Overall, this is a solid contribution that improves the user experience for people in different timezones.

lib/config.ts Outdated
return value;
} catch {
console.warn(`TIMEZONE env var "${value}" is not a valid IANA timezone. Falling back to Asia/Shanghai.`);
return "UTC";

Choose a reason for hiding this comment

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

high

The warning message on the previous line indicates a fallback to 'Asia/Shanghai' for an invalid timezone, but the code returns 'UTC'. This is inconsistent. To maintain consistency with the default behavior and the warning message, the fallback should be 'Asia/Shanghai'.

Suggested change
return "UTC";
return "Asia/Shanghai";

- config.ts: fix catch branch returning 'UTC' while warning says
  'Asia/Shanghai'; both now consistently fall back to Asia/Shanghai

- getOverview() now returns the timezone it used for SQL bucketing,
  and the overview API includes it in the response payload

- page.tsx: buildHourlySeries accepts an optional timezone parameter
  and uses it to create the gap-fill label formatter, so gap labels
  match the server-side bucket labels instead of using the browser's
  local timezone
@etnperlong
Copy link
Contributor Author

The remaining gap is on the three detail pages — records, logs, and explore. They format individual record timestamps using the browser's local timezone rather than config.timezone. This is intentional: those pages show exact "when did this request happen" timestamps, and local time is what you want there. There's no bucketing involved, so the server-side timezone doesn't apply.

The design assumption for this PR is that TIMEZONE is set to match the deployer's local timezone. If that's true, there's no visible discrepancy anywhere. If someone runs the default (Asia/Shanghai) in a European browser without setting the variable, the dashboard chart buckets will use Shanghai day boundaries while record timestamps show local time — but that's the same situation as before this PR, just now fixable by setting TIMEZONE.

The fully automatic alternative — reading the browser's timezone and passing it to the API on every request — avoids this setup step but requires threading a timezone parameter through every fetch call, every cache key, and every page that queries the server. For a single-user self-hosted dashboard, that tradeoff doesn't seem worth it. Happy to revisit if the project ever targets multi-user deployments where per-user timezone would matter.


剩余的差异存在于三个详情页面——记录、日志和探索。这些页面使用浏览器的本地时区而非 config.timezone 来格式化单条记录的时间戳。此设计有意为之:这些页面展示的是精确的“请求发生时间”时间戳,而本地时间正是用户所需。由于不涉及时间分桶,服务器端时区在此不适用。

本次PR的设计假设是TIMEZONE已设置为部署者本地时区。若此假设成立,则全局不会出现可见差异。若用户在欧洲浏览器中运行默认值(Asia/Shanghai)且未设置该变量,仪表板图表分桶将采用上海时区日界,而记录时间戳显示本地时间——但这与PR提交前情况相同,现在只需设置TIMEZONE即可修复。

完全自动化的替代方案——在每次请求时读取浏览器时区并传递给API——虽可省去配置步骤,却需要在每次fetch调用、每个缓存键及每次服务器查询页面中穿插时区参数。对于单用户自托管仪表板,这种权衡似乎得不偿失。若项目未来涉及需区分用户时区的多用户部署场景,可以重新评估此方案。

@vercel
Copy link

vercel bot commented Feb 14, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cli-proxy-api-monitor Ready Ready Preview Feb 14, 2026 6:19am

@sxjeru
Copy link
Owner

sxjeru commented Feb 14, 2026

非常感谢您的代码贡献,相信可以帮到更多需要的人。

@sxjeru sxjeru merged commit 3c9acfa into sxjeru:main Feb 14, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants