Skip to content

feat(aviation): track-tab search, richer flight cards, Google Flights in command bar#2516

Merged
koala73 merged 3 commits intomainfrom
worktree-memoized-soaring-yeti
Mar 29, 2026
Merged

feat(aviation): track-tab search, richer flight cards, Google Flights in command bar#2516
koala73 merged 3 commits intomainfrom
worktree-memoized-soaring-yeti

Conversation

@koala73
Copy link
Copy Markdown
Owner

@koala73 koala73 commented Mar 29, 2026

Summary

  • Track tab search bar: users can now type a flight number (IATA, e.g. EK3), ICAO24 hex, or callsign to look up live status or position data directly from the tracking tab
  • Richer flight status cards: results show carrier name, aircraft type, gate/terminal, departure/arrival times, and delay badge
  • Leading-zero normalization in getFlightStatus: EK03 is normalized to EK3 before querying AeroDataBox, fixing no-result lookups for zero-padded flight numbers
  • AviationCommandBar enrichment: FLIGHT intent now shows carrier name, aircraft type, and gate/terminal; PRICE_WATCH intent tries Google Flights first and falls back to TravelPayouts
  • CSS fade mask on tab-bar right edge to hint overflow without clipping; stripped emoji from tab labels to reduce visual noise
  • Fixed common.search to header.search i18n key on price/dates search buttons

Test plan

  • Open AirlineIntelPanel, go to Track tab, enter EK3 and press Enter or click Track
  • Verify flight card shows carrier, route, times, and delay badge
  • Enter an ICAO24 hex (e.g. A1B2C3) and verify position row appears
  • Enter a callsign (e.g. UAE3) and verify position row appears
  • Type AviationCommandBar query EK3 status and verify enriched card with carrier/gate
  • Type command JFK to LAX prices and verify Google Flights price appears
  • Check tab labels no longer have emoji
  • Confirm price/dates Search buttons render correctly (no missing i18n key)

…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
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 29, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
worldmonitor Ready Ready Preview, Comment Mar 29, 2026 5:50pm

Request Review

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 29, 2026

Greptile Summary

This PR adds a live flight search bar to the Tracking tab, enriches flight cards with carrier, aircraft type, and gate/terminal info in both AirlineIntelPanel and AviationCommandBar, normalises leading-zero flight numbers on the server side (EK03 → EK3), promotes Google Flights as the primary price source in the command bar, removes emoji from tab labels, and fixes an i18n key (common.searchheader.search) on the price/dates search buttons.

Key findings:

  • Loading guard inside renderTracking() is unreachable (P1)renderTab() calls renderLoading() and returns before ever invoking renderTracking() when this.loading === true. The intent to keep the search bar visible during a fetch is never realised; users see a bare spinner with no input field while their query runs.

  • switchTab() tracking condition incomplete (P1) — The auto-load guard (tab === 'tracking' && !this.trackingData.length) is always true when flight-status results are displayed (because trackingData is intentionally empty and trackingFlightData holds the results). Every tab switch away from and back to Track fires a redundant API request.

  • fetchGoogleFlights exception silently drops TravelPayouts fallback (P1) — In AviationCommandBar the fallback to TravelPayouts is only reached when Google Flights returns an empty array. If the call throws, the exception surfaces as a generic error and TravelPayouts data is never attempted.

  • f.status rendered without escapeHtml() (P2) — Inserted directly into HTML in both AirlineIntelPanel (tracking card) and AviationCommandBar.

  • CSS fade mask always active (P2, panels.css) — The mask-image gradient permanently fades the tab bar's right edge regardless of overflow, visually clipping the last tab label even when all tabs fit without scrolling.

Confidence Score: 4/5

Safe 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

Filename Overview
src/components/AirlineIntelPanel.ts Adds track-tab search, richer flight cards, and i18n key fix; contains two P1 bugs: the loading guard inside renderTracking() is unreachable, hiding the search bar during fetches, and switchTab() re-fires tracking API calls on every tab switch when flight-status data is showing.
src/components/AviationCommandBar.ts Enriches FLIGHT intent card and adds Google Flights as primary price source with TravelPayouts fallback; the fallback is silently bypassed when fetchGoogleFlights throws an exception instead of returning an empty array (P1).
server/worldmonitor/aviation/v1/get-flight-status.ts Adds leading-zero normalisation for IATA flight numbers (EK03→EK3); regex is correct and cache key correctly uses the normalised value.
src/styles/panels.css Adds CSS mask-image fade on the tab-bar right edge; mask is always active regardless of overflow, permanently fading the last visible tab label even when all tabs fit.

Sequence Diagram

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

Comments Outside Diff (1)

  1. src/components/AirlineIntelPanel.ts, line 274-281 (link)

    P1 Tracking tab always re-fetches when flight-status results are displayed

    The switchTab() guard for triggering a load is:

    (tab === 'tracking' && !this.trackingData.length)

    After a flight-number search, this.trackingData is intentionally left empty (trackingFlightData holds the results instead). Because of this, !this.trackingData.length is always true while flight cards are showing, so every time the user switches away from and back to the Tracking tab it fires another loadTab('tracking') call, re-querying the server unnecessarily.

Reviews (1): Last reviewed commit: "feat(aviation): track-tab search, richer..." | Re-trigger Greptile

Comment on lines +434 to 437
if (this.loading) {
this.content.innerHTML = `${searchBar}<div class="panel-loading">${t('common.loading')}</div>`;
return;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +113 to +127
// 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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Suggested change
<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.
@koala73 koala73 merged commit 8e93fc1 into main Mar 29, 2026
7 checks passed
@koala73 koala73 deleted the worktree-memoized-soaring-yeti branch March 29, 2026 17:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant