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>
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>
Resolves the working tree that was sitting uncommitted on this machine
when the JSX rewrite (77c59ce, c9992e2) landed on the gitea remote.
- Adds favorites flow (FavoriteRoute model, FavoritesManager service,
ContentView favorites strip with context-menu remove).
- Adds FlightLoad model + FlightLoadDetailView sheet rendering cabin
capacity, upgrade list, standby list, and seat-availability summary.
- Adds WebViewFetcher (the generic WKWebView helper used by the load
service for non-JSX flows).
- Adds RouteMapView for destination map mode and threads it into
DestinationsListView with a list/map toggle.
- Adds AIRLINE_API_SPEC.md capturing the cross-airline load API surface.
- Wires JSXWebViewFetcher.swift into the Flights target in
project.pbxproj (file was added to the repo by the JSX rewrite commit
but never registered with the Xcode target, so the build was broken
on a fresh checkout).
- Misc Airport/AirportDatabase/FlightsApp/FlightScheduleRow/
RouteDetailView tweaks that the rest of this WIP depends on.
Build verified clean against the iOS Simulator destination.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
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>
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>