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
+88
View File
@@ -0,0 +1,88 @@
"use client";
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts";
export interface TodaySample {
localTime: string;
isOpen: boolean;
waitMinutes: number | null;
fastLaneMinutes: number | null;
}
interface Props {
samples: TodaySample[];
hasFastLane: boolean;
}
export default function WaitTimeTodayChart({ samples, hasFastLane }: Props) {
// Map samples: closed periods → null so Recharts breaks the line.
const data = samples.map((s) => ({
time: s.localTime,
wait: s.isOpen ? s.waitMinutes : null,
fl: s.isOpen ? s.fastLaneMinutes : null,
}));
// Show every Nth tick on the X axis so labels don't overlap.
const tickInterval = Math.max(1, Math.floor(data.length / 8));
return (
<div style={{ width: "100%", height: 280 }}>
<ResponsiveContainer>
<LineChart data={data} margin={{ top: 8, right: 16, left: 0, bottom: 4 }}>
<CartesianGrid stroke="#272727" strokeDasharray="3 3" vertical={false} />
<XAxis
dataKey="time"
stroke="#737373"
tick={{ fontSize: 11, fill: "#737373" }}
interval={tickInterval}
tickLine={false}
/>
<YAxis
stroke="#737373"
tick={{ fontSize: 11, fill: "#737373" }}
tickLine={false}
axisLine={false}
label={{ value: "min", position: "insideLeft", angle: -90, offset: 16, style: { fontSize: 10, fill: "#737373" } }}
/>
<Tooltip
contentStyle={{
background: "#1c1c1c",
border: "1px solid #333",
borderRadius: 6,
fontSize: "0.75rem",
color: "#f5f5f5",
}}
labelStyle={{ color: "#b0b0b0" }}
formatter={(value, name) => {
if (value === null || value === undefined) return ["—", name];
return [`${value} min`, name];
}}
/>
<Legend wrapperStyle={{ fontSize: "0.72rem", paddingTop: 4 }} />
<Line
name="Wait"
type="monotone"
dataKey="wait"
stroke="#4ade80"
strokeWidth={2}
dot={false}
connectNulls={false}
isAnimationActive={false}
/>
{hasFastLane && (
<Line
name="Fast Lane"
type="monotone"
dataKey="fl"
stroke="#ff4d8d"
strokeWidth={2}
dot={false}
connectNulls={false}
isAnimationActive={false}
/>
)}
</LineChart>
</ResponsiveContainer>
</div>
);
}