fix: render today's wait chart in viewer's local time + close stale live state
Build and Deploy / Build & Push (push) Successful in 1m8s
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:
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user