docs: update README for web + backend architecture
Build and Deploy / Build & Push (push) Successful in 34s
Build and Deploy / Build & Push (push) Successful in 34s
Remove references to Playwright discovery, RCDB scraping, scraper container, and npm run scripts. Document the new two-container setup, tiered scheduling, backend API endpoints, and local dev workflow. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Thoosie Calendar
|
# 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 scraped from the Six Flags internal API and stored locally in SQLite. Click any park to see its full month calendar and live ride status with current wait times.
|
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
|
## Parks
|
||||||
|
|
||||||
@@ -14,14 +14,26 @@ A week-by-week calendar showing operating hours for all Six Flags Entertainment
|
|||||||
| **Texas & South** | Over Texas, Fiesta Texas (TX), Frontier City (OK) |
|
| **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 |
|
| **West & International** | Magic Mountain (CA), Discovery Kingdom (CA), Knott's Berry Farm (CA), California's Great America (CA), Mexico |
|
||||||
|
|
||||||
|
## 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
|
## Tech Stack
|
||||||
|
|
||||||
- **Next.js 15** — App Router, Server Components, standalone output
|
- **Next.js 15** — App Router, Server Components, standalone output
|
||||||
- **Tailwind CSS v4** — `@theme {}` CSS variables, no config file
|
- **Tailwind CSS v4** — `@theme {}` CSS variables, no config file
|
||||||
- **SQLite** via `better-sqlite3` — persisted in `/app/data/parks.db`
|
- **Hono** — lightweight TypeScript API framework for the backend
|
||||||
- **Playwright** — one-time headless browser run to discover each park's internal API ID
|
- **SQLite** via `better-sqlite3` — owned exclusively by the backend
|
||||||
- **Six Flags CloudFront API** — `https://d18car1k0ff81h.cloudfront.net/operating-hours/park/{id}?date=YYYYMM`
|
- **node-cron** — tiered scheduling (hourly → daily) for data freshness
|
||||||
- **Queue-Times.com API** — live ride open/closed status and wait times, updated every 5 minutes
|
- **Six Flags CloudFront API** — park operating hours and ride schedules
|
||||||
|
- **Queue-Times.com API** — live ride open/closed status and wait times
|
||||||
|
|
||||||
## Ride Status
|
## Ride Status
|
||||||
|
|
||||||
@@ -29,14 +41,31 @@ 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.
|
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)** — the Six Flags operating-hours API drops the current day from its response once a park opens. When Queue-Times data is unavailable, the app falls back to the nearest upcoming date from the Six Flags schedule API as an approximation.
|
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.
|
||||||
|
|
||||||
### Roller Coaster Filter
|
### Roller Coaster Filter
|
||||||
|
|
||||||
When live data is shown, a **Coasters only** toggle appears if roller coaster data has been populated for that park. Coaster lists are sourced from [RCDB](https://rcdb.com) and stored in `data/park-meta.json`. To populate them:
|
When live data is shown, a **Coasters only** toggle filters to roller coasters. Coaster lists are hardcoded in `lib/coaster-data.ts`.
|
||||||
|
|
||||||
1. Open `data/park-meta.json` and set `rcdb_id` for each park to the numeric RCDB park ID (visible in the URL: `https://rcdb.com/4529.htm` → `4529`).
|
## Data Refresh
|
||||||
2. Run `npm run scrape` — coaster lists are fetched from RCDB and stored in the JSON file. They refresh automatically every 30 days on subsequent scrapes.
|
|
||||||
|
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) |
|
||||||
|
|
||||||
|
Past dates are never overwritten. The hourly tier compares live data against the database before writing — unchanged data is skipped.
|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -45,29 +74,29 @@ When live data is shown, a **Coasters only** toggle appears if roller coaster da
|
|||||||
**Prerequisites:** Node.js 22+, npm
|
**Prerequisites:** Node.js 22+, npm
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Install frontend dependencies
|
||||||
npm install
|
npm install
|
||||||
npx playwright install chromium
|
|
||||||
|
# Install backend dependencies
|
||||||
|
cd backend && npm install && cd ..
|
||||||
```
|
```
|
||||||
|
|
||||||
### Seed the database
|
### Start the backend
|
||||||
|
|
||||||
Run once to discover each park's internal API ID (opens a headless browser per park):
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run discover
|
cd backend
|
||||||
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Scrape operating hours for the full year:
|
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
|
```bash
|
||||||
npm run scrape
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Force a full re-scrape (ignores the staleness window):
|
Open [http://localhost:3000](http://localhost:3000). Navigate weeks with the `←` / `→` buttons, or pass `?week=YYYY-MM-DD` directly. Click any park name to open its detail page.
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run scrape:force
|
|
||||||
```
|
|
||||||
|
|
||||||
### Debug a specific park + date
|
### Debug a specific park + date
|
||||||
|
|
||||||
@@ -77,78 +106,38 @@ Inspect raw API data and parsed output for any park and date:
|
|||||||
npm run debug -- --park kingsisland --date 2026-06-15
|
npm run debug -- --park kingsisland --date 2026-06-15
|
||||||
```
|
```
|
||||||
|
|
||||||
Output is printed to the terminal and saved to `debug/{parkId}_{date}.txt`.
|
|
||||||
|
|
||||||
### Run tests
|
### Run tests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm test
|
npm test
|
||||||
```
|
```
|
||||||
|
|
||||||
### Run the dev server
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Open [http://localhost:3000](http://localhost:3000). Navigate weeks with the `←` / `→` buttons, or pass `?week=YYYY-MM-DD` directly. Click any park name to open its detail page.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
The app ships as two separate Docker images that share a named volume for the SQLite database:
|
The app ships as two Docker images:
|
||||||
|
|
||||||
| Image | Tag | Purpose |
|
|
||||||
|-------|-----|---------|
|
|
||||||
| Next.js web server | `:web` | Reads DB, serves content. No scraping tools. |
|
|
||||||
| Scraper + scheduler | `:scraper` | Nightly data refresh. No web server. |
|
|
||||||
|
|
||||||
Images are built and pushed automatically by CI on every push to `main`.
|
|
||||||
|
|
||||||
### First-time setup
|
|
||||||
|
|
||||||
**1. Pull the images**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker pull gitea.thewrightserver.net/josh/sixflagssupercalendar:web
|
|
||||||
docker pull gitea.thewrightserver.net/josh/sixflagssupercalendar:scraper
|
|
||||||
```
|
|
||||||
|
|
||||||
**2. Discover park API IDs**
|
|
||||||
|
|
||||||
This one-time step opens a headless browser for each park to find its internal Six Flags API ID. Run it against the scraper image so Playwright is available:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run --rm -v root_park_data:/app/data \
|
|
||||||
gitea.thewrightserver.net/josh/sixflagssupercalendar:scraper \
|
|
||||||
npm run discover
|
|
||||||
```
|
|
||||||
|
|
||||||
**3. Set RCDB IDs for the coaster filter**
|
|
||||||
|
|
||||||
Open `data/park-meta.json` in the Docker volume and set `rcdb_id` for each park to the numeric ID from the RCDB URL (e.g. `https://rcdb.com/4529.htm` → `4529`). You can curl it directly from the repo:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -o /var/lib/docker/volumes/root_park_data/_data/park-meta.json \
|
|
||||||
https://gitea.thewrightserver.net/josh/SixFlagsSuperCalendar/raw/branch/main/data/park-meta.json
|
|
||||||
```
|
|
||||||
|
|
||||||
**4. Run the initial scrape**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run --rm -v root_park_data:/app/data \
|
|
||||||
gitea.thewrightserver.net/josh/sixflagssupercalendar:scraper \
|
|
||||||
npm run scrape
|
|
||||||
```
|
|
||||||
|
|
||||||
**5. Start services**
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
Both services start. The scraper runs nightly at 3 AM (container timezone, set via `TZ`).
|
Images are built and pushed automatically by CI on every push to `main`.
|
||||||
|
|
||||||
|
### Environment variables
|
||||||
|
|
||||||
|
**web:**
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `BACKEND_URL` | `http://backend:3001` | Backend API base URL (Docker internal networking) |
|
||||||
|
|
||||||
|
**backend:**
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `TZ` | `UTC` | Timezone for cron schedules (e.g. `America/New_York`) |
|
||||||
|
| `PARK_HOURS_STALENESS_HOURS` | `72` | Hours before park schedule data is re-fetched |
|
||||||
|
|
||||||
### Updating
|
### Updating
|
||||||
|
|
||||||
@@ -156,34 +145,13 @@ Both services start. The scraper runs nightly at 3 AM (container timezone, set v
|
|||||||
docker compose pull && docker compose up -d
|
docker compose pull && docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
### Scraper environment variables
|
### Backend API endpoints
|
||||||
|
|
||||||
Set these in `docker-compose.yml` under the `scraper` service to override defaults:
|
| Endpoint | Description |
|
||||||
|
|----------|-------------|
|
||||||
| Variable | Default | 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 |
|
||||||
| `TZ` | `UTC` | Timezone for the nightly 3 AM run (e.g. `America/New_York`) |
|
| `GET /api/parks/:id/rides` | Live rides or schedule fallback |
|
||||||
| `PARK_HOURS_STALENESS_HOURS` | `72` | Hours before park schedule data is re-fetched |
|
| `GET /api/parks` | Park list with metadata |
|
||||||
| `COASTER_STALENESS_HOURS` | `720` | Hours before RCDB coaster lists are re-fetched (720 = 30 days) |
|
| `GET /api/status` | Health check, scrape timestamps, DB stats |
|
||||||
|
| `POST /api/scrape/trigger?scope=...` | Manual scrape trigger |
|
||||||
### Manual scrape
|
|
||||||
|
|
||||||
To trigger a scrape outside the nightly schedule:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose exec scraper npm run scrape
|
|
||||||
```
|
|
||||||
|
|
||||||
Force re-scrape of all data (ignores staleness):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose exec scraper npm run scrape:force
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Data Refresh
|
|
||||||
|
|
||||||
The scraper skips any park + month already scraped within the staleness window (`PARK_HOURS_STALENESS_HOURS`, default 72h). Past dates are never overwritten — once a day occurs, the API stops returning data for it, so the record written when it was a future date is preserved forever. The nightly scraper handles refresh automatically.
|
|
||||||
|
|
||||||
Roller coaster lists (from RCDB) are refreshed per `COASTER_STALENESS_HOURS` (default 720h = 30 days) for parks with a configured `rcdb_id`.
|
|
||||||
|
|||||||
Reference in New Issue
Block a user