4a939340a2af7f869017647b6ccacc2789b9c894
15 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4a939340a2 |
Remove Spirit Airlines (defunct — merged into Frontier)
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> |
||
|
|
62729213d7 |
Add FlightsTests target + fix AA load fetcher (Android UA version bump)
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>
|
||
|
|
0c4777216e |
Make RoutePlannerView the home; merge "Where can I go?" into it
Single unified search at the app root. TO is optional: filled goes through
/route (connections); blank flips to /departures with a time-window picker
("Where can I go?"). Same per-leg load card detail screen for any tap, so
direct flights and multi-stop connections share the same UX.
- Drop ContentView entirely (favorites + browse + entry cards). FlightsApp
instantiates RoutePlannerView directly.
- Delete WhereToGoView; DepartureLegRow is inlined into RoutePlannerView
as the where-can-I-go result row.
- SearchRoute enum trimmed to just the cases DestinationsListView still
references and moved to its own file (Models/SearchRoute.swift).
Sort bar moved out of the controls cards into a dedicated row between the
Search button and the results — only visible once results exist. Switched
from segmented to dropdown menu picker. Options narrowed to the four
the user asked for: Departure Earliest / Departure Latest / Fewest Stops
/ Most Stops in connection mode, just the two time-based options for
where-can-I-go (single-leg, stop-count is meaningless). All sorts apply
client-side; upstream still gets `departure_time` for a stable base order.
Two real bugs fixed in connection search:
- Past flights weren't filtered. Same-day searches return mostly already-
departed itineraries because the API sorts earliest-first. Added a
`firstDeparture > now` filter applied before sort. Header surfaces the
dropped count ("12 itineraries · 38 already departed"). When every
result is past, the error message says so explicitly instead of going
blank.
- 100-result limit was way too low for hub→hub with maxStops:2 — the
combinatorial explosion of valid permutations filled the cap with
morning flights and never reached afternoon. Bumped to 500.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
df4a74726c |
Route Explorer: unified per-leg load card + multi-leg fan-out
Single ConnectionLoadDetailView is now the universal detail screen for
both Find Connections (1+ legs) and Where Can I Go (single-leg). For
multi-stop connections it fetches each leg's load in parallel via
withTaskGroup so the slowest carrier doesn't block the rest. Each leg
card shows airline + flight + IATAs + airport names + aircraft + an
open/standby summary, with a "Full details" drill-down to
FlightLoadDetailView for waitlists/passenger lists.
Bug fixes along the way:
- Empty origin/destination in carrier API URLs (HTTP 400 from AA): the
4 separate @State vars feeding .sheet(item:) raced — sheet captured
empty strings before the other writes settled. Bundled into one
Identifiable RouteLoadDetailRequest / ConnectionLoadRequest so updates
are atomic.
- Flight numbers rendered with locale separators ("AA 6,380", "3,189").
Text("\(int)") resolves to the LocalizedStringKey initializer; switched
to Text(verbatim:).
- "Load data not available for {airline}" was misleading when the
airline IS supported but a specific flight has no data. Reworded to
flight-scoped copy.
- AA fetcher had no logging — added URL/status/body/keys diagnostics
matching the UA pattern.
UI cleanup:
- DepartureLegRow: big IATAs on their own row, full airport names on a
middle-truncated subtitle, aircraft pill single-line tail-truncated.
- LegSummary (ConnectionRow): airport-name subtitle line below
times+IATAs row.
- airportName priority: bundled airports.json first ("Dallas-Fort
Worth") over the route-explorer appendix ("Dallas Dallas/Fort Worth
Intl") which truncated to garbage.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
4bd7a74042 |
Add ROUTE_EXPLORER_GUIDE.md for the new connection finder + departures features
Mirrors the existing AIRLINE_INTEGRATION_GUIDE / JSX_NOTES style: file layout, RouteExplorerClient public API, the bridge to FlightLoadDetailView via RouteFlight.toFlightSchedule, known limitations (rate limit, schedule- not-loads, no test target, tz display, tenancy risk), and how-to-extend recipes (new sort order, new airline, new upstream endpoint). Includes a manual smoke-test walkthrough and a pointer to api_docs/ for the upstream surface details. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
4d026ef530 |
RouteExplorerClient: send browser-shaped headers (fix 403 on /api/token)
The route-explorer.com proxy gates /api/token and /api/flight-search by Origin/Referer in production. Without those headers iOS got 403. Mirror the existing United pattern (applyUnitedBrowserHeaders): set User-Agent, Accept-Language, Referer, Origin on every request. Also log the token response body on non-200 so future failures are diagnosable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b403bfd970 |
Add route-explorer.com integration: connection finder + departures board
- RouteExplorerClient: anonymous HMAC token (route-explorer.com/api/token,
IP rate-limited 10/min), POST /api/flight-search with X-API-Token; auto
retry on 401/403 token rotation. Wraps the SuperJSON {json:{...}} envelope
for the upstream tRPC endpoints.
- RouteExplorerModels: Codable types for /route, /departures responses
(RouteConnection, RouteFlight, cabins, appendix). Custom ISO-8601
decoder for the dateTime-with-offset timestamps. Bridge helper
RouteFlight.toFlightSchedule(...) so route-explorer legs reuse the
existing FlightLoadDetailView and AirlineLoadService flow for
supported carriers (UA/AA/NK/KE/B6/AS/EK/XE).
- RoutePlannerView: feature (a) — direct + multi-stop A→B routing via
/route with maxStops 0/1/2, sortBy departure_time/duration, optional
interline-only filter. Renders one ConnectionRow per itinerary with
chained legs and layover indicators.
- WhereToGoView: feature (b) — "where can I go" departures board for an
airport over a 2/4/6/12/24h window. Capacity pills (F/J/W/Y), color-
coded countdown, cross-midnight rollover. Tap any leg → load detail.
- IATAAirportPicker: lightweight local-only picker against
AirportDatabase (no flightconnections roundtrip needed since
route-explorer keys on IATA, not FC IDs).
- ContentView: two new entry-point cards (Find Connections, Where can I
go?) above the favorites list.
- api_docs/route_explorer_api.md + captures: full endpoint reference and
representative response samples (DFW→LAS direct, DFW→KOA 1-stop,
LBB→KOA 2-stop, AA2178 schedule, DFW departures).
No tests yet — project has no test target and adding TDD would require
scaffolding XCTest first. Worth backfilling tests for the date decoder,
layover math, and toFlightSchedule bridge using the saved fixtures.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
6005146e75 |
Airline integration work: AirlineLoadService updates, docs, JSX scripts
- AirlineLoadService: pass airport DB for timezone-aware date strings, add browser-shaped headers for United, expand JetBlue/Alaska/Emirates signatures to take origin, log/parse fixes for Korean Air. - FlightsApp: build AirlineLoadService with the airport DB and inject it. - JSX: continued WebView-based fetcher work plus updated JSX_NOTES. - Docs: add AIRLINE_INTEGRATION_GUIDE.md, drop the old AIRLINE_API_SPEC.md, add api_docs/ (StaffTraveler reverse-engineering captures + findings). - Scripts: jsx_cdp_probe, jsx_live_monitor, jsx_swift_smoke for JSX protocol exploration. - .gitignore: exclude airlines/ (local-only APK/IPA reverse-engineering). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
1e74552184 |
Revert "JSX: clean up dead WKWebView fallback paths"
This reverts commit
|
||
|
|
5f19e48172 |
JSX: clean up dead WKWebView fallback paths
Now that we've confirmed the direct in-page fetch() POST to /api/nsk/v4/availability/search/simple works end-to-end on real iOS devices (and is the only thing that does — simulator is blocked at the transport layer by Akamai per-endpoint fingerprinting), delete the dead simulator-era attempts that were kept around as hopeful fallbacks: - Delete nativePOSTSearchSimple and all the URLSession+cookie-replay plumbing. URLSession can't reach /search/simple from iOS Simulator either (TLS fingerprint same as WKWebView), and on real device the in-page fetch already works so the URLSession path is never useful. - Delete the ~150 lines of SPA state-harvest JavaScript that walked __ngContext__ to find the parsed availability payload inside Angular services as an attempt-2 fallback. The state-harvest was a proxy for "maybe the POST went through but our interceptor swallowed the response" — that theory is dead now that we know the POST itself is what's blocked in the simulator. - Delete the capturedBody instance property that only nativePOST wrote to. Step 17 is now exactly what it claims to be: read the sessionStorage token, fire a single direct fetch() POST from the page context, return the body on success. ~400 lines removed from JSXWebViewFetcher.swift (2148 -> 1748). Step 18's low-fare fallback stays as graceful degradation when the POST fails (which happens on iOS Simulator). The fallback cabin is now labeled "Route day-total (fallback)" instead of "Route (day total)" so the UI clearly distinguishes a per-flight seat count from a route estimate. JSX_NOTES.md corrected: removed the inaccurate claim that WKWebView POSTs to /search/simple just work. The anti-bot-surface table now separates iOS Simulator (fails) from real iOS device (works) with the specific error modes for each. TL;DR adds a visible caveat at the top that the working path requires a real device; develop with the low-fare fallback in the simulator. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
4d46b836a1 |
JSX: per-flight loads via in-page fetch POST (real device only)
After a long debug, the working approach for fetching per-flight
availability from JSX in WKWebView is a direct fetch() POST to
/api/nsk/v4/availability/search/simple from inside the loaded
jsx.com page context, using the anonymous auth token from
sessionStorage["navitaire.digital.token"]. Confirmed end-to-end on
a real iOS device: returns status 200 with the full 14 KB payload,
parses into per-flight JSXFlight objects with correct per-class
seat counts (e.g. XE286 = 6 seats = 3 Hop-On + 3 All-In).
Architecture:
- JSXWebViewFetcher drives the jsx.com SPA through 18 step-by-step
verified phases: create WKWebView, navigate, install passive
PerformanceObserver, dismiss Osano, select One Way, open origin
station picker and select, open destination picker and select,
open depart datepicker (polling for day cells to render), click
the target day cell by aria-label, click the picker's DONE
button to commit, force Angular form revalidation, then fire
the POST directly from the page context.
- The POST attempt is wrapped in a fallback chain: if the direct
fetch fails, try walking __ngContext__ to find the minified-name
flight-search component ("Me" in the current build) by shape
(beginDate + origin + destination + search method) and call
search() directly, then poll Angular's own state for the parsed
availability response. Final fallback is a direct GET to
/api/nsk/v1/availability/lowfare/estimate which returns a
day-total count when all per-flight paths fail.
- JSXSearchResult.flights contains one JSXFlight per unique
journey in data.results[].trips[].journeysAvailableByMarket,
with per-class breakdowns joined against data.faresAvailable.
- Every step has an action + one or more post-condition
verifications that log independently. Step failures dump
action data fields, page state, error markers, and any
PerformanceObserver resource entries so the next iteration
has ground truth, not guesses.
Known environment limitation:
- iOS Simulator CANNOT reach POST /availability/search/simple.
Simulator WebKit runs against macOS's CFNetwork stack, which
Akamai's per-endpoint protection tier treats as a different
TLS/H2 client from real iOS Safari. Every in-page or native
request (fetch, XHR, URLSession with cookies from the WKWebView
store) fails with TypeError: Load failed / error -1005 on that
specific endpoint. Other api.jsx.com endpoints (token, graph/*,
lowfare/estimate) work fine from the simulator because they're
in a looser Akamai group. On real iOS hardware the POST goes
through with status 200.
AirlineLoadService.fetchJSXLoad now threads departureTime into the
XE-specific path so the caller can disambiguate multiple flights
with the same number. Match order: (1) exact flight number match
if unique, (2) departureTime tie-break if multiple, (3) first
same-number flight as last resort. Each branch logs which match
strategy won so caller ambiguity shows up in the log.
FlightLoadDetailView logs full tap metadata (id, flight number,
extracted number, departureTime, route) and received load
(flight number, total available, total capacity) so the
fetch-to-display data flow is traceable end-to-end per tap.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
847000d059 |
Land local WIP on top of JSX rewrite + wire JSXWebViewFetcher into target
Resolves the working tree that was sitting uncommitted on this machine when the JSX rewrite ( |
||
|
|
c9992e2d11 |
Add JSX reverse-engineering notes
Capture the findings from the JSXWebViewFetcher rewrite session: - The Navitaire /api/nsk/v4/availability/search/simple endpoint that actually contains the flight load data, including the request body shape and the nested response structure (journeysAvailableByMarket → fares joined with faresAvailable for price/class). - How the anonymous auth token lands in sessionStorage (navitaire.digital.token) and how to use it as the Authorization header on a direct fetch() from inside the page context. - The jsx.com SPA one-way form structure (trip-type mat-select, station pickers, custom two-month range picker with DONE button, Find Flights submit), and the selectors / strategies that work for each one. - The Osano cookie banner gotcha — its role="dialog" fools calendar detection, so the banner node must be force-removed after accepting. - The datepicker quirks: JSX uses a custom picker (not mat-calendar), renders in two phases (shell then cells), and day cells carry aria-labels in the format "Saturday, April 11, 2026" with a weekday prefix — so exact-match "April 11, 2026" fails but loose month+year+day-word-boundary matching works. - The central finding: WKWebView's synthetic DOM events have isTrusted=false, so JSX's datepicker never commits its day-cell selection into the Angular FormControl, Angular's search() sees the form as invalid, and no POST fires. Playwright doesn't hit this because CDP's Input.dispatchMouseEvent produces trusted events. - The Akamai surface: external HTTP clients are blocked, Playwright's own launch() is blocked on POST /search/simple (ERR_HTTP2_PROTOCOL _ERROR), connectOverCDP to a plain Chrome works, and WKWebView's same-origin fetch() from inside a loaded jsx.com page works. - The working approach (direct POST from page context using the sessionStorage token) and why it sidesteps both the trusted-events and the Akamai problems. - The network interceptor pattern that distinguishes "Angular never fired the POST" from "Angular fired it but the network rejected it" — critical for diagnosing the trusted-events trap. - Code pointers to the Swift runtime (JSXWebViewFetcher.swift), the iOS call site (AirlineLoadService.fetchJSXLoad), and the Playwright reference (scripts/jsx_playwright_search.mjs). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
77c59ce2c2 |
Rewrite JSX flow with per-step verification + direct API call
Full rewrite of Flights/Services/JSXWebViewFetcher.swift implementing a 19-step WKWebView flow that drives the jsx.com one-way search UI, then calls POST /api/nsk/v4/availability/search/simple directly via fetch() from within the page context using the anonymous auth token read from sessionStorage["navitaire.digital.token"]. Why the direct call instead of clicking Find Flights: WKWebView's synthetic MouseEvents have isTrusted=false, and JSX's custom datepicker commits its day-cell selection into the Angular FormControl only on trusted user gestures. The result is that the date input displays "Sat, Apr 11" but the underlying FormControl stays null, so Angular's search() sees form.invalid === true and silently returns without firing a request. Playwright sidesteps this because CDP's Input.dispatchMouseEvent produces trusted events; WKWebView has no equivalent. The fix is to drive the UI steps (for page warm-up and smoke testing) but then call the API directly — the same-origin fetch inherits the browser's cookies and TLS fingerprint so Akamai sees it as legitimate traffic, same as the lowfare/estimate GET that already works through the page. Every step has an action and one or more post-condition verifications. On failure the runner dumps the action's returned data fields, page state (URL, selector counts, form error markers), and both the last initiated AND last completed api.jsx.com calls so network-level blocks and form-validation bails can be distinguished. New return type JSXSearchResult exposes every unique flight from the search/simple response as [JSXFlight] with per-class load breakdowns (classOfService, productClass, availableCount, fareTotal, revenueTotal) so callers can see all flights, not just one. Flights/Services/AirlineLoadService.swift: fetchJSXLoad now consumes the [JSXFlight] array, logs every returned flight, and picks the requested flight by digit-match. Deleted 495 lines of dead JSX helpers (_fetchJSXLoad_oldMultiStep, parseJSXResponse, findJSXJourneys, extractJSXFlightNumber, extractJSXAvailableSeats, collectJSXAvailableCounts, parseJSXLowFareEstimate, normalizeFlightNumber). scripts/jsx_playwright_search.mjs: standalone Playwright reference implementation of the same flow. Launches real Chrome with --remote- debugging-port and attaches via chromium.connectOverCDP() — this bypasses Akamai's fingerprint check on Playwright's own launch and produced the UI-flow steps and per-flight extractor logic that the Swift rewrite mirrors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
3790792040 |
Initial commit: Flights iOS app
Flight search app built on FlightConnections.com API data. Features: airport search with autocomplete, browse by country/state/map, flight schedules by route and date, multi-airline support with per-airline schedule loading. Includes 4,561-airport GPS database for map browsing. Adaptive light/dark mode UI inspired by Flighty. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |