docs: add comprehensive project documentation
Build and Deploy / Build & Push (push) Successful in 1m31s
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>
This commit is contained in:
+449
@@ -0,0 +1,449 @@
|
||||
# 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
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user