feat: use dateless Six Flags API endpoint for live today data
All checks were successful
Build and Deploy / Build & Push (push) Successful in 17s

The API without a date param returns today's operating data directly,
invalidating the previous assumption that today's date was always missing.

- Add fetchToday(apiId, revalidate?) to sixflags.ts — calls the dateless
  endpoint with optional ISR cache
- Extract parseApiDay() helper shared by scrapeMonth and fetchToday
- Update upsertDay WHERE clause: >= date('now') so today can be updated
  (was > date('now'), which froze today after first write)
- scrape.ts: add a today-scrape pass after the monthly loop so each run
  always writes fresh today data to the DB
- app/page.tsx: fetch live today data for all parks (5-min ISR) and merge
  into the data map before computing open/closing/weatherDelay status
- app/park/[id]/page.tsx: prefer live today data from API for todayData
  so weather delays and hour changes surface within 5 minutes
- scrapeRidesForDay: update comment only — role unchanged (QT fallback)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Josh Wright
2026-04-05 16:54:06 -04:00
parent 08db97faa8
commit f0faff412c
5 changed files with 102 additions and 41 deletions

View File

@@ -166,13 +166,48 @@ function apiDateToIso(apiDate: string): string {
return `${yyyy}-${mm}-${dd}`;
}
/** Parse a single ApiDay into a DayResult. Shared by scrapeMonth and fetchToday. */
function parseApiDay(d: ApiDay): DayResult {
const date = parseApiDate(d.date);
const operating =
d.operatings?.find((o) => o.operatingTypeName === "Park") ??
d.operatings?.[0];
const item = operating?.items?.[0];
const hoursLabel =
item?.timeFrom && item?.timeTo
? `${fmt24(item.timeFrom)} ${fmt24(item.timeTo)}`
: undefined;
const isPassholderPreview = d.events?.some((e) =>
e.extEventName.toLowerCase().includes("passholder preview")
) ?? false;
const isBuyout = item?.isBuyout ?? false;
const isOpen = !d.isParkClosed && hoursLabel !== undefined && (!isBuyout || isPassholderPreview);
const specialType: DayResult["specialType"] = isPassholderPreview ? "passholder_preview" : undefined;
return { date, isOpen, hoursLabel: isOpen ? hoursLabel : undefined, specialType };
}
/**
* Fetch ride operating status for a given date.
* Fetch today's operating data directly (no date param = API returns today).
* Pass `revalidate` (seconds) for Next.js ISR caching; omit for a fully fresh fetch.
*/
export async function fetchToday(apiId: number, revalidate?: number): Promise<DayResult | null> {
try {
const url = `${API_BASE}/${apiId}`;
const raw = await fetchApi(url, 0, 0, revalidate);
if (!raw.dates.length) return null;
return parseApiDay(raw.dates[0]);
} catch {
return null;
}
}
/**
* Fetch ride operating status for a given date. Used as a fallback when
* Queue-Times live data is unavailable.
*
* The Six Flags API drops dates that have already started (including today),
* returning only tomorrow onwards. When the requested date is missing, we fall
* back to the nearest available upcoming date in the same month's response so
* the UI can still show a useful (if approximate) schedule.
* The monthly API endpoint (`?date=YYYYMM`) may not include today; use
* `fetchToday(apiId)` to get today's park hours directly. The fallback
* chain here will find the nearest upcoming date if an exact match is missing.
*
* Returns null if no ride data could be found at all (API error, pre-season,
* no venues in response).
@@ -286,30 +321,7 @@ export async function scrapeMonth(
const data = await fetchApi(url);
return data.dates.map((d): DayResult => {
const date = parseApiDate(d.date);
// Prefer the "Park" operating entry; fall back to first entry
const operating =
d.operatings?.find((o) => o.operatingTypeName === "Park") ??
d.operatings?.[0];
const item = operating?.items?.[0];
const hoursLabel =
item?.timeFrom && item?.timeTo
? `${fmt24(item.timeFrom)} ${fmt24(item.timeTo)}`
: undefined;
const isPassholderPreview = d.events?.some((e) =>
e.extEventName.toLowerCase().includes("passholder preview")
) ?? false;
const isBuyout = item?.isBuyout ?? false;
// Buyout days are private events — treat as closed unless it's a passholder preview
const isOpen = !d.isParkClosed && hoursLabel !== undefined && (!isBuyout || isPassholderPreview);
const specialType: DayResult["specialType"] = isPassholderPreview ? "passholder_preview" : undefined;
return { date, isOpen, hoursLabel: isOpen ? hoursLabel : undefined, specialType };
});
return data.dates.map(parseApiDay);
}
/**