web: fix ✨ timezone issues ✨ so midnight ISR happens the right day
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user