feat: add per-ride history charts with wait time and uptime tracking
Build and Deploy / Build & Push (push) Successful in 3m7s

Adds a cron-driven sampler that snapshots Queue-Times waits and Six Flags
Fast Lane data every 5 minutes into a new ride_wait_samples table, and a
clickable per-ride detail page at /park/[id]/ride/[slug] with Today / 7d /
30d Recharts views plus a 30d uptime pill. Rides are keyed by Queue-Times'
stable qt_ride_id so renames don't fragment history. Samples store
pre-bucketed local_date and local_time in the park's IANA timezone so
aggregations are pure SQL and DST-safe.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-29 23:35:27 -04:00
parent bfe099322f
commit 4f838d99c1
25 changed files with 2052 additions and 18 deletions
+13 -6
View File
@@ -1,15 +1,18 @@
"use client";
import { useState, useEffect } from "react";
import Link from "next/link";
import type { LiveRidesResult, LiveRide } from "@/lib/scrapers/queuetimes";
import { slugifyRideName } from "@/lib/ride-slug";
interface LiveRidePanelProps {
parkId: string;
liveRides: LiveRidesResult;
parkOpenToday: boolean;
isWeatherDelay?: boolean;
}
export function LiveRidePanel({ liveRides, parkOpenToday, isWeatherDelay }: LiveRidePanelProps) {
export function LiveRidePanel({ parkId, liveRides, parkOpenToday, isWeatherDelay }: LiveRidePanelProps) {
const { rides } = liveRides;
const hasCoasters = rides.some((r) => r.isCoaster);
const hasFastLane = rides.some((r) => r.hasFastLane);
@@ -183,21 +186,22 @@ export function LiveRidePanel({ liveRides, parkOpenToday, isWeatherDelay }: Live
gridTemplateColumns: "repeat(auto-fill, minmax(280px, 1fr))",
gap: 6,
}}>
{openRides.map((ride) => <RideRow key={ride.name} ride={ride} fastLaneMode={fastLaneMode} />)}
{closedRides.map((ride) => <RideRow key={ride.name} ride={ride} fastLaneMode={fastLaneMode} />)}
{openRides.map((ride) => <RideRow key={ride.name} parkId={parkId} ride={ride} fastLaneMode={fastLaneMode} />)}
{closedRides.map((ride) => <RideRow key={ride.name} parkId={parkId} ride={ride} fastLaneMode={fastLaneMode} />)}
</div>
</div>
);
}
function RideRow({ ride, fastLaneMode }: { ride: LiveRide; fastLaneMode: boolean }) {
function RideRow({ parkId, ride, fastLaneMode }: { parkId: string; ride: LiveRide; fastLaneMode: boolean }) {
const showWait = ride.isOpen && ride.waitMinutes > 0;
const fastLaneActive = fastLaneMode && ride.hasFastLane;
const flWait = ride.fastLaneMinutes ?? 0;
const slug = ride.slug ?? slugifyRideName(ride.name);
return (
<div style={{
<Link href={`/park/${parkId}/ride/${slug}`} className="ride-row-link" style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
@@ -207,6 +211,9 @@ function RideRow({ ride, fastLaneMode }: { ride: LiveRide; fastLaneMode: boolean
border: `1px solid ${ride.isOpen ? "var(--color-open-border)" : "var(--color-border)"}`,
borderRadius: 8,
opacity: ride.isOpen ? 1 : 0.6,
textDecoration: "none",
color: "inherit",
cursor: "pointer",
}}>
<div style={{ display: "flex", alignItems: "center", gap: 8, minWidth: 0 }}>
<span style={{
@@ -277,6 +284,6 @@ function RideRow({ ride, fastLaneMode }: { ride: LiveRide; fastLaneMode: boolean
)}
</>
)}
</div>
</Link>
);
}