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
+110
View File
@@ -0,0 +1,110 @@
/**
* Timezone bucketing tests for ride wait samples.
*
* Samples are stored with UTC `recorded_at` and pre-bucketed `local_date`
* + `local_time` columns in the park's IANA timezone. These columns are what
* the aggregation queries group on, so the bucketing has to be DST-safe
* across spring forward and fall back.
*
* Run with: npm test
*/
import { test } from "node:test";
import assert from "node:assert/strict";
import { formatLocalDate, formatLocalTime } from "../lib/timezone";
// ── Basic cases ──────────────────────────────────────────────────────────────
test("formatLocalDate produces YYYY-MM-DD in the target zone", () => {
// 2026-05-29 17:00 UTC = 2026-05-29 13:00 ET = 2026-05-29 10:00 PT
const d = new Date("2026-05-29T17:00:00Z");
assert.equal(formatLocalDate(d, "America/New_York"), "2026-05-29");
assert.equal(formatLocalDate(d, "America/Los_Angeles"), "2026-05-29");
});
test("formatLocalDate rolls to previous day for late UTC times in west zones", () => {
// 2026-05-30 04:00 UTC = 2026-05-29 21:00 PT
const d = new Date("2026-05-30T04:00:00Z");
assert.equal(formatLocalDate(d, "America/Los_Angeles"), "2026-05-29");
assert.equal(formatLocalDate(d, "America/New_York"), "2026-05-30");
});
test("formatLocalTime produces HH:MM in 24-hour format", () => {
// 2026-05-29 23:30 UTC = 2026-05-29 19:30 ET = 2026-05-29 16:30 PT
const d = new Date("2026-05-29T23:30:00Z");
assert.equal(formatLocalTime(d, "America/New_York"), "19:30");
assert.equal(formatLocalTime(d, "America/Los_Angeles"), "16:30");
});
// ── DST: spring forward (2026-03-08 in US: 2 AM → 3 AM) ──────────────────────
test("spring forward: time before transition shows in standard offset", () => {
// 2026-03-08 07:30 UTC = 2026-03-08 02:30 EST (before transition completes)
// Actually: at 2026-03-08 07:00 UTC = 2026-03-08 03:00 EDT (after transition)
// Use a clearly-before time:
const before = new Date("2026-03-08T06:30:00Z"); // 01:30 EST
assert.equal(formatLocalDate(before, "America/New_York"), "2026-03-08");
assert.equal(formatLocalTime(before, "America/New_York"), "01:30");
});
test("spring forward: time after transition shows in DST offset", () => {
// 2026-03-08 07:30 UTC = 2026-03-08 03:30 EDT (DST in effect)
const after = new Date("2026-03-08T07:30:00Z");
assert.equal(formatLocalDate(after, "America/New_York"), "2026-03-08");
assert.equal(formatLocalTime(after, "America/New_York"), "03:30");
});
test("spring forward: local_date is consistent across the missing hour", () => {
// The skipped hour is 02:0003:00 EST. Samples bracketing it should still
// bucket to the same local_date.
const before = new Date("2026-03-08T06:30:00Z"); // 01:30 EST
const after = new Date("2026-03-08T07:30:00Z"); // 03:30 EDT
assert.equal(formatLocalDate(before, "America/New_York"), formatLocalDate(after, "America/New_York"));
});
// ── DST: fall back (2026-11-01 in US: 2 AM → 1 AM) ────────────────────────────
test("fall back: time before transition shows in DST offset", () => {
// 2026-11-01 05:30 UTC = 2026-11-01 01:30 EDT (before fall-back at 2 AM EDT)
const beforeFallback = new Date("2026-11-01T05:30:00Z");
assert.equal(formatLocalDate(beforeFallback, "America/New_York"), "2026-11-01");
assert.equal(formatLocalTime(beforeFallback, "America/New_York"), "01:30");
});
test("fall back: time after transition shows in standard offset", () => {
// 2026-11-01 07:30 UTC = 2026-11-01 02:30 EST (after fall-back)
const afterFallback = new Date("2026-11-01T07:30:00Z");
assert.equal(formatLocalDate(afterFallback, "America/New_York"), "2026-11-01");
assert.equal(formatLocalTime(afterFallback, "America/New_York"), "02:30");
});
test("fall back: the same local hour repeats but local_date stays stable", () => {
// 2026-11-01 05:30 UTC = 01:30 EDT
// 2026-11-01 06:30 UTC = 01:30 EST (second occurrence of 01:30 — fall back)
const first = new Date("2026-11-01T05:30:00Z");
const second = new Date("2026-11-01T06:30:00Z");
assert.equal(formatLocalTime(first, "America/New_York"), "01:30");
assert.equal(formatLocalTime(second, "America/New_York"), "01:30");
assert.equal(formatLocalDate(first, "America/New_York"), formatLocalDate(second, "America/New_York"));
});
// ── Cross-zone: a single UTC moment buckets differently per park ─────────────
test("midnight UTC straddles the local-date boundary for west-coast parks", () => {
const utcMidnight = new Date("2026-06-15T00:00:00Z");
// Eastern: still 2026-06-14 20:00
assert.equal(formatLocalDate(utcMidnight, "America/New_York"), "2026-06-14");
// Pacific: 2026-06-14 17:00
assert.equal(formatLocalDate(utcMidnight, "America/Los_Angeles"), "2026-06-14");
});
test("Mountain and Central parks bucket distinctly during the late-evening hour", () => {
// 2026-07-04 04:30 UTC
// = 2026-07-03 21:30 MDT (UTC-6) → date 2026-07-03
// = 2026-07-03 23:30 CDT (UTC-5) → date 2026-07-03
const d = new Date("2026-07-04T04:30:00Z");
assert.equal(formatLocalDate(d, "America/Denver"), "2026-07-03");
assert.equal(formatLocalDate(d, "America/Chicago"), "2026-07-03");
assert.equal(formatLocalTime(d, "America/Denver"), "22:30");
assert.equal(formatLocalTime(d, "America/Chicago"), "23:30");
});