# Module group: `src/features/flights/`

> **Note**: this is a deliberately compact doc covering all 15 flights modules. Behaviour is mostly an in-progress port of `mission-planner/` into the React 19 SPA. For the canonical product spec see `_docs/ui_design/README.md` (Flights Page Layout) and `../../../_docs/02_flights.md` / `11_gps_denied.md` in the parent suite repo (Flights API contract + GPS-Denied semantics).

## Scope

Owns the `/flights` route. Lets the user:
1. Browse / create / delete `Flight` rows via `endpoints.flights.collection()` (POST) and `endpoints.flights.flight(id)` (DELETE).
2. Plan a mission on a Leaflet map: add waypoints, draw work-area / no-go rectangles, edit altitude + purpose per point, see live total distance, time, battery %.
3. Toggle into GPS-Denied mode — opens an SSE stream `endpoints.flights.flightLiveGps(id)` (= `/api/flights/{id}/live-gps`) (and the panel for orthophoto upload + correction once that backend wiring lands; today only the live-GPS readout is connected).
4. Save waypoints back to the Flights API via `endpoints.flights.flightWaypoints(id)` and `endpoints.flights.flightWaypoint(flightId, waypointId)`.
5. Import / export the plan as JSON.

Currently handles only the planning surface; the gps-denied orthophoto upload / correction inputs in `_docs/ui_design/flights.html` are not yet implemented.

## Module map

| Module | Layer | Responsibility |
|---|---|---|
| `types.ts` | leaf | All flight-feature-only types (`FlightPoint`, `CalculatedPointInfo`, `MapRectangle`, `WindParams`, `AircraftParams`, `MovingPointInfo`, `ActionMode`), plus the single self-hosted satellite tile URL (`TILE_URL`, AZ-498 — env-var `VITE_SATELLITE_TILE_URL`, dev default `http://localhost:5100/tiles/{z}/{x}/{y}`), `PURPOSES` (`tank` / `artillery`), and `COORDINATE_PRECISION = 8`. |
| `mapIcons.ts` | leaf | Three coloured Leaflet `Icon` instances + the default Leaflet pin (loaded from a CDN — see Findings). |
| `flightPlanUtils.ts` | leaf | Pure-ish helpers: `newGuid`, haversine `calculateDistance` (with plane climb/cruise/descend profile), OpenWeatherMap fetch, semi-empirical `calculateBatteryPercentUsed`, `calculateAllPoints` (sequential reduce), `parseCoordinates`, `getMockAircraftParams`. |
| `WaypointList.tsx` | sub-component | `@hello-pangea/dnd` reorderable list, hover-only Edit/Remove buttons, shows distance / time / battery / altitude per point. |
| `AltitudeChart.tsx` | sub-component | `react-chartjs-2` Line chart of altitude over normalized distance; pulls all controllers via `chart.js/auto`. |
| `WindEffect.tsx` | sub-component | Two number inputs (heading 0–360°, speed 0–30 m/s) with a small SVG arrow preview. |
| `MiniMap.tsx` | sub-component | 240×180 `react-leaflet` thumbnail anchored to a moving point; `attributionControl={false}`. |
| `AltitudeDialog.tsx` | sub-component | Add / Edit waypoint modal: lat / lng / altitude / `meta: string[]` purpose multi-select. Fully controlled. |
| `MapPoint.tsx` | sub-component | One waypoint marker: draggable, popup with altitude slider, purpose checkboxes, remove button. |
| `DrawControl.tsx` | sub-component | Headless Leaflet handler that draws work-area / prohibited-area rectangles via `mousedown / mousemove / mouseup`. |
| `FlightListSidebar.tsx` | sub-component | Left rail: flight list, "+ Create", inline-create row, telemetry date stub. |
| `JsonEditorDialog.tsx` | sub-component | Modal `<textarea>` over the plan JSON with live `JSON.parse` validation. |
| `FlightParamsPanel.tsx` | composite | Hosts `WaypointList` + `AltitudeChart` + `WindEffect` + all per-flight inputs (aircraft, initial altitude, FoV, comm address, action-mode buttons, totals strip, Save / Upload / EditAsJSON / Export). |
| `FlightMap.tsx` | composite | Wraps `MapContainer`; mounts `MapPoint` × N, `DrawControl`, `MiniMap` (when a point is moving), polyline + arrow decorator. Single satellite-only `<TileLayer>` with `crossOrigin="use-credentials"` (AZ-498); the prior classic/satellite toggle was retired. |
| `FlightsPage.tsx` | page | Orchestrator: owns all state, talks to `api/client`, opens the SSE stream, mediates between sidebar / params panel / map / dialogs. |

