Surfaces features that landed after the last big docs pass: per-ride
history pages, Fast Lane wait times, outage shading on the today chart,
Tier-5 wait-time sampler, production-hardening pieces (rate limiter,
structured logger, env validation, graceful shutdown), and the new
rides + ride_wait_samples tables. Also corrects the weather-delay rule
to match the "open" vs "closing" gate now in rides.ts.
The Max bar uses a dark fill with bright stroke for the outlined look,
but Recharts pulled tooltip/legend text color from the dark fill — making
the Max row unreadable against the dark tooltip background. Render the
tooltip via a custom content fn and recolor the legend's Max label so
both pick up the bright stroke color instead.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The ride detail and park pages fetched with `next: { revalidate: 60 }`,
which is stale-while-revalidate. After hours of no traffic the Data Cache
held a morning snapshot; the first click served that stale value and only
the second request (e.g. a browser refresh) got the just-revalidated
payload. The endpoint also bundles live state with chart history, so one
stale fetch made the whole page wrong.
Switch the live-data fetches to `cache: "no-store"`. The calendar-month
fetch keeps its 5-min ISR since operating hours change slowly.
Same root cause as 52f7efd — the /rides endpoint also used
isWithinOperatingWindow, which includes the post-close buffer. Switch
to getOperatingStatus and gate the badge on status === "open" so the
park page matches the calendar.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Parks naturally wind down rides over the 1-hour buffer after their
scheduled close, so all-rides-closed in that window isn't a weather
delay — it's just closing time. Both the calendar UI badge and the
sampler's telemetry counter were misclassifying this.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The homepage was flagging every park as weather delay because calendar.ts
collapsed "fetchLiveRides returned null" into the same openRides=0 bucket as
"all rides actually closed." Meanwhile every scraper (queuetimes, sixflags
operating-hours, sixflags wait-times) was swallowing non-OK responses and
exceptions silently, so logs gave no signal which upstream was failing or how.
Add a small scraperWarn helper that emits in the same shape as backend/log.ts
(without importing it — lib/scrapers is shared with the Next frontend). Use it
in all three scrapers to record HTTP status and error name+message before each
return null. Add parksSkipped to the tier-5 summary log so we can tell when the
openParks filter is rejecting everyone vs the fetcher silently failing.
Convert calendar.ts ridesCache to a discriminated union { kind: "ok" | "unknown" }.
Weather delay only fires on { kind: "ok", openRides: 0 }; unknown entries get
a 30s TTL so we recover quickly when upstream comes back and don't thunder-herd
in the meantime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two refinements after the previous label change:
1. Outage labels rendered at position="top" were clipping against the
chart's 8px top margin. Bumped to 24px so #N · Hh Mm sits above the
band fully visible.
2. Fast Lane line was only rendered when the ride's metadata flag
has_fast_lane was true. Some rides report Fast Lane waits without
getting flagged, so we now also render the line whenever today's
samples carry any non-null fastLaneMinutes — catches rides that are
walk-on all day with a flat line at 0.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The outage marker now reads "#N · 1h 28m" instead of just "#N" so the
duration is visible at a glance without hovering. Positioned above the
band ("position: top") rather than inside it — when the label string is
wider than the band, Recharts' insideTop placement silently drops the
ReferenceArea rect; placing the label above sidesteps that.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>