fix: suppress weather-delay flag during post-close wind-down
Parks naturally wind down rides over the 1-hour buffer after their scheduled close, so all-rides-closed in that window isn't a weather delay — it's just closing time. Both the calendar UI badge and the sampler's telemetry counter were misclassifying this. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -124,11 +124,14 @@ app.get("/week", async (c) => {
|
||||
|
||||
// Only flag weather delay when we know rides are actually closed. An
|
||||
// "unknown" entry means our upstream fetch failed — claim no badge rather
|
||||
// than falsely showing the storm icon for an outage.
|
||||
// than falsely showing the storm icon for an outage. Parks in the
|
||||
// post-close wind-down buffer ("closing") naturally have rides shutting
|
||||
// off, so suppress the weather badge there too.
|
||||
const closingSet = new Set(closingParkIds);
|
||||
for (const { id, entry } of results) {
|
||||
if (entry.kind !== "ok") continue;
|
||||
if (entry.openRides === 0) {
|
||||
weatherDelayParkIds.push(id);
|
||||
if (!closingSet.has(id)) weatherDelayParkIds.push(id);
|
||||
} else {
|
||||
rideCounts[id] = entry.openRides;
|
||||
}
|
||||
|
||||
@@ -18,12 +18,11 @@ import { PARKS } from "../../../lib/parks";
|
||||
import type { Park } from "../../../lib/scrapers/types";
|
||||
import { QUEUE_TIMES_IDS } from "../../../lib/queue-times-map";
|
||||
import { getCoasterSet } from "../../../lib/coaster-data";
|
||||
import { getTodayLocal } from "../../../lib/env";
|
||||
import { getTodayLocal, getOperatingStatus } from "../../../lib/env";
|
||||
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";
|
||||
import { log } from "../log";
|
||||
@@ -39,7 +38,11 @@ export interface SampleRunResult {
|
||||
errors: number;
|
||||
}
|
||||
|
||||
async function samplePark(park: Park, now: Date): Promise<{
|
||||
async function samplePark(
|
||||
park: Park,
|
||||
now: Date,
|
||||
status: "open" | "closing",
|
||||
): Promise<{
|
||||
ridesUpserted: number;
|
||||
samplesWritten: number;
|
||||
weatherDelayed: boolean;
|
||||
@@ -65,10 +68,17 @@ async function samplePark(park: Park, now: Date): Promise<{
|
||||
return { ridesUpserted: 0, samplesWritten: 0, weatherDelayed: false, error: false };
|
||||
}
|
||||
|
||||
// Weather-delay heuristic — skip writing so uptime stays honest.
|
||||
// Weather-delay heuristic — skip writing so uptime stays honest. Only
|
||||
// applies during scheduled hours; in the post-close wind-down buffer,
|
||||
// rides legitimately finish operating, so don't mislabel that as weather.
|
||||
const anyOpen = liveRides.rides.some((r) => r.isOpen);
|
||||
if (!anyOpen) {
|
||||
return { ridesUpserted: 0, samplesWritten: 0, weatherDelayed: true, error: false };
|
||||
return {
|
||||
ridesUpserted: 0,
|
||||
samplesWritten: 0,
|
||||
weatherDelayed: status === "open",
|
||||
error: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Fast Lane — reuse cache; fetch on miss.
|
||||
@@ -150,21 +160,25 @@ export async function sampleAllOpenParks(): Promise<SampleRunResult> {
|
||||
};
|
||||
|
||||
// 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) => {
|
||||
// window (including the 1-hour post-close wind-down). 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: Array<{ park: Park; status: "open" | "closing" }> = [];
|
||||
for (const park of PARKS) {
|
||||
const day = getDayData(park.id, today);
|
||||
if (!day?.isOpen || !day.hoursLabel) return false;
|
||||
return isWithinOperatingWindow(day.hoursLabel, park.timezone);
|
||||
});
|
||||
if (!day?.isOpen || !day.hoursLabel) continue;
|
||||
const status = getOperatingStatus(day.hoursLabel, park.timezone);
|
||||
if (status === "closed") continue;
|
||||
openParks.push({ park, status });
|
||||
}
|
||||
result.parksSkipped = PARKS.length - openParks.length;
|
||||
|
||||
// Fan out in bounded chunks so we don't blast 24 requests in parallel.
|
||||
for (let i = 0; i < openParks.length; i += PARALLEL_CHUNK) {
|
||||
const chunk = openParks.slice(i, i + PARALLEL_CHUNK);
|
||||
const chunkResults = await Promise.all(chunk.map((park) => samplePark(park, now)));
|
||||
const chunkResults = await Promise.all(chunk.map(({ park, status }) => samplePark(park, now, status)));
|
||||
for (const r of chunkResults) {
|
||||
if (r.error) result.errors++;
|
||||
else if (r.weatherDelayed) result.weatherDelayed++;
|
||||
|
||||
Reference in New Issue
Block a user