## Key contracts (read by other docs)

- **`FlightPoint`**: `{ id: string; position: { lat; lng }; altitude: number; meta: string[] }`. `meta` ⊆ `{ 'tank', 'artillery' }`. The shape diverges from the legacy WPF `Point` (radio, single purpose) — the React SPA uses checkboxes (multi).
- **`CalculatedPointInfo`**: `{ bat: number /* % */; time: number /* hours */ }`. Index `i` = state at point `i` after the segment from `i-1`. `lastInfo.bat` drives the Good / Caution / Low colour status (`>12 / >5 / ≤5`).
- **`PURPOSES = [{ value: 'tank', label: 'options.tank' }, { value: 'artillery', label: 'options.artillery' }]`** — i18n keys are `flights.planner.${label}`.
- **JSON plan shape** (`handleEditJson` / `handleExport` / `handleJsonSave`): `{ operational_height: { currentAltitude }, geofences: { polygons: [{ northWest, southEast, fence_type: 'EXCLUSION'|'INCLUSION' }] }, action_points: [{ point: { lat, lon }, height, action: 'search', action_specific: { targets: string[] } }] }`. Used for both export-to-file and the JSON editor.
- **Tile URL** (post AZ-498): single `TILE_URL` constant in `types.ts` resolved from `VITE_SATELLITE_TILE_URL` (dev default `http://localhost:5100/tiles/{z}/{x}/{y}`). Production builds MUST set the env var to a same-origin path so the satellite-provider auth cookie rides. The classic/satellite toggle, the prior OSM (`VITE_OSM_TILE_URL`) and Esri (`VITE_ESRI_TILE_URL`) env vars, and the `MiniMap.Props.mapType` prop are all gone. Contract: `_docs/02_document/contracts/satellite-provider/tiles.md` v1.0.0.

## External integrations

| Builder → Path | Where | Direction | Notes |
|---|---|---|---|
| `endpoints.flights.aircrafts()` → `GET /api/flights/aircrafts` | `FlightsPage` | egress | Aircraft selector population. |
| `endpoints.flights.flightWaypoints(id)` → `GET /api/flights/{id}/waypoints` | `FlightsPage` | egress | On flight select. |
| `endpoints.flights.collection()` → `POST /api/flights` | `FlightsPage` | egress | Create flight from sidebar. |
| `endpoints.flights.flight(id)` → `DELETE /api/flights/{id}` | `FlightsPage` | egress | After ConfirmDialog. |
| `endpoints.flights.flightWaypoints(id)` + `endpoints.flights.flightWaypoint(flightId, wp)` → `POST/DELETE /api/flights/{id}/waypoints[/{wp}]` | `FlightsPage.handleSave` | egress | Delete-all-then-recreate, sequentially. |
| `endpoints.flights.flightLiveGps(id)` → `GET /api/flights/{id}/live-gps` (SSE) | `FlightsPage` | egress | Open while in GPS mode + flight selected. |
| `https://api.openweathermap.org/...` | `flightPlanUtils.getWeatherData` | egress | Direct browser→3rd-party. **Hardcoded API key.** See Findings. |
| `satellite-provider /tiles/{z}/{x}/{y}` (via `VITE_SATELLITE_TILE_URL`) | `FlightMap`, `MiniMap` | egress | Same-origin in production (cookie auth); `tile-stub` in e2e; `localhost:5100` dev default. AZ-498 retired the OSM + Esri direct calls. |
| `unpkg.com/leaflet@1.7.1/dist/images/marker-icon-2x.png` | `mapIcons.defaultIcon` | egress | CDN, version pinned to 1.7.1 while package is 1.9.4 (drift). |
| `navigator.geolocation.getCurrentPosition` | `FlightsPage` mount | browser API | Fallback to hardcoded `47.242, 35.024` (Zaporizhzhia). |

## Findings carried into Step 4 / 6 / 8

These are the real findings; the per-module rationale is in git history of the deleted per-file docs. Numbered for cross-reference from `state.json.notes`.

1. **HARDCODED OPENWEATHER API KEY** — `flightPlanUtils.ts:60`. HIGH severity. Step 4 source-code fix; upstream rotation is a parallel user task.
2. **`flightPlanUtils.calculateAllPoints` does N sequential `await`s** to OpenWeatherMap — N × RTT latency. Not parallelisable as-is because `info[i].bat` depends on `info[i-1]`. Step 8 refactor: fetch weather first in parallel, then reduce.
3. **`flightPlanUtils.calculateDistance` mixes km and metres** (`R = 6371`, altitudes converted `/1000` inline, returned `time = dist / aircraft.speed` assumes km/h). No type forces correctness. Step 4.
4. **`AircraftParams.batteryCapacity` unit is ambiguous** (Wh vs W·s). `calculateBatteryPercentUsed` divides W·s by it × 100 — only correct if W·s. Verify against `mission-planner/src/services/calculateBatteryUsage.ts`. Step 4.
5. **`flightPlanUtils.getWeatherData` swallows errors silently** (`catch { return null }`); callers can't distinguish "no wind" from "key revoked". Step 4.
6. **`mapIcons.defaultIcon` CDN URL is leaflet@1.7.1** while `package.json` is 1.9.4. Step 4 — switch to bundled assets or match version.
7. ~~**`FlightMap` and `MiniMap` bypass the suite `satellite-provider/` proxy**~~ — **resolved by AZ-498 (cycle 2)**. Both maps now consume `satellite-provider /tiles/{z}/{x}/{y}` via `VITE_SATELLITE_TILE_URL` with `crossOrigin="use-credentials"` cookie auth; the OSM + Esri direct calls and the classic/satellite toggle are gone. Cross-workspace prerequisite: `satellite-provider` cookie-auth migration on the same endpoint (user-filed separately).
8. **`MiniMap` sets `attributionControl={false}`** — drops OSM / Esri attribution. Possible licence-compliance gap. Step 4.
9. **`MiniMap` is fixed 240×180 + zoom 18 hardcoded** — overflows below the 640px mobile breakpoint. Step 4 vs `_docs/ui_design/README.md` responsive specs.
10. **`AltitudeDialog` lacks Esc-to-close, backdrop-click-to-cancel, `role="dialog"`, `aria-modal`** — inconsistent with `ConfirmDialog`. Same for `JsonEditorDialog`. Pick one modal convention in Step 4.
11. **`AltitudeDialog` accepts any number for lat/lng** (no `[-90,90]` / `[-180,180]` guard). `AltitudeDialog.altitude` and `WindEffect` inputs use `Number('')` → 0 silently — same pitfall noted in `SettingsPage`. Step 4.
12. **`AltitudeDialog` purpose multi-select** vs WPF radio (single choice). Confirm intent in Step 6: is the SPA expanding the data model on purpose, or is this a UI bug?
13. **`WindEffect` `max=360`** allows duplicate heading at 0 vs 360. Step 4.
14. **`WaypointList` drag handle is the entire row** (prevents text selection); Edit/Remove buttons are hover-only (unusable on touch); no a11y reorder announcements. Step 4 / Step 8 a11y.
15. **`WaypointList.calculatedPointInfo[i]`** silently degrades to alt-only label if length mismatches; tightly coupled to `FlightsPage` keeping arrays in lockstep.
16. **`AltitudeChart` pulls every chart.js controller** via `chart.js/auto` (bundle bloat); colours are duplicated as hex literals (drift from the `az-*` Tailwind tokens). Step 8.
17. **`AltitudeDialog` naming**: file is "AltitudeDialog" but the dialog covers lat / lng / altitude / purpose. Port-vestige from `mission-planner/`. Rename to `WaypointDialog` in Step 8.
18. **`flightPlanUtils.ts` is single-file** — newGuid + geo + weather + battery + parser + mock all together. Splitting into `geo.ts` / `weather.ts` / `battery.ts` / `mockAircraft.ts` is a Step 8 SRP candidate.
19. **`FlightsPage.handleSave`** deletes all existing waypoints then recreates — N+M sequential PUT/DELETE round-trips, not transactional, no progress UI, partial failure leaves the flight half-saved.
20. **`FlightsPage.handleSave` body shape does NOT match the Flights API spec — UI will likely 400 on a strict server**. Code POSTs `{ name, latitude, longitude, order }`. Parent `../../../../_docs/02_flights.md §3` `CreateWaypointRequest` requires `{ Geopoint: {Lat, Lon, MGRS}, Source: WaypointSource, Objective: WaypointObjective, OrderNum, Height }`. Mismatches: (a) lat/lon not nested under `Geopoint`; (b) field is `order`, spec is `OrderNum`; (c) `Source`, `Objective`, `Height` not sent at all; (d) UI sends `name` which the spec does not define on `Waypoint` (`Waypoint` interface in `src/types/index.ts:76` invents `name`). This collides with finding #19 — every save will round-trip waypoints in the wrong shape. **PRIORITY** for Step 4. Open question for Step 6: are these columns being added to the Flights API schema, or is the React UI to be aligned to the spec? Same comment for `altitude` and `meta` which have no place in the current spec.
21. **`FlightsPage.useEffect` bootstraps `getMockAircraftParams()`** as the active `aircraft` regardless of what the backend returns — the dropdown choice is cosmetic for now. Real wiring is a Step 6 / Step 8 follow-up.
22. **GPS-Denied panel is partial** — only the SSE live-GPS readout is wired; orthophoto upload, GPS correction (per `_docs/ui_design/README.md`) are not. Step 6 problem extraction.
23. **`handleImport` silently drops the file picker** if the user cancels (`if (!file) return`) — fine. But `handleJsonSave`'s catch uses `alert(...)` for a UX-grade error — replace with the project's modal/toast pattern in Step 4.
24. **`MapPoint` popup recomputes the marker DOM offset on every drag move** to choose dx/dy for the moving-point indicator. Acceptable, but the `(marker as unknown as { _icon: HTMLElement })._icon` cast leaks Leaflet internals.
25. **`DrawControl` registers global `mousedown`/`mousemove`/`mouseup` on the map** while a draw mode is active and disables `map.dragging` for the duration — fine, but no Esc-to-cancel mid-draw.
26. **`FlightContext` ceiling**: `FlightsPage` reads `flights` from `useFlight()` which fetches with `pageSize=1000` (already flagged in `FlightContext` doc). Won't surface here, but `selectFlight` is fire-and-forget — if the PUT to `endpoints.annotations.settingsUser()` (= `/api/annotations/settings/user`) fails the next page reload reverts the choice without notice. (Note: the underlying call goes to the annotations settings store, not a hypothetical `/api/flights/select`; see `src__components__FlightContext.md` for the actual PUT path.)
27. **Path builders (since AZ-486 / F7)**: every callsite in this page family now imports `endpoints` from `../../api` (barrel). The wire contract (the path strings) is unchanged; only the JS source surface migrated. Static gate `STC-ARCH-02` forbids re-introducing literal `/api/flights/...` strings.

## What's intentionally NOT here

- The orthophoto-upload / GPS-correction sub-panel (in `_docs/ui_design/flights.html` but not in source).
- Any per-segment annotation-time-window logic (that's the Annotations module).
- Any aircraft battery model that respects altitude-dependent air density (constant 1.05 kg/m³ in source).

## Tests

None — confirmed by `00_discovery.md §5`. Documented test gap is owned by Steps 3 / 5–7 of autodev (test-spec → implement → run).

## Cross-doc references

- Parent suite Flights API contract: `../../../../_docs/02_flights.md` (DTOs, endpoint shapes).
- Parent suite GPS-Denied: `../../../../_docs/11_gps_denied.md` (SSE event shape, correction flow).
- UI spec: `../../ui_design/README.md` (Flights Page Layout, GPS-Denied panel toggle, mobile breakpoint).
- Port-source: `mission-planner/src/flightPlanning/*` — covered separately as one consolidated doc.
