From a678adc9d324133ea9fe8f0fad4a704ed47c4634 Mon Sep 17 00:00:00 2001 From: Jonas Braathen Date: Fri, 4 Sep 2026 21:28:17 +0200 Subject: [PATCH] =?UTF-8?q?web:=20fix=20=E2=9C=A8=20timezone=20issues=20?= =?UTF-8?q?=E2=9C=A8=20so=20midnight=20ISR=20happens=20the=20right=20day?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/Dockerfile | 2 ++ web/src/lib/date.test.ts | 16 ++++++++++++++++ web/src/lib/date.ts | 5 +++++ web/src/lib/event.test.ts | 19 +++++++++++++++++++ web/src/lib/event.ts | 10 +++++++--- web/src/lib/openinghours.test.ts | 19 +++++++++++++++++++ web/src/lib/openinghours.ts | 14 ++++---------- 7 files changed, 72 insertions(+), 13 deletions(-) diff --git a/web/Dockerfile b/web/Dockerfile index f8f8670..3dc1411 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -33,6 +33,8 @@ WORKDIR /app ENV NODE_ENV production # Uncomment the following line in case you want to disable telemetry during runtime. ENV NEXT_TELEMETRY_DISABLED 1 +# Date logic is Oslo-aware in code, but keep the process TZ matching anyway +ENV TZ=Europe/Oslo RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs diff --git a/web/src/lib/date.test.ts b/web/src/lib/date.test.ts index e9aa054..7bcaef3 100644 --- a/web/src/lib/date.test.ts +++ b/web/src/lib/date.test.ts @@ -10,6 +10,7 @@ import { groupConsecutiveDates, isConsecutiveDays, isTodayOrFuture, + startOfTodayInOslo, toLocalTime, } from "./date.ts"; @@ -30,6 +31,21 @@ describe("toLocalTime", () => { }); }); +describe("startOfTodayInOslo", () => { + it("returns the Oslo date even when the server runs in UTC", () => { + const tz = process.env.TZ; + process.env.TZ = "UTC"; + try { + vi.setSystemTime(new Date("2026-07-07T22:20:00Z")); + const today = startOfTodayInOslo(); + expect(today.getDate()).toBe(8); + expect(today.getHours()).toBe(0); + } finally { + process.env.TZ = tz; + } + }); +}); + describe("formatDate", () => { it("formats in Oslo time with Norwegian locale", () => { expect(formatDate("2026-07-07T18:00:00Z", "dd.MM.yyyy 'kl.' HH:mm")).toBe( diff --git a/web/src/lib/date.ts b/web/src/lib/date.ts index 0f79093..b765f69 100644 --- a/web/src/lib/date.ts +++ b/web/src/lib/date.ts @@ -20,6 +20,11 @@ export function toLocalTime(date: Date | string | number) { return toZonedTime(date, timeZone); } +// Start of the current Oslo day, regardless of the server's TZ +export function startOfTodayInOslo() { + return startOfDay(toLocalTime(new Date())); +} + export function formatDate(date: Date | string | number, formatStr: string) { return format(toLocalTime(date), formatStr, { timeZone, locale: nb }); } diff --git a/web/src/lib/event.test.ts b/web/src/lib/event.test.ts index 556fa34..d48b491 100644 --- a/web/src/lib/event.test.ts +++ b/web/src/lib/event.test.ts @@ -164,6 +164,25 @@ describe("getFutureOccurrences", () => { "2026-07-14T18:00:00Z", ]); }); + + it("rolls over at Oslo midnight when the server runs in UTC", () => { + const tz = process.env.TZ; + process.env.TZ = "UTC"; + try { + // 22:20 UTC on tirsdag is 00:20 onsdag in Oslo + vi.setSystemTime(new Date("2026-07-07T22:20:00Z")); + const event = makeEvent("a", [ + "2026-07-07T18:00:00Z", + "2026-07-08T18:00:00Z", + ]); + + expect(getFutureOccurrences(event).map((o) => o.start)).toEqual([ + "2026-07-08T18:00:00Z", + ]); + } finally { + process.env.TZ = tz; + } + }); }); describe("getEventPig", () => { diff --git a/web/src/lib/event.ts b/web/src/lib/event.ts index b6acaf4..5eb9529 100644 --- a/web/src/lib/event.ts +++ b/web/src/lib/event.ts @@ -1,13 +1,17 @@ import { endOfWeek, - startOfToday, startOfWeek, isAfter, eachDayOfInterval, addWeeks, parseISO, } from "date-fns"; -import { toLocalTime, formatDate, compareDates } from "./date"; +import { + toLocalTime, + formatDate, + compareDates, + startOfTodayInOslo, +} from "./date"; import { graphql, unmaskFragment } from "@/gql"; import { type EventCategoryFragment, @@ -338,7 +342,7 @@ export function organizeEventsByDate( export function getFutureOccurrences( event: E ): E["occurrences"][number][] { - const today = startOfToday(); + const today = startOfTodayInOslo(); const occurrences = event?.occurrences ?? []; const futureOccurrences = occurrences.filter((occurrence) => isAfter(toLocalTime(occurrence.start), today) diff --git a/web/src/lib/openinghours.test.ts b/web/src/lib/openinghours.test.ts index 9ed6320..0a25639 100644 --- a/web/src/lib/openinghours.test.ts +++ b/web/src/lib/openinghours.test.ts @@ -161,4 +161,23 @@ describe("getTodaysOpeningHoursForFunction", () => { vi.setSystemTime(new Date("2026-07-07T10:00:00Z")); expect(getTodaysOpeningHoursForFunction(openingHours, "kafé")).toBe("?"); }); + + it("uses the Oslo weekday when the server runs in UTC", () => { + const tz = process.env.TZ; + process.env.TZ = "UTC"; + try { + // 22:20 UTC on torsdag is 00:20 fredag in Oslo (CEST) + vi.setSystemTime(new Date("2026-07-09T22:20:00Z")); + expect(getTodaysOpeningHoursForFunction(openingHours, "bar")).toBe( + "15:00—01:00" + ); + // 23:20 UTC on torsdag is 00:20 fredag in Oslo (CET) + vi.setSystemTime(new Date("2026-01-08T23:20:00Z")); + expect(getTodaysOpeningHoursForFunction(openingHours, "bar")).toBe( + "15:00—01:00" + ); + } finally { + process.env.TZ = tz; + } + }); }); diff --git a/web/src/lib/openinghours.ts b/web/src/lib/openinghours.ts index fcfe67d..dabce19 100644 --- a/web/src/lib/openinghours.ts +++ b/web/src/lib/openinghours.ts @@ -1,13 +1,7 @@ -import { - compareDesc, - getISODay, - isAfter, - isSameDay, - parseISO, - startOfToday, -} from "date-fns"; +import { compareDesc, getISODay, isAfter, isSameDay, parseISO } from "date-fns"; import { getClient } from "@/app/client"; +import { startOfTodayInOslo } from "@/lib/date"; import { cachedUntilOsloMidnight } from "@/lib/revalidation"; import { graphql, unmaskFragment } from "@/gql"; import type { @@ -63,7 +57,7 @@ export async function fetchOpeningHoursSets() { } export async function getOpeningHours() { - const today = startOfToday(); + const today = startOfTodayInOslo(); const sets = await fetchOpeningHoursSets(); const validSets = sets .filter((set) => { @@ -213,7 +207,7 @@ export function getTodaysOpeningHoursForFunction( if (!week) { return "?"; } - const weekdayIndex = getISODay(startOfToday()) - 1; + const weekdayIndex = getISODay(startOfTodayInOslo()) - 1; const weekday = WEEKDAYS[weekdayIndex]; const hours = week[weekday]; if (hours?.timeFrom && hours?.timeTo) {