docs: add comprehensive project documentation
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:
2026-04-23 22:15:02 -04:00
parent 4922dce8ac
commit a53e3ffa9f
5 changed files with 1703 additions and 0 deletions
+449
View File
@@ -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
}
```