feat(flamingoland): add Flamingo Land Resort destination with schedule + map scraping#173
Open
feat(flamingoland): add Flamingo Land Resort destination with schedule + map scraping#173
Conversation
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>
Contributor
There was a problem hiding this comment.
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 on lines
+53
to
+54
| if (v?.integerValue !== undefined) return Number(v.integerValue); | ||
| if (v?.doubleValue !== undefined) return v.doubleValue; |
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'; |
|
|
||
| let status: LiveData['status']; | ||
| if (underMaintenance) status = 'REFURBISHMENT'; | ||
| else if (downAllDay || !statusOpen) status = 'CLOSED'; |
3 tasks
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
"Today the Theme Park will close at 5PM") → today's actual closing time"Open daily from 10am between 21st March and 1st November 2026") → season windowWe emit
OPERATINGentries 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 amarkersDataJS array. Markeridmatches each Firestore ride doc'sparkMapMarkerId. 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_dataalso contains zone documents (e.g. Dino-Stone Park, Children's Planet) which have no map presence. A sharedgetAttractionRideIds()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
restrictionsfield (cm) for minimum height. Wired throughTagBuilder.minimumHeightwhere the value is non-zero.Implementation notes
EMAIL_NOT_FOUND). 401/403 on Firestore invalidate the cached id-token.X-Android-PackageandX-Android-Certare required by the API-key restriction; injected on all Firestore + identity calls.scrapeSeasonWindow()returns ISO date strings (notDateobjects) so the@cacheSQLite layer can JSON-roundtrip safely.isoDateInTimezonehelper becauseformatInTimezone(date, tz, 'date')returns US-localeMM/DD/YYYY, which doesn't sort against ISO strings.Test plan
npm run buildcleannpm run dev -- flamingoland4/4 passes:🤖 Generated with Claude Code