feat: use dateless Six Flags API endpoint for live today data
All checks were successful
Build and Deploy / Build & Push (push) Successful in 17s
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:
10
lib/db.ts
10
lib/db.ts
@@ -46,12 +46,10 @@ export function upsertDay(
|
||||
hoursLabel?: string,
|
||||
specialType?: string
|
||||
) {
|
||||
// Today and past dates: INSERT new rows freely, but NEVER overwrite existing records.
|
||||
// Once an operating day begins the API drops that date from its response, so a
|
||||
// re-scrape would incorrectly record the day as closed. The DB row written when
|
||||
// the date was still in the future is the permanent truth for that day.
|
||||
// Today and future dates: full upsert — hours can change (e.g. weather delays,
|
||||
// early closures) and the dateless API endpoint now returns today's live data.
|
||||
//
|
||||
// Future dates only: full upsert — hours can change and closures can be added.
|
||||
// Past dates: INSERT-only — never overwrite once the day has passed.
|
||||
db.prepare(`
|
||||
INSERT INTO park_days (park_id, date, is_open, hours_label, special_type, scraped_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
@@ -60,7 +58,7 @@ export function upsertDay(
|
||||
hours_label = excluded.hours_label,
|
||||
special_type = excluded.special_type,
|
||||
scraped_at = excluded.scraped_at
|
||||
WHERE park_days.date > date('now')
|
||||
WHERE park_days.date >= date('now')
|
||||
`).run(parkId, date, isOpen ? 1 : 0, hoursLabel ?? null, specialType ?? null, new Date().toISOString());
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user