feat(aviation): track-tab search, richer flight cards, Google Flights in command bar#2516
feat(aviation): track-tab search, richer flight cards, Google Flights in command bar#2516
Conversation
…Flights fallback in command bar - Add search bar to tracking tab: routes by flight number (IATA), ICAO24 hex, or callsign - Render fetchFlightStatus results as cards with carrier, gate, terminal, delay and times - Normalize leading zeros in getFlightStatus (EK03 to EK3) so AeroDataBox resolves correctly - Enrich AviationCommandBar FLIGHT intent with carrier name, aircraft type, gate/terminal - Prefer fetchGoogleFlights in PRICE_WATCH intent with TravelPayouts as fallback - Strip emoji from tab labels; apply fade-mask gradient on tab-bar overflow - Fix common.search to header.search i18n key on price/dates search buttons
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis PR adds a live flight search bar to the Tracking tab, enriches flight cards with carrier, aircraft type, and gate/terminal info in both Key findings:
Confidence Score: 4/5Safe to merge once the three P1 defects are addressed — none cause data loss but two break the primary tracking-tab UX path and one silently suppresses the price fallback. Three P1 issues: unreachable loading guard in renderTracking(), incomplete switchTab() condition for the new trackingFlightData state, and unguarded fetchGoogleFlights exception in AviationCommandBar. All are straightforward fixes but represent real behavioural defects introduced by this PR. Server-side normalisation and i18n fix are clean and correct. src/components/AirlineIntelPanel.ts (loading guard + switchTab condition) and src/components/AviationCommandBar.ts (Google Flights exception handling) Important Files Changed
Sequence DiagramsequenceDiagram
actor User
participant TrackTab as AirlineIntelPanel Track Tab
participant FlightSvc as fetchFlightStatus
participant PosSvc as fetchAircraftPositions
User->>TrackTab: types query + Enter or clicks Track
TrackTab->>TrackTab: handleTrackSearch clears data
TrackTab->>TrackTab: loadTab tracking, loading=true, renderTab
note over TrackTab: renderTab calls renderLoading early, search bar disappears
alt IATA flight number e.g. EK3
TrackTab->>FlightSvc: fetchFlightStatus(query)
FlightSvc-->>TrackTab: FlightInstance[]
TrackTab->>TrackTab: trackingFlightData = result
else ICAO24 hex e.g. A1B2C3
TrackTab->>PosSvc: fetchAircraftPositions with icao24
PosSvc-->>TrackTab: PositionSample[]
TrackTab->>TrackTab: trackingData = result
else callsign e.g. UAE3
TrackTab->>PosSvc: fetchAircraftPositions with callsign
PosSvc-->>TrackTab: PositionSample[]
TrackTab->>TrackTab: trackingData = result
end
TrackTab->>TrackTab: loading=false, renderTab
TrackTab->>User: renders flight card or position rows
User->>TrackTab: switches away and back to Track tab
note over TrackTab: switchTab checks only trackingData.length, always zero for flight results, triggers redundant re-fetch
TrackTab->>FlightSvc: fetchFlightStatus again unnecessarily
|
| if (this.loading) { | ||
| this.content.innerHTML = `${searchBar}<div class="panel-loading">${t('common.loading')}</div>`; | ||
| return; | ||
| } |
There was a problem hiding this comment.
Dead loading guard — search bar disappears during fetch
renderTab() already returns early with the bare renderLoading() spinner whenever this.loading === true, so the if (this.loading) branch inside renderTracking() is unreachable. The search bar never appears alongside the loading indicator as intended, and the user sees a plain spinner with no input while their query is executing.
The root cause is in renderTab():
private renderTab(): void {
if (this.loading && this.activeTab !== 'tracking') { this.renderLoading(); return; }
// OR: remove the early return entirely and let each renderX() manage its own loading state
...
}Decide on one approach — either each tab owns its loading rendering (remove the guard from renderTab()), or the tracking-tab branch needs to call renderTracking() even when loading === true.
| // Try Google Flights first (real data) | ||
| const gfResult = await fetchGoogleFlights({ origin: intent.origin, destination: intent.destination, departureDate: date }); | ||
| if (gfResult.flights.length) { | ||
| const best = gfResult.flights[0]!; | ||
| const leg = best.legs[0]; | ||
| const carrier = leg ? `${leg.airlineCode} ${leg.flightNumber}` : ''; | ||
| const stops = best.stops === 0 ? 'nonstop' : `${best.stops} stop`; | ||
| return { | ||
| html: `<div class="cmd-section"> | ||
| <strong>💸 ${escapeHtml(intent.origin)} → ${escapeHtml(intent.destination)}</strong> | ||
| <div>Best: <strong style="color:#60a5fa">$${Math.round(best.price).toLocaleString()}</strong> · ${escapeHtml(carrier)} · ${stops} · ${escapeHtml(fmtDur(best.durationMinutes))}</div> | ||
| ${gfResult.degraded ? '<div style="color:#f59e0b;font-size:11px">Partial results</div>' : ''} | ||
| </div>` }; | ||
| } | ||
| // Fallback to TravelPayouts / demo |
There was a problem hiding this comment.
fetchGoogleFlights exception bypasses TravelPayouts fallback
The TravelPayouts fallback is only reached when gfResult.flights.length === 0. If fetchGoogleFlights throws (network error, 5xx, API key issue), the exception propagates out of executeIntent and is caught by run()'s outer try/catch, displaying a generic error — the TravelPayouts/demo path is never attempted.
Wrap the Google Flights call so a thrown exception is treated the same as an empty result:
let gfResult: Awaited<ReturnType<typeof fetchGoogleFlights>> | null = null;
try {
gfResult = await fetchGoogleFlights({ origin: intent.origin, destination: intent.destination, departureDate: date });
} catch { /* fall through to TravelPayouts */ }
if (gfResult?.flights.length) {
// … render Google Flights card
}
// Fallback to TravelPayouts / demo
const { quotes, isDemoMode } = await fetchFlightPrices(…);| <div style="display:flex;gap:8px;align-items:baseline"> | ||
| <strong>${escapeHtml(f.flightNumber)}</strong> | ||
| <span style="color:#9ca3af;font-size:11px">${escapeHtml(f.carrier.name || f.carrier.iata)}</span> | ||
| <span style="color:${color};font-size:11px;margin-left:auto">${f.status}</span> |
There was a problem hiding this comment.
f.status inserted into HTML without escaping
f.status is rendered directly into HTML here and in AviationCommandBar.ts line 105. While the server currently normalises this to a fixed enum string, the client-side render trusts an API-origin value. If the upstream value ever contains unexpected characters, this would be an XSS vector.
| <span style="color:${color};font-size:11px;margin-left:auto">${f.status}</span> | |
| <span style="color:${color};font-size:11px;margin-left:auto">${escapeHtml(f.status)}</span> |
The same applies in AviationCommandBar.ts line 105:
<div>${escapeHtml(f.origin.iata)} → ${escapeHtml(f.destination.iata)} · ${escapeHtml(f.status)}${depStr ? ` · ${depStr}` : ''}${arrStr}</div>- Guard updateLivePositions: skip overwrite when trackingQuery is set, preventing live position pushes from clobbering filtered search results - Fix gate/terminal render: decouple from aircraftType check so gate info shows even when aircraft type is absent - Extend command parser: accept <flight> STATUS order and <ORG> TO <DST> PRICES pattern alongside existing canonical forms
Empty input now clears trackingQuery so live position updates resume. Add x clear button when a search is active to make this discoverable.
Summary
EK3), ICAO24 hex, or callsign to look up live status or position data directly from the tracking tabgetFlightStatus:EK03is normalized toEK3before querying AeroDataBox, fixing no-result lookups for zero-padded flight numberscommon.searchtoheader.searchi18n key on price/dates search buttonsTest plan
EK3and press Enter or click TrackA1B2C3) and verify position row appearsUAE3) and verify position row appearsEK3 statusand verify enriched card with carrier/gateJFK to LAX pricesand verify Google Flights price appears