From 648a8ddf5bc19be1b9375e281a70aaaebd81cb0e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 12 Nov 2026 07:15:05 +0000 Subject: [PATCH 1/2] fix(web): sync calendar timezone hint when calendars load Apply the primary calendar zone as soon as the calendars query resolves so redirectToToday uses Denver civil day before day view column picking runs. Co-authored-by: Tyler Dane --- .../src/calendars/calendar.query.ts | 10 ++- .../src/timezone/CalendarTimeZoneHintSync.tsx | 7 +-- .../timezone/calendar-timezone-hint.test.ts | 36 ++++++++++- .../src/timezone/calendar-timezone-hint.ts | 10 +++ .../Calendar/dayCalendarColumns.util.test.ts | 62 +++++++++++++++++++ 5 files changed, 116 insertions(+), 9 deletions(-) diff --git a/apps/calendar-web/src/calendars/calendar.query.ts b/apps/calendar-web/src/calendars/calendar.query.ts index 3454e47690..3b4ee451da 100644 --- a/apps/calendar-web/src/calendars/calendar.query.ts +++ b/apps/calendar-web/src/calendars/calendar.query.ts @@ -16,6 +16,7 @@ import { isSentinelCalendarList, synthesizeLocalCalendar, } from "@web/calendars/local-calendar.sentinel"; +import { syncCalendarTimeZoneHintFromCalendars } from "@web/timezone/calendar-timezone-hint"; export const calendarQueryKeys = { all: ["calendars"] as const, @@ -36,10 +37,14 @@ export function calendarsQueryOptions(authenticated: boolean) { // covers the gap before React re-renders after the session store flips, // so a still-mounted anonymous observer cannot write the sentinel back. if (!authenticated && !readAuthenticated()) { - return [synthesizeLocalCalendar(getLocalCalendarSentinelId())]; + const local = [synthesizeLocalCalendar(getLocalCalendarSentinelId())]; + syncCalendarTimeZoneHintFromCalendars(local); + return local; } - return CalendarApi.list(); + const calendars = await CalendarApi.list(); + syncCalendarTimeZoneHintFromCalendars(calendars); + return calendars; }, staleTime: 60_000, }); @@ -97,6 +102,7 @@ export async function ensureRemoteCalendars( const calendars = await client.fetchQuery(calendarsQueryOptions(true)); if (isSentinelCalendarList(calendars)) return undefined; + syncCalendarTimeZoneHintFromCalendars(calendars); return calendars; } diff --git a/apps/calendar-web/src/timezone/CalendarTimeZoneHintSync.tsx b/apps/calendar-web/src/timezone/CalendarTimeZoneHintSync.tsx index 89b3562eb2..6630f4ebcc 100644 --- a/apps/calendar-web/src/timezone/CalendarTimeZoneHintSync.tsx +++ b/apps/calendar-web/src/timezone/CalendarTimeZoneHintSync.tsx @@ -1,16 +1,13 @@ import { useLayoutEffect } from "react"; import { useCalendarsQuery } from "@web/calendars/calendar.query"; -import { pickCalendarTimeZoneHint } from "@web/timezone/calendar-timezone-hint"; -import { setCalendarTimeZoneHint } from "@web/timezone/effective-timezone.store"; +import { syncCalendarTimeZoneHintFromCalendars } from "@web/timezone/calendar-timezone-hint"; /** Keeps the Auto effective zone aligned with connected calendar timezones. */ export function CalendarTimeZoneHintSync() { const { data: calendars } = useCalendarsQuery(); useLayoutEffect(() => { - setCalendarTimeZoneHint( - calendars ? pickCalendarTimeZoneHint(calendars) : null, - ); + syncCalendarTimeZoneHintFromCalendars(calendars); }, [calendars]); return null; diff --git a/apps/calendar-web/src/timezone/calendar-timezone-hint.test.ts b/apps/calendar-web/src/timezone/calendar-timezone-hint.test.ts index bbd90110c2..919c813928 100644 --- a/apps/calendar-web/src/timezone/calendar-timezone-hint.test.ts +++ b/apps/calendar-web/src/timezone/calendar-timezone-hint.test.ts @@ -1,7 +1,39 @@ import { TimeZoneSchema } from "@core/types/domain-primitives"; +import dayjs from "@core/util/date/dayjs"; import { createMockCalendar } from "@web/__tests__/utils/factories/calendar.factory"; -import { pickCalendarTimeZoneHint } from "@web/timezone/calendar-timezone-hint"; -import { describe, expect, it } from "bun:test"; +import { + pickCalendarTimeZoneHint, + syncCalendarTimeZoneHintFromCalendars, +} from "@web/timezone/calendar-timezone-hint"; +import { + getEffectiveTimeZone, + resetEffectiveTimeZoneStoreForTests, + setBrowserTimeZoneForTests, +} from "@web/timezone/effective-timezone.store"; +import { afterEach, describe, expect, it, setSystemTime } from "bun:test"; + +describe("syncCalendarTimeZoneHintFromCalendars", () => { + afterEach(() => { + resetEffectiveTimeZoneStoreForTests(); + setSystemTime(); + }); + + it("applies denver today before navigation when the browser is UTC at the merge-queue boundary", () => { + setBrowserTimeZoneForTests("UTC"); + setSystemTime(new Date("2026-11-12T00:23:00.000Z")); + syncCalendarTimeZoneHintFromCalendars([ + createMockCalendar({ + name: "miscellaneous", + timeZone: TimeZoneSchema.parse("America/Denver"), + isPrimary: true, + }), + ]); + expect(getEffectiveTimeZone()).toBe("America/Denver"); + expect(dayjs().tz(getEffectiveTimeZone()).format("YYYY-MM-DD")).toBe( + "2026-11-11", + ); + }); +}); describe("pickCalendarTimeZoneHint", () => { it("prefers the primary calendar zone", () => { diff --git a/apps/calendar-web/src/timezone/calendar-timezone-hint.ts b/apps/calendar-web/src/timezone/calendar-timezone-hint.ts index 566f6baf4c..0f0261ec7c 100644 --- a/apps/calendar-web/src/timezone/calendar-timezone-hint.ts +++ b/apps/calendar-web/src/timezone/calendar-timezone-hint.ts @@ -1,5 +1,6 @@ import { type Calendar } from "@core/types/calendar.contracts"; import { isValidTimeZone } from "@web/timezone/browser-timezone"; +import { setCalendarTimeZoneHint } from "@web/timezone/effective-timezone.store"; import { isUtcLikeTimeZone } from "@web/timezone/utc-like-timezone"; /** @@ -32,3 +33,12 @@ export function pickCalendarTimeZoneHint( return null; } + +/** Keep Auto effective zone aligned with connected calendars (see CalendarTimeZoneHintSync). */ +export function syncCalendarTimeZoneHintFromCalendars( + calendars: readonly Calendar[] | null | undefined, +): void { + setCalendarTimeZoneHint( + calendars ? pickCalendarTimeZoneHint(calendars) : null, + ); +} diff --git a/apps/calendar-web/src/views/Day/components/Calendar/dayCalendarColumns.util.test.ts b/apps/calendar-web/src/views/Day/components/Calendar/dayCalendarColumns.util.test.ts index 93caeff47e..ba9b360d89 100644 --- a/apps/calendar-web/src/views/Day/components/Calendar/dayCalendarColumns.util.test.ts +++ b/apps/calendar-web/src/views/Day/components/Calendar/dayCalendarColumns.util.test.ts @@ -216,6 +216,68 @@ describe("calendarIdsWithVisibleEventsOnDay", () => { resetEffectiveTimeZoneStoreForTests(); }); + it("shows every stress calendar column when viewed day matches denver event day", () => { + setBrowserTimeZoneForTests("UTC"); + const stressCalendars = Array.from({ length: 12 }, (_, index) => + makeCalendar({ + id: `f${String(index).padStart(23, "0")}` as Calendar["id"], + name: `stress-${index}`, + isPrimary: index === 0, + }), + ); + const dateInViewDenver = dayjs.tz("2026-11-11T17:23:00", "America/Denver"); + const timed = stressCalendars.map((calendar, index) => + gridEvent({ + _id: String(50 + index), + calendarId: calendar.id, + startDate: "2026-11-11 09:00:00.000", + endDate: "2026-11-11 10:00:00.000", + timeZone: "America/Denver", + }), + ); + + expect( + getDayViewCalendars({ + allDayEvents: [], + calendars: stressCalendars, + dateInView: dateInViewDenver, + hiddenEventIds: new Set(), + timedEvents: timed, + }), + ).toHaveLength(12); + }); + + it("falls back to primary when viewed utc day mismatches denver stress events", () => { + setBrowserTimeZoneForTests("UTC"); + const stressCalendars = Array.from({ length: 12 }, (_, index) => + makeCalendar({ + id: `f${String(index).padStart(23, "0")}` as Calendar["id"], + name: `stress-${index}`, + isPrimary: index === 0, + }), + ); + const dateInViewUtc = dayjs.tz("2026-11-12T00:23:00", "UTC"); + const timed = stressCalendars.map((calendar, index) => + gridEvent({ + _id: String(50 + index), + calendarId: calendar.id, + startDate: "2026-11-11 09:00:00.000", + endDate: "2026-11-11 10:00:00.000", + timeZone: "America/Denver", + }), + ); + + expect( + getDayViewCalendars({ + allDayEvents: [], + calendars: stressCalendars, + dateInView: dateInViewUtc, + hiddenEventIds: new Set(), + timedEvents: timed, + }), + ).toHaveLength(1); + }); + it("keeps both day columns at the merge-queue UTC boundary (browser UTC, no calendar hint)", () => { setBrowserTimeZoneForTests("UTC"); const primary = makeCalendar({ From ee44b96c32b32746b3875c9c34783296bb6267f0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 12 Nov 2026 07:17:24 +0000 Subject: [PATCH 2/2] fix(web): apply cached calendar hint in loadTodayData redirect Week stress E2E can press d before CalendarTimeZoneHintSync runs; syncing from the calendars query cache in loadTodayData closes that race. Co-authored-by: Tyler Dane --- apps/calendar-web/src/routers/loaders.test.ts | 39 ++++++++++++++++++- apps/calendar-web/src/routers/loaders.ts | 12 ++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/apps/calendar-web/src/routers/loaders.test.ts b/apps/calendar-web/src/routers/loaders.test.ts index dd58bd0c41..a35988d779 100644 --- a/apps/calendar-web/src/routers/loaders.test.ts +++ b/apps/calendar-web/src/routers/loaders.test.ts @@ -5,6 +5,10 @@ import { createRouter, isRedirect, } from "@tanstack/react-router"; +import { TimeZoneSchema } from "@core/types/domain-primitives"; +import { createMockCalendar } from "@web/__tests__/utils/factories/calendar.factory"; +import { queryClient } from "@web/api/query-client"; +import { calendarQueryKeys } from "@web/calendars/calendar.query"; import { ROOT_ROUTES } from "@web/common/constants/routes"; import * as prefetchWeekEvents from "@web/events/queries/prefetch-week-events"; import { @@ -16,7 +20,18 @@ import { redirectToToday, validateWeekDateParam, } from "@web/routers/loaders"; -import { afterEach, describe, expect, it, spyOn } from "bun:test"; +import { + resetEffectiveTimeZoneStoreForTests, + setBrowserTimeZoneForTests, +} from "@web/timezone/effective-timezone.store"; +import { + afterEach, + describe, + expect, + it, + setSystemTime, + spyOn, +} from "bun:test"; function getRedirect(fn: () => unknown) { try { @@ -126,6 +141,28 @@ describe("router redirects", () => { }); }); +describe("loadTodayData", () => { + afterEach(() => { + resetEffectiveTimeZoneStoreForTests(); + queryClient.clear(); + setSystemTime(); + }); + + it("applies a cached calendar hint before picking today at the merge-queue boundary", () => { + setBrowserTimeZoneForTests("UTC"); + setSystemTime(new Date("2026-11-12T00:23:00.000Z")); + queryClient.setQueryData(calendarQueryKeys.all, [ + createMockCalendar({ + name: "miscellaneous", + timeZone: TimeZoneSchema.parse("America/Denver"), + isPrimary: true, + }), + ]); + + expect(loadTodayData().dateString).toBe("2026-11-11"); + }); +}); + describe("loadDateParam", () => { it("shapes the loader data for an already-valid dateString param", () => { const result = loadDateParam({ diff --git a/apps/calendar-web/src/routers/loaders.ts b/apps/calendar-web/src/routers/loaders.ts index 2ca4669557..2512166948 100644 --- a/apps/calendar-web/src/routers/loaders.ts +++ b/apps/calendar-web/src/routers/loaders.ts @@ -1,6 +1,9 @@ import { redirect } from "@tanstack/react-router"; +import { type Calendar } from "@core/types/calendar.contracts"; import { zYearMonthDayString } from "@core/types/type.utils"; import dayjs, { type Dayjs } from "@core/util/date/dayjs"; +import { queryClient } from "@web/api/query-client"; +import { calendarQueryKeys } from "@web/calendars/calendar.query"; import { DEFAULT_CALENDAR_ROUTE, ROOT_ROUTES, @@ -11,6 +14,7 @@ import { prefetchWeekEventsQuery, weekDateStringFromPathname, } from "@web/events/queries/prefetch-week-events"; +import { syncCalendarTimeZoneHintFromCalendars } from "@web/timezone/calendar-timezone-hint"; import { getEffectiveTimeZone } from "@web/timezone/effective-timezone.store"; export interface DayLoaderData { @@ -38,7 +42,15 @@ export async function loadAuthenticated() { return { authenticated }; } +function syncCachedCalendarTimeZoneHintBeforeToday(): void { + const calendars = queryClient.getQueryData(calendarQueryKeys.all); + if (calendars?.length) { + syncCalendarTimeZoneHintFromCalendars(calendars); + } +} + export function loadTodayData(): DayLoaderData { + syncCachedCalendarTimeZoneHintBeforeToday(); const dateInView = dayjs().tz(getEffectiveTimeZone()); const dateFormat = dayjs.DateFormat.YEAR_MONTH_DAY_FORMAT;