feat: timezone UI with modal, toggle, and full page coverage#2
feat: timezone UI with modal, toggle, and full page coverage#2veronoicc wants to merge 3 commits into
Conversation
Add TimezoneModal with smart input parsing, live preview, and quick-pick buttons. Globe badge in target header opens modal. Insights page (routine + sleep) gets timezone toggle to switch between target's TZ and viewer's TZ. Heatmap supports hourShift prop for column reordering. Timezone parsing/formatting utilities added to lib/utils.ts. Target type updated with timezone field.
Replace formatDateTime/formatTime/formatDate with their timezone-aware variants (formatDateTimeInTz, formatTimeInTz, formatDateInTz) across all target-scoped pages: overview, timeline, messages, alerts, backfill, briefs, profile, and analytics. Timeline bar tooltips and analytics relationship/ baseline timestamps now also respect target timezone.
|
@veronoicc is attempting to deploy a commit to the privex-chat's projects Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughThis PR introduces timezone awareness throughout the application, allowing users to specify their timezone and have all timestamps display in that timezone rather than the viewer's local time. It adds timezone utilities, extends the data model, integrates a modal UI for timezone selection, and threads timezone parameters through multiple pages and chart components. ChangesTimezone Management
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 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 |
- Reject fractional numeric inputs (e.g. '5.5') in parseTimezone - Replace toLocaleString round-trip with portable formatToParts in getTimezoneOffsetMinutes
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/targets/[userId]/timeline/page-client.tsx (1)
40-47:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse target-midnight boundaries for the live timeline.
Line 46-Line 47 and Line 73-Line 86 still build day filters in the viewer’s local timezone, but Line 202 and Line 335-Line 336 render the results in the target’s timezone. For targets that are offset from the viewer, events around midnight will show under one day while the query/windowing logic fetches another. Build
since/untiland the liveganttStart/ganttEndfromtzas well, otherwise the page becomes internally inconsistent.Also applies to: 72-86, 202-202, 335-336
🤖 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 `@app/targets/`[userId]/timeline/page-client.tsx around lines 40 - 47, The since/until query params and the live ganttStart/ganttEnd are being calculated in the viewer’s local timezone, causing off-by-one-day mismatches for targets in other timezones; update the code that builds timelineParams (keys: since, until) and the live timeline window (variables ganttStart and ganttEnd) to compute midnight boundaries using the target timezone variable tz instead of the viewer locale—i.e., convert the target date strings into epoch millis anchored to tz (so "YYYY-MM-DDT00:00:00" and "YYYY-MM-DDT23:59:59" are interpreted in tz) before String()ing them and sending to the server, and use the same tz-based boundaries when setting ganttStart/ganttEnd to keep query and rendering consistent with target time.
🤖 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.
Inline comments:
In `@app/targets/`[userId]/messages/page-client.tsx:
- Around line 205-207: The timestamp selection in MessageItem currently always
prefers created_at; change the ts logic to pick the primary timestamp based on
view: if deleted is true use msg.deleted_at, else if showEditHistory is true use
msg.edited_at, otherwise use msg.created_at, and then fall back to any remaining
timestamps if that primary is undefined; update the ts variable used for
formatting (and likewise adjust the same logic in the other occurrence
referenced around lines 256-260) so edited and deleted tabs display their
respective event times consistently.
In `@app/targets/`[userId]/target-layout-client.tsx:
- Around line 121-125: The handleTimezoneSave flow currently evicts only
per-target cache keys (api.clearCacheForTarget) so refreshTargets() may read a
stale /api/targets entry; modify handleTimezoneSave to also clear the
targets-list cache before calling refreshTargets() by invoking the appropriate
cache-clear method for the list (e.g., api.clearCacheForTargets or
api.clearCache('/api/targets')) immediately after api.updateTarget(userId, ...)
and before await refreshTargets(), keeping the existing
api.clearCacheForTarget(userId) call in place.
In `@components/charts/heatmap.tsx`:
- Around line 12-13: The hourShift prop currently rounds fractional values and
rotates each day's row in place, which breaks cross-day remapping; update the
rendering logic that consumes hourShift (reference the hourShift prop in
components/charts/heatmap.tsx) to either (a) reject non-integer hourShift at the
prop boundary (validate and throw/log if hourShift is not an integer) or (b)
perform proper bucket remapping across both hour and weekday boundaries before
rendering: compute a new bucket index = (originalWeekday*24 + originalHour +
hourShift) modulo (7*24), then derive target weekday and hour from that index so
times that cross midnight move to the correct next/previous day; also remove the
current per-row in-place rotation and use this global remapping for all buckets.
In `@lib/utils.ts`:
- Around line 168-174: buildOffset currently returns offsets like "UTC+2" which
Intl.DateTimeFormat rejects; update buildOffset (and ensure parseTimezone uses
it) to produce canonical offsets in the "UTC±HH:MM" form: pad hours with two
digits (String(hours).padStart(2,"0")) and always include a minutes component
(use `:MM` or `:00` when minutes === 0), e.g. canonical =
`UTC${sign}${paddedHours}${minPart || ":00"}`, while keeping the special-case
return for UTC zero as "UTC".
---
Outside diff comments:
In `@app/targets/`[userId]/timeline/page-client.tsx:
- Around line 40-47: The since/until query params and the live
ganttStart/ganttEnd are being calculated in the viewer’s local timezone, causing
off-by-one-day mismatches for targets in other timezones; update the code that
builds timelineParams (keys: since, until) and the live timeline window
(variables ganttStart and ganttEnd) to compute midnight boundaries using the
target timezone variable tz instead of the viewer locale—i.e., convert the
target date strings into epoch millis anchored to tz (so "YYYY-MM-DDT00:00:00"
and "YYYY-MM-DDT23:59:59" are interpreted in tz) before String()ing them and
sending to the server, and use the same tz-based boundaries when setting
ganttStart/ganttEnd to keep query and rendering consistent with target time.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 4171ae98-5ebb-4d5b-b256-a2bbc8441a0c
📒 Files selected for processing (17)
app/targets/[userId]/alerts/page-client.tsxapp/targets/[userId]/analytics/page-client.tsxapp/targets/[userId]/backfill/page-client.tsxapp/targets/[userId]/briefs/page-client.tsxapp/targets/[userId]/insights/page-client.tsxapp/targets/[userId]/messages/page-client.tsxapp/targets/[userId]/page-client.tsxapp/targets/[userId]/profile/page-client.tsxapp/targets/[userId]/target-layout-client.tsxapp/targets/[userId]/timeline/page-client.tsxcomponents/charts/heatmap.tsxcomponents/charts/timeline-bar.tsxcomponents/dashboard/timezone-modal.tsxlib/api.tslib/context.tsxlib/types.tslib/utils.ts
| function MessageItem({ msg, deleted, showEditHistory, tz }: MessageItemProps) { | ||
| const content = msg.content || "[No content]" | ||
| const ts = msg.created_at || msg.deleted_at || msg.edited_at |
There was a problem hiding this comment.
Choose the primary timestamp by view before formatting it.
ts always prefers created_at, so the deleted and edited tabs still render the creation time on the right. That means edited messages never show when the edit happened, and deleted messages show two different timestamps in the same row. Pick deleted_at for deleted, edited_at for showEditHistory, and fall back to created_at otherwise.
Proposed fix
function MessageItem({ msg, deleted, showEditHistory, tz }: MessageItemProps) {
const content = msg.content || "[No content]"
- const ts = msg.created_at || msg.deleted_at || msg.edited_at
+ const ts = deleted
+ ? msg.deleted_at || msg.created_at
+ : showEditHistory
+ ? msg.edited_at || msg.created_at
+ : msg.created_atAlso applies to: 256-260
🤖 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 `@app/targets/`[userId]/messages/page-client.tsx around lines 205 - 207, The
timestamp selection in MessageItem currently always prefers created_at; change
the ts logic to pick the primary timestamp based on view: if deleted is true use
msg.deleted_at, else if showEditHistory is true use msg.edited_at, otherwise use
msg.created_at, and then fall back to any remaining timestamps if that primary
is undefined; update the ts variable used for formatting (and likewise adjust
the same logic in the other occurrence referenced around lines 256-260) so
edited and deleted tabs display their respective event times consistently.
| const handleTimezoneSave = async (tz: string | null) => { | ||
| await api.updateTarget(userId, { timezone: tz }) | ||
| api.clearCacheForTarget(userId) | ||
| await refreshTargets() | ||
| } |
There was a problem hiding this comment.
Clear the targets-list cache before refreshing after a timezone save.
This only evicts per-target keys, but the updated timezone is read back from api.getTargets() via refreshTargets(). Since /api/targets is cached for 5 seconds in lib/api.ts, the header can immediately reload the stale timezone and make the save look like it failed.
Suggested fix
const handleTimezoneSave = async (tz: string | null) => {
await api.updateTarget(userId, { timezone: tz })
- api.clearCacheForTarget(userId)
+ api.clearCache()
await refreshTargets()
}🤖 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 `@app/targets/`[userId]/target-layout-client.tsx around lines 121 - 125, The
handleTimezoneSave flow currently evicts only per-target cache keys
(api.clearCacheForTarget) so refreshTargets() may read a stale /api/targets
entry; modify handleTimezoneSave to also clear the targets-list cache before
calling refreshTargets() by invoking the appropriate cache-clear method for the
list (e.g., api.clearCacheForTargets or api.clearCache('/api/targets'))
immediately after api.updateTarget(userId, ...) and before await
refreshTargets(), keeping the existing api.clearCacheForTarget(userId) call in
place.
| /** Shift to apply to hour labels (viewer offset - target offset, in hours). 0 = no shift. */ | ||
| hourShift?: number |
There was a problem hiding this comment.
Timezone shifting needs cross-day remapping, not per-row rotation.
hourShift currently rounds to whole hours and then rotates each day row in place. That makes the viewer-TZ routine view wrong whenever the offset crosses midnight: e.g. a Monday 23:00 bucket shifted by +2 should land on Tuesday 01:00, but this code keeps it on Monday. The same path also mislabels half/quarter-hour zones because 5.5, 9.5, etc. are snapped to an integer. Please remap buckets across both hour and weekday boundaries before rendering, or reject unsupported non-integral shifts at the prop boundary.
Also applies to: 25-42
🤖 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 `@components/charts/heatmap.tsx` around lines 12 - 13, The hourShift prop
currently rounds fractional values and rotates each day's row in place, which
breaks cross-day remapping; update the rendering logic that consumes hourShift
(reference the hourShift prop in components/charts/heatmap.tsx) to either (a)
reject non-integer hourShift at the prop boundary (validate and throw/log if
hourShift is not an integer) or (b) perform proper bucket remapping across both
hour and weekday boundaries before rendering: compute a new bucket index =
(originalWeekday*24 + originalHour + hourShift) modulo (7*24), then derive
target weekday and hour from that index so times that cross midnight move to the
correct next/previous day; also remove the current per-row in-place rotation and
use this global remapping for all buckets.
| function buildOffset(sign: string, hours: number, minutes: number): ParsedTimezone | null { | ||
| if (hours > 14 || minutes > 59) return null | ||
| if (hours === 0 && minutes === 0) return { canonical: "UTC", display: "UTC" } | ||
| const minPart = minutes > 0 ? `:${String(minutes).padStart(2, "0")}` : "" | ||
| const canonical = `UTC${sign}${hours}${minPart}` | ||
| return { canonical, display: canonical } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Does Intl.DateTimeFormataccepttimeZonevalues likeUTC+2/UTC-5, or must fixed-offset zones be passed as offset identifiers like +02:00/-05:00?
💡 Result:
Intl.DateTimeFormat does not accept informal time zone strings like "UTC+2" or "UTC-5" as values for the timeZone option [1][2]. Instead, you must use either: 1. IANA time zone names (e.g., "America/New_York", "UTC", "Etc/GMT+8") [1][3]. 2. Fixed-offset identifiers, which must be formatted as specific offset strings (e.g., "+02:00", "-05:00") [1][2]. According to official documentation, the timeZone option supports IANA names, "Etc/GMT" style identifiers, and explicit offset strings [1][2]. Attempting to pass an informal format like "UTC+2" will generally result in the constructor throwing a RangeError due to an invalid time zone name.
Citations:
- 1: https://dev.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat
- 2: https://github.com/mdn/content/blob/main/files/en-us/web/javascript/reference/global_objects/intl/datetimeformat/datetimeformat/index.md
- 3: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat
🏁 Script executed:
#!/bin/bash
set -euo pipefail
ls -la
rg -n "function parseTimezone|parseTimezone|function buildOffset|buildOffset|formatTimeInTz|formatDateTimeInTz|formatDateInTz" lib/utils.ts
nl -ba lib/utils.ts | sed -n '130,260p'Repository: Privex-chat/sentinel-web
Length of output: 2117
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "export function parseTimezone|function buildOffset|export function formatTimeInTz|export function formatDateTimeInTz|export function formatDateInTz" lib/utils.ts
echo "---- parseTimezone/buildOffset ----"
sed -n '130,260p' lib/utils.ts | cat -n
echo "---- formatters catch/fallback ----"
sed -n '200,250p' lib/utils.ts | cat -nRepository: Privex-chat/sentinel-web
Length of output: 6962
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "function formatTime\\b|function formatDateTime\\b|function formatDate\\b|export function formatTime\\b|export function formatDateTime\\b|export function formatDate\\b" lib/utils.ts
sed -n '1,140p' lib/utils.ts | cat -nRepository: Privex-chat/sentinel-web
Length of output: 5885
Fix formatting of saved fixed offsets (UTC±H[:MM]) so Intl.DateTimeFormat respects them.
parseTimezone()/buildOffset() persist strings like UTC+2 / UTC-5, but formatTimeInTz/formatDateTimeInTz/formatDateInTz pass them directly to Intl.DateTimeFormat({ timeZone: tz }). For UTC+2-style values, Intl throws and the code falls back to toLocale* formatting, so the saved offset is effectively ignored on render.
Suggested fix
+function toIntlTimeZone(tz: string): string {
+ const m = tz.match(/^UTC([+-])(\d{1,2})(?::(\d{2}))?$/)
+ if (!m) return tz
+ return `${m[1]}${m[2].padStart(2, "0")}:${m[3] ?? "00"}`
+}
+
export function formatTimeInTz(ts: number, tz: string | null | undefined): string {
if (!tz) return formatTime(ts)
try {
- return new Intl.DateTimeFormat("en-GB", { timeZone: tz, hour: "2-digit", minute: "2-digit", hour12: false }).format(new Date(ts))
+ return new Intl.DateTimeFormat("en-GB", {
+ timeZone: toIntlTimeZone(tz),
+ hour: "2-digit",
+ minute: "2-digit",
+ hour12: false,
+ }).format(new Date(ts))
} catch { return formatTime(ts) }
}
export function formatDateTimeInTz(ts: number, tz: string | null | undefined): string {
if (!tz) return formatDateTime(ts)
try {
return new Intl.DateTimeFormat("en-US", {
- timeZone: tz,
+ timeZone: toIntlTimeZone(tz),
month: "short", day: "numeric", hour: "2-digit", minute: "2-digit",
}).format(new Date(ts))
} catch { return formatDateTime(ts) }
}
export function formatDateInTz(ts: number, tz: string | null | undefined): string {
if (!tz) return formatDate(ts)
try {
return new Intl.DateTimeFormat("en-US", {
- timeZone: tz,
+ timeZone: toIntlTimeZone(tz),
month: "short", day: "numeric",
}).format(new Date(ts))
} catch { return formatDate(ts) }
}🤖 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 `@lib/utils.ts` around lines 168 - 174, buildOffset currently returns offsets
like "UTC+2" which Intl.DateTimeFormat rejects; update buildOffset (and ensure
parseTimezone uses it) to produce canonical offsets in the "UTC±HH:MM" form: pad
hours with two digits (String(hours).padStart(2,"0")) and always include a
minutes component (use `:MM` or `:00` when minutes === 0), e.g. canonical =
`UTC${sign}${paddedHours}${minPart || ":00"}`, while keeping the special-case
return for UTC zero as "UTC".
Summary
TimezoneModalcomponent with smart input parsing, live preview, and quick-pick buttons (UTC, EST, CET, JST, etc.)hourShiftprop for column reordering when toggling timezone viewformatDateTimeInTz,formatTimeInTz,formatDateInTz,parseTimezone,getTimezoneOffsetMinutes,tzLabelutilitiesCompanion PR: Privex-chat/sentinel-selfbot#3 — backend timezone support (DB schema, command, analyzers, API).
Summary by CodeRabbit