fix: use explicit Eastern timezone for day boundary instead of system TZ
Build and Deploy / Build & Push (push) Successful in 2m50s
Build and Deploy / Build & Push (push) Successful in 2m50s
getTodayLocal() relied on system clock hours, which broke in the web container (TZ defaulting to UTC) — the day flipped at 11 PM EDT (3 AM UTC) instead of 3 AM Eastern. Now uses Intl.DateTimeFormat with an explicit America/New_York timezone. Also replaced all toISOString() date formatting with local-component helpers to avoid UTC conversion. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
import { HomePageClient } from "@/components/HomePageClient";
|
import { HomePageClient } from "@/components/HomePageClient";
|
||||||
import { getTodayLocal } from "@/lib/env";
|
import { getTodayLocal, formatDateLocal } from "@/lib/env";
|
||||||
|
|
||||||
const BACKEND_URL = process.env.BACKEND_URL ?? "http://localhost:3001";
|
const BACKEND_URL = process.env.BACKEND_URL ?? "http://localhost:3001";
|
||||||
|
|
||||||
@@ -12,13 +12,13 @@ function getWeekStart(param: string | undefined): string {
|
|||||||
const d = new Date(param + "T00:00:00");
|
const d = new Date(param + "T00:00:00");
|
||||||
if (!isNaN(d.getTime())) {
|
if (!isNaN(d.getTime())) {
|
||||||
d.setDate(d.getDate() - d.getDay());
|
d.setDate(d.getDate() - d.getDay());
|
||||||
return d.toISOString().slice(0, 10);
|
return formatDateLocal(d);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const todayIso = getTodayLocal();
|
const todayIso = getTodayLocal();
|
||||||
const d = new Date(todayIso + "T00:00:00");
|
const d = new Date(todayIso + "T00:00:00");
|
||||||
d.setDate(d.getDate() - d.getDay());
|
d.setDate(d.getDate() - d.getDay());
|
||||||
return d.toISOString().slice(0, 10);
|
return formatDateLocal(d);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function HomePage({ searchParams }: PageProps) {
|
export default async function HomePage({ searchParams }: PageProps) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type Database from "better-sqlite3";
|
import type Database from "better-sqlite3";
|
||||||
import { getDb } from "./index";
|
import { getDb } from "./index";
|
||||||
|
import { getTodayLocal } from "../../../lib/env";
|
||||||
|
|
||||||
import type { DayData } from "../../../lib/types";
|
import type { DayData } from "../../../lib/types";
|
||||||
export type { DayData };
|
export type { DayData };
|
||||||
@@ -126,7 +127,7 @@ export function isMonthScraped(
|
|||||||
): boolean {
|
): boolean {
|
||||||
const daysInMonth = new Date(year, month, 0).getDate();
|
const daysInMonth = new Date(year, month, 0).getDate();
|
||||||
const lastDay = `${year}-${String(month).padStart(2, "0")}-${String(daysInMonth).padStart(2, "0")}`;
|
const lastDay = `${year}-${String(month).padStart(2, "0")}-${String(daysInMonth).padStart(2, "0")}`;
|
||||||
const today = new Date().toISOString().slice(0, 10);
|
const today = getTodayLocal();
|
||||||
|
|
||||||
if (lastDay < today) return true;
|
if (lastDay < today) return true;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Hono } from "hono";
|
|||||||
import { PARKS } from "../../../lib/parks";
|
import { PARKS } from "../../../lib/parks";
|
||||||
import { QUEUE_TIMES_IDS } from "../../../lib/queue-times-map";
|
import { QUEUE_TIMES_IDS } from "../../../lib/queue-times-map";
|
||||||
import { getCoasterSet } from "../../../lib/coaster-data";
|
import { getCoasterSet } from "../../../lib/coaster-data";
|
||||||
import { getTodayLocal, isWithinOperatingWindow, getOperatingStatus } from "../../../lib/env";
|
import { getTodayLocal, formatDateLocal, isWithinOperatingWindow, getOperatingStatus } from "../../../lib/env";
|
||||||
import { fetchToday } from "../../../lib/scrapers/sixflags";
|
import { fetchToday } from "../../../lib/scrapers/sixflags";
|
||||||
import { fetchLiveRides } from "../../../lib/scrapers/queuetimes";
|
import { fetchLiveRides } from "../../../lib/scrapers/queuetimes";
|
||||||
import { getDateRange, getParkMonthData, type DayData } from "../db/queries";
|
import { getDateRange, getParkMonthData, type DayData } from "../db/queries";
|
||||||
@@ -22,7 +22,7 @@ app.get("/week", async (c) => {
|
|||||||
const weekDates = Array.from({ length: 7 }, (_, i) => {
|
const weekDates = Array.from({ length: 7 }, (_, i) => {
|
||||||
const d = new Date(startParam + "T00:00:00");
|
const d = new Date(startParam + "T00:00:00");
|
||||||
d.setDate(d.getDate() + i);
|
d.setDate(d.getDate() + i);
|
||||||
return d.toISOString().slice(0, 10);
|
return formatDateLocal(d);
|
||||||
});
|
});
|
||||||
const endDate = weekDates[6];
|
const endDate = weekDates[6];
|
||||||
const today = getTodayLocal();
|
const today = getTodayLocal();
|
||||||
@@ -53,7 +53,7 @@ app.get("/week", async (c) => {
|
|||||||
const currentWeekStart = (() => {
|
const currentWeekStart = (() => {
|
||||||
const d = new Date(today + "T00:00:00");
|
const d = new Date(today + "T00:00:00");
|
||||||
d.setDate(d.getDate() - d.getDay());
|
d.setDate(d.getDate() - d.getDay());
|
||||||
return d.toISOString().slice(0, 10);
|
return formatDateLocal(d);
|
||||||
})();
|
})();
|
||||||
const isCurrentWeek = startParam === currentWeekStart;
|
const isCurrentWeek = startParam === currentWeekStart;
|
||||||
|
|
||||||
|
|||||||
@@ -25,10 +25,17 @@ function formatLabel(dates: string[]): string {
|
|||||||
return `${startStr} – ${endStr}`;
|
return `${startStr} – ${endStr}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDateLocal(d: Date): string {
|
||||||
|
const y = d.getFullYear();
|
||||||
|
const m = String(d.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(d.getDate()).padStart(2, "0");
|
||||||
|
return `${y}-${m}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
function shiftWeek(weekStart: string, delta: number): string {
|
function shiftWeek(weekStart: string, delta: number): string {
|
||||||
const d = new Date(weekStart + "T00:00:00");
|
const d = new Date(weekStart + "T00:00:00");
|
||||||
d.setDate(d.getDate() + delta * 7);
|
d.setDate(d.getDate() + delta * 7);
|
||||||
return d.toISOString().slice(0, 10);
|
return formatDateLocal(d);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WeekNav({ weekStart, weekDates, isCurrentWeek }: WeekNavProps) {
|
export function WeekNav({ weekStart, weekDates, isCurrentWeek }: WeekNavProps) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
- BACKEND_URL=http://backend:3001
|
- BACKEND_URL=http://backend:3001
|
||||||
|
- TZ=America/New_York
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
|
|||||||
+43
-15
@@ -12,26 +12,54 @@ export function parseStalenessHours(envVar: string | undefined, defaultHours: nu
|
|||||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : defaultHours;
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : defaultHours;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const APP_TIMEZONE = "America/New_York";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns today's date as YYYY-MM-DD using local wall-clock time with a 3 AM
|
* Returns today's date as YYYY-MM-DD in Eastern time with a 3 AM switchover.
|
||||||
* switchover. Before 3 AM local time we still consider it "yesterday", so the
|
* Uses Intl.DateTimeFormat so it works regardless of the system/container TZ.
|
||||||
* calendar doesn't flip to the next day at midnight while people are still out
|
|
||||||
* at the park.
|
|
||||||
*
|
|
||||||
* Important: `new Date().toISOString()` returns UTC, which causes the date to
|
|
||||||
* advance at 8 PM EDT (UTC-4) or 7 PM EST (UTC-5) — too early. This helper
|
|
||||||
* corrects that by using local year/month/day components and rolling back one
|
|
||||||
* day when the local hour is before 3.
|
|
||||||
*/
|
*/
|
||||||
export function getTodayLocal(): string {
|
export function getTodayLocal(): string {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
if (now.getHours() < 3) {
|
const fmt = new Intl.DateTimeFormat("en-US", {
|
||||||
now.setDate(now.getDate() - 1);
|
timeZone: APP_TIMEZONE,
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
hour: "numeric",
|
||||||
|
hour12: false,
|
||||||
|
});
|
||||||
|
const parts = fmt.formatToParts(now);
|
||||||
|
const hour = parseInt(parts.find((p) => p.type === "hour")!.value, 10) % 24;
|
||||||
|
|
||||||
|
const target = hour < 3 ? new Date(now.getTime() - 86_400_000) : now;
|
||||||
|
return formatDateTZ(target, APP_TIMEZONE);
|
||||||
}
|
}
|
||||||
const y = now.getFullYear();
|
|
||||||
const m = String(now.getMonth() + 1).padStart(2, "0");
|
/**
|
||||||
const d = String(now.getDate()).padStart(2, "0");
|
* Format a Date as YYYY-MM-DD in a specific IANA timezone.
|
||||||
return `${y}-${m}-${d}`;
|
*/
|
||||||
|
export function formatDateTZ(d: Date, tz: string): string {
|
||||||
|
const parts = new Intl.DateTimeFormat("en-US", {
|
||||||
|
timeZone: tz,
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
}).formatToParts(d);
|
||||||
|
const y = parts.find((p) => p.type === "year")!.value;
|
||||||
|
const m = parts.find((p) => p.type === "month")!.value;
|
||||||
|
const day = parts.find((p) => p.type === "day")!.value;
|
||||||
|
return `${y}-${m}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a Date as YYYY-MM-DD using its local (system-timezone) components.
|
||||||
|
* Use this instead of d.toISOString().slice(0,10) which converts to UTC.
|
||||||
|
*/
|
||||||
|
export function formatDateLocal(d: Date): string {
|
||||||
|
const y = d.getFullYear();
|
||||||
|
const m = String(d.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(d.getDate()).padStart(2, "0");
|
||||||
|
return `${y}-${m}-${day}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user