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>
8.7 KiB
Development
See also: Architecture | Operations | API Reference
Prerequisites
- Node.js 22+ and npm
- No database tools needed (SQLite is auto-created by the backend)
- No Docker needed for local development
Setup
# 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
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:
curl -X POST http://localhost:3001/api/scrape/trigger?scope=full
Terminal 2: Frontend
npm run dev
This starts the Next.js dev server on port 3000 with hot reload. Open http://localhost:3000.
Navigation:
- Use the
←/→buttons to navigate weeks, or pass?week=YYYY-MM-DDin 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 toHomePageClient./park/[id](app/park/[id]/page.tsx) -- Park detail page. Fetches month calendar and live rides in parallel viaPromise.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:
{
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:
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, navigate to the park, and note the numeric ID in the URL.
3. lib/coaster-data.ts
Add the coaster name set:
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). 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:
npm run debug -- --park <parkId> --date <YYYY-MM-DD>
Example:
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
apiIdis correct
Testing
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.jsonfiles - Frontend uses
bundlermodule resolution with@/*path alias - Backend uses
CommonJSmodules 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 inapp/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
tsxfor runtime TypeScript execution (no build step in development)
Building Docker Images Locally
# 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.