From 7c88a3e5685d245536b8e0d5f3266a7e9f667ee4 Mon Sep 17 00:00:00 2001 From: josh Date: Sat, 30 May 2026 07:57:08 -0400 Subject: [PATCH] fix: only sample wait times within each park's operating window MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Queue-Times keeps reporting yesterday's last wait with isOpen=true overnight, so the per-ride open check wasn't enough — the sampler was recording phantom "open" samples between close and the next morning's first refresh, padding both wait-time averages and uptime% with stale data. Add isWithinOperatingWindow gate (same check the /rides route uses) so the sampler only runs during the park's actual hours plus the 1-hour closing buffer. Includes a one-off wipe script for the accumulated bad data. Co-Authored-By: Claude Opus 4.7 --- backend/scripts/wipe-samples.ts | 26 ++++++++++++++++++++++++++ backend/src/services/wait-sampler.ts | 10 ++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 backend/scripts/wipe-samples.ts diff --git a/backend/scripts/wipe-samples.ts b/backend/scripts/wipe-samples.ts new file mode 100644 index 0000000..3245651 --- /dev/null +++ b/backend/scripts/wipe-samples.ts @@ -0,0 +1,26 @@ +/** + * One-off cleanup: wipe ride history accumulated before the operating-window + * gate was added to the sampler. Safe to re-run; truncates only the two + * sample-related tables, leaves park_days untouched. + */ +import Database from "better-sqlite3"; +import path from "path"; + +const dbPath = path.join(__dirname, "..", "data", "parks.db"); +const db = new Database(dbPath); + +const before = { + samples: (db.prepare("SELECT COUNT(*) AS c FROM ride_wait_samples").get() as { c: number }).c, + rides: (db.prepare("SELECT COUNT(*) AS c FROM rides").get() as { c: number }).c, +}; +console.log("Before:", before); + +db.exec("DELETE FROM ride_wait_samples; DELETE FROM rides;"); + +const after = { + samples: (db.prepare("SELECT COUNT(*) AS c FROM ride_wait_samples").get() as { c: number }).c, + rides: (db.prepare("SELECT COUNT(*) AS c FROM rides").get() as { c: number }).c, +}; +console.log("After: ", after); + +db.close(); diff --git a/backend/src/services/wait-sampler.ts b/backend/src/services/wait-sampler.ts index 169383d..858cf0b 100644 --- a/backend/src/services/wait-sampler.ts +++ b/backend/src/services/wait-sampler.ts @@ -23,6 +23,7 @@ import { fetchLiveRides } from "../../../lib/scrapers/queuetimes"; import { fetchFastLaneWaits, lookupFastLane } from "../../../lib/scrapers/sixflags-waittimes"; import { slugifyRideName } from "../../../lib/ride-slug"; import { formatLocalDate, formatLocalTime } from "../../../lib/timezone"; +import { isWithinOperatingWindow } from "../../../lib/env"; import { liveRidesCache, fastLaneCache } from "./live-cache"; import { getDayData, upsertRide, insertSample, transact } from "../db/queries"; @@ -133,10 +134,15 @@ export async function sampleAllOpenParks(): Promise { errors: 0, }; - // Filter to parks open today. + // Filter to parks that are open today AND currently within their operating + // window. Queue-Times keeps reporting yesterday's last wait with isOpen=true + // overnight, so the per-ride open check isn't enough on its own — we'd + // otherwise pollute uptime stats with phantom open samples between close + // and the next morning's first refresh. const openParks = PARKS.filter((park) => { const day = getDayData(park.id, today); - return day?.isOpen ?? false; + if (!day?.isOpen || !day.hoursLabel) return false; + return isWithinOperatingWindow(day.hoursLabel, park.timezone); }); result.parksSkipped = PARKS.length - openParks.length;