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>
151 lines
5.5 KiB
TypeScript
151 lines
5.5 KiB
TypeScript
import { HomePageClient } from "@/components/HomePageClient";
|
|
import { PARKS } from "@/lib/parks";
|
|
import { openDb, getDateRange, getApiId } from "@/lib/db";
|
|
import { getTodayLocal, isWithinOperatingWindow, getOperatingStatus } from "@/lib/env";
|
|
import { fetchLiveRides } from "@/lib/scrapers/queuetimes";
|
|
import { fetchToday } from "@/lib/scrapers/sixflags";
|
|
import { QUEUE_TIMES_IDS } from "@/lib/queue-times-map";
|
|
import { readParkMeta, getCoasterSet } from "@/lib/park-meta";
|
|
import type { DayData } from "@/lib/db";
|
|
|
|
interface PageProps {
|
|
searchParams: Promise<{ week?: string }>;
|
|
}
|
|
|
|
function getWeekStart(param: string | undefined): string {
|
|
if (param && /^\d{4}-\d{2}-\d{2}$/.test(param)) {
|
|
const d = new Date(param + "T00:00:00");
|
|
if (!isNaN(d.getTime())) {
|
|
d.setDate(d.getDate() - d.getDay());
|
|
return d.toISOString().slice(0, 10);
|
|
}
|
|
}
|
|
const todayIso = getTodayLocal();
|
|
const d = new Date(todayIso + "T00:00:00");
|
|
d.setDate(d.getDate() - d.getDay());
|
|
return d.toISOString().slice(0, 10);
|
|
}
|
|
|
|
function getWeekDates(sundayIso: string): string[] {
|
|
return Array.from({ length: 7 }, (_, i) => {
|
|
const d = new Date(sundayIso + "T00:00:00");
|
|
d.setDate(d.getDate() + i);
|
|
return d.toISOString().slice(0, 10);
|
|
});
|
|
}
|
|
|
|
function getCurrentWeekStart(): string {
|
|
const todayIso = getTodayLocal();
|
|
const d = new Date(todayIso + "T00:00:00");
|
|
d.setDate(d.getDate() - d.getDay());
|
|
return d.toISOString().slice(0, 10);
|
|
}
|
|
|
|
export default async function HomePage({ searchParams }: PageProps) {
|
|
const params = await searchParams;
|
|
const weekStart = getWeekStart(params.week);
|
|
const weekDates = getWeekDates(weekStart);
|
|
const endDate = weekDates[6];
|
|
const today = getTodayLocal();
|
|
const isCurrentWeek = weekStart === getCurrentWeekStart();
|
|
|
|
const db = openDb();
|
|
const data = getDateRange(db, weekStart, endDate);
|
|
|
|
// Merge live today data from the Six Flags API (dateless endpoint, 5-min ISR cache).
|
|
// This ensures weather delays, early closures, and hour changes surface within 5 minutes
|
|
// without waiting for the next scheduled scrape. Only fetched when viewing the current week.
|
|
if (weekDates.includes(today)) {
|
|
const todayResults = await Promise.all(
|
|
PARKS.map(async (p) => {
|
|
const apiId = getApiId(db, p.id);
|
|
if (!apiId) return null;
|
|
const live = await fetchToday(apiId, 300); // 5-min ISR cache
|
|
return live ? { parkId: p.id, live } : null;
|
|
})
|
|
);
|
|
for (const result of todayResults) {
|
|
if (!result) continue;
|
|
const { parkId, live } = result;
|
|
if (!data[parkId]) data[parkId] = {};
|
|
data[parkId][today] = {
|
|
isOpen: live.isOpen,
|
|
hoursLabel: live.hoursLabel ?? null,
|
|
specialType: live.specialType ?? null,
|
|
} satisfies DayData;
|
|
}
|
|
}
|
|
|
|
db.close();
|
|
|
|
const scrapedCount = Object.values(data).reduce(
|
|
(sum, parkData) => sum + Object.keys(parkData).length,
|
|
0
|
|
);
|
|
|
|
// Always fetch both ride and coaster counts — the client decides which to display.
|
|
const parkMeta = readParkMeta();
|
|
const hasCoasterData = PARKS.some((p) => (parkMeta[p.id]?.coasters.length ?? 0) > 0);
|
|
|
|
let rideCounts: Record<string, number> = {};
|
|
let coasterCounts: Record<string, number> = {};
|
|
let closingParkIds: string[] = [];
|
|
let openParkIds: string[] = [];
|
|
let weatherDelayParkIds: string[] = [];
|
|
if (weekDates.includes(today)) {
|
|
// Parks within operating hours right now (for open dot — independent of ride counts)
|
|
const openTodayParks = PARKS.filter((p) => {
|
|
const dayData = data[p.id]?.[today];
|
|
if (!dayData?.isOpen || !dayData.hoursLabel) return false;
|
|
return isWithinOperatingWindow(dayData.hoursLabel, p.timezone);
|
|
});
|
|
openParkIds = openTodayParks.map((p) => p.id);
|
|
closingParkIds = openTodayParks
|
|
.filter((p) => {
|
|
const dayData = data[p.id]?.[today];
|
|
return dayData?.hoursLabel
|
|
? getOperatingStatus(dayData.hoursLabel, p.timezone) === "closing"
|
|
: false;
|
|
})
|
|
.map((p) => p.id);
|
|
// Only fetch ride counts for parks that have queue-times coverage
|
|
const trackedParks = openTodayParks.filter((p) => QUEUE_TIMES_IDS[p.id]);
|
|
const results = await Promise.all(
|
|
trackedParks.map(async (p) => {
|
|
const coasterSet = getCoasterSet(p.id, parkMeta);
|
|
const result = await fetchLiveRides(QUEUE_TIMES_IDS[p.id], coasterSet, 300);
|
|
const rideCount = result ? result.rides.filter((r) => r.isOpen).length : null;
|
|
const coasterCount = result ? result.rides.filter((r) => r.isOpen && r.isCoaster).length : 0;
|
|
return { id: p.id, rideCount, coasterCount };
|
|
})
|
|
);
|
|
// Parks with queue-times coverage but 0 open rides = likely weather delay
|
|
weatherDelayParkIds = results
|
|
.filter(({ rideCount }) => rideCount === 0)
|
|
.map(({ id }) => id);
|
|
rideCounts = Object.fromEntries(
|
|
results.filter(({ rideCount }) => rideCount != null && rideCount > 0).map(({ id, rideCount }) => [id, rideCount!])
|
|
);
|
|
coasterCounts = Object.fromEntries(
|
|
results.filter(({ coasterCount }) => coasterCount > 0).map(({ id, coasterCount }) => [id, coasterCount])
|
|
);
|
|
}
|
|
|
|
return (
|
|
<HomePageClient
|
|
weekStart={weekStart}
|
|
weekDates={weekDates}
|
|
today={today}
|
|
isCurrentWeek={isCurrentWeek}
|
|
data={data}
|
|
rideCounts={rideCounts}
|
|
coasterCounts={coasterCounts}
|
|
openParkIds={openParkIds}
|
|
closingParkIds={closingParkIds}
|
|
weatherDelayParkIds={weatherDelayParkIds}
|
|
hasCoasterData={hasCoasterData}
|
|
scrapedCount={scrapedCount}
|
|
/>
|
|
);
|
|
}
|