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>
This commit is contained in:
Trey t
2026-04-24 23:21:30 -05:00
parent 1e74552184
commit 6005146e75
26 changed files with 61519 additions and 1334 deletions
+50 -80
View File
@@ -14,20 +14,25 @@ hardware against the live production site in April 2026.
2. You **cannot** call that endpoint from outside a loaded `jsx.com` page —
Akamai will reject the request with an HTTP/2 protocol error based on the
TLS fingerprint of whatever's making the call.
3. You **can** call it from inside a loaded `jsx.com` page, using the
browser's own `fetch()` with the anonymous token the SPA stashes in
`sessionStorage["navitaire.digital.token"]`. Same-origin + browser TLS +
browser cookies means Akamai sees it as a normal user request.
4. Driving the on-page UI to submit the search form is a dead end in
**WKWebView** (but works in Playwright) because of a subtle trusted-event
issue described below.
3. Real Chrome succeeds when you actually drive the full one-way flow:
select `One Way`, fill origin and destination, pick the depart date,
click `DONE`, then click `FIND FLIGHTS`. That fires
`POST /api/nsk/v4/availability/search/simple` and lands on
`/booking/select`.
4. In WKWebView, the safest runtime strategy is layered: drive the same UI
flow first, try the real `FIND FLIGHTS` button, then fall back to the
component's `search()` method, then finally to a direct in-page
`fetch()` if page state still does not materialize.
The working pattern is therefore:
Navigate browser → jsx.com (real page load)
Wait for SPA bootstrap (station buttons exist, token call finishes)
Read token from sessionStorage
POST /api/nsk/v4/availability/search/simple ← from inside the page
Select One Way / route / date
Click DONE
Click FIND FLIGHTS
Wait for booking/select or Angular availability state
Fallback: component.search() or direct in-page POST if needed
Parse the response
---
@@ -281,70 +286,40 @@ won't surface any error to the DOM.
---
## WKWebView vs Playwright — the trusted-events trap
## Browser validation and WKWebView strategy
This is the single most important finding from this session.
The earlier "WKWebView cannot submit the form" theory was too strong. The
browser probe that only failed `POST /search/simple` had not actually driven
the same successful user flow Chrome uses.
### The symptom
### What is confirmed
In WKWebView, after driving the entire UI (one-way, origin, destination,
day cell, DONE button), the depart-date input visually shows `"Sat, Apr 11"`
and the Find Flights button is visually enabled. Clicking it fires the click
handler (you can prove it with a capture listener), but no POST to
`/availability/search/simple` happens — ever. The only network call that
fires after the click is the token refresh that was already scheduled.
- Real Chrome does succeed when the automation performs the whole flow:
`One Way` -> origin -> destination -> depart date -> `DONE` ->
`FIND FLIGHTS`.
- That path produces a live `POST /api/nsk/v4/availability/search/simple`
with HTTP `200` and advances the page to `https://www.jsx.com/booking/select`.
- The response contains per-flight availability counts that the app can map
back to `XE` flights.
### The cause
### What remains true in WKWebView
WKWebView's synthetic DOM events produced by
`element.dispatchEvent(new MouseEvent('click', {...}))` have
`event.isTrusted === false`. JSX's custom datepicker only commits its
day-cell selection into its Angular `FormControl` on a **trusted** user
gesture. So the synthetic click on a day cell visually highlights the cell,
lets DONE close the picker, and updates the input's display value — but the
underlying FormControl stays `null`.
Synthetic events can still be brittle. A visually-filled form is not enough;
the page may still reject the search if the component's internal state is not
fully committed. Because of that, the iOS fetcher should not rely on exactly
one trigger path.
When Find Flights is then clicked, the handler runs `search()`, which does:
### Current iOS runtime strategy
```ts
search() {
if (this.form.invalid) {
this.form.markAllAsTouched();
return;
}
this.api.post('/availability/search/simple', this.buildBody());
}
```
`Flights/Services/JSXWebViewFetcher.swift` step 17 now does this:
Form is invalid because `departDate` FormControl is `null`, so the method
returns early. No POST. Silent failure.
1. Prime the Angular search component with the target date.
2. Click the real `FIND FLIGHTS` button and wait for availability state.
3. If that does not materialize data, call `component.search()`.
4. If that still fails, issue the direct in-page `fetch()` POST as the last fallback.
### Why Playwright doesn't hit this
Playwright's `page.click()` and `locator.click()` don't produce synthetic
DOM events — they go through CDP's `Input.dispatchMouseEvent`, which
produces events indistinguishable from real user input, including
`isTrusted === true`. JSX's picker accepts those and commits the date.
### Why Angular's `markAllAsTouched` workaround doesn't help
You can walk `__ngContext__` on every element looking for `FormControl`
instances and call `markAllAsTouched()` + `updateValueAndValidity()`. This
does trigger revalidation and reveals all the currently-invalid fields, but
it doesn't populate the empty date control — it just marks it dirty. The
underlying value is still `null` because nothing wrote to it.
### The WKWebView workaround
Don't click Find Flights. Instead, call
`/api/nsk/v4/availability/search/simple` directly via `fetch()` from the
page context after reading the token out of `sessionStorage`. You still do
the UI-driving steps (for page warm-up, session establishment, and as a
smoke test that the site is functional), but the actual data fetch happens
via a direct API call whose request shape matches exactly what
`search()` would have posted.
This is what `Flights/Services/JSXWebViewFetcher.swift` step 17 does.
That keeps the app aligned with the proven Chrome behavior without betting the
entire integration on one fragile DOM event path.
---
@@ -357,8 +332,8 @@ JSX fronts `api.jsx.com` with Akamai Bot Manager. Observed behavior:
| Plain `curl`, `fetch` from Node, any external HTTP client | Blocked. Almost all endpoints return HTML challenge page or Akamai error. |
| Playwright's built-in `chromium.launch()` (both bundled chromium and `channel: "chrome"`) | GET requests succeed, but `POST /availability/search/simple` specifically returns `ERR_HTTP2_PROTOCOL_ERROR`. Playwright injects enough automation bits for Akamai to flag the TLS/H2 fingerprint. |
| Real Chrome spawned as a plain process + Playwright attached via `chromium.connectOverCDP()` | **Works reliably.** Chrome has the expected fingerprint and Playwright is only driving it via CDP, not altering it. |
| WKWebView on macOS / iOS | GET requests succeed. Direct `POST /availability/search/simple` from inside a loaded `jsx.com` page via `fetch()` also succeeds. The browser session's cookies and TLS fingerprint are trusted. |
| WKWebView with UI-driven Find Flights click | Fails — but for an unrelated reason (the trusted-events trap above). Angular never fires the POST in the first place, so Akamai never sees it. |
| WKWebView on macOS / iOS | Same-origin traffic from a real loaded `jsx.com` page can work, but the most robust app strategy is still layered because DOM-driven submission can be sensitive to page state and timing. |
| WKWebView with the current app flow | Try real `FIND FLIGHTS` first, then `component.search()`, then direct in-page `fetch()`. This is the runtime path the app now uses. |
Observations:
@@ -368,25 +343,20 @@ Observations:
---
## Network interceptor pattern
## Network observation
If you want to capture JSX traffic programmatically (e.g. in WKWebView or
Playwright), override `window.fetch` and `XMLHttpRequest` to tee every
request/response to a probe object. Two things to track separately:
For the app runtime, prefer passive observation over wrapping `fetch()` or
`XMLHttpRequest`. The current iOS flow uses a `PerformanceObserver` to record
`api.jsx.com` resource entries without touching the request pipeline.
1. **Initiated calls** — every URL passed to `fetch()` or
`XMLHttpRequest.send()`, recorded *before* awaiting the response.
2. **Completed calls** — every call that returned a response (any status),
plus an error marker on network failure.
That gives enough signal to answer:
This lets you tell apart "Angular's `search()` never fired a POST" (nothing
in initiated calls) from "Angular fired a POST but the network rejected it"
(initiated but not completed, error flag set). This distinction was critical
for diagnosing the WKWebView trusted-events issue — without it, both failure
modes look like "no searchSimple response seen".
1. Did the page attempt `/availability/search/simple` at all?
2. Did the page advance to `/booking/select`?
3. Did Angular availability state appear after the search trigger?
See the `window.__jsxProbe` setup in
`Flights/Services/JSXWebViewFetcher.swift` step 4 for a working
`Flights/Services/JSXWebViewFetcher.swift` step 4 for the current
implementation.
---