9e1dbfbf9041d9096cb3281c46c4886667bd9fb4
36 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9e1dbfbf90 |
Re-enable flights:// URL scheme (with UISceneConfigurations)
Switching the app target's Info.plist back to a manual file at Flights/Info.plist so we can register CFBundleURLTypes for the flights:// scheme. The Info.plist now includes UISceneConfigurations inside UIApplicationSceneManifest — missing that key was the likely cause of the device-launch crash on the previous attempt. Verified launch + xcrun simctl openurl flights://import?... on the iPhone 17 Pro simulator. App stays running after URL handoff. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
f40b02f68d |
Fix launch crash: drop CloudKit init, lazy Wallet, revert plist
Three changes that together stop the crash-on-launch the previous build hit on device: 1. FlightsApp: stop attempting `cloudKitDatabase: .private(...)` when the iCloud entitlement isn't set — SwiftData's ModelContainer init can fatalError validating the cap (not just throw), so try? doesn't save us. Go straight to local config, with an in-memory fallback if the disk store is incompatible with the current schema. 2. WalletPassObserver: don't touch PKPassLibrary in init(). `@StateObject` accesses .shared at view body time, which on first launch can race against PassKit subsystem init. Move the library bring-up into an explicit start() called from RootView's .task. 3. Flights app target Info.plist: revert from manual INFOPLIST_FILE back to GENERATE_INFOPLIST_FILE = YES (with the INFOPLIST_KEY_* entries restored). The manual plist I wrote was missing some auto-generated keys the device launch path needs. Loses the custom URL scheme — `.onOpenURL` handler stays in code but won't fire until we re-add the scheme via a manual plist that's been verified end-to-end. Verified launch on iPhone 17 Pro simulator — scene becomes key window, no fatalError. The earlier on-device crash was almost certainly the CloudKit init. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
d444a5caac |
Share Extension: back out target, keep URL scheme
The signed export needs a provisioning profile for the extension's bundle id (com.flights.app.share) which the dev portal hasn't provisioned for this team. Reverted the pbxproj surgery that added the FlightsShareExtension target. Kept: - Flights/Info.plist with CFBundleURLTypes registering flights:// - `.onOpenURL` handler in RootView that consumes `flights://import?carrier=...&num=...&dep=...&arr=...&date=...` - FlightsShareExtension/ source files (ShareViewController.swift + Info.plist) in the repo for later — when the extension bundle id is provisioned, the target can be re-added in Xcode UI in a few minutes (no need to redo the Swift work) Until the extension is built, users can still get Mail integration by installing a one-time Apple Shortcut that takes selected text and opens `flights://import?...`. The host app handles the rest. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
803c812f86 |
History v2: everything — Wallet auto-prompt, age, track replay, share
Adds the deferred pieces from the v1 ship, plus a Mail Share
Extension target so the iOS share sheet picks up flight emails.
Track replay
- `LoggedFlight.icao24` field — populated from FR24 enrichment on
live-tap adds.
- HistoryDetailView's track query now fires for any flight younger
than 7 days that has an icao24, pulling the actual flown path
from OpenSky's /tracks/all endpoint. Falls back to a clean
great-circle arc otherwise.
Wallet auto-prompt
- RootView subscribes to WalletPassObserver.shared. When the user
adds a boarding pass to Apple Wallet, the observer's published
`pendingPass` flips and we present AddFlightView pre-filled with
the parsed origin / destination / flight # / date.
Airframe age + first-flight date
- `AirframeMetadataService` queries OpenSky's
/api/metadata/aircraft/icao/{icao24} endpoint. Caches results in
the existing `AirframeMetadata` SwiftData model so we never
re-fetch the same airframe twice. (jetphotos and planespotters
pages are both Cloudflare-gated; OpenSky's metadata API is the
cleanest free source.)
- HistoryDetailView fires the lookup on appear and persists the
result; the aircraft card already renders "Age" when a date is
cached.
Mail Share Extension
- New `FlightsShareExtension` Xcode target (app-extension product
type) built into the app bundle via an Embed Foundation
Extensions copy phase.
- `ShareViewController` (SLComposeServiceViewController) parses
shared text + URLs for flight-shaped codes ("AA 2178"), route
hints ("DFW → ORD"), and date strings.
- On Save, the extension builds a `flights://import?carrier=…&num=
…&dep=…&arr=…&date=…` URL and opens it via the responder-chain
openURL trick (Share Extensions can't access UIApplication
directly).
- Host app handles the URL via `.onOpenURL` in RootView, switches
to the History tab and presents AddFlightView prefilled.
- App now has an actual Info.plist (CFBundleURLTypes registered
for `flights://`); switched from GENERATE_INFOPLIST_FILE to
INFOPLIST_FILE for the app target.
If the dev portal hasn't registered bundle id
`com.flights.app.share` for the team, the signed archive will
fail. In that case the simpler URL-scheme path still works —
users can hit `flights://import?...` from a Shortcut.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
8308d9cf03 |
History: drop iCloud entitlement (cap not provisioned)
The signed iPhone archive failed because the team's iOS provisioning profile doesn't include the iCloud capability or our iCloud.com.flights.app container. Empty out the entitlements payload so signing succeeds with no caps requested. The app's ModelContainer init already falls back to local-only SwiftData when the CloudKit container isn't reachable — history persists on-device, just doesn't sync across devices yet. When the iCloud cap is provisioned for com.flights.app, restore the entitlements payload (one diff) and CloudKit sync turns back on automatically. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
847e5c6035 |
Flight History (v1): logbook, stats, animated route map, year-in-review
Adds a new History tab implementing the core of Flighty's Passport feature set, free + iCloud-synced. Data layer - `LoggedFlight` and `AirframeMetadata` @Model classes (SwiftData) - ModelContainer with CloudKit private DB; falls back to local-only when CloudKit cap isn't provisioned so the app stays functional. - `FlightHistoryStore` wraps the ModelContext for save/delete + dedupe + great-circle distance / duration helpers + tail-repeat counting. History UI - `HistoryView` — list grouped by year, totals strip at top, swipe to delete, empty state with instructions. - `HistoryRowView` — airframe photo thumbnail (planespotters), flight#, route, type, date. - `HistoryDetailView` — title → route → photo → flown-path / great- circle map → aircraft card (type, tail, age, "Nth time on this airframe") → editable notes → delete. Add paths - "+ Add to my flights" button on the live aircraft sheet — pre-fills the form from FR24 enrichment (carrier, flight#, route, aircraft type, tail). - Manual entry form (`AddFlightView`) with route-explorer autofill via `searchSchedule(carrierCode:flightNumber:startDate:endDate:)`. - Calendar scan (`CalendarFlightImporter` + `CalendarImportView`) — EventKit access prompt → regex-detect flight-shaped events across last 5 years → dedupe → batch-confirm with route-explorer enrichment. - `WalletPassObserver` (PassKit) — observes the library for new boarding passes and parses origin/destination/flight#/seat. Service is wired; explicit UI prompt deferred to follow-up. Stats + visualization - `StatsEngine` — totals (flights / miles / hours / airports / airlines / aircraft / countries) + narrative stats (top airline, top route, top airport, longest, shortest, repeated tails). - `LifetimeStatsView` — big-number tile grid + highlights cards + repeated airframes list. - `HistoryRouteMapView` — every great-circle arc the user has flown, animating in oldest → newest on first appear. Airport dots sized log-scale by visit count. - `YearInReviewView` — Spotify-Wrapped-style horizontal card deck for the current year: total miles, airports + countries, hours airborne, top airline, top route, longest flight. Entitlements - New `Flights.entitlements` with `iCloud.com.flights.app` CloudKit container. Risk note: the build falls back to local-only SwiftData if the CloudKit container isn't provisioned for team V3PF3M6B6U / bundle id com.flights.app. The History feature works fully either way; sync requires the cap to land. Deferred to follow-ups - Wallet auto-prompt UI binding (service exists, view hook TBD) - Mail Share Extension (separate app-extension target) - Jetphotos first-flight-date scraping - OpenSky historical track replay (great-circle fallback ships) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
a1831d0034 |
Detail sheet: reorder to title → route → photo
User asked for this order. Photo now slots between routeSection and LIVE STATE rather than living as a hero above the title. Used a -16 horizontal padding so the photo still goes full-bleed inside the otherwise-16pt-padded content stack. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
16b874a7ad |
Detail sheet: hero aircraft photo via planespotters
Adds a 200pt-tall hero image at the top of the detail sheet showing
the actual airframe (registration-keyed), surfacing special liveries
naturally — photographers chase one-off paint jobs first, so the
most recent photo is usually the most recent livery scheme.
AircraftPhotoService wraps planespotters.net's public API:
- Lookup by registration (FR24 enrichment) first
- Falls back to ICAO24 hex when no registration
- In-memory cache (hits and misses) so we never re-query the same
airframe twice in a session
- User-Agent includes contact URL per planespotters' TOS
- Photographer attribution rendered in the AIRCRAFT section,
tap to open the planespotters page
Sheet hides the banner entirely when no photo exists.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
92bc6ed52e |
Live feed: FR24 primary, OpenSky fallback
OpenSky's free anonymous tier has sparse ground coverage — at DAL it
returned a single airborne aircraft when there were 3+ SWA jets
visibly parked on the apron. FR24's feed.js aggregates ASDE-X, MLAT,
and multiple community ADS-B feeds and reliably surfaces ground
aircraft at major airports. We now query FR24 first and fall back to
OpenSky only when FR24 errors.
FR24's payload also carries departure + arrival IATA + flight number
+ aircraft type + tail number inline, so we shortcut the
route-explorer schedule lookup in the detail sheet: a new
`LiveAircraft.Enrichment` struct holds those fields, and the
ResolvedRoute cascade gains a `.fromFR24` first-tier case that uses
them directly. The `typeCode` and `airlineICAO` computed properties
prefer enrichment values over the AircraftDatabase / callsign-prefix
heuristics — this also fixes the case where FR24 callsigns use the
IATA carrier ("AA0013") which our 3-letter-prefix derivation would
have rejected.
OpenSky still owns trail polylines and recent-flights history; only
the live position fetch swapped sources.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
68c60ec087 |
Detail sheet: lead with depart → arrival, push LIVE STATE below
When you tap an aircraft you almost always want to know where it came from and where it's going. Moving routeSection above the divider so it's the first block under the callsign header — no scrolling to find it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
dee6df1ac6 |
Live map: center on user, persist region, zoom-based aircraft cap
- Initial region cascade: restore last viewed region → on location grant, animate to a city-level view centered on user; otherwise fall back to the saved/continental default. User pans are detected via a center-delta threshold so a late location grant doesn't yank the camera away from where the user is looking. - LocationService: thin one-shot CLLocationManager wrapper with CheckedContinuation. Added NSLocationWhenInUseUsageDescription via INFOPLIST_KEY_ build setting. - Visible-aircraft cap scales with zoom (<2°: uncapped, <8°: 100, <25°: 150, else 200). Active filters bypass the cap entirely so every match always renders. When capped, we keep the N closest to the map center. - Footer shows "Showing N of M" when the cap clips, "N aircraft" otherwise. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
de7a70b198 |
Live tab: hardening pass for smooth, snappy feel
Targeted every hitch in the live-tracker flow:
1) Async DB loading. AircraftDatabase (1.5MB JSON) and
AircraftRegistry (200KB JSON) were parsing synchronously on the
main thread the first time anything touched them — typically when
the user first opened the Live tab. Both now bootstrap with a
safe fallback in init() and parse the full JSON on a background
Task at app launch (FlightsApp.init calls preload() on both).
Reads are NSLock-guarded with lock.withLock {} (async-safe).
2) Crossfade suppression on refresh. The 15s auto-refresh swaps the
`aircraft` array wholesale, which made SwiftUI try to crossfade
every annotation. Wrapped the assignment in a Transaction with
disablesAnimations = true so the swap is instant.
3) Cached filtered aircraft. `filteredAircraft` was a computed
property running through every aircraft on every body re-render
(e.g. while a sheet was animating in). Moved to @State,
recomputed via .onChange handlers on each dependency.
4) Lighter pin view. AircraftPin no longer carries the full
LiveAircraft struct or contains a conditional ZStack — just the
minimal {tint, rotation, isSelected} props, conforms to
Equatable so SwiftUI can skip diffing identical pins, and uses
.animation(nil) on the tint to prevent color crossfades during
refresh.
5) Coarse relative-time bucketing. The footer's "updated 5s ago"
text was ticking every second, which dirtied the footer subtree
on every body pass. Now snaps to {just now, <30s ago, <1m ago,
Nm ago} — no second-by-second ticks.
Net effect: tab opening is instant (DBs are warm), refreshes don't
flicker, filter sheet animation is smooth, map panning isn't fighting
view-tree rebuilds.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
390a158487 |
Live tab: restore type filter via bundled aircraft DB + sheet pickers
Two issues:
1) Menu list stutters. SwiftUI's Menu renders all rows in a
non-virtualized popover. With 30+ airlines and Buttons containing
Labels, scrolling jank starts immediately. Switched the airline +
type filters to a sheet-based picker (LiveFilterPicker) backed by
a real List — virtualized scrolling, plus a search bar that
filters as you type.
2) Type filter was non-functional because OpenSky's anonymous tier
returns ADS-B emitter category as null for most aircraft.
Replaced with a real type-code lookup: bundled aircraftDB.json
(1.5MB slimmed copy of OpenSky's aircraft metadata, 100k
commercial-class airframes, filtered to skip GA / gliders /
ultralights). AircraftDatabase.shared.typeCode(forICAO24:)
returns the ICAO type designator (B738, A21N, etc.).
AircraftDatabase.displayName(forTypeCode:) maps the top ~130
common codes to friendly names ("B738" → "Boeing 737-800").
LiveAircraft now exposes a `typeCode` computed property that
indexes into the DB. The type filter chip → sheet flow uses the
same LiveFilterPicker as airlines, with multi-select + counts +
search.
Both pickers keep the "Selected (N)" group pinned at the top so
the user always sees what they have active without scrolling.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
d6fb73db2c |
Live tab: stable refresh loop, replace dead Type filter with Altitude
Two reported bugs: 1) Bottom bar appeared to be constantly refreshing. Cause: .task(id: refreshTick) restarted every time isLoading flipped (twice per refresh cycle) AND every time aircraft.count changed. On top of that, on-pan refresh fired on every map camera settlement — including the micro-settlements that happen when annotations re-render after each fetch. The cumulative effect looked like a tight loop. Replaced the cascade with a single long-lived .task running a while-loop that sleeps refreshInterval then refreshes. SwiftUI cancels it when the view disappears. Added a bbox-delta gate (refreshIfRegionChanged) so pan-triggered refreshes only fire when the visible center moved >15% of the box width or the zoom changed >20%. Removed nextFetchAllowedAt — the throttle is now structural rather than time-based. 2) Tapping the "Type" filter did nothing. Cause: OpenSky's anonymous tier returns the ADS-B emitter category as null/0 for the vast majority of aircraft, so cachedCategoryItems was empty and the menu opened with no rows. Replaced the Type filter with an Altitude filter (below 10k / 10–25k / 25–40k / above 40k ft), driven by data we always have (baroAltitude / geoAltitude). The menu always has 4 rows with live counts. Also: tightened boundingBox(of:) to return a labeled tuple so the delta-gate code can use .latMin / .lonMax directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
a031a1aafd |
Live tab: resolve dep/arr for every aircraft via schedule cascade
OpenSky alone can't answer "where is this plane going" — /flights/aircraft
only returns landed flights, so an in-progress flight produced an empty
route card. Built a 3-tier resolution cascade that always lands somewhere
useful:
1) Scheduled lookup via route-explorer's /schedule endpoint. Parse the
ADS-B callsign (AAL3055 → carrier AA, number 3055), pull the day's
operating record, get real departure + arrival airports and times.
Works for every carrier route-explorer indexes (mainline + many
regionals). Smoke-tested live: AAL3055 → DFW→MIA, DAL1050 → IAH→MSP,
AAL1753 → XNA→DFW, AAL2978 → XNA→PHL. All from live aircraft caught
in the DFW area at the moment.
2) OpenSky historical (/flights/aircraft). If route-explorer doesn't
have the carrier, fall back to whatever OpenSky last logged for
this airframe. Labeled "LAST FLIGHT · 3h AGO" etc.
3) Trail-derived inference. Last resort for ICAO24s nothing knows about
(private jets, cargo, ad-hoc callsigns). Pull the OpenSky track,
take the first position, find the nearest airport in the bundled
3,900-entry DB (new AirportDatabase.nearestAirport(to:)). Shows
"Departed from KAUS — Austin Bergstrom" with "Heading to —"
acknowledging arrival is unknown.
Plumbed RouteExplorerClient through LiveFlightsView → RootView →
FlightsApp. Added searchSchedule(carrierCode:flightNumber:startDate:
endDate:) that returns [RouteFlight] directly (the /schedule envelope
is `{ flights: [...] }`, distinct from /route's `{ connections: [...] }`).
Three distinct route cards now render based on what we resolved:
- scheduled → green live dot + "Departed / Heading to"
- openSky → "Departed / Arrived" with age label
- inferred → "Departed from X / Heading to —"
- none → explicit "Route unavailable" message
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
ddfcf3e0e4 |
Live tab: cached filter lists, tighten airline filter, clearer route fallback
Three fixes from the latest round of testing: 1) Tapping the airline filter froze the app for several seconds. Cause: `visibleAirlines` / `visibleCategories` were computed properties evaluated on every body re-render, each iterating the aircraft array and hitting the 2,695-entry registry. Caching them in @State and refreshing only via .onChange(of: aircraft) takes the menu-open cost to near-zero. 2) Picking "Southwest" in the airline filter still left other carriers on screen. Cause: when no callsign-derivable airline ICAO was present (N-number GA traffic, ad-hoc callsigns), the filter `guard let` silently let the aircraft through. Tightened to require an ICAO match when any airline filter is active. 3) The detail sheet showed no departure / arrival airport on most selections. Cause: OpenSky's /flights/aircraft endpoint only returns flights *after they've landed*, so an in-progress flight has no entry. We were waiting for one to appear forever. Rewrote the route section: now always shows whatever's most recent, labeled "IN FLIGHT" when the snapshot is < 6h old (with a green live-dot), "LAST FLIGHT · 3h AGO" otherwise, and an explicit "Route not available from OpenSky for this aircraft" card when the endpoint returned nothing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
0550376e3d |
Live tab: fix sheet-collision freeze, safe-area layout, A→Z filters
Three reported bugs: 1) Tap-a-plane freezes the app — two .sheet modifiers stacked on the same view fight each other in SwiftUI. Consolidated into a single .sheet(item:) backed by an ActiveSheet enum (aircraft / settings). Also dropped the Map's selection binding; relying purely on the pin's .onTapGesture eliminates the dual-binding race. 2) Filter bar sits behind the nav title / tab bar — replaced the ZStack overlay layout with safeAreaInset(edge:) so the search + chip bar at the top and the count/refresh/gear strip at the bottom are first-class inset views. Map fills the rest properly. 3) Aircraft type / airline menus not A→Z — both filter lists sorted by displayed name (localizedCaseInsensitiveCompare) instead of by ICAO code / category number. AirlineFilterItem and CategoryFilterItem now carry the displayed `name` separately and sort on it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
6b33a104c8 |
Live tab: bundled airline DB, OpenSky login, in-flight trails
Three follow-ups to the live tab landed together:
1) Bundled airline registry
- airlines.json (208KB, 2,695 entries sourced from FR24's
/mobile/airlines feed and slimmed to {icao,iata,name,logo}).
- AircraftRegistry rewritten as an instance singleton that loads
the bundle at startup, indexes by both ICAO and IATA, and falls
back to a small hardcoded subset if the bundle is unavailable.
- Detail sheet now shows the airline's logo (loaded from FR24's
CDN via AsyncImage) alongside the callsign. Filter chips use
the real names everywhere.
2) OpenSky account login
- OpenSkyCredentials: Keychain wrapper that stores username +
password using SecItem APIs. Posts a notification on change so
the OpenSkyClient can refresh its in-memory copy.
- OpenSkyClient now sends HTTP Basic auth when credentials are
present. Anonymous fallback unchanged.
- OpenSkySettingsView: tap the gear in the footer to sign in.
Credentials are verified against /states/all before being
stored; sign-out clears Keychain. Raises the quota from ~100
to ~4000 requests/day.
3) Flight trails
- AircraftTrack model decodes OpenSky's /tracks/all heterogeneous
path array into typed TrackPoint entries.
- OpenSkyClient.track(icao24:) fetches the current/most-recent
track for an aircraft.
- LiveFlightsView renders a MapPolyline trail along the path of
whichever aircraft is currently selected. Cleared on
deselection; race-guarded against rapid selection changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
888943deb4 |
Add Live Flights tab: real-time aircraft map with filters + tap detail
New top-level TabView (RootView) splits the app into: Tab 1 (Search): existing RoutePlannerView home Tab 2 (Live): live flight tracker Live tab features: - MapKit map showing every aircraft in the visible viewport, rotated to true heading. Color-coded by vertical state: climbing/level/ descending/on-ground. - Auto-refresh every 15s + on map pan/zoom (debounced); manual refresh button. Rate-limit aware (60s backoff on HTTP 429). - Tap any aircraft → modal sheet with live state grid (altitude, speed, heading, vertical rate, squawk, last-contact), current route (lazily fetched per-aircraft from OpenSky's /flights/ aircraft endpoint, mapped from ICAO to IATA airport codes), and recent flight history (up to 8 prior legs). - Filters: airline (multi-select from currently visible callsigns, with counts), aircraft type (ADS-B emitter category), airborne- only toggle. All filters render as horizontal chips and clear with a single tap. - Search bar: callsign/flight number — submitting centers the map on the match and opens its detail sheet. Data source: OpenSky Network REST API. Free, anonymous (~100 req/ day cap), JSON. Same ADS-B data FR24 starts with — without satellite ADS-B coverage but more than enough for the in-flight tracker use case. Reviewed FR24's APK and confirmed they migrated their live feed to gRPC+protobuf with anti-bot device-id headers; OpenSky's plain JSON is the right tradeoff for our build. Implementation: - LiveAircraft model: decodes OpenSky's mixed-type position arrays into a typed struct; computed properties for ft/knots/heading and airline ICAO extracted from callsign. - OpenSkyClient: actor with /states/all + /flights/aircraft. Bbox query, throttle-aware errors. - AircraftRegistry: ~80 ICAO → (IATA, name) entries for the major carriers; everything else falls through to the raw ICAO code. - LiveFlightsView: the main map + filter UI. - LiveFlightDetailSheet: tap modal with live state + route history. - RootView: TabView wrapping RoutePlannerView (Search) and the new LiveFlightsView (Live). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
92a69cf16c |
Add Sun Country (SY) load integration
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> |
||
|
|
398862e88b |
Add Aeromexico (AM) load integration
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>
|
||
|
|
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> |