Skip to content

feat(flamingoland): add Flamingo Land Resort destination with schedule + map scraping#173

Open
cubehouse wants to merge 3 commits intomainfrom
park/flamingoland
Open

feat(flamingoland): add Flamingo Land Resort destination with schedule + map scraping#173
cubehouse wants to merge 3 commits intomainfrom
park/flamingoland

Conversation

@cubehouse
Copy link
Copy Markdown
Member

Summary

Adds Flamingo Land Resort (UK) as a new destination, with live ride status, wait times, schedules, per-attraction coordinates, and minimum-height tags.

The first commit establishes the Firebase/Firestore plumbing (token-based auth, Android API-key restrictions, ride and ride-category collections). The follow-up commit fills the remaining gaps:

Schedules

The mobile app's Firestore source has no schedule data, and probing confirmed there's no Remote Config template, no RTDB instance, no public Cloud Function. Two website signals fill the gap:

  • Homepage banner ("Today the Theme Park will close at 5PM") → today's actual closing time
  • Webshop overview ("Open daily from 10am between 21st March and 1st November 2026") → season window

We emit OPERATING entries from today through season end (inclusive of 1st November). Today uses the scraped close; future days fall back to a 10am-5pm default since closing times for future dates aren't published.

Coordinates and zone filter

The /map/ page embeds a markersData JS array. Marker id matches each Firestore ride doc's parkMapMarkerId. Where empty in Firestore (5 rides), fall back to exact-title and then title-prefix match (with apostrophe/whitespace normalisation).

