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>
The script was already in layout.tsx but CSP blocked both loading
and sending beacons to tracking.thewrightserver.net.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update /app/data → /app/backend/data across all docs to match the
volume mount fix from 3c91d9a. Add missing TZ env var to the web
container snippet in OPERATIONS.md. Correct Midwest (6→7) and
West & International (6→5) park counts in ARCHITECTURE.md.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
WORKDIR is /app/backend so the DB lands at /app/backend/data/parks.db,
but the volume was mounted at /app/data — a different directory. The DB
lived in the container's ephemeral layer and was wiped on every pull.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
getTodayLocal() relied on system clock hours, which broke in the web
container (TZ defaulting to UTC) — the day flipped at 11 PM EDT (3 AM
UTC) instead of 3 AM Eastern. Now uses Intl.DateTimeFormat with an
explicit America/New_York timezone. Also replaced all toISOString()
date formatting with local-component helpers to avoid UTC conversion.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fires scrapeToday() then scrapeFullYear() as a background task on
startup so fresh deploys have data immediately instead of waiting
for the first cron tick. Staleness check makes warm restarts a no-op.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
Remove references to Playwright discovery, RCDB scraping, scraper
container, and npm run scripts. Document the new two-container setup,
tiered scheduling, backend API endpoints, and local dev workflow.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace scraper container with backend API container. Web image no
longer mounts a data volume or ships SQLite. Backend image runs Hono
server with node-cron scheduler, owns the database exclusively.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Server components now fetch composed data from the backend instead of
directly querying SQLite and external APIs. Removes better-sqlite3
dependency from the frontend entirely.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Delete scripts/scrape.ts and scripts/scrape-schedule.sh — their
functionality now lives in the backend's node-cron tiered scheduler
(backend/src/services/scheduler.ts + scraper.ts).
Remove scrape and scrape:force npm scripts from package.json.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Standalone Node.js backend that owns the SQLite database and serves
composed data via REST endpoints. Replaces the shell-scheduled scraper
with in-process node-cron tiered scheduling.
Backend structure:
- Hono HTTP server on port 3001 with CORS and request logging
- Singleton SQLite connection with WAL mode
- In-memory TTL cache for Queue-Times and fetchToday responses
- Comparison check on fetchToday (read-before-write, only upserts on change)
API endpoints:
- GET /api/calendar/week — week schedule + live ride counts for all parks
- GET /api/calendar/:parkId/month — month calendar for one park
- GET /api/parks — park list with metadata
- GET /api/parks/:id — single park detail
- GET /api/parks/:id/rides — live rides with Queue-Times/schedule fallback
- GET /api/status — health check, scrape stats
- POST /api/scrape/trigger — manual scrape (scope: today/month/upcoming/full)
Scheduler tiers:
- Tier 1: today — hourly (Mar-Dec)
- Tier 2: current month — every 6 hours
- Tier 3: upcoming — twice daily (3 AM + 3 PM)
- Tier 4: full year — daily at 3 AM
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Embed Six Flags API IDs directly in the park registry and snapshot
coaster lists from park-meta.json into a TypeScript module. This
eliminates the Playwright-based discovery script, RCDB scraper, and
runtime dependency on park-meta.json — preparing for the backend
API transition.
- Add apiId field to Park type and all 24 park entries
- Create lib/coaster-data.ts with hardcoded coaster lists
- Update page components to use park.apiId and new getCoasterSet()
- Remove scripts/discover.ts, lib/scrapers/rcdb.ts, lib/park-meta.ts
- Remove data/park-meta.json from shared volume
- Remove playwright devDependency and discover npm script
- Simplify scripts/scrape.ts (no RCDB, no discovery checks)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 🎢 Coasters button in nav bar (URL-driven: ?coasters=1)
- When active, swaps ride counts for coaster counts per park
- Label switches between "X rides operating" / "X coasters operating"
- Arrow key navigation preserves coaster filter state
- Only shown when coaster data exists in park-meta
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- "X rides open" → "X rides operating" (desktop + mobile)
- Green glowing dot next to park name when actively operating
- Hours text in calendar cells: larger (0.78rem) and bolder (600)
- Parks open badge: green tint when parks are open, larger text
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Extract isWithinOperatingWindow() to lib/env.ts (shared)
- Park detail page: always fetch Queue-Times, but force all rides
closed when outside the ±1h operating window
- LiveRidePanel: always show closed ride count badge (not just when
some rides are also open); label reads "X rides total" when none
are open vs "X closed / down" when some are
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fetch Queue-Times ride counts for parks open today (5min cache)
- Only shown within 1h before open to 1h after scheduled close
- Count displayed on the right of the park name/location cell (desktop)
and below the open badge (mobile)
- Whole park cell is now a clickable link
- Hover warms the park cell background; no row-wide highlight
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove today background/border from data row cells so the yellow
highlight only appears on the day label, not the entire column.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
new Date().toISOString() returns UTC, causing the calendar to advance
to the next day at 8pm EDT / 7pm EST. getTodayLocal() reads local
wall-clock time and rolls back one day before 3am so the calendar
stays on the current day through the night.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- WeekNav: add keydown listener for ArrowLeft/ArrowRight week nav
- EmptyState: replace dev-facing "No data scraped yet" + npm commands
with customer-friendly "Schedule not available yet" message
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Dockerfile: replace single runner stage with web + scraper named targets
- web: Next.js standalone only — no playwright, tsx, or scripts
- scraper: scripts/lib/node_modules/playwright only — no Next.js output
- docker-compose.yml: each service pulls its dedicated image tag
- .gitea/workflows/deploy.yml: build both targets on push to main
- lib/db.ts: STALE_AFTER_MS reads PARK_HOURS_STALENESS_HOURS env var (default 72h)
- lib/park-meta.ts: COASTER_STALE_MS reads COASTER_STALENESS_HOURS env var (default 720h)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- isCoaster → isCoasterMatch on line 109 (missed rename causing runtime crash
which returned null from fetchLiveRides, breaking the entire ride panel)
- Rewrite test as two flat arrays: SHOULD_MATCH and SHOULD_NOT_MATCH pairs,
each with the QT name, RCDB name, and park for context
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Imports must appear before other statements in ES modules.
Also drop the explicit .ts extension from import paths — Next.js
bundler resolves them without it, and the extension after const
was causing the module to silently fail in the app.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extract matching logic into lib/coaster-match.ts (isCoasterMatch + normalizeForMatch)
so it can be imported by both the scraper and tests without duplication.
Add tests/coaster-matching.test.ts covering all known match/false-positive cases:
- Trademark symbols, leading THE, possessives, punctuation
- Subtitle variants in both directions (Apocalypse, New Revolution - Classic)
- Space-split brand words (BAT GIRL vs Batgirl)
- 4D subtitle extension (THE JOKER™ 4D Free Fly Coaster vs Joker)
- False positives: Joker y Harley Quinn, conjunction connectors
Run with: npm test
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Restores fixed-height ride rows with ellipsis truncation.
Adds title attribute so hovering shows the full name in a native tooltip.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Matching fixes:
- normalize() now strips all non-word/non-space chars via [^\w\s] instead of
a hand-rolled list, catching !, curly apostrophe (U+2019), and any future edge cases
- Add isCoaster() helper with prefix matching (min 5 chars) to handle subtitle
mismatches in either direction (e.g. "Apocalypse" vs "Apocalypse the Ride",
"The New Revolution - Classic" vs "New Revolution")
- Fix top-level rides loop which still used coasterNames.has(normalize()) instead
of isCoaster() — this was the recurring bug causing top-level rides to miss
UI:
- Dark neutral base (#111) replacing cold navy and muddy purple
- Neon accent palette: hot pink, electric green, vivid yellow, cyan
- Park page max-width 960→1280px, calendar cells 72→96px tall
- Scrollbar accent matches theme
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add lib/park-meta.ts to manage data/park-meta.json (rcdb_id + coaster lists)
- Add lib/scrapers/rcdb.ts to scrape operating coaster names from RCDB park pages
- discover.ts now seeds park-meta.json with skeleton entries for all parks
- scrape.ts now refreshes RCDB coaster lists (30-day staleness) for parks with rcdb_id set
- fetchLiveRides() accepts a coasterNames Set; isCoaster uses normalize() on both sides
to handle trademark symbols, 'THE ' prefixes, and punctuation differences between
Queue-Times and RCDB names — applies correctly to both land rides and top-level rides
- Commit park-meta.json so it ships in the Docker image (fresh volumes get it automatically)
- Update .gitignore / .dockerignore to exclude only *.db files, not all of data/
- Dockerfile copies park-meta.json into image before VOLUME declaration
- README: document coaster filter setup and correct staleness window (72h not 7d)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Queue-Times groups rides into lands (e.g. "Coasters", "Family", "Kids").
Capture that categorisation in LiveRide.isCoaster and surface it as a
toggle in the new LiveRidePanel client component.
- lib/scrapers/queuetimes.ts: add isCoaster: boolean to LiveRide,
derived from land.name.toLowerCase().includes("coaster")
- components/LiveRidePanel.tsx: client component replacing the old
inline LiveRideList; adds a "🎢 Coasters only" pill toggle that
filters the grid; toggle only appears when the park has coaster-
categorised rides; amber when active, muted when inactive
- app/park/[id]/page.tsx: swap LiveRideList for LiveRidePanel,
remove now-dead LiveRideList/LiveRideRow functions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Scraper automation (docker-compose):
- Add scraper service to docker-compose.yml using the same image and
shared park_data volume; overrides CMD to run scrape-schedule.sh
- scripts/scrape-schedule.sh: runs an initial scrape on container start,
then sleeps until 3:00 AM (respects TZ env var) and repeats nightly;
logs timestamps and next-run countdown; non-fatal on scrape errors
Staleness window: 7 days → 72 hours in lib/db.ts so data refreshes
more frequently with the automated schedule in place
Remove favicon: delete app/icon.tsx and public/logo.svg
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Change upsertDay WHERE guard from >= to > date('now') so today is
treated identically to past dates. Once a park's operating day starts
the API drops that date, making it appear closed. The record written
when the date was still future is the correct one and must be preserved.
Only strictly future dates (> today) are now eligible for upserts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
upsertDay: add WHERE park_days.date >= date('now') to the ON CONFLICT
DO UPDATE clause. Past dates now behave as INSERT OR IGNORE — new rows
are written freely but existing historical records are never overwritten.
The API stops returning elapsed dates, so the DB row is the permanent
source of truth for any date that has already occurred.
isMonthScraped: months whose last day is before today are permanently
skipped regardless of staleness age. The API has no data for past months
so re-scraping them wastes API calls and cannot improve the records.
Current and future months continue to use the 7-day staleness window.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the cramped 7-column day grid with a clean open-days list.
Each card now shows:
- Park name + "Open today" / "Closed today" badge in the header
- One row per open day (Today, Monday, Friday...) with full hours
- Today row highlighted in amber; passholder days labeled inline
- Whole card is a tap target linking to the park detail page
Also:
- Hide the legend below sm breakpoint (not needed on phones)
- Reduce horizontal padding to 16px on mobile (was 24px)
- Tighten MobileCardList vertical spacing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- public/logo.svg: amber coaster silhouette (lift hill + vertical loop +
camelback) on transparent background; used in header and README
- app/icon.tsx: PNG favicon via Next.js ImageResponse (works in all
browsers including Safari); renders simplified hill + loop on dark
rounded-square background
- app/page.tsx: logo img added next to "Thoosie Calendar" title in header
- README.md: logo displayed at top of document
- Remove app/icon.svg (replaced by icon.tsx → PNG)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Rename app title and header from "Six Flags Calendar" to "Thoosie Calendar"
- Update layout metadata title and description
- Update README title
- Add app/icon.svg favicon: amber T on dark background, picked up
automatically by Next.js App Router
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Park detail pages now show real-time ride open/closed status and wait
times sourced from Queue-Times.com (updates every 5 min) when a park
is operating. Falls back to the Six Flags schedule API for off-hours
or parks without a Queue-Times mapping.
- lib/queue-times-map.ts: maps all 24 park IDs to Queue-Times park IDs
- lib/scrapers/queuetimes.ts: fetches and parses queue_times.json with
5-minute ISR cache; returns LiveRidesResult with isOpen + waitMinutes
- app/park/[id]/page.tsx: tries Queue-Times first; renders LiveRideList
with Live badge and per-ride wait times; falls back to RideList for
schedule data when live data is unavailable
- README: documents two-tier ride status approach
Attribution: Queue-Times.com (displayed in UI per their API terms)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add park detail pages and ride status to description
- Replace flat park list with regional table
- Add debug command documentation
- Remove CI/CD section (Gitea Actions config docs)
- Clean up deployment section
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove unused formatDate() from park page (replaced by formatShortDate)
- Remove unused date prop from DayCell component
- Change let to const for cellBorderRadius in ParkCard
- Change let to const for bg in ParkMonthCalendar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Visual overhaul:
- Warmer color system with amber accent for Today, better text hierarchy
- Row hover highlighting, sticky column shadow on horizontal scroll
- Closed cells replaced with dot (·) instead of "Closed" text
- Regional grouping (Northeast/Southeast/Midwest/Texas & South/West)
- Two-row header with park count badge and WeekNav on separate lines
- Amber "Today" button in WeekNav when off current week
- Mobile card layout (< 1024px) with 7-day grid per park; table on desktop
- Skeleton loading state via app/loading.tsx
Park detail pages (/park/[id]):
- Month calendar view with ← → navigation via ?month= param
- Live ride status fetched from Six Flags API (cached 1h)
- Ride hours only shown when they differ from park operating hours
- Fallback to nearest upcoming open day when today is dropped by API,
including cross-month fallback for end-of-month edge case
Data layer:
- Park type gains region field; parks.ts exports groupByRegion()
- db.ts gains getParkMonthData() for single-park month queries
- sixflags.ts gains scrapeRidesForDay() returning RidesFetchResult
with rides, dataDate, isExact, and parkHoursLabel
Removed: CalendarGrid.tsx, MonthNav.tsx (dead code)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Buyout days are now treated as closed unless they carry a Passholder
Preview event, in which case they surface as a distinct purple cell
in the UI showing "Passholder" + hours
- DB gains a special_type column (auto-migrated on next startup)
- scrape.ts threads specialType through to upsertDay
- debug.ts now shows events, isBuyout, isPassholderPreview, and
specialType in the parsed result section
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Creates debug/ folder (txt files gitignored). Output is printed to
the terminal and written to the file simultaneously.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
npm run debug -- --park greatadventure --date 2026-07-04
Prints the raw API response for that day alongside the parsed result
so mismatched or missing hours can be traced to their source.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each park prints a row of █ (fetched) and · (skipped) as months
complete, then ends with open day count, "up to date", or error count.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
docker-compose no longer needs REGISTRY_URL env var.
README now uses the actual registry host throughout.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
REGISTRY_URL var was empty so docker login fell through to Docker Hub.
Now strips protocol from gitea.server_url to get the registry hostname —
no manual variable needed. docker-compose defaults to the known host.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Next.js 15 + Tailwind CSS v4 week calendar showing Six Flags park hours.
Scrapes the internal CloudFront API, stores results in SQLite.
Includes Dockerfile (Debian/Playwright-compatible), docker-compose, and
Gitea Actions pipeline that builds and pushes to the container registry.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>