# 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 } 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 /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[] }>('/api/flights?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('/api/annotations/settings/user')` → - if `settings?.selectedFlightId` is truthy: `api.get('/api/flights/${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('/api/annotations/settings/user', { 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/client` — `api`. - `../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 (string-literal): `/api/flights?pageSize=1000`, `/api/flights/{id}`, `/api/annotations/settings/user`. 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. ## 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.