Commit Graph

15 Commits

Author SHA1 Message Date
josh 52f7efd21a fix: suppress weather-delay flag during post-close wind-down
Build and Deploy / Lint, typecheck, test (push) Successful in 28s
Build and Deploy / Build & Push (push) Successful in 1m15s
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>
2026-05-31 20:49:14 -04:00
josh e1657f07d7 fix: surface silent scraper failures and stop falsely claiming weather delay
Build and Deploy / Lint, typecheck, test (push) Successful in 33s
Build and Deploy / Build & Push (push) Successful in 1m4s
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>
2026-05-31 20:28:25 -04:00
josh e888261ed9 feat: prefer Six Flags regular waits, show Fast Lane at 0, mark outages on chart
Build and Deploy / Lint, typecheck, test (push) Successful in 35s
Build and Deploy / Build & Push (push) Successful in 1m8s
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>
2026-05-30 18:54:02 -04:00
josh 5d9daee627 refactor: production-essentials hardening pass
Build and Deploy / Lint, typecheck, test (push) Successful in 30s
Build and Deploy / Build & Push (push) Successful in 1m39s
Backend: structured logger, env-validated config, graceful SIGTERM/SIGINT
shutdown, per-IP rate limiter, per-tier scheduler concurrency latch, error
context on previously-silent catches, compiled-JS Dockerfile stage.

Frontend: lib/api.ts consolidates BACKEND_URL with lazy production-required
check, root + per-segment error.tsx / not-found.tsx / loading.tsx,
generateMetadata on park and ride pages, graceful fallback when backend is
unreachable, Plausible script gated on env vars.

Infra: CI runs lint + typecheck + tests on both packages before docker build,
compose adds healthchecks, log rotation, and memory limits; .env.example
documents every variable.

Cleanup: removed empty app/api/parks/ dir and 0-byte root parks.db, moved
wait-times-urls.txt into docs/, dropped an `as any` cast.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 10:17:52 -04:00
josh 44d079efb9 fix: render today's wait chart in viewer's local time + close stale live state
Build and Deploy / Build & Push (push) Successful in 1m8s
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>
2026-05-30 08:23:00 -04:00
josh 7c88a3e568 fix: only sample wait times within each park's operating window
Build and Deploy / Build & Push (push) Successful in 1m5s
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>
2026-05-30 07:57:08 -04:00
josh 4f838d99c1 feat: add per-ride history charts with wait time and uptime tracking
Build and Deploy / Build & Push (push) Successful in 3m7s
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>
2026-05-29 23:35:27 -04:00
josh bfe099322f feat: add Fast Lane wait times toggle on park pages
Build and Deploy / Build & Push (push) Successful in 1m3s
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>
2026-05-29 22:51:52 -04:00
josh aa46cc1b3d fix: run startup scrape only when database is empty
Build and Deploy / Build & Push (push) Successful in 3m37s
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>
2026-05-29 14:45:57 -04:00
josh 8027bfc5cf rename project from SixFlagsSuperCalendar to Thoosie Calendar
Build and Deploy / Build & Push (push) Successful in 1m39s
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
josh deb8e4169b Revert "feat: run full scrape on backend container start"
Build and Deploy / Build & Push (push) Successful in 1m18s
This reverts commit db668c0787.
2026-04-23 23:14:22 -04:00
josh 06b911917d fix: use explicit Eastern timezone for day boundary instead of system TZ
Build and Deploy / Build & Push (push) Successful in 2m50s
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>
2026-04-23 23:11:33 -04:00
josh db668c0787 feat: run full scrape on backend container start
Build and Deploy / Build & Push (push) Successful in 1m44s
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>
2026-04-23 22:28:07 -04:00
josh 3815da2d3f refactor: make frontend a pure presentation layer fetching from backend API
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>
2026-04-23 21:43:59 -04:00
josh 70b56158d4 feat: add Hono backend API server with tiered scheduler
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>
2026-04-23 21:32:38 -04:00