Commit Graph

96 Commits

Author SHA1 Message Date
8e969165b4 feat: show live open ride count in park name cell
- 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>
2026-04-04 20:38:12 -04:00
43feb4cef0 fix: restrict today highlight to date header only
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>
2026-04-04 20:18:19 -04:00
a87f97ef53 fix: use local time with 3am cutover for today's date
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>
2026-04-04 20:15:52 -04:00
fdea8443fb fix: restore arrow key week navigation; improve empty state copy
All checks were successful
Build and Deploy / Build & Push (push) Successful in 1m6s
- 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>
2026-04-04 20:07:43 -04:00
6bb35d468f security: add headers, fetch timeouts, Retry-After cap, env validation
All checks were successful
Build and Deploy / Build & Push (push) Successful in 3m50s
- next.config.ts: CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy
- sixflags.ts: cap Retry-After at 5 min; add 15s AbortSignal.timeout()
- queuetimes.ts: add 10s AbortSignal.timeout()
- rcdb.ts: add 15s AbortSignal.timeout()
- lib/env.ts: parseStalenessHours() guards against NaN from invalid env vars
- db.ts + park-meta.ts: use parseStalenessHours() for staleness window config

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 17:13:01 -04:00
e1b0e5e44d chore: remove redundant 'done' line from discover output
All checks were successful
Build and Deploy / Build & Push (push) Successful in 3m27s
The → ID line already confirms success.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 16:59:11 -04:00
edd044a1f8 docs: fix volume name to root_park_data
All checks were successful
Build and Deploy / Build & Push (push) Successful in 56s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 16:54:36 -04:00
eeed646203 docs: rewrite deployment section for two-image setup
All checks were successful
Build and Deploy / Build & Push (push) Successful in 52s
- Document web/:web and scraper/:scraper image split
- Full first-time setup flow: pull → discover → RCDB IDs → scrape → up
- Document all scraper env vars (TZ, PARK_HOURS_STALENESS_HOURS, COASTER_STALENESS_HOURS)
- Add manual scrape commands, update workflow, add npm test to dev section

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 16:48:22 -04:00
eeb4a649c1 feat: split web and scraper into separate Docker images
All checks were successful
Build and Deploy / Build & Push (push) Successful in 3m4s
- 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>
2026-04-04 16:40:31 -04:00
766fc296a1 fix: isCoaster typo in top-level rides loop; simplify test structure
All checks were successful
Build and Deploy / Build & Push (push) Successful in 3m0s
- 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>
2026-04-04 15:49:47 -04:00
8324f31972 fix: correct import paths for coaster-match module
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>
2026-04-04 15:46:07 -04:00
9cac86d241 test: add coaster name matching test suite
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>
2026-04-04 15:43:20 -04:00
dc4fbeb7ec chore: ignore .claude/ directory
All checks were successful
Build and Deploy / Build & Push (push) Successful in 3m36s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 15:26:00 -04:00
c1e42d6aa1 fix: truncate long ride names with tooltip instead of wrapping
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>
2026-04-04 15:25:01 -04:00
e9da6f3120 fix: robust coaster matching + dark carnival color scheme
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>
2026-04-04 15:22:59 -04:00
bad366d5ea revert: remove park-meta.json copy from Dockerfile
All checks were successful
Build and Deploy / Build & Push (push) Successful in 3m34s
Volume mount hides image-layer files on existing deployments anyway.
Deploy manually: curl -o park-meta.json <gitea-raw-url>

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 14:02:39 -04:00
9700d0bd9a feat: RCDB-backed roller coaster filter with fuzzy name matching
All checks were successful
Build and Deploy / Build & Push (push) Successful in 2m54s
- 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>
2026-04-04 13:49:49 -04:00
819e716197 feat: coaster-only toggle on live ride status panel
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>
2026-04-04 12:56:20 -04:00
da083c125c feat: automated nightly scraper + housekeeping
All checks were successful
Build and Deploy / Build & Push (push) Successful in 3m11s
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>
2026-04-04 12:47:14 -04:00
20f1058e9e fix: protect today's record from scrape overwrites
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>
2026-04-04 12:42:03 -04:00
5ea2dafc0e fix: preserve historical day records, skip scraping past months
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>
2026-04-04 12:37:27 -04:00
d4c8046515 improve: redesign mobile card layout for usability
All checks were successful
Build and Deploy / Build & Push (push) Successful in 3m24s
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>
2026-04-04 12:33:02 -04:00
b4183507a8 chore: remove logo from header and README
All checks were successful
Build and Deploy / Build & Push (push) Successful in 3m3s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 12:28:39 -04:00
81ff6ea659 feat: roller coaster logo and working favicon
- 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>
2026-04-04 12:26:45 -04:00
8b1c8dcb29 chore: rebrand to Thoosie Calendar, add favicon
- 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>
2026-04-04 12:18:22 -04:00
e7b72ff95b feat: add live ride status via Queue-Times.com API
All checks were successful
Build and Deploy / Build & Push (push) Successful in 2m51s
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>
2026-04-04 12:15:36 -04:00
ba8cd46e75 docs: update README with current feature set, remove CI/CD section
- 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>
2026-04-04 12:03:32 -04:00
b0bbb4d465 fix: resolve ESLint errors blocking Docker build
All checks were successful
Build and Deploy / Build & Push (push) Successful in 2m45s
- 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>
2026-04-04 11:55:17 -04:00
e48038c399 feat: UI redesign with park detail pages and ride status
Some checks failed
Build and Deploy / Build & Push (push) Failing after 22s
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>
2026-04-04 11:53:06 -04:00
5f82407fea feat: show full park names in calendar (e.g. Six Flags Great Adventure)
All checks were successful
Build and Deploy / Build & Push (push) Successful in 3m14s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 11:02:28 -04:00
e2d90b3c5b feat: hide parks with no open days in the current week
Some checks failed
Build and Deploy / Build & Push (push) Has been cancelled
Header subtitle now shows "X of 24 parks open" for context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 11:00:53 -04:00
91e09b0548 feat: detect passholder preview days and filter plain buyouts
All checks were successful
Build and Deploy / Build & Push (push) Successful in 3m9s
- 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>
2026-04-04 10:53:05 -04:00
7c28d8f89f feat: save debug output to debug/{parkId}_{date}.txt
All checks were successful
Build and Deploy / Build & Push (push) Successful in 5m13s
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>
2026-04-04 10:45:55 -04:00
8a68251beb feat: add debug script for inspecting raw API data per park+date
All checks were successful
Build and Deploy / Build & Push (push) Successful in 4m23s
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>
2026-04-04 10:39:37 -04:00
5bef17aa41 refactor: one-line-per-park output with inline month progress
All checks were successful
Build and Deploy / Build & Push (push) Successful in 3m11s
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>
2026-04-04 10:31:11 -04:00
bc5777c9e2 refactor: scrape all 12 months per park before moving to next park
All checks were successful
Build and Deploy / Build & Push (push) Successful in 2m54s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 10:25:20 -04:00
09dcfde16e ci: use docker/metadata-action and build-push-action
All checks were successful
Build and Deploy / Build & Push (push) Successful in 14m31s
Cleaner pipeline using official Docker actions. Supports semver tags
alongside latest. Registry driven by vars.REGISTRY variable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 01:37:09 -04:00
2bc10c98d9 chore: remove build from compose, pull from registry only
Some checks failed
Build and Deploy / build-and-push (push) Has been cancelled
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 01:26:45 -04:00
cd3c3fab7e fix: add public/ directory so Docker COPY doesn't fail
Some checks failed
Build and Deploy / build-and-push (push) Has been cancelled
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 01:15:54 -04:00
12af38c5f6 chore: hardcode registry URL and clean up readme
Some checks failed
Build and Deploy / build-and-push (push) Failing after 38s
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>
2026-04-04 01:13:16 -04:00
a2b59b2f8d fix: derive registry host from gitea.server_url instead of unset variable
Some checks failed
Build and Deploy / build-and-push (push) Failing after 1m7s
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>
2026-04-04 01:10:20 -04:00
4118d31df8 Update README.md
Some checks failed
Build and Deploy / build-and-push (push) Failing after 6s
2026-04-04 01:00:07 -04:00
c32967ebf1 Update README.md 2026-04-04 00:56:39 -04:00
548c7ae09e feat: initial project scaffold with CI/CD and Docker deployment
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>
2026-04-04 00:48:09 -04:00
af6aa29474 chore: add project config 2026-04-03 22:06:54 -04:00
6a139597ad docs: initialize project 2026-04-03 22:05:47 -04:00