# Module: `src/components/FlightContext.tsx`

> **Source**: `src/components/FlightContext.tsx` (52 lines)
> **Topo batch**: B3 (depends on B2 leaves: `api/client`, `types/index`)

## Purpose

A React context that holds the SPA's currently-selected flight and the cached flight list, keeps the selection in sync with the per-user backend setting, and exposes a refresh trigger. Sibling of `AuthContext.tsx` — both are mounted near the root (`App.tsx`) so any feature page can access flight state without prop-drilling. Replaces the legacy WPF "current mission" singleton (`_docs/legacy/wpf-era.md` §"What survived").

## Public interface

```ts
interface FlightState {
  flights: Flight[]
  selectedFlight: Flight | null
  selectFlight: (f: Flight | null) => void
  refreshFlights: () => Promise<void>
}

export function useFlight(): FlightState
export function FlightProvider({ children }: { children: ReactNode }): JSX.Element
```

`FlightContext` itself is module-private. Consumers must go through `useFlight()`.

## Internal logic

State:

- `flights: Flight[]` — most recent list returned by `GET endpoints.flights.collection('pageSize=1000')` (= `/api/flights?pageSize=1000`).
- `selectedFlight: Flight | null` — the active flight, or `null` if none. Survives across pages because the provider is mounted above the route tree.

**`refreshFlights()`** (`useCallback`, no deps):

```ts
const data = await api.get<{ items: Flight[] }>(endpoints.flights.collection('pageSize=1000'))
setFlights(data.items ?? [])
```

Errors are silently swallowed (`try { ... } catch {}`). `pageSize=1000` is a hardcoded ceiling — see Configuration.

**Bootstrap effect** (`useEffect` keyed on `[refreshFlights]`, runs once because `refreshFlights` is `useCallback([])`):

1. `refreshFlights()` (no `await` — runs in parallel with #2).
2. `api.get<UserSettings>(endpoints.annotations.settingsUser())` →
   - if `settings?.selectedFlightId` is truthy: `api.get<Flight>(endpoints.flights.flight(settings.selectedFlightId))` → `setSelectedFlight(f)`.
   - errors at every step silently swallowed.

The selected flight is therefore looked up by **its own GET**, not by indexing into the cached `flights` list. This is intentional — the user might have a `selectedFlightId` that fell off the first 1000 flights.

**`selectFlight(f)`** (`useCallback`, no deps):

```ts
setSelectedFlight(f)
api.put(endpoints.annotations.settingsUser(), { selectedFlightId: f?.id ?? null }).catch(() => {})
```

Optimistic — local state updates immediately; the persisted setting is fire-and-forget. If the PUT fails, the next reload will fall back to the previously-stored ID and the user's selection silently reverts. Flag for Step 4.

## Dependencies

- **Internal**:
  - `../api` (barrel) — `api`, `endpoints`. (Since AZ-485 / F4 + AZ-486 / F7: barrel import + endpoint builders.)
  - `../types` — `Flight`, `UserSettings` types.
- **External**: `react` (`createContext`, `useContext`, `useState`, `useEffect`, `useCallback`, `ReactNode`).

## Consumers (intra-repo)

From the §7a dependency graph:

- `src/App.tsx` — mounts `FlightProvider` inside `ProtectedRoute` and above the route tree (so `selectedFlight` survives navigation between `/flights`, `/annotations`, `/dataset`).
- `src/components/Header.tsx` — displays the currently-selected flight name.
- `src/features/flights/FlightsPage.tsx` — the primary editor; calls `selectFlight` on row click.
- `src/features/annotations/MediaList.tsx` — filters media by `selectedFlight.id`.
- `src/features/dataset/DatasetPage.tsx` — same.

## Data models

- `Flight` and `UserSettings` from `src/types/index.ts` — see `_docs/02_document/modules/src__types__index.md`.

## Configuration

Endpoints (typed builders from `../api/endpoints`, since AZ-486 / F7): `endpoints.flights.collection('pageSize=1000')`, `endpoints.flights.flight(id)`, `endpoints.annotations.settingsUser()` — producing `/api/flights?pageSize=1000`, `/api/flights/{id}`, `/api/annotations/settings/user` respectively. Routed by `nginx.conf` to the `flights/` and `annotations/` services.

`pageSize=1000` is a **hardcoded ceiling** — if the system ever has >1000 flights, the cache is silently truncated. The `flights/` service contract probably caps at 1000; verify and document in `data_parameters.md` (Step 6). Flag. (Note: the literal lives in the caller, not in the `endpoints.flights.collection` builder — moving the ceiling into the builder is a future change.)

## External integrations

- `flights/` (.NET) — `GET /api/flights`, `GET /api/flights/{id}`.
- `annotations/` (.NET) — `GET /api/annotations/settings/user`, `PUT /api/annotations/settings/user`. The user-settings store is reused for the selected-flight pointer; this is a slight abstraction leak (selected flight is logically a UI-state preference, not an annotation setting), but it works as long as `UserSettings` keeps a `selectedFlightId` field.

## Security

- **No auth checks here** — relies on `api/client.ts` to inject the bearer; backend enforces visibility.
- **Silent failure on every async call** — same caveat as `AuthContext`. A misconfigured `/api/flights` will leave the user with an empty list and no error indication. Flag for Step 6 `security_approach.md` and Step 8 hardening.

## Tests

None.

## Notes / open questions

- **Race**: bootstrap fires `refreshFlights()` AND the user-settings GET in parallel. If the user-settings GET resolves first with a `selectedFlightId` that's not in the 1000 cached flights, the per-flight GET still succeeds. If the 1000 list is somehow stale and the per-flight GET 404s, `selectedFlight` stays `null` and the user sees nothing selected. Acceptable but worth documenting.
- **`selectFlight(null)` PUTs `{ selectedFlightId: null }`** — this clears the persisted preference. Confirm the `annotations/` service treats this as "unset" and not as an error or no-op. Flag for Step 4.
- **No realtime invalidation**: if another tab / user creates a flight, this client won't know until the next `refreshFlights()` call. The SPA also has SSE (`api/sse.ts`) but does NOT subscribe to flight updates. Mark as a Step 8 / future-feature hook.
- **`useCallback([])` for `refreshFlights`** is fine because `setFlights` is stable. The empty deps array means the bootstrap effect fires exactly once (as intended) — but ESLint with `react-hooks/exhaustive-deps` would still flag both `useCallback`s for missing `setFlights` (technically stable, but the linter doesn't know). Acceptable; project-wide ESLint config does not currently enforce.
