Files
ui/_docs/02_document/modules/src__features__flights.md
T
Oleksandr Bezdieniezhnykh b016fd8207 [AZ-498] [AZ-499] Cycle 2 batch 11: satellite tiles + OWM hardening
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>
2026-05-12 04:34:39 +03:00

15 KiB
Raw Blame History

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 0360°, speed 030 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 KEYflightPlanUtils.ts:60. HIGH severity. Step 4 source-code fix; upstream rotation is a parallel user task.
  2. flightPlanUtils.calculateAllPoints does N sequential awaits 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/ proxyresolved 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 / 57 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.