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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>