AZ-498 — self-hosted satellite tiles + drop classic/satellite toggle: - Single TILE_URL via getTileUrl() (mirrors getOwmBaseUrl/getApiBase pattern from AZ-449/AZ-450); env-var VITE_SATELLITE_TILE_URL with dev default http://localhost:5100/tiles/{z}/{x}/{y}. - FlightMap + MiniMap render one TileLayer with crossOrigin="use-credentials" so Leaflet's <img> tile fetcher attaches the same-origin satellite-provider auth cookie. - ImportMetaEnv + .env.example collapse the prior OSM/Esri pair into one var. The flights.planner.satellite i18n key is removed in lockstep across en.json + ua.json (parity preserved). - E2E harness wired end-to-end: compose passes the new var to azaion-ui; tile-stub serves /tiles/{z}/{x}/{y} with Content-Type=image/jpeg + Cache-Control + ETag matching the contract; infrastructure.e2e.ts AC-2 asserts the new path; dead OSM defenses removed from EXTERNAL_HOSTS route guard. - Fast-profile MSW handlers rewritten for the cookie-auth path shape. - 8 colocated fast tests under src/features/flights/__tests__/. AZ-499 — mission-planner OWM env-var hardening + AZ-482 source-scan gap close: - WeatherService.ts reads VITE_OWM_API_KEY + VITE_OWM_BASE_URL; fail-soft null when key unset (mirrors AZ-448 main-SPA contract). Public signature getWeatherData(lat, lon) preserved. - mission-planner/.env.example + vite-env.d.ts declare both vars. - New owm_key_in_source banned-deps kind scans src/ AND mission-planner/ for the rotated literal; STC-SEC1C row added to scripts/run-tests.sh; check-banned-deps.mjs dispatch extended. - 7 fast tests under tests/mission_planner_weather.test.ts cover AC-1..AC-4 + trailing-slash + happy path + network-error fail-soft. Spec drift (recorded in batch_11_report.md, user-approved Choose B on 2026-05-12): - AZ-498 AC-8 dropped (named tile_split_zoom* files belong to AZ-474 image-annotation surface, not map tiles). - 4 missing files added in-scope (msw tiles handler, tile-stub server, compose env, dead VITE_TILE_BASE_URL replaced). - AZ-499 STC-S6 ID conflict resolved by using STC-SEC1C. Pending USER ACTION (BLOCKING for AZ-499 close): - Revoke OpenWeatherMap key 335799082893fad97fa36118b131f919 at home.openweathermap.org/api_keys; capture evidence on AZ-499. Cross-workspace deploy gate (handled at autodev Step 16, not a Step-10 blocker for AZ-498): - satellite-provider cookie-auth on GET /tiles/{z}/{x}/{y} (separate AZAION ticket on the satellite-provider workspace). Reports: _docs/03_implementation/batch_11_report.md and _docs/03_implementation/reviews/batch_11_review.md (verdict PASS_WITH_WARNINGS — 1 Low, pre-existing trim-trailing-slash duplication across vite roots). Static gates: STC-ARCH-01, STC-ARCH-02, STC-T1, STC-FP22, STC-FP23, STC-SEC1C all PASS post-refactor. +15 fast tests; +1 STC-SEC1C row. Co-authored-by: Cursor <cursoragent@cursor.com>
15 KiB
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.mdin the parent suite repo (Flights API contract + GPS-Denied semantics).
Scope
Owns the /flights route. Lets the user:
- Browse / create / delete
Flightrows viaendpoints.flights.collection()(POST) andendpoints.flights.flight(id)(DELETE). - 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 %.
- 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). - Save waypoints back to the Flights API via
endpoints.flights.flightWaypoints(id)andendpoints.flights.flightWaypoint(flightId, waypointId). - 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 WPFPoint(radio, single purpose) — the React SPA uses checkboxes (multi).CalculatedPointInfo:{ bat: number /* % */; time: number /* hours */ }. Indexi= state at pointiafter the segment fromi-1.lastInfo.batdrives the Good / Caution / Low colour status (>12 / >5 / ≤5).PURPOSES = [{ value: 'tank', label: 'options.tank' }, { value: 'artillery', label: 'options.artillery' }]— i18n keys areflights.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_URLconstant intypes.tsresolved fromVITE_SATELLITE_TILE_URL(dev defaulthttp://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 theMiniMap.Props.mapTypeprop are all gone. Contract:_docs/02_document/contracts/satellite-provider/tiles.mdv1.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.
- HARDCODED OPENWEATHER API KEY —
flightPlanUtils.ts:60. HIGH severity. Step 4 source-code fix; upstream rotation is a parallel user task. flightPlanUtils.calculateAllPointsdoes N sequentialawaits to OpenWeatherMap — N × RTT latency. Not parallelisable as-is becauseinfo[i].batdepends oninfo[i-1]. Step 8 refactor: fetch weather first in parallel, then reduce.flightPlanUtils.calculateDistancemixes km and metres (R = 6371, altitudes converted/1000inline, returnedtime = dist / aircraft.speedassumes km/h). No type forces correctness. Step 4.AircraftParams.batteryCapacityunit is ambiguous (Wh vs W·s).calculateBatteryPercentUseddivides W·s by it × 100 — only correct if W·s. Verify againstmission-planner/src/services/calculateBatteryUsage.ts. Step 4.flightPlanUtils.getWeatherDataswallows errors silently (catch { return null }); callers can't distinguish "no wind" from "key revoked". Step 4.mapIcons.defaultIconCDN URL is leaflet@1.7.1 whilepackage.jsonis 1.9.4. Step 4 — switch to bundled assets or match version.— resolved by AZ-498 (cycle 2). Both maps now consumeFlightMapandMiniMapbypass the suitesatellite-provider/proxysatellite-provider /tiles/{z}/{x}/{y}viaVITE_SATELLITE_TILE_URLwithcrossOrigin="use-credentials"cookie auth; the OSM + Esri direct calls and the classic/satellite toggle are gone. Cross-workspace prerequisite:satellite-providercookie-auth migration on the same endpoint (user-filed separately).MiniMapsetsattributionControl={false}— drops OSM / Esri attribution. Possible licence-compliance gap. Step 4.MiniMapis fixed 240×180 + zoom 18 hardcoded — overflows below the 640px mobile breakpoint. Step 4 vs_docs/ui_design/README.mdresponsive specs.AltitudeDialoglacks Esc-to-close, backdrop-click-to-cancel,role="dialog",aria-modal— inconsistent withConfirmDialog. Same forJsonEditorDialog. Pick one modal convention in Step 4.AltitudeDialogaccepts any number for lat/lng (no[-90,90]/[-180,180]guard).AltitudeDialog.altitudeandWindEffectinputs useNumber('')→ 0 silently — same pitfall noted inSettingsPage. Step 4.AltitudeDialogpurpose 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?WindEffectmax=360allows duplicate heading at 0 vs 360. Step 4.WaypointListdrag 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.WaypointList.calculatedPointInfo[i]silently degrades to alt-only label if length mismatches; tightly coupled toFlightsPagekeeping arrays in lockstep.AltitudeChartpulls every chart.js controller viachart.js/auto(bundle bloat); colours are duplicated as hex literals (drift from theaz-*Tailwind tokens). Step 8.AltitudeDialognaming: file is "AltitudeDialog" but the dialog covers lat / lng / altitude / purpose. Port-vestige frommission-planner/. Rename toWaypointDialogin Step 8.flightPlanUtils.tsis single-file — newGuid + geo + weather + battery + parser + mock all together. Splitting intogeo.ts/weather.ts/battery.ts/mockAircraft.tsis a Step 8 SRP candidate.FlightsPage.handleSavedeletes all existing waypoints then recreates — N+M sequential PUT/DELETE round-trips, not transactional, no progress UI, partial failure leaves the flight half-saved.FlightsPage.handleSavebody 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 §3CreateWaypointRequestrequires{ Geopoint: {Lat, Lon, MGRS}, Source: WaypointSource, Objective: WaypointObjective, OrderNum, Height }. Mismatches: (a) lat/lon not nested underGeopoint; (b) field isorder, spec isOrderNum; (c)Source,Objective,Heightnot sent at all; (d) UI sendsnamewhich the spec does not define onWaypoint(Waypointinterface insrc/types/index.ts:76inventsname). 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 foraltitudeandmetawhich have no place in the current spec.FlightsPage.useEffectbootstrapsgetMockAircraftParams()as the activeaircraftregardless of what the backend returns — the dropdown choice is cosmetic for now. Real wiring is a Step 6 / Step 8 follow-up.- 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. handleImportsilently drops the file picker if the user cancels (if (!file) return) — fine. ButhandleJsonSave's catch usesalert(...)for a UX-grade error — replace with the project's modal/toast pattern in Step 4.MapPointpopup 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 })._iconcast leaks Leaflet internals.DrawControlregisters globalmousedown/mousemove/mouseupon the map while a draw mode is active and disablesmap.draggingfor the duration — fine, but no Esc-to-cancel mid-draw.FlightContextceiling:FlightsPagereadsflightsfromuseFlight()which fetches withpageSize=1000(already flagged inFlightContextdoc). Won't surface here, butselectFlightis fire-and-forget — if the PUT toendpoints.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; seesrc__components__FlightContext.mdfor the actual PUT path.)- Path builders (since AZ-486 / F7): every callsite in this page family now imports
endpointsfrom../../api(barrel). The wire contract (the path strings) is unchanged; only the JS source surface migrated. Static gateSTC-ARCH-02forbids re-introducing literal/api/flights/...strings.
What's intentionally NOT here
- The orthophoto-upload / GPS-correction sub-panel (in
_docs/ui_design/flights.htmlbut 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.