Skip to content

fix(calendar): respect configured timezone - #2427

Open
mpiniarski wants to merge 7 commits into
BuilderIO:mainfrom
mpiniarski:changes-50
Open

fix(calendar): respect configured timezone#2427
mpiniarski wants to merge 7 commits into
BuilderIO:mainfrom
mpiniarski:changes-50

Conversation

@mpiniarski

@mpiniarski mpiniarski commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • use the saved calendar timezone for event ranges, calendar rendering, interactions, and agent screen context
  • initialize new calendar settings from the request timezone, with New York as the fallback
  • validate saved timezones and make list-event date ranges honor the configured timezone

Gmail does use configured timezone instead of browser timezone as well.

Bug

When the configured timezone differed from the browser timezone, the week grid used browser-local wall-clock values while event creation used the configured timezone. For example, clicking 7:00 AM in a New York calendar from Central Europe created an event displayed at 1:00 PM.

image SCR-20260727-ofvc

Testing

  • pnpm --dir templates/calendar test -- actions/list-events.spec.ts actions/update-settings.spec.ts
  • pnpm --dir templates/calendar typecheck

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Visual recap — skipped

The visual recap job did not run for this pull request. This is informational only and does not block the PR.

Recap skipped for ecf0bdc: external fork PR requires a maintainer to apply the recap label to the current head SHA.

@builder-io-integration builder-io-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Builder reviewed your changes and found 3 potential issues 🔴

Review Details

Code Review Summary

This incremental review covers the timezone propagation added across the calendar actions, server range resolution, calendar views, event cards, and drag interactions. The overall direction is sound: saved IANA timezone validation is centralized, date-only server ranges account for DST, and event instants are projected into the configured timezone for rendering. The focused tests reported by the agents pass, but the new coverage is primarily action-level and does not exercise browser-local timezone differences or DST interaction behavior.

Risk assessment: Standard (business/UI date-range and interaction logic).

Key findings

  • 🔴 HIGH — Calendar boundary calculations still operate on selectedDate in the browser timezone while event instants are projected into the configured timezone, so non-browser timezone users can get wrong ranges and positions.
  • 🔴 HIGH — Dragging/resizing through a spring-forward gap can independently convert a nonexistent local end time and persist a zero or incorrect duration.
  • 🟡 MEDIUM — Adjacent-range prefetching still uses unzoned ranges and therefore does not warm the same query keys as the primary range.

The centralized settings helper and server-side validation/defaulting are good improvements, and the range tests cover a date-only request plus a DST boundary. The prefetch issue is included in the summary because its unchanged lines are not commentable in this diff.

🧪 Browser testing: Will run after this review (PR touches UI code)

