Files
SixFlagsSuperCalendar/docs/DEVELOPMENT.md
T
josh 8027bfc5cf
Build and Deploy / Build & Push (push) Successful in 1m39s
rename project from SixFlagsSuperCalendar to Thoosie Calendar
Update package names, Docker image tags, CI/CD workflow, and
documentation to reflect the public brand name. References to
the actual Six Flags theme park chain/API are intentionally kept.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-24 10:06:17 -04:00

264 lines
8.7 KiB
Markdown

# Development
> See also: [Architecture](ARCHITECTURE.md) | [Operations](OPERATIONS.md) | [API Reference](API.md)
## Prerequisites
- **Node.js 22+** and **npm**
- No database tools needed (SQLite is auto-created by the backend)
- No Docker needed for local development
---
## Setup
```bash
# Clone the repository
git clone <repo-url>
cd ThoosieCalendar
# Install frontend dependencies
npm install
# Install backend dependencies
cd backend && npm install && cd ..
```
---
## Running Locally
The project requires two terminals -- one for the backend, one for the frontend.
### Terminal 1: Backend
```bash
cd backend
npm run dev
```
This starts the Hono API server on port 3001 using `tsx` (TypeScript runtime with watch mode). On first run:
- Creates an empty SQLite database at `backend/data/parks.db`
- Registers the four-tier cron scheduler
- The schedulers will populate data automatically over time, or you can trigger a manual scrape immediately:
```bash
curl -X POST http://localhost:3001/api/scrape/trigger?scope=full
```
### Terminal 2: Frontend
```bash
npm run dev
```
This starts the Next.js dev server on port 3000 with hot reload. Open [http://localhost:3000](http://localhost:3000).
**Navigation:**
- Use the `←` / `→` buttons to navigate weeks, or pass `?week=YYYY-MM-DD` in the URL
- Click any park name to open its detail page with month calendar and ride status
---
## Project Structure Walkthrough
### `app/` -- Next.js Pages
Two routes:
- `/` (`app/page.tsx`) -- Home page. Server component that fetches week data from the backend and passes everything to `HomePageClient`.
- `/park/[id]` (`app/park/[id]/page.tsx`) -- Park detail page. Fetches month calendar and live rides in parallel via `Promise.all`.
### `components/` -- React Components
10 components, split between server and client:
| Component | Type | Purpose |
|-----------|------|---------|
| `HomePageClient` | Client | Top-level state: coaster filter, auto-refresh, keyboard nav |
| `WeekCalendar` | Server | Desktop 7-column table with region groupings |
| `MobileCardList` | Server | Mobile card layout (below `lg` breakpoint) |
| `ParkCard` | Server | Individual park card for mobile |
| `ParkMonthCalendar` | Server | Month grid for park detail page |
| `LiveRidePanel` | Client | Live ride list with coaster toggle and wait times |
| `WeekNav` | Client | Week navigation arrows |
| `Legend` | Server | Color legend for status indicators |
| `EmptyState` | Server | Empty database message |
| `BackToCalendarLink` | Client | Back link using localStorage for last week |
### `lib/` -- Shared Code
Imported by both frontend and backend:
| File | Purpose |
|------|---------|
| `types.ts` | Core `DayData` interface |
| `env.ts` | `getTodayLocal()` (3 AM switchover), `isWithinOperatingWindow()`, `getOperatingStatus()`, `parseStalenessHours()` |
| `parks.ts` | All 24 park definitions, `PARK_MAP`, `groupByRegion()` |
| `coaster-data.ts` | Static RCDB coaster name sets per park, `getCoasterSet()` |
| `coaster-match.ts` | `normalizeForMatch()`, `isCoasterMatch()` -- fuzzy name matching |
| `queue-times-map.ts` | `QUEUE_TIMES_IDS` -- park ID to Queue-Times park ID mapping |
| `scrapers/sixflags.ts` | Six Flags CloudFront API client -- `scrapeMonth()`, `fetchToday()`, `scrapeRidesForDay()`, rate limiting |
| `scrapers/queuetimes.ts` | Queue-Times.com API client -- `fetchLiveRides()` |
| `scrapers/types.ts` | `Park`, `DayStatus`, `MonthCalendar`, `ScraperAdapter` interfaces |
### `backend/src/` -- Hono API Server
| File | Purpose |
|------|---------|
| `index.ts` | Entry point -- middleware (CORS, logger), route registration, DB init, scheduler start |
| `db/index.ts` | SQLite connection singleton, schema creation, WAL mode |
| `db/queries.ts` | All SQL queries -- `upsertDay`, `getDateRange`, `getParkMonthData`, `isMonthScraped`, etc. |
| `routes/calendar.ts` | `/api/calendar/*` -- week and month data with live today merging |
| `routes/parks.ts` | `/api/parks/*` -- park metadata |
| `routes/rides.ts` | `/api/parks/:id/rides` -- live ride status with schedule fallback |
| `routes/status.ts` | `/api/status` -- health check |
| `routes/scrape.ts` | `/api/scrape/trigger` -- manual scrape |
| `services/scheduler.ts` | Four-tier cron job registration |
| `services/scraper.ts` | Scraping orchestration -- `scrapeToday()`, `scrapeMonths()`, `scrapeFullYear()` |
| `services/cache.ts` | Generic `TtlCache<T>` class with configurable TTL |
---
## Adding a New Park
Adding a park requires changes to three files. The park will be automatically picked up by the scheduler, the API, and the frontend.
### 1. `lib/parks.ts`
Add an entry to the `PARKS` array:
```typescript
{
id: "newpark", // URL-safe unique identifier
apiId: 123, // Six Flags CloudFront API park ID
name: "Six Flags New Park",
shortName: "New Park",
chain: "sixflags",
slug: "newpark", // Should match sixflags.com URL path
region: "Midwest", // One of: Northeast, Southeast, Midwest, Texas & South, West & International
location: {
lat: 40.0,
lng: -80.0,
city: "Anytown",
state: "OH",
},
timezone: "America/New_York", // IANA timezone
website: "https://www.sixflags.com",
},
```
**Finding the API ID:** Use the debug script or inspect network requests on the Six Flags website. The `apiId` is the numeric park identifier in the CloudFront API URL.
### 2. `lib/queue-times-map.ts`
Add the Queue-Times.com park ID mapping:
```typescript
export const QUEUE_TIMES_IDS: Record<string, number> = {
// ... existing mappings
newpark: 456, // Queue-Times park ID
};
```
**Finding the Queue-Times ID:** Browse [queue-times.com](https://queue-times.com), navigate to the park, and note the numeric ID in the URL.
### 3. `lib/coaster-data.ts`
Add the coaster name set:
```typescript
export function getCoasterSet(parkId: string): Set<string> | null {
// ... existing cases
case "newpark":
return new Set([
normalizeForMatch("Coaster Name One"),
normalizeForMatch("Coaster Name Two"),
]);
}
```
**Finding coaster names:** Look up the park on [RCDB (Roller Coaster Database)](https://rcdb.com). List all operating roller coasters. Names should be the official RCDB names before normalization.
---
## Debug Script
Inspect raw API data and parsed output for any park and date:
```bash
npm run debug -- --park <parkId> --date <YYYY-MM-DD>
```
**Example:**
```bash
npm run debug -- --park kingsisland --date 2026-06-15
```
This fetches the raw Six Flags API response for the park and date, displays the parsed result, and saves the raw JSON to the `debug/` directory for inspection. Useful for:
- Investigating API response format changes
- Debugging parsing issues for specific parks/dates
- Verifying that a park's `apiId` is correct
---
## Testing
```bash
npm test
```
Uses the **Node.js built-in test runner** (`node --test`). Test files live in `tests/`.
**Current test coverage:**
| File | Tests | Coverage |
|------|-------|---------|
| `tests/coaster-matching.test.ts` | 13 cases | Coaster name matching: exact, prefix, compact, conjunction rejection |
Tests verify the `isCoasterMatch()` function handles edge cases like trademark symbols, possessives, subtitles, space-split brand words, and conjunction-joined compound ride names.
---
## Code Conventions
### TypeScript
- **Strict mode** enabled in both `tsconfig.json` files
- Frontend uses `bundler` module resolution with `@/*` path alias
- Backend uses `CommonJS` modules with `@lib/*` alias resolving to `../lib/*`
### Styling
- **Inline styles** via `style={{}}` props for most component styling
- **Tailwind CSS v4** for responsive utilities (`hidden lg:block`, `sm:flex`, `px-4 sm:px-6`)
- Theme defined via `@theme {}` block and CSS custom properties in `app/globals.css`
- No CSS modules, no styled-components, no component library
### Code Organization
- Shared types and utilities live in `lib/` and are imported by both frontend and backend
- No component library -- all UI is built from scratch
- Backend uses `tsx` for runtime TypeScript execution (no build step in development)
---
## Building Docker Images Locally
```bash
# Build the web image
docker build --target web -t thoosiecalendar:web .
# Build the backend image
docker build --target backend -t thoosiecalendar:backend .
# Run locally with Docker Compose
docker compose up -d
# Or run individual containers
docker run -d -p 3001:3001 -v park_data:/app/backend/data -e TZ=America/New_York thoosiecalendar:backend
docker run -d -p 3000:3000 -e BACKEND_URL=http://host.docker.internal:3001 thoosiecalendar:web
```
When running individual containers outside of Docker Compose, use `host.docker.internal` instead of `backend` for the `BACKEND_URL`, since Docker's internal DNS won't resolve service names without Compose.