a53e3ffa9f
Build and Deploy / Build & Push (push) Successful in 1m31s
Add docs/ folder with architecture, operations, API reference, and development guides covering system design, deployment, troubleshooting, all backend endpoints, and contributor workflows. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
450 lines
12 KiB
Markdown
450 lines
12 KiB
Markdown
# API Reference
|
||
|
||
> See also: [Architecture](ARCHITECTURE.md) | [Operations](OPERATIONS.md) | [Development](DEVELOPMENT.md)
|
||
|
||
## Base URL
|
||
|
||
| Context | URL |
|
||
|---------|-----|
|
||
| Local development | `http://localhost:3001` |
|
||
| Docker internal (web → backend) | `http://backend:3001` |
|
||
| External access (via host port) | `http://<host>:3001` |
|
||
|
||
## Authentication
|
||
|
||
None. All endpoints are public and unauthenticated. The scrape trigger endpoint is also unprotected -- restrict access at the network/proxy level if needed.
|
||
|
||
---
|
||
|
||
## Endpoints
|
||
|
||
### GET /api/calendar/week
|
||
|
||
Returns a 7-day calendar for all parks, starting from the given Sunday.
|
||
|
||
**Query Parameters:**
|
||
|
||
| Param | Required | Format | Description |
|
||
|-------|----------|--------|-------------|
|
||
| `start` | Yes | `YYYY-MM-DD` | Week start date (should be a Sunday) |
|
||
|
||
**Cache:** `Cache-Control: public, max-age=120, stale-while-revalidate=300`
|
||
|
||
**Response:**
|
||
|
||
```json
|
||
{
|
||
"weekStart": "2026-04-19",
|
||
"weekDates": ["2026-04-19", "2026-04-20", "2026-04-21", "2026-04-22", "2026-04-23", "2026-04-24", "2026-04-25"],
|
||
"today": "2026-04-23",
|
||
"isCurrentWeek": true,
|
||
"data": {
|
||
"cedarpoint": {
|
||
"2026-04-23": {
|
||
"isOpen": true,
|
||
"hoursLabel": "10am – 8pm",
|
||
"specialType": null
|
||
},
|
||
"2026-04-24": {
|
||
"isOpen": false,
|
||
"hoursLabel": null,
|
||
"specialType": null
|
||
}
|
||
}
|
||
},
|
||
"rideCounts": {
|
||
"cedarpoint": 42,
|
||
"greatadventure": 38
|
||
},
|
||
"coasterCounts": {
|
||
"cedarpoint": 12,
|
||
"greatadventure": 9
|
||
},
|
||
"openParkIds": ["cedarpoint", "greatadventure"],
|
||
"closingParkIds": [],
|
||
"weatherDelayParkIds": [],
|
||
"hasCoasterData": true,
|
||
"scrapedCount": 168
|
||
}
|
||
```
|
||
|
||
**Response fields:**
|
||
|
||
| Field | Type | Description |
|
||
|-------|------|-------------|
|
||
| `weekStart` | `string` | Echo of the `start` parameter |
|
||
| `weekDates` | `string[]` | Array of 7 date strings (Sun-Sat) |
|
||
| `today` | `string` | Current date (with 3 AM switchover) |
|
||
| `isCurrentWeek` | `boolean` | Whether this week contains today |
|
||
| `data` | `Record<parkId, Record<date, DayData>>` | Schedule data keyed by park ID and date |
|
||
| `rideCounts` | `Record<parkId, number>` | Number of open rides per park (only for currently operating parks with rides reporting) |
|
||
| `coasterCounts` | `Record<parkId, number>` | Number of open coasters per park |
|
||
| `openParkIds` | `string[]` | Parks currently within their operating window |
|
||
| `closingParkIds` | `string[]` | Parks past close but within 1-hour wind-down |
|
||
| `weatherDelayParkIds` | `string[]` | Parks within window but all rides closed |
|
||
| `hasCoasterData` | `boolean` | Always `true` (coaster data is static) |
|
||
| `scrapedCount` | `number` | Total day records returned (sanity check for empty database) |
|
||
|
||
**Errors:**
|
||
|
||
| Status | Body | Condition |
|
||
|--------|------|-----------|
|
||
| 400 | `{ "error": "Missing or invalid ?start=YYYY-MM-DD" }` | Missing or malformed `start` parameter |
|
||
|
||
---
|
||
|
||
### GET /api/calendar/:parkId/month
|
||
|
||
Returns a month calendar for a single park.
|
||
|
||
**Path Parameters:**
|
||
|
||
| Param | Description |
|
||
|-------|-------------|
|
||
| `parkId` | Park identifier (e.g. `cedarpoint`, `greatadventure`) |
|
||
|
||
**Query Parameters:**
|
||
|
||
| Param | Required | Format | Description |
|
||
|-------|----------|--------|-------------|
|
||
| `month` | Yes | `YYYY-MM` | Month to fetch |
|
||
|
||
**Cache:** `Cache-Control: public, max-age=300, stale-while-revalidate=600`
|
||
|
||
**Response:**
|
||
|
||
```json
|
||
{
|
||
"parkId": "cedarpoint",
|
||
"year": 2026,
|
||
"month": 6,
|
||
"monthData": {
|
||
"2026-06-01": {
|
||
"isOpen": true,
|
||
"hoursLabel": "10am – 10pm",
|
||
"specialType": null
|
||
},
|
||
"2026-06-02": {
|
||
"isOpen": true,
|
||
"hoursLabel": "10am – 8pm",
|
||
"specialType": null
|
||
}
|
||
},
|
||
"today": "2026-04-23"
|
||
}
|
||
```
|
||
|
||
If viewing the current month, today's data is fetched live from the Six Flags API and merged into the response.
|
||
|
||
**Errors:**
|
||
|
||
| Status | Body | Condition |
|
||
|--------|------|-----------|
|
||
| 400 | `{ "error": "Missing or invalid ?month=YYYY-MM" }` | Missing or malformed `month` parameter |
|
||
| 400 | `{ "error": "Month must be 1-12" }` | Month value out of range |
|
||
|
||
---
|
||
|
||
### GET /api/parks
|
||
|
||
Returns metadata for all 24 parks.
|
||
|
||
**Cache:** `Cache-Control: public, max-age=3600`
|
||
|
||
**Response:**
|
||
|
||
```json
|
||
{
|
||
"parks": [
|
||
{
|
||
"id": "cedarpoint",
|
||
"apiId": 70,
|
||
"name": "Cedar Point",
|
||
"shortName": "Cedar Point",
|
||
"chain": "sixflags",
|
||
"slug": "cedarpoint",
|
||
"region": "Midwest",
|
||
"location": {
|
||
"lat": 41.4784,
|
||
"lng": -82.6834,
|
||
"city": "Sandusky",
|
||
"state": "OH"
|
||
},
|
||
"timezone": "America/New_York",
|
||
"website": "https://www.sixflags.com"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### GET /api/parks/:id
|
||
|
||
Returns metadata for a single park.
|
||
|
||
**Path Parameters:**
|
||
|
||
| Param | Description |
|
||
|-------|-------------|
|
||
| `id` | Park identifier |
|
||
|
||
**Cache:** `Cache-Control: public, max-age=3600`
|
||
|
||
**Response:** A single `Park` object (same shape as one element of the `/api/parks` array).
|
||
|
||
**Errors:**
|
||
|
||
| Status | Body | Condition |
|
||
|--------|------|-----------|
|
||
| 404 | `{ "error": "Park not found" }` | Unknown park ID |
|
||
|
||
---
|
||
|
||
### GET /api/parks/:id/rides
|
||
|
||
Returns live ride status or schedule fallback for a park.
|
||
|
||
**Path Parameters:**
|
||
|
||
| Param | Description |
|
||
|-------|-------------|
|
||
| `id` | Park identifier |
|
||
|
||
**Cache:** `Cache-Control: public, max-age=60, stale-while-revalidate=120`
|
||
|
||
**Response:**
|
||
|
||
```json
|
||
{
|
||
"parkId": "cedarpoint",
|
||
"today": "2026-04-23",
|
||
"parkOpenToday": true,
|
||
"withinWindow": true,
|
||
"isWeatherDelay": false,
|
||
"liveRides": {
|
||
"rides": [
|
||
{
|
||
"name": "Steel Vengeance",
|
||
"isOpen": true,
|
||
"waitMinutes": 45,
|
||
"lastUpdated": "2026-04-23T18:30:00.000Z",
|
||
"isCoaster": true
|
||
},
|
||
{
|
||
"name": "Millennium Force",
|
||
"isOpen": false,
|
||
"waitMinutes": 0,
|
||
"lastUpdated": "2026-04-23T18:30:00.000Z",
|
||
"isCoaster": true
|
||
}
|
||
],
|
||
"fetchedAt": "2026-04-23T18:35:00.000Z"
|
||
},
|
||
"scheduleFallback": null
|
||
}
|
||
```
|
||
|
||
**Response fields:**
|
||
|
||
| Field | Type | Description |
|
||
|-------|------|-------------|
|
||
| `parkId` | `string` | Echo of the park ID |
|
||
| `today` | `string` | Current date (with 3 AM switchover) |
|
||
| `parkOpenToday` | `boolean` | Whether the park has hours scheduled today |
|
||
| `withinWindow` | `boolean` | Whether current time is within operating hours |
|
||
| `isWeatherDelay` | `boolean` | Park is open but all rides are closed |
|
||
| `liveRides` | `LiveRidesResult \| null` | Queue-Times live data (null if park has no mapping or is outside window) |
|
||
| `scheduleFallback` | `RidesFetchResult \| null` | Six Flags schedule data (only populated when liveRides is null) |
|
||
|
||
**Data priority:**
|
||
1. If a Queue-Times mapping exists and the park is tracked, `liveRides` is populated.
|
||
2. If outside the operating window, all rides in `liveRides` are forced to `isOpen: false`.
|
||
3. If no live data is available, `scheduleFallback` is fetched from the Six Flags schedule API for the nearest open date.
|
||
|
||
**Errors:**
|
||
|
||
| Status | Body | Condition |
|
||
|--------|------|-----------|
|
||
| 404 | `{ "error": "Park not found" }` | Unknown park ID |
|
||
|
||
---
|
||
|
||
### GET /api/status
|
||
|
||
Health check endpoint with database statistics.
|
||
|
||
**Response:**
|
||
|
||
```json
|
||
{
|
||
"status": "ok",
|
||
"uptime": 86400,
|
||
"parks": 24,
|
||
"database": {
|
||
"totalDays": 8760,
|
||
"lastScrape": "2026-04-23T14:00:12.000Z"
|
||
},
|
||
"lastScrapeResult": {
|
||
"scope": "today",
|
||
"fetched": 24,
|
||
"skipped": 0,
|
||
"errors": 0,
|
||
"updated": 3,
|
||
"startedAt": "2026-04-23T14:00:00.000Z",
|
||
"finishedAt": "2026-04-23T14:00:12.000Z"
|
||
}
|
||
}
|
||
```
|
||
|
||
| Field | Type | Description |
|
||
|-------|------|-------------|
|
||
| `status` | `string` | Always `"ok"` |
|
||
| `uptime` | `number` | Process uptime in seconds |
|
||
| `parks` | `number` | Number of tracked parks (24) |
|
||
| `database.totalDays` | `number` | Total rows in `park_days` table |
|
||
| `database.lastScrape` | `string \| null` | ISO timestamp of the most recent `scraped_at` value |
|
||
| `lastScrapeResult` | `ScrapeResult \| null` | Result of the last completed scrape (null if none has run yet) |
|
||
|
||
---
|
||
|
||
### POST /api/scrape/trigger
|
||
|
||
Manually triggers a data scrape. See [Operations > Manual Scraping](OPERATIONS.md#manual-scraping) for detailed usage.
|
||
|
||
**Query Parameters:**
|
||
|
||
| Param | Required | Default | Values | Description |
|
||
|-------|----------|---------|--------|-------------|
|
||
| `scope` | No | `today` | `today`, `month`, `upcoming`, `full`, `force` | What to scrape |
|
||
|
||
**Scope details:**
|
||
|
||
| Scope | What it scrapes | Staleness check | Inter-park delay |
|
||
|-------|----------------|-----------------|------------------|
|
||
| `today` | Today's hours only | No (uses diff-before-write) | 500ms |
|
||
| `month` | Current month | Yes (skips if fresh) | 1000ms |
|
||
| `upcoming` | Current + next month | Yes | 1000ms |
|
||
| `full` | All 12 months | Yes | 1000ms |
|
||
| `force` | All 12 months | **No** (ignores staleness) | 1000ms |
|
||
|
||
**Response:**
|
||
|
||
```json
|
||
{
|
||
"scope": "today",
|
||
"fetched": 24,
|
||
"skipped": 0,
|
||
"errors": 0,
|
||
"updated": 3,
|
||
"startedAt": "2026-04-23T14:00:00.000Z",
|
||
"finishedAt": "2026-04-23T14:00:12.000Z"
|
||
}
|
||
```
|
||
|
||
**Errors:**
|
||
|
||
| Status | Body | Condition |
|
||
|--------|------|-----------|
|
||
| 400 | `{ "error": "Invalid scope. Use: today, month, upcoming, full, force" }` | Unknown scope value |
|
||
|
||
---
|
||
|
||
## Data Types
|
||
|
||
### DayData
|
||
|
||
Core schedule data for a single park on a single day.
|
||
|
||
```typescript
|
||
interface DayData {
|
||
isOpen: boolean; // Whether the park is open
|
||
hoursLabel: string | null; // e.g. "10am – 6pm", null when closed
|
||
specialType: string | null; // "passholder_preview" or null
|
||
}
|
||
```
|
||
|
||
### Park
|
||
|
||
Park metadata (static, defined in `lib/parks.ts`).
|
||
|
||
```typescript
|
||
interface Park {
|
||
id: string; // Unique identifier (e.g. "cedarpoint")
|
||
apiId: number; // Six Flags CloudFront API park ID
|
||
name: string; // Full display name
|
||
shortName: string; // Abbreviated name
|
||
chain: string; // "sixflags"
|
||
slug: string; // URL-safe slug
|
||
region: string; // Geographic region
|
||
location: {
|
||
lat: number;
|
||
lng: number;
|
||
city: string;
|
||
state: string;
|
||
};
|
||
timezone: string; // IANA timezone (e.g. "America/New_York")
|
||
website: string; // Park website URL
|
||
}
|
||
```
|
||
|
||
### LiveRide
|
||
|
||
A single ride from the Queue-Times.com API.
|
||
|
||
```typescript
|
||
interface LiveRide {
|
||
name: string; // Ride display name
|
||
isOpen: boolean; // Currently operating
|
||
waitMinutes: number; // Current wait time (0 if closed)
|
||
lastUpdated: string; // ISO 8601 timestamp from Queue-Times
|
||
isCoaster: boolean; // Classified as a roller coaster via RCDB data
|
||
}
|
||
```
|
||
|
||
### LiveRidesResult
|
||
|
||
Container for live ride data.
|
||
|
||
```typescript
|
||
interface LiveRidesResult {
|
||
rides: LiveRide[]; // All rides, sorted: open first, then alphabetical
|
||
fetchedAt: string; // ISO timestamp of when we fetched from Queue-Times
|
||
}
|
||
```
|
||
|
||
### RidesFetchResult
|
||
|
||
Schedule-based ride data (fallback when live data is unavailable).
|
||
|
||
```typescript
|
||
interface RidesFetchResult {
|
||
rides: RideStatus[]; // Rides with scheduled open/close times
|
||
dataDate: string; // YYYY-MM-DD the data came from (may differ from requested date)
|
||
isExact: boolean; // true if dataDate matches requested date
|
||
parkHoursLabel?: string; // Park-level hours for the data date
|
||
}
|
||
|
||
interface RideStatus {
|
||
name: string;
|
||
isOpen: boolean; // Has scheduled operating hours
|
||
hoursLabel?: string; // e.g. "10am – 10pm"
|
||
}
|
||
```
|
||
|
||
### ScrapeResult
|
||
|
||
Result of a scraping operation.
|
||
|
||
```typescript
|
||
interface ScrapeResult {
|
||
scope: string; // What was scraped (e.g. "today", "months(2026-04)")
|
||
fetched: number; // API calls made successfully
|
||
skipped: number; // Skipped due to staleness or null response
|
||
errors: number; // Failed API calls
|
||
updated: number; // Database rows written
|
||
startedAt: string; // ISO timestamp
|
||
finishedAt: string; // ISO timestamp
|
||
}
|
||
```
|