return {
from: startOfWeek(ms).toISOString(),
to: endOfWeek(me).toISOString(),
from: fromZonedTime(startOfWeek(ms), calendarTimezone).toISOString(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Compute calendar boundaries in the configured timezone

startOfMonth/endOfMonth are applied directly to selectedDate, which is initialized and navigated as a browser-local Date, before the result is passed to fromZonedTime. When the browser and configured calendar zones differ, the selected calendar day/month boundary can be wrong. Convert the selected instant with toZonedTime(selectedDate, calendarTimezone) before deriving month/week/day boundaries, and keep the same representation for the child views.

Fix in Builder

date: Date,
timezone: string,
) {
const start = toZonedTime(event.start, timezone);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Use one timezone context for day event positioning

The event start/end are projected with toZonedTime, but dayStart/dayEnd are derived directly from date. With a browser timezone different from the configured timezone, differenceInMinutes compares incompatible wall-clock representations and places events at the wrong vertical time. Project date into timezone before calculating the day boundaries.

Fix in Builder

onEventTimeChange(state.eventId, newStart, newEnd);
onEventTimeChange(
state.eventId,
fromZonedTime(newStart, timezone),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Preserve elapsed duration across DST gaps

The drag path performs wall-clock addMinutes arithmetic on a zoned pseudo-Date, then converts the new start and end independently. If the end lands in a spring-forward gap, fromZonedTime can collapse the nonexistent local time onto the transition and persist a zero or incorrect duration. Calculate the new instants from the original elapsed duration (or explicitly resolve DST gaps) and add a spring-forward drag test.

Fix in Builder

@steve8708

Copy link
Copy Markdown
Contributor

thanks @mpiniarski! some CI errors and some PR feedback here worth taking a look at

@builder-io-integration builder-io-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Builder reviewed your changes and found 5 potential issues 🔴

Review Details

Code Review Summary

This incremental review evaluated the latest PR head while excluding the three unresolved findings from the prior review. The timezone propagation remains directionally sound, and the centralized IANA validation/defaulting plus DST-aware server range tests are useful improvements. However, the latest implementation still has several newly identified edge cases in inventory pagination, month drag/drop, legacy settings compatibility, and timezone-projected rendering.

Risk assessment: Standard (business logic and UI date/time behavior).

Key findings

  • 🔴 HIGH — Command palette can call toZonedTime with its optional timezone prop unset.
  • 🟡 MEDIUM — Inventory cursor preflight reconstructs a different range than the first page when saved and request timezones differ.
  • 🟡 MEDIUM — Month drag/drop does not preserve displayed wall-clock time across a DST transition.
  • 🟡 MEDIUM — Existing invalid saved timezone values now make event listing fail without a recovery path.
  • 🟡 MEDIUM — Week view compares a timezone-shifted display proxy with the real current instant for past-event styling.

The previous comments remain open and were intentionally not reposted or resolved because their underlying code is still present.

🧪 Browser testing: Will run after this review (PR touches UI code)

Comment thread templates/calendar/app/components/calendar/CommandPalette.tsx
): Promise<CalendarEventsResult> {
const email = getRequestUserEmail();
if (!email) throw new Error("no authenticated user");
const timezone = await getCalendarTimezone(email);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Use the saved timezone when validating inventory cursors

The first inventory page resolves its range with the saved calendar timezone, but the cursor preflight later calls resolveCalendarEventRange without timezone. For date-only or omitted bounds, page two can compute a different query key using the request timezone and reject the cursor as query-bound. Resolve and pass the same saved timezone in the cursor path.

Fix in Builder

Comment on lines +121 to +123
const evStart = e.allDay
? parseISO(e.start)
: toZonedTime(e.start, timezone);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Preserve local time when moving events across DST

Timed events are now grouped by their configured-calendar wall clock, but CalendarView.handleEventDrop still changes the dates of the original UTC instants with browser-local setFullYear. Moving an event across a DST boundary therefore changes its displayed local time (for example 10:00 becomes 11:00 after the offset changes). Convert the event times to the calendar timezone before replacing the date and convert them back with fromZonedTime.

Fix in Builder

timezone?: unknown;
} | null;
if (settings?.timezone === undefined) return getDefaultSettings().timezone;
if (!isCalendarTimezone(settings.timezone)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Provide recovery for legacy invalid timezone settings

Reads now strictly validate the saved timezone, but older settings could contain arbitrary strings because the previous update path accepted them. getCalendarTimezone throws for those accounts, so list-events fails before any events can load, while get-settings returns the invalid record and offers no repair path. Fall back to a valid timezone or expose a recoverable settings update path instead of making the calendar unusable.

Fix in Builder

: null;
const start = parseISO(event.start);
const end = parseISO(event.end);
const start = toZonedTime(event.start, timezone);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Compare past status using the real event instant

end is now a toZonedTime wall-clock proxy used for grid geometry, but it is also compared directly with now at the past-status check. Those values are not on the same instant timeline, so an event can be marked past while it is still active (or vice versa) when the configured zone differs from the browser zone. Keep a separately parsed real end instant for this comparison.

Fix in Builder

@builder-io-integration builder-io-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Builder reviewed your changes and found 2 potential issues 🔴

Review Details

Code Review Summary

This incremental review evaluated the latest PR head and skipped all seven unresolved findings from the prior review because they remain present and unchanged; none were resolved. The latest changes continue to propagate the configured timezone through calendar rendering and settings while adding focused action-level coverage. The dev server is healthy, but browser execution remains unavailable in this environment.

Risk assessment: Standard (calendar business logic and UI interaction behavior).

New findings

  • 🔴 HIGHview-screen parses a bare YYYY-MM-DD navigation date as a server-local date before applying the configured timezone, so agent screen context can report the wrong week when the server timezone differs.
  • 🔴 HIGH — The drag callbacks use timezone but omit it from their dependency arrays, leaving stale closures after a live timezone setting change.

The existing settings validation/defaulting and date-range tests remain useful, but browser-level verification is needed for timezone changes and drag interactions.

🧪 Browser testing: Attempted after this review; all 17 planned flows were blocked because Chrome/browser automation tools were unavailable in the executor environment.

if (!email) throw new Error("no authenticated user");
const timezone = await getCalendarTimezone(email);
const viewDate = nav?.date
? parseISO(nav.date)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Parse navigation dates in the configured timezone

navigate documents date as a bare YYYY-MM-DD, but parseISO(nav.date) creates a server-local date before startOfWeek and fromZonedTime are applied. When the server timezone differs from the saved calendar timezone, view-screen can compute a shifted week and return context for the wrong dates; parse the date-only value as a wall-clock date in timezone instead.

Fix in Builder

onEventTimeChange(state.eventId, newStart, newEnd);
onEventTimeChange(
state.eventId,
fromZonedTime(newStart, timezone),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Refresh drag callbacks when the timezone changes

The newly timezone-aware startDrag and onPointerUp callbacks use timezone, but their dependency arrays omit it. After a live settings change, a drag can retain the previous timezone closure and save the event using the old zone; include timezone in both callback dependencies.

Fix in Builder

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