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
+45
View File
@@ -0,0 +1,45 @@
/**
* Slug determinism tests. The slug is a URL-safe secondary key on the
* `rides` table — same name must always produce the same slug.
*
* Run with: npm test
*/
import { test } from "node:test";
import assert from "node:assert/strict";
import { slugifyRideName } from "../lib/ride-slug";
const CASES: [name: string, expected: string][] = [
["Goliath", "goliath"],
["X²", "x"],
["Lex Luthor: Drop of Doom", "lex-luthor-drop-of-doom"],
["Catwoman's Whip", "catwoman-s-whip"],
["Façade", "facade"],
["Le Monstre", "le-monstre"],
["Batman™ The Ride", "batman-the-ride"],
["THE RIDDLER's Revenge", "the-riddler-s-revenge"],
["Joker y Harley Quinn", "joker-y-harley-quinn"],
["Apocalypse the Ride", "apocalypse-the-ride"],
[" Leading and trailing ", "leading-and-trailing"],
["123 Numeric", "123-numeric"],
["!!!", ""],
];
for (const [name, expected] of CASES) {
test(`slugify "${name}" → "${expected}"`, () => {
assert.equal(slugifyRideName(name), expected);
});
}
test("slug is idempotent — slugifying the result yields the same value", () => {
for (const [name] of CASES) {
const once = slugifyRideName(name);
if (once === "") continue;
assert.equal(slugifyRideName(once), once, `Expected idempotent slug for "${name}"`);
}
});
test("same name always produces the same slug", () => {
const name = "Twisted Cyclone";
assert.equal(slugifyRideName(name), slugifyRideName(name));
});