# Thoosie Calendar A week-by-week calendar showing operating hours for all Six Flags Entertainment Group theme parks — including the former Cedar Fair parks. Data is fetched from the Six Flags internal API via a backend service and stored in SQLite. Click any park to see its full month calendar and live ride status with current wait times. ## Parks 24 theme parks across the US, Canada, and Mexico, grouped by region: | Region | Parks | |--------|-------| | **Northeast** | Great Adventure (NJ), New England (MA), Great Escape (NY), Darien Lake (NY), Dorney Park (PA), Canada's Wonderland (ON) | | **Southeast** | Over Georgia, Carowinds (NC), Kings Dominion (VA) | | **Midwest** | Great America (IL), St. Louis (MO), Cedar Point (OH), Kings Island (OH), Valleyfair (MN), Worlds of Fun (MO), Michigan's Adventure (MI) | | **Texas & South** | Over Texas, Fiesta Texas (TX), Frontier City (OK) | | **West & International** | Magic Mountain (CA), Discovery Kingdom (CA), Knott's Berry Farm (CA), California's Great America (CA), Mexico | ## Documentation Detailed docs live in the [`docs/`](docs/) folder: - [**Architecture**](docs/ARCHITECTURE.md) -- system design, data flow, caching layers, database schema, external APIs - [**Operations**](docs/OPERATIONS.md) -- deployment, monitoring, troubleshooting, backup, scheduler management - [**API Reference**](docs/API.md) -- complete backend endpoint documentation with request/response examples - [**Development**](docs/DEVELOPMENT.md) -- local setup, project structure, adding parks, testing, code conventions ## Architecture The app runs as two containers: | Container | Port | Purpose | |-----------|------|---------| | **web** | 3000 | Next.js frontend — pure presentation layer, fetches all data from the backend API | | **backend** | 3001 | Hono API server — owns the SQLite database, runs tiered cron scheduling, handles all external API calls | The frontend makes no direct database or external API calls. All data flows through the backend. ## Tech Stack - **Next.js 15** — App Router, Server Components, standalone output - **Tailwind CSS v4** — `@theme {}` CSS variables, no config file - **Hono** — lightweight TypeScript API framework for the backend - **SQLite** via `better-sqlite3` — owned exclusively by the backend - **node-cron** — tiered scheduling (hourly → daily) for data freshness - **Six Flags CloudFront API** — park operating hours and ride schedules - **Queue-Times.com API** — live ride open/closed status and wait times ## Ride Status The park detail page shows ride open/closed status using a two-tier approach: 1. **Live data (Queue-Times.com)** — when a park is operating, ride status and wait times are fetched from the [Queue-Times.com API](https://queue-times.com/en-US/pages/api) and cached for 5 minutes. All 24 parks are mapped. Displays a **Live** badge with per-ride wait times. 2. **Schedule fallback (Six Flags API)** — when Queue-Times data is unavailable, the app falls back to the nearest upcoming date from the Six Flags schedule API as an approximation. ### Fast Lane wait times A second wait number is fetched from Six Flags' `/wait-times/park/{apiId}` endpoint and joined onto each ride by name. The park page has a **Fast Lane** toggle (persisted in `localStorage.fastLaneMode`) that swaps the displayed wait between regular and Fast Lane. On the today chart, Fast Lane appears as a second line. ### Per-ride history Click any ride name on a park page to open `/park/[id]/ride/[slug]` — a detail page with three tabs: - **Today** — 5-minute wait-time samples (regular + Fast Lane) with outage markers - **7 days** — daily average / max wait and uptime percentage - **30 days** — same aggregates over a longer window Samples are stored in the `ride_wait_samples` table by a Tier-5 cron job that runs every 5 minutes for parks currently within their operating window. Contiguous "ride closed during park hours" runs are shaded on the today chart with a `#N — Hh Mm` label. ### Roller Coaster Filter When live data is shown, a **Coasters only** toggle filters to roller coasters. Coaster lists are hardcoded in `lib/coaster-data.ts`. ## Data Refresh The backend runs a tiered scraping schedule via node-cron: | Tier | Schedule | Scope | |------|----------|-------| | 1 | Hourly (Mar–Dec) | Today's hours for all parks | | 2 | Every 6 hours | Current month for all parks | | 3 | Twice daily (3 AM, 3 PM) | Current + next month | | 4 | Daily at 3 AM | Full year (respects 72h staleness window) | | 5 | Every 5 minutes | Wait-time samples for all currently-open parks (writes `ride_wait_samples`) | Past dates are never overwritten. The hourly tier compares live data against the database before writing — unchanged data is skipped. Each tier has its own concurrency latch — if a tick is still running when the next would fire, the new tick is skipped and logged rather than stacked. A manual trigger is available via the backend API: ```bash curl -X POST http://localhost:3001/api/scrape/trigger?scope=today # scope: today | month | upcoming | full | force ``` --- ## Local Development **Prerequisites:** Node.js 22+, npm ```bash # Install frontend dependencies npm install # Install backend dependencies cd backend && npm install && cd .. ``` ### Start the backend ```bash cd backend npm run dev ``` The backend starts on port 3001, initializes the database, and begins the cron schedule. On first run it creates an empty database — the schedulers will populate it automatically, or trigger a manual scrape. ### Start the frontend ```bash npm run dev ``` Open [http://localhost:3000](http://localhost:3000). Navigate weeks with the `←` / `→` buttons (or arrow keys); your selected week persists across visits via the `tcWeek` cookie. Click any park name to open its detail page. ### Debug a specific park + date Inspect raw API data and parsed output for any park and date: ```bash npm run debug -- --park kingsisland --date 2026-06-15 ``` ### Run tests ```bash npm test ``` --- ## Deployment The app ships as two Docker images: ```bash docker compose up -d ``` Images are built and pushed automatically by CI on every push to `main`. ### Environment variables See [`.env.example`](.env.example) for the full list and defaults. **web:** | Variable | Default | Description | |----------|---------|-------------| | `BACKEND_URL` | _(required in prod)_ | Backend API base URL. Throws at startup if unset when `NODE_ENV=production`. | | `NEXT_PUBLIC_PLAUSIBLE_SRC` | — | Plausible script URL. Analytics only render when both this and the website ID are set. | | `NEXT_PUBLIC_PLAUSIBLE_WEBSITE_ID` | — | Plausible website ID. | **backend:** | Variable | Default | Description | |----------|---------|-------------| | `PORT` | `3001` | Port the Hono server listens on. | | `TZ` | `UTC` | Timezone for cron schedules (e.g. `America/New_York`). | | `PARK_HOURS_STALENESS_HOURS` | `72` | Hours before park schedule data is re-fetched. | | `RATE_LIMIT_PER_MIN` | `60` | Per-IP request limit for the public API, per minute. Enforced by `backend/src/middleware/rate-limit.ts`; over-limit requests get a `429` with a `Retry-After` header. | ### Updating ```bash docker compose pull && docker compose up -d ``` ### Backend API endpoints | Endpoint | Description | |----------|-------------| | `GET /api/calendar/week?start=YYYY-MM-DD` | Week calendar for all parks | | `GET /api/calendar/:parkId/month?month=YYYY-MM` | Month calendar for one park | | `GET /api/parks/:id/rides` | Live rides or schedule fallback | | `GET /api/parks/:id/rides/:slug` | Per-ride detail + today/7d/30d wait-time history | | `GET /api/parks` | Park list with metadata | | `GET /api/status` | Health check, scrape timestamps, DB stats | | `POST /api/scrape/trigger?scope=...` | Manual scrape trigger |