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
|
ENV NODE_ENV production
|
||||||
# Uncomment the following line in case you want to disable telemetry during runtime.
|
# Uncomment the following line in case you want to disable telemetry during runtime.
|
||||||
ENV NEXT_TELEMETRY_DISABLED 1
|
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 addgroup --system --gid 1001 nodejs
|
||||||
RUN adduser --system --uid 1001 nextjs
|
RUN adduser --system --uid 1001 nextjs
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
groupConsecutiveDates,
|
groupConsecutiveDates,
|
||||||
isConsecutiveDays,
|
isConsecutiveDays,
|
||||||
isTodayOrFuture,
|
isTodayOrFuture,
|
||||||
|
startOfTodayInOslo,
|
||||||
toLocalTime,
|
toLocalTime,
|
||||||
} from "./date.ts";
|
} 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", () => {
|
describe("formatDate", () => {
|
||||||
it("formats in Oslo time with Norwegian locale", () => {
|
it("formats in Oslo time with Norwegian locale", () => {
|
||||||
expect(formatDate("2026-07-07T18:00:00Z", "dd.MM.yyyy 'kl.' HH:mm")).toBe(
|
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);
|
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) {
|
export function formatDate(date: Date | string | number, formatStr: string) {
|
||||||
return format(toLocalTime(date), formatStr, { timeZone, locale: nb });
|
return format(toLocalTime(date), formatStr, { timeZone, locale: nb });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -164,6 +164,25 @@ describe("getFutureOccurrences", () => {
|
|||||||
"2026-07-14T18:00:00Z",
|
"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", () => {
|
describe("getEventPig", () => {
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import {
|
import {
|
||||||
endOfWeek,
|
endOfWeek,
|
||||||
startOfToday,
|
|
||||||
startOfWeek,
|
startOfWeek,
|
||||||
isAfter,
|
isAfter,
|
||||||
eachDayOfInterval,
|
eachDayOfInterval,
|
||||||
addWeeks,
|
addWeeks,
|
||||||
parseISO,
|
parseISO,
|
||||||
} from "date-fns";
|
} from "date-fns";
|
||||||
import { toLocalTime, formatDate, compareDates } from "./date";
|
import {
|
||||||
|
toLocalTime,
|
||||||
|
formatDate,
|
||||||
|
compareDates,
|
||||||
|
startOfTodayInOslo,
|
||||||
|
} from "./date";
|
||||||
import { graphql, unmaskFragment } from "@/gql";
|
import { graphql, unmaskFragment } from "@/gql";
|
||||||
import {
|
import {
|
||||||
type EventCategoryFragment,
|
type EventCategoryFragment,
|
||||||
@@ -338,7 +342,7 @@ export function organizeEventsByDate<E extends EventListable>(
|
|||||||
export function getFutureOccurrences<E extends EventListable>(
|
export function getFutureOccurrences<E extends EventListable>(
|
||||||
event: E
|
event: E
|
||||||
): E["occurrences"][number][] {
|
): E["occurrences"][number][] {
|
||||||
const today = startOfToday();
|
const today = startOfTodayInOslo();
|
||||||
const occurrences = event?.occurrences ?? [];
|
const occurrences = event?.occurrences ?? [];
|
||||||
const futureOccurrences = occurrences.filter((occurrence) =>
|
const futureOccurrences = occurrences.filter((occurrence) =>
|
||||||
isAfter(toLocalTime(occurrence.start), today)
|
isAfter(toLocalTime(occurrence.start), today)
|
||||||
|
|||||||
@@ -161,4 +161,23 @@ describe("getTodaysOpeningHoursForFunction", () => {
|
|||||||
vi.setSystemTime(new Date("2026-07-07T10:00:00Z"));
|
vi.setSystemTime(new Date("2026-07-07T10:00:00Z"));
|
||||||
expect(getTodaysOpeningHoursForFunction(openingHours, "kafé")).toBe("?");
|
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 {
|
import { compareDesc, getISODay, isAfter, isSameDay, parseISO } from "date-fns";
|
||||||
compareDesc,
|
|
||||||
getISODay,
|
|
||||||
isAfter,
|
|
||||||
isSameDay,
|
|
||||||
parseISO,
|
|
||||||
startOfToday,
|
|
||||||
} from "date-fns";
|
|
||||||
|
|
||||||
import { getClient } from "@/app/client";
|
import { getClient } from "@/app/client";
|
||||||
|
import { startOfTodayInOslo } from "@/lib/date";
|
||||||
import { cachedUntilOsloMidnight } from "@/lib/revalidation";
|
import { cachedUntilOsloMidnight } from "@/lib/revalidation";
|
||||||
import { graphql, unmaskFragment } from "@/gql";
|
import { graphql, unmaskFragment } from "@/gql";
|
||||||
import type {
|
import type {
|
||||||
@@ -63,7 +57,7 @@ export async function fetchOpeningHoursSets() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getOpeningHours() {
|
export async function getOpeningHours() {
|
||||||
const today = startOfToday();
|
const today = startOfTodayInOslo();
|
||||||
const sets = await fetchOpeningHoursSets();
|
const sets = await fetchOpeningHoursSets();
|
||||||
const validSets = sets
|
const validSets = sets
|
||||||
.filter((set) => {
|
.filter((set) => {
|
||||||
@@ -213,7 +207,7 @@ export function getTodaysOpeningHoursForFunction(
|
|||||||
if (!week) {
|
if (!week) {
|
||||||
return "?";
|
return "?";
|
||||||
}
|
}
|
||||||
const weekdayIndex = getISODay(startOfToday()) - 1;
|
const weekdayIndex = getISODay(startOfTodayInOslo()) - 1;
|
||||||
const weekday = WEEKDAYS[weekdayIndex];
|
const weekday = WEEKDAYS[weekdayIndex];
|
||||||
const hours = week[weekday];
|
const hours = week[weekday];
|
||||||
if (hours?.timeFrom && hours?.timeTo) {
|
if (hours?.timeFrom && hours?.timeTo) {
|
||||||
|
|||||||
Reference in New Issue
Block a user