web: fix timezone issues so midnight ISR happens the right day

This commit is contained in:
2026-09-04 21:28:17 +02:00
parent b7ff7a3ddc
commit a678adc9d3
7 changed files with 72 additions and 13 deletions
+2
View File
@@ -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
+16
View File
@@ -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(
+5
View File
@@ -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 });
}
+19
View File
@@ -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", () => {
+7 -3
View File
@@ -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<E extends EventListable>(
export function getFutureOccurrences<E extends EventListable>(
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)
+19
View File
@@ -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;
}
});
});
+4 -10
View File
@@ -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) {