route-explorer's /api/token sits behind invisible Cloudflare Turnstile
that requires Apple's Private Access Token attestation. Third-party
iOS apps don't qualify for PAT issuance, and Linux Docker containers
can't pass it either (cross-OS fingerprint, even with patchright /
Camoufox). Migrates direct-flight search to FlightAware; multi-stop
and where-can-I-go remain via embedded SFSafariViewController.
- FlightAwareScheduleClient — scrapes route.rvt + trackpoll JSON for
real schedules without auth. T+0..2 day window. Tests against
captured HTML fixtures.
- BlobRouteClient — pulls the public Vercel blob route catalog
route-explorer's frontend reads (no auth, no Turnstile).
- DiagnosticLogger + LoggingURLSessionDelegate + DiagnosticsView —
device-shareable forensic trace. Boot header captures device, OS,
locale, UA; share-sheet export of session logs.
- TurnstileDebugView — live WKWebView gate inspector. Used to prove
the PAT-entitlement gap on a real device.
- RouteExplorerBrowserView — SFSafariViewController wrapper. Real
Safari clears Turnstile naturally; the in-app browser opens at
pre-filled search URLs. Surfaced from Search ("Open in
route-explorer") and Settings → Tools.
- RouteExplorerTokenStore + RouteExplorerSetupView — bookmarklet
capture flow (token round-tripped via flights://routeexplorer-token
URL scheme). Kept dormant for future use.
backend/ — Docker proxy attempts (Playwright, patchright, Camoufox).
All fail on Linux because Cloudflare auto-denies before the Turnstile
widget renders. Documented; kept as scaffolding for a future paid-
solver integration.
scripts/probe_flightaware.py — reference algorithm for the FA path.
scripts/probe_nodriver.py — local-Mac sanity check confirming the
gate clears with real macOS Chrome (proves the blocker is
fingerprint-level, not network-level).
Sun Country runs Navitaire (same PSS as JSX) but exposes their public
availability search endpoint that returns BETTER load data than AA:
per-flight `capacity` AND `sold` (booked passenger count), so we can
compute exact load factor.
Implementation:
- AirlineLoadService.fetchSunCountryLoad: POSTs to
syprod-api.suncountry.com/api/nsk/v4/availability/search/simple.
Parses results→trips→journeysAvailableByMarket, matches by flight
number, pulls capacity + sold + equipmentType from legInfo.
- Returns a single Economy CabinLoad with capacity/booked = sold.
No standby program — SY is single-cabin Y.
- Auth: Azure APIM subscription key + a long-lived dotREZ JWT
(both static, captured from suncountry.com network traffic, neither
is a user session token).
- Anti-bot: Imperva WAF in front of syprod-api.suncountry.com is gated
on User-Agent + Referer + Origin headers. applySunCountryBrowserHeaders
mirrors the pattern we use for UA / AA. NO WebView needed.
- Explicit ⚠️ log when 403 Incapsula response detected, pointing at
the header helper.
Test infrastructure:
- knownDailyFlights now carries a dayOffset (today vs tomorrow) per
carrier — different upstreams have different snapshot windows:
AM is T-1d..T+0 (today); SY's Navitaire only returns future flights
(tomorrow); others default to tomorrow as a safer choice.
- Added test_SY_sunCountry with hubs MSP/LAS/MCO/DEN. Fallback is
SY104 LAS-MSP tomorrow.
Docs:
- AIRLINE_INTEGRATION_GUIDE: SY status row + full section 5c covering
endpoint, auth, headers, response shape, failure modes, and how to
re-capture tokens when they rotate.
Reverse-engineering notes:
- SY app is Flutter (Dart AOT) — bridge smali is minimal. Strings
extracted from libapp.so revealed isNonRevTrip/isStandby/
inventoryControl keywords + the syprod-api hostname.
- Token endpoint is PUT (not POST). Returns {"data":null} — token is
the existing Authorization JWT, not a session refresh.
- Confirmed working from plain curl with browser headers (no Imperva
TLS-fingerprint gate beyond UA/Referer/Origin).
Test run 2026-05-26 (xcodebuild test):
✅ AA, AM, AS, B6, EK, KE, SY (capacity=186 sold=184 load=99%), UA
⏭️ XE
8 passing, 1 skipped, 0 failures, 11s total.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AM exposes a public Sabre GetPassengerListRQ proxy via AWS API Gateway —
no auth, no API key — used by the consumer app's flight-status widget.
The endpoint returns per-cabin authorized/available plus full standby +
upgrade passenger lists with isStaff flag, numeric priority, fare class,
position movement, and PII (matching what we get from AA but with
better cabin capacity data).
Implementation:
- AirlineLoadService.fetchAeromexicoLoad: parallel GETs against
/rb/passengerliststandby and /rb/passengerlistupgrade, merging
cabin info + per-list passengers into a single FlightLoad. Headers
channel=web / flow=CHECKIN extracted from the AM APK Constant.smali.
Cabin codes Y/C/P/F mapped to readable names (Economy / Clase Premier /
Premier One / First).
- 4-digit zero-padding of the operating flight code (server validates
^[0-9]{4}$).
- "NONE LISTED" warning treated as nil (snapshot outside T-1d/T+2d
window or no pax yet); explicit log so future failures are
diagnosable.
Test infrastructure:
- Added test_AM_aeromexico using MEX/GDL/MTY/CUN hubs.
- Cascading fallback in runAirlineLoadTest: try the route-explorer
discovered flight first; if it returns nil (typical for AM Connect
regionals that aren't in Sabre), fall back to the known-daily flight
(AM0058 MEX-MTY). Pattern useful for any future carrier whose
regional ops don't show up in the load system.
- knownDailyFlights extended with AM0058 MEX-MTY.
Docs:
- AIRLINE_INTEGRATION_GUIDE: AM status row + full section 5b with
endpoint params, response shape, snapshot window timing, failure
modes, cabin code mapping, regional carrier caveat.
Test run 2026-05-26:
✅ AA, AM (cabins=1 upgrade=1), AS, B6, EK, KE, UA ⏭️ XE
7 passing, 1 skipped, 0 failures, 12s total.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Spirit ceased operations, so the fetchSpiritStatus path and all NK
references are dead code. Pulled out:
- AirlineLoadService: drop `case "NK"` from the router, delete
fetchSpiritStatus (the GetFlightInfoBI POST that was returning 403
even after our APIM key was accepted).
- FlightLoadDetailView: drop the `schedule.airline.iata == "NK"` branch
and the spiritUnavailableView placeholder.
- FlightLoad model: update the airlineCode comment.
- AirlineLoadIntegrationTests: remove test_NK_spirit and drop "NK" from
statusOnlyAirlines / knownDailyFlights fallback table.
- AIRLINE_INTEGRATION_GUIDE.md: tombstone the Spirit section and remove
it from the cheat-sheets and recommendations.
Test suite now: 6 airlines passing (AA, AS, B6, EK, KE, UA), 1 skipped
(XE — WKWebView host required), 0 failures, runs in ~10s.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AA was silently returning nil because the server now rejects User-Agent
"Android/2025.31" with HTTP 403 ("Please update your version of the
American Airlines app"). Bumped to "2026.14" (matches the APK in
airlines/) and centralized to a constant so the next bump is one line.
Added comprehensive logging to fetchAmericanLoad (was zero) so the next
breakage won't be silent — including an explicit ⚠️ when the server
returns the "update your version" payload.
New FlightsTests target with AirlineLoadIntegrationTests — hits live
airline APIs to verify each fetcher still returns data. Per-airline
strategy:
- Try route-explorer /departures from carrier hubs for a flight in the
next 24h (works for AA/UA/AS/B6).
- Fall back to a known-good daily flight when route-explorer doesn't
have the carrier in its data (NK/EK/KE — ULCC + some intl carriers).
- B6/EK/NK are status-only by design (no standby data without a PNR);
asserted as non-nil only.
- XE (JSX) skipped: needs WKWebView host.
Retries on route-explorer 429 by parsing the `retryAfter` field and
sleeping the indicated number of seconds. Static-shared client+services
across tests so the token cache survives.
Results 2026-05-26 (xcodebuild test -scheme Flights):
✅ AA, AS, B6, EK, KE, UA ❌ NK ⏭️ XE
NK (Spirit) is now broken: GetFlightInfoBI returns HTTP 403 with
{"getFlightInfoBIResult":null}. APIM key still accepted (401 without
it), but the call itself is rejected. Documented in
AIRLINE_INTEGRATION_GUIDE.md as a known regression to fix; likely
needs reverse-engineering against the current Spirit APK in airlines/.
Also: enable shared schemes in .gitignore so `xcodebuild test` works
out of the box for anyone cloning the repo.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>