The same matching is reused as a structural filter: rides_data also contains zone documents (e.g. Dino-Stone Park, Children's Planet) which have no map presence. A shared getAttractionRideIds() helper drops them so the zone never appears as an entity, and live data stays in sync with the entity list.

Tags

Ride docs carry a restrictions field (cm) for minimum height. Wired through TagBuilder.minimumHeight where the value is non-zero.

Implementation notes

  • Auth: Firebase Identity Toolkit. The mobile app provisions an anonymous-style account on first launch — we mirror that (sign-in, fall back to sign-up only on EMAIL_NOT_FOUND). 401/403 on Firestore invalidate the cached id-token.
  • Android client headers: X-Android-Package and X-Android-Cert are required by the API-key restriction; injected on all Firestore + identity calls.
  • Cache JSON-safety: scrapeSeasonWindow() returns ISO date strings (not Date objects) so the @cache SQLite layer can JSON-roundtrip safely.
  • Date formatting gotcha: added an isoDateInTimezone helper because formatInTimezone(date, tz, 'date') returns US-locale MM/DD/YYYY, which doesn't sort against ISO strings.

Test plan

  • npm run build clean
  • npm run dev -- flamingoland 4/4 passes:
    • 1 destination, 1 park, 31 attractions (down from 32 — Dino-Stone Park and Children's Planet correctly filtered as zones; Flamingo Land's data conflates them with rides)
    • All attractions have coordinates (via id-join, exact-title, or prefix fallback)
    • Live data stays in sync — 31 entries, 0 orphans
    • 177 schedule days from today (2026-05-09) through 2026-11-01 inclusive
  • Verified today's schedule entry uses scraped close-time; tomorrow uses 10am-5pm default
  • Verified Nov 1 schedule entry exists with correct GMT (post-DST) offset

🤖 Generated with Claude Code

cubehouse and others added 2 commits May 9, 2026 10:24
Reads rides and ride categories from the park's Firebase Firestore
project via authenticated REST calls. Emits status (OPERATING / CLOSED /
REFURBISHMENT) for all 32 rides plus STANDBY wait times for categories
flagged as queue-tracked in the source.

Schedules left empty for now — no public feed located; can be revisited
if the destination proves worth keeping.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Schedules
=========
The Flamingo Land mobile app's Firestore source has no schedule data
(verified — only rides_data and ride_categories collections exist; no
Remote Config template, no RTDB instance, no public Cloud Function).
Two website signals fill the gap:

- The homepage banner ("Today the Theme Park will close at 5PM") gives
  today's actual closing time.
- The webshop overview ("Open daily from 10am between 21st March and
  1st November 2026") gives the season window.

We emit OPERATING entries from today through season end (inclusive of
1st November). Today uses the scraped close; future days fall back to a
10am-5pm default since closing times for future dates aren't published.

Coordinates + zone filter
=========================
The /map/ page embeds a markersData JS array. Marker `id` matches each
Firestore ride doc's `parkMapMarkerId`. Where that field is empty in
Firestore, fall back to exact-title and then title-prefix match (with
apostrophe/whitespace normalisation).

The same matching is reused as a structural filter: rides_data also
contains zone documents (e.g. Dino-Stone Park, Children's Planet) which
have no map presence. A shared `getAttractionRideIds()` helper drops
them so the zone never appears as an entity, and live data stays in
sync with the entity list.

Tags
====
Ride docs carry a `restrictions` field (cm) for minimum height. Wired
through TagBuilder.minimumHeight where the value is non-zero.

Polish
======
- Clarified the EMAIL_NOT_FOUND auto-sign-up rationale.
- Trimmed the verbose category-filter comment.
- Added an isoDateInTimezone helper to avoid the
  formatInTimezone(... 'date') return-format gotcha
  (US-locale MM/DD/YYYY vs. ISO YYYY-MM-DD).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 9, 2026 13:56
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new FlamingoLand destination implementation to the parksapi framework, integrating Firebase/Firestore-backed live attraction data plus website scraping to fill in schedules and map coordinates.

Changes:

  • Implement Firebase Identity Toolkit auth + Firestore collection paging for rides and ride categories.
  • Scrape /map/ marker data to attach per-attraction coordinates and to filter out non-ride “zone” docs.
  • Scrape homepage + webshop pages to derive an operating-season window and emit park schedules.

Comment thread src/parks/flamingoland/flamingoland.ts Outdated
Comment on lines +53 to +54
if (v?.integerValue !== undefined) return Number(v.integerValue);
if (v?.doubleValue !== undefined) return v.doubleValue;
Comment thread src/parks/flamingoland/flamingoland.ts Outdated
Comment on lines +268 to +278
// We accept "open at" too — the morning banner may show that instead.
// Returns "HH:mm" 24-hour, or null if no banner is present (off-season / closed).
@cache({ttlSeconds: 60 * 30})
async scrapeTodayCloseTime(): Promise<string | null> {
const resp = await this.fetchHomepage();
const html = await resp.text();
const m = html.match(/Today the Theme Park will (close|open) at\s*(\d{1,2})(?::(\d{2}))?\s*(AM|PM)/i);
if (!m) return null;
const hour12 = parseInt(m[2], 10);
const minute = m[3] ? parseInt(m[3], 10) : 0;
const isPM = m[4].toUpperCase() === 'PM';
Comment thread src/parks/flamingoland/flamingoland.ts Outdated

let status: LiveData['status'];
if (underMaintenance) status = 'REFURBISHMENT';
else if (downAllDay || !statusOpen) status = 'CLOSED';
Three fixes from the Copilot review of #173:

1. fsInt() now rejects non-finite values (NaN from malformed input,
   Infinity, and empty integerValue strings — Number('') is 0 which
   would silently pollute Set membership checks).

2. parseTodayCloseBanner only parses "close at" — the homepage may
   show a morning "open at 10AM" banner whose time is the *opening*
   time. Re-using it as the closing time would generate a schedule
   entry with open=10:00 close=10:00.

3. downAllDay maps to DOWN, not CLOSED. statusOpen=false (park-level
   closure) keeps mapping to CLOSED, but a ride that's down for the
   day with the park open is operationally distinct, and the typelib
   has a dedicated DOWN status for it.

Refactor: pulled the substantive logic into pure module-scope
functions (parseTodayCloseBanner, parseSeasonWindow, parseMapMarkers,
findMarkerForRide, decideRideStatus, iterateScheduleDays) so they're
testable without Firebase or live network access. Class methods are
now thin wrappers over the pure functions.

Tests: src/parks/flamingoland/__tests__/flamingoland.test.ts —
41 tests covering happy paths, edge cases, and the three review
fixes. Follows the plopsa pattern of testing exported pure helpers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants