diff --git a/app/park/[id]/ride/[slug]/page.tsx b/app/park/[id]/ride/[slug]/page.tsx index e1b6777..775aa7f 100644 --- a/app/park/[id]/ride/[slug]/page.tsx +++ b/app/park/[id]/ride/[slug]/page.tsx @@ -10,6 +10,7 @@ const BACKEND_URL = process.env.BACKEND_URL ?? "http://localhost:3001"; type Tab = "today" | "7d" | "30d"; interface TodaySample { + recordedAt: string; localTime: string; isOpen: boolean; waitMinutes: number | null; diff --git a/backend/src/db/queries.ts b/backend/src/db/queries.ts index 876c28d..f622bdd 100644 --- a/backend/src/db/queries.ts +++ b/backend/src/db/queries.ts @@ -278,6 +278,7 @@ export function listRidesForPark(parkId: string): RideRow[] { } export interface DailySample { + recordedAt: string; localTime: string; isOpen: boolean; waitMinutes: number | null; @@ -291,18 +292,20 @@ export function getRideSamplesForDay( ): DailySample[] { const rows = getDb() .prepare( - `SELECT local_time, is_open, wait_minutes, fast_lane_minutes + `SELECT recorded_at, local_time, is_open, wait_minutes, fast_lane_minutes FROM ride_wait_samples WHERE park_id = ? AND qt_ride_id = ? AND local_date = ? - ORDER BY local_time`, + ORDER BY recorded_at`, ) .all(parkId, qtRideId, localDate) as { + recorded_at: string; local_time: string; is_open: number; wait_minutes: number | null; fast_lane_minutes: number | null; }[]; return rows.map((r) => ({ + recordedAt: r.recorded_at, localTime: r.local_time, isOpen: r.is_open === 1, waitMinutes: r.wait_minutes, diff --git a/backend/src/routes/ride-history.ts b/backend/src/routes/ride-history.ts index f25c794..0eccf87 100644 --- a/backend/src/routes/ride-history.ts +++ b/backend/src/routes/ride-history.ts @@ -12,6 +12,7 @@ import { Hono } from "hono"; import { PARK_MAP } from "../../../lib/parks"; import { + getDayData, getRideBySlug, getRideSamplesForDay, getRideDailyAggregates, @@ -22,6 +23,7 @@ import { import { liveRidesCache, fastLaneCache } from "../services/live-cache"; import { slugifyRideName } from "../../../lib/ride-slug"; import { lookupFastLane } from "../../../lib/scrapers/sixflags-waittimes"; +import { getTodayLocal, isWithinOperatingWindow } from "../../../lib/env"; const app = new Hono(); @@ -72,6 +74,15 @@ app.get("/:parkId/rides/:slug", (c) => { const fastLaneCacheEntry = fastLaneCache.get(parkId); const flMatch = liveMatch && fastLaneCacheEntry ? lookupFastLane(liveMatch.name, fastLaneCacheEntry) : null; + // Operating-window gate. Queue-Times keeps reporting yesterday's last wait + // with isOpen=true overnight, so we override to closed when we're outside + // the park's hours — same behaviour as the /rides route. + const todayData = getDayData(parkId, getTodayLocal()); + const withinWindow = todayData?.hoursLabel + ? isWithinOperatingWindow(todayData.hoursLabel, park.timezone) + : false; + const liveIsOpen = Boolean(liveMatch?.isOpen) && withinWindow; + c.header("Cache-Control", "public, max-age=60, stale-while-revalidate=120"); return c.json({ park: { @@ -91,10 +102,10 @@ app.get("/:parkId/rides/:slug", (c) => { }, live: liveMatch ? { - isOpen: liveMatch.isOpen, - waitMinutes: liveMatch.waitMinutes, + isOpen: liveIsOpen, + waitMinutes: liveIsOpen ? liveMatch.waitMinutes : 0, hasFastLane: Boolean(flMatch?.hasFastLane), - fastLaneMinutes: liveMatch.isOpen ? (flMatch?.fastLaneMinutes ?? null) : null, + fastLaneMinutes: liveIsOpen ? (flMatch?.fastLaneMinutes ?? null) : null, lastUpdated: liveMatch.lastUpdated, } : null, diff --git a/components/charts/WaitTimeTodayChart.tsx b/components/charts/WaitTimeTodayChart.tsx index 53d0325..fdb9ca1 100644 --- a/components/charts/WaitTimeTodayChart.tsx +++ b/components/charts/WaitTimeTodayChart.tsx @@ -3,6 +3,7 @@ import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts"; export interface TodaySample { + recordedAt: string; localTime: string; isOpen: boolean; waitMinutes: number | null; @@ -14,10 +15,18 @@ interface Props { hasFastLane: boolean; } +const TIME_FMT = new Intl.DateTimeFormat([], { + hour: "2-digit", + minute: "2-digit", + hour12: false, +}); + export default function WaitTimeTodayChart({ samples, hasFastLane }: Props) { // Map samples: closed periods → null so Recharts breaks the line. + // X-axis time is rendered in the viewer's local timezone (Intl with no + // tz arg) so an Eastern-time user sees ET regardless of which park. const data = samples.map((s) => ({ - time: s.localTime, + time: TIME_FMT.format(new Date(s.recordedAt)), wait: s.isOpen ? s.waitMinutes : null, fl: s.isOpen ? s.fastLaneMinutes : null, }));