f87462385c
Surfaces features that landed after the last big docs pass: per-ride history pages, Fast Lane wait times, outage shading on the today chart, Tier-5 wait-time sampler, production-hardening pieces (rate limiter, structured logger, env validation, graceful shutdown), and the new rides + ride_wait_samples tables. Also corrects the weather-delay rule to match the "open" vs "closing" gate now in rides.ts.
189 lines
7.7 KiB
Markdown
189 lines
7.7 KiB
Markdown
# 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 |
|