feat: add Hono backend API server with tiered scheduler
Standalone Node.js backend that owns the SQLite database and serves composed data via REST endpoints. Replaces the shell-scheduled scraper with in-process node-cron tiered scheduling. Backend structure: - Hono HTTP server on port 3001 with CORS and request logging - Singleton SQLite connection with WAL mode - In-memory TTL cache for Queue-Times and fetchToday responses - Comparison check on fetchToday (read-before-write, only upserts on change) API endpoints: - GET /api/calendar/week — week schedule + live ride counts for all parks - GET /api/calendar/:parkId/month — month calendar for one park - GET /api/parks — park list with metadata - GET /api/parks/:id — single park detail - GET /api/parks/:id/rides — live rides with Queue-Times/schedule fallback - GET /api/status — health check, scrape stats - POST /api/scrape/trigger — manual scrape (scope: today/month/upcoming/full) Scheduler tiers: - Tier 1: today — hourly (Mar-Dec) - Tier 2: current month — every 6 hours - Tier 3: upcoming — twice daily (3 AM + 3 PM) - Tier 4: full year — daily at 3 AM Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import { Hono } from "hono";
|
||||
import { PARK_MAP } from "../../../lib/parks";
|
||||
import { QUEUE_TIMES_IDS } from "../../../lib/queue-times-map";
|
||||
import { getCoasterSet } from "../../../lib/coaster-data";
|
||||
import { getTodayLocal, isWithinOperatingWindow } from "../../../lib/env";
|
||||
import { fetchLiveRides } from "../../../lib/scrapers/queuetimes";
|
||||
import { scrapeRidesForDay } from "../../../lib/scrapers/sixflags";
|
||||
import { getDayData } from "../db/queries";
|
||||
import { TtlCache } from "../services/cache";
|
||||
import type { LiveRidesResult } from "../../../lib/scrapers/queuetimes";
|
||||
|
||||
const liveRidesCache = new TtlCache<LiveRidesResult | null>(5 * 60 * 1000);
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
app.get("/:id/rides", async (c) => {
|
||||
const id = c.req.param("id");
|
||||
const park = PARK_MAP.get(id);
|
||||
if (!park) return c.json({ error: "Park not found" }, 404);
|
||||
|
||||
const today = getTodayLocal();
|
||||
const todayData = getDayData(id, today);
|
||||
const withinWindow = todayData?.hoursLabel
|
||||
? isWithinOperatingWindow(todayData.hoursLabel, park.timezone)
|
||||
: false;
|
||||
|
||||
const queueTimesId = QUEUE_TIMES_IDS[id];
|
||||
let liveRides: LiveRidesResult | null = null;
|
||||
|
||||
if (queueTimesId) {
|
||||
liveRides = liveRidesCache.get(id);
|
||||
if (liveRides === null) {
|
||||
const coasterSet = getCoasterSet(id);
|
||||
liveRides = await fetchLiveRides(queueTimesId, coasterSet).catch(() => null);
|
||||
if (liveRides) liveRidesCache.set(id, liveRides);
|
||||
}
|
||||
|
||||
if (liveRides && !withinWindow) {
|
||||
liveRides = {
|
||||
...liveRides,
|
||||
rides: liveRides.rides.map((r) => ({ ...r, isOpen: false, waitMinutes: 0 })),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const isWeatherDelay =
|
||||
withinWindow && liveRides !== null && liveRides.rides.length > 0 && liveRides.rides.every((r) => !r.isOpen);
|
||||
|
||||
let scheduleFallback = null;
|
||||
if (!liveRides) {
|
||||
scheduleFallback = await scrapeRidesForDay(park.apiId, today).catch(() => null);
|
||||
}
|
||||
|
||||
c.header("Cache-Control", "public, max-age=60, stale-while-revalidate=120");
|
||||
return c.json({
|
||||
parkId: id,
|
||||
today,
|
||||
parkOpenToday: !!(todayData?.isOpen && todayData?.hoursLabel),
|
||||
withinWindow,
|
||||
isWeatherDelay,
|
||||
liveRides,
|
||||
scheduleFallback,
|
||||
});
|
||||
});
|
||||
|
||||
export default app;
|
||||
Reference in New Issue
Block a user