Three usability fixes after a day of using the ride detail page.
1. Six Flags is now the primary source for regular wait times. SF's
/wait-times endpoint reports regular waits alongside Fast Lane, and it
updates more promptly than Queue-Times around park-open. The sampler
and the live /rides + ride-history routes all prefer SF's regularWaittime
when its createdDateTime is non-empty; Queue-Times remains the fallback
and the authoritative isOpen source.
2. The today chart's Fast Lane line now stays visible when its value is 0
(walk-on). Y-axis bottom padding ensures the line sits clearly above the
X-axis frame instead of being clipped against it. The tooltip shows
"walk-on" instead of "0 min" for that case.
3. Outages are now explicit on the chart instead of just being gaps.
computeOutages walks today's samples to find contiguous closed runs and
numbers them chronologically. Each outage renders as a translucent pink
ReferenceArea with a "#N" label. The custom tooltip detects when the
cursor is over an outage span and shows "Outage #N — Hh Mm" (e.g.
"Outage #2 — 1h 28m") in place of the wait/Fast Lane rows.
Includes a seed-test-samples.ts dev script for eyeballing the chart with
synthetic outage data.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The home page no longer reads ?week=YYYY-MM-DD from the URL. Selected week
lives in the tcWeek cookie, set via a server action that revalidates the
home page so the next render reflects it. The URL stays at "/" regardless
of which week the user is viewing.
WeekNav prev/next/today buttons (and the arrow-key bindings) call the
server action directly — no router.refresh dance, no client-side cookie
write. BackToCalendarLink drops its localStorage-based href reconstruction
and just links to "/" since the cookie already remembers the right week
across navigations.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two related polish fixes for the ride detail page:
1. Wait-time chart x-axis now uses Intl.DateTimeFormat with no timezone
argument, so an Eastern-time user viewing a Pacific park sees ET on
the axis. Backend now sends recorded_at (UTC) alongside local_time.
2. Ride-history endpoint now applies the same operating-window gate the
/rides route uses. Queue-Times keeps reporting yesterday's last wait
with isOpen=true overnight, which made the "Right now" pill show a
live wait time when the park was actually closed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Queue-Times keeps reporting yesterday's last wait with isOpen=true overnight,
so the per-ride open check wasn't enough — the sampler was recording phantom
"open" samples between close and the next morning's first refresh, padding
both wait-time averages and uptime% with stale data.
Add isWithinOperatingWindow gate (same check the /rides route uses) so the
sampler only runs during the park's actual hours plus the 1-hour closing
buffer. Includes a one-off wipe script for the accumulated bad data.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a cron-driven sampler that snapshots Queue-Times waits and Six Flags
Fast Lane data every 5 minutes into a new ride_wait_samples table, and a
clickable per-ride detail page at /park/[id]/ride/[slug] with Today / 7d /
30d Recharts views plus a 30d uptime pill. Rides are keyed by Queue-Times'
stable qt_ride_id so renames don't fragment history. Samples store
pre-bucketed local_date and local_time in the park's IANA timezone so
aggregations are pure SQL and DST-safe.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Join Fast Lane waits from the Six Flags /wait-times endpoint onto
Queue-Times rides by name. A new toggle on the live ride panel swaps
the shown wait to the Fast Lane number; regular waits and open status
still come from Queue-Times.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Restores the startup scrape removed in deb8e41, gated on
getParkDayCount() < 50 so warm restarts don't hammer the API.
Cold containers (e.g. after the volume mount fix) populate
immediately instead of waiting up to 24h for tier-4 cron.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
The API without a date param returns today's operating data directly,
invalidating the previous assumption that today's date was always missing.
- Add fetchToday(apiId, revalidate?) to sixflags.ts — calls the dateless
endpoint with optional ISR cache
- Extract parseApiDay() helper shared by scrapeMonth and fetchToday
- Update upsertDay WHERE clause: >= date('now') so today can be updated
(was > date('now'), which froze today after first write)
- scrape.ts: add a today-scrape pass after the monthly loop so each run
always writes fresh today data to the DB
- app/page.tsx: fetch live today data for all parks (5-min ISR) and merge
into the data map before computing open/closing/weatherDelay status
- app/park/[id]/page.tsx: prefer live today data from API for todayData
so weather delays and hour changes surface within 5 minutes
- scrapeRidesForDay: update comment only — role unchanged (QT fallback)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removed flex:1 from left side so ride count has a real minimum width to
trigger wrapping. Added whiteSpace:nowrap so flexbox knows when to wrap it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Padding on the parent main element doesn't work with overflow:auto — the
fix belongs on the scrollable div wrapping the table itself.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Increased pill vertical padding (3px→5px) and internal line gaps (1-2px→2-3px)
so the stacked hours/timezone text feels less cramped.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Open parks get a colored left border (green/amber/blue) instead of a dot.
Region headers lose their accent line; distinguished by "— REGION —" format
with higher-contrast text instead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Dot turns blue (instead of green) during weather delay on homepage
- Mobile card "Open today" badge becomes blue "⛈ Weather Delay"
- Park page LiveRidePanel shows a blue "⛈ Weather Delay — all rides currently closed" badge instead of "Not open yet — check back soon"
- Added --color-weather-* CSS variables (blue palette)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Park open indicator now derives from scheduled hours, not ride counts.
Parks with queue-times coverage but 0 open rides (e.g. storm) show a
"⛈ Weather Delay" notice instead of a ride count on both desktop and mobile.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The actual overflow fix was removing whiteSpace:nowrap from the td.
With that gone, 240px is sufficient and content wraps naturally when tight.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Column uses clamp(220px, 22%, 280px) — scales on small screens, caps at
280px on large ones. Park name gets whiteSpace:nowrap so it stays one line.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause was a hardcoded 240px column width + whiteSpace:nowrap that
prevented content from ever fitting on smaller displays. Now uses 25%
width so the column scales with the viewport, removed nowrap so text
wraps naturally when squeezed, and reverted clamp() back to fixed sizes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removed flex:1/minWidth:0 from name span so dot stays snug to the right
of the park name. Removed flexShrink:0 from ride count so both sides can
compress. All text uses clamp() to scale proportionally with viewport.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>