From c152f6d1177c6dc46b15364dbc8c847be0701c1b Mon Sep 17 00:00:00 2001 From: Johnnie Wong Date: Tue, 2 Jun 2026 15:23:35 +0800 Subject: [PATCH 1/3] Use device timezone in desktop renderer --- src/renderer/src/App.vue | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/App.vue b/src/renderer/src/App.vue index 6813b23..2857a86 100644 --- a/src/renderer/src/App.vue +++ b/src/renderer/src/App.vue @@ -68,9 +68,12 @@ const { data: meResponse } = useQuery({ queryFn: () => getMe(), }) +const deviceTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC' + +window.getTimezoneSetting = () => deviceTimezone + watch(meResponse, () => { if (meResponse.value?.data) { - window.getTimezoneSetting = () => meResponse.value.data.timezone window.getWeekStartSetting = () => meResponse.value.data.week_start } }) @@ -83,7 +86,6 @@ watch(isActive, (active) => { }) onMounted(async () => { - window.getTimezoneSetting = () => 'Europe/Vienna' window.getWeekStartSetting = () => 'monday' initializeAuth(queryClient) From 0cee790c3bdc515ed691704efa88fd2a4e0c2eb1 Mon Sep 17 00:00:00 2001 From: Gregor Vostrak Date: Tue, 2 Jun 2026 14:17:45 +0200 Subject: [PATCH 2/3] mount router after me request loaded to fix timezone setting desync --- src/renderer/src/App.vue | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/renderer/src/App.vue b/src/renderer/src/App.vue index 2857a86..bc3094d 100644 --- a/src/renderer/src/App.vue +++ b/src/renderer/src/App.vue @@ -66,17 +66,16 @@ watchEffect(() => { const { data: meResponse } = useQuery({ queryKey: ['me'], queryFn: () => getMe(), + enabled: isLoggedIn, }) const deviceTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC' +window.getTimezoneSetting = () => meResponse.value?.data?.timezone || deviceTimezone +window.getWeekStartSetting = () => meResponse.value?.data?.week_start || 'monday' -window.getTimezoneSetting = () => deviceTimezone - -watch(meResponse, () => { - if (meResponse.value?.data) { - window.getWeekStartSetting = () => meResponse.value.data.week_start - } -}) +// Time-formatting UI must not mount before the real timezone is loaded, or +// will cache it with the fallback for the whole session. +const isMeLoaded = computed(() => !!meResponse.value?.data) // Watch timer state and notify main process for idle detection watch(isActive, (active) => { @@ -86,8 +85,6 @@ watch(isActive, (active) => { }) onMounted(async () => { - window.getWeekStartSetting = () => 'monday' - initializeAuth(queryClient) useTheme() @@ -169,7 +166,7 @@ whenever(cmdComma, () => {
-
+
@@ -194,6 +191,9 @@ whenever(cmdComma, () => {
+
+
Loading…
+
Date: Tue, 2 Jun 2026 15:35:58 +0200 Subject: [PATCH 3/3] add e2e regression test for timezone loading --- e2e/tests/timezone.spec.ts | 61 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 e2e/tests/timezone.spec.ts diff --git a/e2e/tests/timezone.spec.ts b/e2e/tests/timezone.spec.ts new file mode 100644 index 0000000..48f5c17 --- /dev/null +++ b/e2e/tests/timezone.spec.ts @@ -0,0 +1,61 @@ +import { test, expect } from '../fixtures/electron-test' + +test.describe('Account timezone application', () => { + test('renders a UTC time entry in the account timezone, not the fallback', async ({ + page, + mockState, + }) => { + test.setTimeout(60_000) + + // UTC+5:30, no DST — unambiguously different from CI/device zones and from + // the pre-fix Europe/Vienna fallback, and it keeps every mock entry on its + // original calendar day (so day-grouping is unaffected). + const ACCOUNT_TZ = 'Asia/Kolkata' + const ACCOUNT_TIME = /14:30\s*-\s*17:00/ // 09:00Z–11:30Z in Asia/Kolkata + const FALLBACK_TIME = /10:00\s*-\s*12:30/ // the same entry in Europe/Vienna (pre-fix) + + mockState.user.timezone = ACCOUNT_TZ + + // Hold GET /users/me open. Registered after the catch-all, so it wins for + // this exact path (Playwright runs handlers last-registered-first). The + // /users/me/memberships and /time-entries/active paths still hit the + // catch-all, so the entry list can load while the timezone is pending. + let releaseMe: () => void = () => {} + const meGate = new Promise((resolve) => { + releaseMe = resolve + }) + await page.route(/\/users\/me(\?.*)?$/, async (route) => { + await meGate + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ data: mockState.user }), + }) + }) + + // Re-bootstrap cold: the vue-query cache is cleared by the reload while the + // seeded auth token persists in localStorage (so we are "logged in"). + await page.reload({ waitUntil: 'domcontentloaded' }) + + // Give the pre-fix build time to mount the time page and render the entry + // with its fallback timezone (and freeze it under ). The fixed + // build renders nothing here — it withholds the UI until the tz is known. + await page.waitForTimeout(3_000) + + // Deliver the account timezone. + releaseMe() + + // The entry is shown — and it must be in the account timezone, not the fallback. + await expect(page.getByText('Implement navigation component').first()).toBeVisible({ + timeout: 10_000, + }) + await expect(page.getByText(ACCOUNT_TIME).first()).toBeVisible({ timeout: 10_000 }) + await expect(page.getByText(FALLBACK_TIME)).toHaveCount(0) + + // ...and the timezone actually in effect is the account one. + const tzInEffect = await page.evaluate(() => + (window as Window & { getTimezoneSetting: () => string }).getTimezoneSetting() + ) + expect(tzInEffect).toBe(ACCOUNT_TZ) + }) +})