Commit Graph

10 Commits

Author SHA1 Message Date
Trey T 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>
2026-05-27 07:37:34 -05:00
Trey T 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>
2026-05-27 07:21:49 -05:00
Trey T 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>
2026-05-27 06:16:49 -05:00
Trey T 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>
2026-05-27 06:08:58 -05:00
Trey T 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>
2026-05-26 14:14:09 -05:00
Trey t 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>
2026-04-28 11:50:02 -05:00
Trey t 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>
2026-04-28 11:19:20 -05:00
Trey T 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>
2026-04-28 10:23:17 -05:00
Trey T 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 (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>
2026-04-11 11:55:15 -05:00
Trey t 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>
2026-04-08 15:01:07 -05:00