fix: make display timezone configurable via TIMEZONE env var#34
fix: make display timezone configurable via TIMEZONE env var#34sxjeru merged 3 commits intosxjeru:mainfrom
Conversation
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.
|
@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. |
审阅者指南通过 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
更新后的 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
文件级变更
提示与命令与 Sourcery 交互
自定义你的使用体验访问你的 dashboard 以:
获取帮助Original review guide in EnglishReviewer's GuideMakes 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 APIsequenceDiagram
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
Class diagram for updated config and overview query timezone handlingclassDiagram
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
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Summary of ChangesHello @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 Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
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>帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈持续改进代码评审质量。
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 toAsia/Shanghaibut actually returns"UTC", which contradicts both the description and the empty-input behavior; align the fallback value and log message with the intended default. getOverviewstill hardcodes"Asia/Shanghai"as the default whenopts?.timezoneis unset, instead of usingconfig.timezone, which can lead to inconsistent behavior if other callers don’t passtimezone; consider defaulting toconfig.timezoneinside 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>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| end, | ||
| timezone: config.timezone |
There was a problem hiding this comment.
question (bug_risk): 概览聚合现在使用固定的服务器时区,而 UI 日期格式化使用浏览器本地时区,可能会让用户感到困惑。
随着 config.timezone 传入 getOverview,聚合和分桶逻辑会在该服务器时区下运行,而更新后的页面(logs、records、explore、根路径)会按浏览器本地时区格式化日期。这种不匹配会导致位于 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.
There was a problem hiding this comment.
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"; |
There was a problem hiding this comment.
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'.
| 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
|
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 The design assumption for this PR is that 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. 剩余的差异存在于三个详情页面——记录、日志和探索。这些页面使用浏览器的本地时区而非 本次PR的设计假设是 完全自动化的替代方案——在每次请求时读取浏览器时区并传递给API——虽可省去配置步骤,却需要在每次fetch调用、每个缓存键及每次服务器查询页面中穿插时区参数。对于单用户自托管仪表板,这种权衡似乎得不偿失。若项目未来涉及需区分用户时区的多用户部署场景,可以重新评估此方案。 |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
非常感谢您的代码贡献,相信可以帮到更多需要的人。 |
The dashboard hardcoded
Asia/Shanghaiin two places: the PostgreSQLdate_truncexpressions that determine day/hour bucket boundaries, and theIntl.DateTimeFormatcalls 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— addsnormalizeTimezone(), which validates theTIMEZONEenv var viaIntl.DateTimeFormatand falls back toAsia/Shanghaiif the value is missing or unrecognized.lib/queries/overview.ts— the timezone passed toAT TIME ZONEin thedate_truncexpressions now comes from config. The value is embedded withsql.raw()rather than a query parameter. Passing it as$1caused PostgreSQL error 42803—the planner seesAT TIME ZONE $1in SELECT andAT TIME ZONE $3in GROUP BY as different expressions, even when both parameters hold the same string.Frontend (four pages) — removed the hardcoded
timeZone: "Asia/Shanghai"option fromIntl.DateTimeFormatandtoLocaleStringcalls. Without an explicit timezone, they fall back to the browser's local timezone.Configuration
Add to
.env.local:Defaults to
Asia/Shanghaiwhen unset, so existing deployments keep their current behavior.Database
No migration needed. All timestamps are stored as
timestamptz(UTC internally).TIMEZONEonly changes how records are grouped into day/hour buckets—the underlying data is unchanged.仪表盘原来在两处写死了
Asia/Shanghai:PostgreSQLdate_trunc表达式(决定"今天"从哪个时刻算起)和前端的格式化调用。对中国以外的用户来说,这会导致统计错位——英国用户下午 5 点的请求,会被计入上海时间凌晨 1 点之后的"明天"。改动
lib/config.ts— 新增normalizeTimezone(),用Intl.DateTimeFormat校验TIMEZONE环境变量的值,无效或未设置时回退到Asia/Shanghai。lib/queries/overview.ts—date_trunc中AT TIME ZONE的时区值现在从配置读取。这里用了sql.raw()将时区嵌入为 SQL 字面量,而不是查询参数。最初用参数传递时,PostgreSQL 报错 42803——SELECT 里的AT TIME ZONE $1和 GROUP BY 里的AT TIME ZONE $3参数索引不同,规划器无法确认两者是同一个表达式,就报了"must appear in GROUP BY"。前端(四个页面) — 删除了
Intl.DateTimeFormat和toLocaleString调用中硬编码的timeZone: "Asia/Shanghai",让浏览器自动使用本地时区。配置方式
在
.env.local中添加:不设置时默认
Asia/Shanghai,已有部署无须任何改动。数据库
不需要迁移数据。所有时间戳以
timestamptz存储,PostgreSQL 内部始终用 UTC 保存。TIMEZONE只影响分组时用的边界,不修改原始数据。Summary by Sourcery
使仪表盘的时区可配置,并移除在后端聚合和前端展示中对 Asia/Shanghai 的硬编码使用。
新功能:
TIMEZONE环境变量支持,用于控制仪表盘的时区。改进:
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:
Enhancements: