fix: render today's wait chart in viewer's local time + close stale live state
Build and Deploy / Build & Push (push) Successful in 1m8s

Two related polish fixes for the ride detail page:

1. Wait-time chart x-axis now uses Intl.DateTimeFormat with no timezone
   argument, so an Eastern-time user viewing a Pacific park sees ET on
   the axis. Backend now sends recorded_at (UTC) alongside local_time.

2. Ride-history endpoint now applies the same operating-window gate the
   /rides route uses. Queue-Times keeps reporting yesterday's last wait
   with isOpen=true overnight, which made the "Right now" pill show a
   live wait time when the park was actually closed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-30 08:23:00 -04:00
parent 7c88a3e568
commit 44d079efb9
4 changed files with 30 additions and 6 deletions
+1
View File
@@ -10,6 +10,7 @@ const BACKEND_URL = process.env.BACKEND_URL ?? "http://localhost:3001";
type Tab = "today" | "7d" | "30d"; type Tab = "today" | "7d" | "30d";
interface TodaySample { interface TodaySample {
recordedAt: string;
localTime: string; localTime: string;
isOpen: boolean; isOpen: boolean;
waitMinutes: number | null; waitMinutes: number | null;
+5 -2
View File
@@ -278,6 +278,7 @@ export function listRidesForPark(parkId: string): RideRow[] {
} }
export interface DailySample { export interface DailySample {
recordedAt: string;
localTime: string; localTime: string;
isOpen: boolean; isOpen: boolean;
waitMinutes: number | null; waitMinutes: number | null;
@@ -291,18 +292,20 @@ export function getRideSamplesForDay(
): DailySample[] { ): DailySample[] {
const rows = getDb() const rows = getDb()
.prepare( .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 FROM ride_wait_samples
WHERE park_id = ? AND qt_ride_id = ? AND local_date = ? WHERE park_id = ? AND qt_ride_id = ? AND local_date = ?
ORDER BY local_time`, ORDER BY recorded_at`,
) )
.all(parkId, qtRideId, localDate) as { .all(parkId, qtRideId, localDate) as {
recorded_at: string;
local_time: string; local_time: string;
is_open: number; is_open: number;
wait_minutes: number | null; wait_minutes: number | null;
fast_lane_minutes: number | null; fast_lane_minutes: number | null;
}[]; }[];
return rows.map((r) => ({ return rows.map((r) => ({
recordedAt: r.recorded_at,
localTime: r.local_time, localTime: r.local_time,
isOpen: r.is_open === 1, isOpen: r.is_open === 1,
waitMinutes: r.wait_minutes, waitMinutes: r.wait_minutes,
+14 -3
View File
@@ -12,6 +12,7 @@
import { Hono } from "hono"; import { Hono } from "hono";
import { PARK_MAP } from "../../../lib/parks"; import { PARK_MAP } from "../../../lib/parks";
import { import {
getDayData,
getRideBySlug, getRideBySlug,
getRideSamplesForDay, getRideSamplesForDay,
getRideDailyAggregates, getRideDailyAggregates,
@@ -22,6 +23,7 @@ import {
import { liveRidesCache, fastLaneCache } from "../services/live-cache"; import { liveRidesCache, fastLaneCache } from "../services/live-cache";
import { slugifyRideName } from "../../../lib/ride-slug"; import { slugifyRideName } from "../../../lib/ride-slug";
import { lookupFastLane } from "../../../lib/scrapers/sixflags-waittimes"; import { lookupFastLane } from "../../../lib/scrapers/sixflags-waittimes";
import { getTodayLocal, isWithinOperatingWindow } from "../../../lib/env";
const app = new Hono(); const app = new Hono();
@@ -72,6 +74,15 @@ app.get("/:parkId/rides/:slug", (c) => {
const fastLaneCacheEntry = fastLaneCache.get(parkId); const fastLaneCacheEntry = fastLaneCache.get(parkId);
const flMatch = liveMatch && fastLaneCacheEntry ? lookupFastLane(liveMatch.name, fastLaneCacheEntry) : null; 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"); c.header("Cache-Control", "public, max-age=60, stale-while-revalidate=120");
return c.json({ return c.json({
park: { park: {
@@ -91,10 +102,10 @@ app.get("/:parkId/rides/:slug", (c) => {
}, },
live: liveMatch live: liveMatch
? { ? {
isOpen: liveMatch.isOpen, isOpen: liveIsOpen,
waitMinutes: liveMatch.waitMinutes, waitMinutes: liveIsOpen ? liveMatch.waitMinutes : 0,
hasFastLane: Boolean(flMatch?.hasFastLane), hasFastLane: Boolean(flMatch?.hasFastLane),
fastLaneMinutes: liveMatch.isOpen ? (flMatch?.fastLaneMinutes ?? null) : null, fastLaneMinutes: liveIsOpen ? (flMatch?.fastLaneMinutes ?? null) : null,
lastUpdated: liveMatch.lastUpdated, lastUpdated: liveMatch.lastUpdated,
} }
: null, : null,
+10 -1
View File
@@ -3,6 +3,7 @@
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts"; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts";
export interface TodaySample { export interface TodaySample {
recordedAt: string;
localTime: string; localTime: string;
isOpen: boolean; isOpen: boolean;
waitMinutes: number | null; waitMinutes: number | null;
@@ -14,10 +15,18 @@ interface Props {
hasFastLane: boolean; hasFastLane: boolean;
} }
const TIME_FMT = new Intl.DateTimeFormat([], {
hour: "2-digit",
minute: "2-digit",
hour12: false,
});
export default function WaitTimeTodayChart({ samples, hasFastLane }: Props) { export default function WaitTimeTodayChart({ samples, hasFastLane }: Props) {
// Map samples: closed periods → null so Recharts breaks the line. // 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) => ({ const data = samples.map((s) => ({
time: s.localTime, time: TIME_FMT.format(new Date(s.recordedAt)),
wait: s.isOpen ? s.waitMinutes : null, wait: s.isOpen ? s.waitMinutes : null,
fl: s.isOpen ? s.fastLaneMinutes : null, fl: s.isOpen ? s.fastLaneMinutes : null,
})); }));