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:
2026-04-23 21:32:38 -04:00
parent 4652a92c29
commit 70b56158d4
15 changed files with 1914 additions and 0 deletions
+39
View File
@@ -0,0 +1,39 @@
import cron from "node-cron";
import { scrapeToday, scrapeCurrentMonth, scrapeUpcomingMonths, scrapeFullYear } from "./scraper";
let initialized = false;
export function startScheduler(): void {
if (initialized) return;
initialized = true;
// Tier 1: Today — every hour during operating season (Mar-Dec)
cron.schedule("0 * * 3-12 *", async () => {
console.log(`[scheduler] tier-1: scraping today @ ${new Date().toISOString()}`);
await scrapeToday().catch((err) => console.error("[scheduler] tier-1 error:", err));
});
// Tier 2: This week — every 6 hours, current month for all parks
cron.schedule("0 */6 * * *", async () => {
console.log(`[scheduler] tier-2: scraping current month @ ${new Date().toISOString()}`);
await scrapeCurrentMonth().catch((err) => console.error("[scheduler] tier-2 error:", err));
});
// Tier 3: Upcoming — twice daily (3 AM, 3 PM), current + next month
cron.schedule("0 3,15 * * *", async () => {
console.log(`[scheduler] tier-3: scraping upcoming months @ ${new Date().toISOString()}`);
await scrapeUpcomingMonths().catch((err) => console.error("[scheduler] tier-3 error:", err));
});
// Tier 4: Full season — once daily at 3 AM
cron.schedule("0 3 * * *", async () => {
console.log(`[scheduler] tier-4: scraping full year @ ${new Date().toISOString()}`);
await scrapeFullYear().catch((err) => console.error("[scheduler] tier-4 error:", err));
});
console.log("[scheduler] cron jobs registered");
console.log(" tier-1: today — hourly (Mar-Dec)");
console.log(" tier-2: current month — every 6h");
console.log(" tier-3: upcoming — 3 AM + 3 PM");
console.log(" tier-4: full year — 3 AM daily");
}