Files
ui/_docs/02_tasks/done/AZ-498_satellite_tile_swap.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

13 KiB
Raw Blame History

Replace external map tiles with self-hosted satellite-provider

Task: AZ-498_satellite_tile_swap Name: Self-hosted satellite tiles + drop map-type toggle Description: Replace OpenStreetMap (classic) and Esri (satellite) tile sources with the suite's own satellite-provider /tiles/{z}/{x}/{y} endpoint, drop the classic/satellite toggle (satellite-provider serves satellite imagery only), and wire cookie-based authentication for tile fetches. Complexity: 5 points Dependencies: AZ-450 (Externalize map tile URLs). Cross-workspace prerequisite — satellite-provider must publish a cookie-auth variant of /tiles/{z}/{x}/{y} before this task can be merged. The user files that ticket separately on the satellite-provider workspace. Component: 05_flights (with adjustments to 10_app-shell and the e2e harness) Tracker: AZ-498 Epic: AZ-497

Problem

src/features/flights/types.ts (post AZ-450) reads two tile-URL env vars and exposes them to FlightMap and MiniMap via a { classic, satellite } shape. Today those URLs resolve to external providers (OpenStreetMap, Esri ArcGIS World Imagery). This:

  • Sends pilot flight-area coordinates to third-party CDNs (privacy/operational risk for sensitive missions).
  • Adds an external network dependency the air-gap NFR (NFT-RES-03 / restriction E1) was meant to eliminate — the e2e profile only papers over it via the tile-stub.
  • Wastes bandwidth re-downloading tiles that the suite's own satellite-provider service already caches on disk (./tiles/{z}/{x}/{y}.jpg).

The suite already runs a satellite-provider .NET service that exposes a slippy-tile XYZ endpoint (GET /tiles/{z}/{x}/{y}) backed by an on-disk cache plus on-demand Google Maps download, with Cache-Control and ETag headers wired. The UI does not consume it.

Outcome

  • The SPA's map renders satellite tiles served by the suite's own satellite-provider, on the same origin as the SPA in production.
  • The classic/satellite toggle is removed; the map is satellite-only.
  • Tile fetches authenticate via a same-origin cookie, not via an Authorization: Bearer … header (Leaflet <img> requests cannot send the header).
  • Air-gap restriction E1 is satisfied for tiles in production without requiring a stub.
  • _docs/02_document/contracts/satellite-provider/tiles.md documents the contract both sides commit to.

Scope

Included

  • Collapse TILE_URLS in src/features/flights/types.ts to a single URL string read from import.meta.env.VITE_SATELLITE_TILE_URL.
  • Remove the classic/satellite toggle from FlightMap.tsx: the mapType state, the toggle <button>, and the mapType prop passed to MiniMap.
  • Update MiniMap.tsx to render a single <TileLayer> without a mapType prop.
  • Both <TileLayer> instances MUST include crossOrigin="use-credentials" so the browser attaches the auth cookie on same-origin requests.
  • Update .env.example: add VITE_SATELLITE_TILE_URL, remove VITE_OSM_TILE_URL and VITE_ESRI_TILE_URL, refresh the comment block.
  • Update src/vite-env.d.ts: add VITE_SATELLITE_TILE_URL?: string, remove the two OSM/Esri declarations.
  • Update _docs/02_document/contracts/satellite-provider/tiles.md to reference this task in the Consumer tasks field once the ticket ID is assigned.
  • Update e2e/docker-compose.suite-e2e.yml: replace tile-stub wiring with either (a) a redirect of the SPA's VITE_SATELLITE_TILE_URL to the actual satellite-provider Docker service, or (b) repurpose e2e/stubs/tile/server.ts to serve the /tiles/{z}/{x}/{y} path used by the new contract. The choice is made during implementation to minimize churn in the e2e harness.
  • Update e2e/tests/infrastructure.e2e.ts AC-2 path assertion and e2e/tests/tile_split_zoom.e2e.ts to point at the new path/host.
  • Remove the i18n key flights.planner.satellite from src/i18n/en.json and src/i18n/ua.json (the toggle that referenced it is gone). Verify no other call site references the key.
  • Update _docs/02_document/modules/src__features__flights.md and _docs/02_document/components/05_flights/description.md to reflect the new tile source and the removed toggle.
  • New blackbox test that asserts the <TileLayer> URL resolves to the env-var value AND that crossOrigin="use-credentials" is present on the rendered DOM element.
  • New blackbox test that asserts the toggle button and the mapType state are absent from the rendered FlightMap.

Excluded

  • The satellite-provider server-side change to switch /tiles/{z}/{x}/{y} from JWT bearer to cookie authentication. Filed separately on the satellite-provider workspace; this task assumes that work lands first.
  • Bringing back any street-tile fallback. Re-introducing OSM-style classic view is a future task.
  • Pre-warming tile caches via POST /api/satellite/request. The SPA does not call that endpoint; on-demand server-side cache fill is sufficient.
  • Refactoring mission-planner/ map tiles. Task 2 handles mission-planner separately for OWM, and mission-planner's tile config is independent (its own VITE_SATELLITE_TILE_URL).
  • Adding If-None-Match / 304 handling on the consumer side. Leaflet's built-in caching is sufficient.

Acceptance Criteria

AC-1: Single env-var resolves the tile URL Given VITE_SATELLITE_TILE_URL=http://satellite-provider:5100/tiles/{z}/{x}/{y} at build time, When FlightMap mounts, Then the rendered <TileLayer> url prop equals that exact string.

AC-2: Default URL when env var is unset Given VITE_SATELLITE_TILE_URL is unset at build time, When the bundle runs, Then <TileLayer> url resolves to http://localhost:5100/tiles/{z}/{x}/{y} (dev default, per the cycle-2 assumption-validation decision).

AC-3: Cookie auth is wired Given the satellite-provider expects an HttpOnly; SameSite=Lax cookie, When <TileLayer> issues a tile request via Leaflet, Then the rendered <img> element exposes crossOrigin="use-credentials" so the browser sends the cookie on same-origin requests.

AC-4: Map-type toggle removed Given FlightMap mounts, When the user inspects the rendered output, Then there is no toggle button, no mapType state, and MiniMap's Props no longer accepts a mapType value.

AC-5: Env declarations stay in sync Given a TypeScript build, Then ImportMetaEnv declares only VITE_SATELLITE_TILE_URL (the two prior OSM/Esri vars are gone), and .env.example lists VITE_SATELLITE_TILE_URL in the same documented style.

AC-6: E2E suite-e2e harness exercises the new path Given the e2e profile is brought up via e2e/docker-compose.suite-e2e.yml, When the harness asserts the tile endpoint via infrastructure.e2e.ts AC-2, Then the request URL is http://<tile-host>:<port>/tiles/{z}/{x}/{y} (not /{z}/{x}/{y}.png and not the /sat/... Esri shape), and the response is a 256×256 image.

AC-7: Contract documented Given _docs/02_document/contracts/satellite-provider/tiles.md exists, When code-review Phase 2 runs against this task, Then the contract's Shape section matches the URL pattern and headers used by the rendered <TileLayer> and assert no Spec-Gap finding.

AC-8: Legacy tile-aware tests still pass Given tests/tile_split_zoom.test.tsx and e2e/tests/tile_split_zoom.e2e.ts are updated to the new URL, When the test suite runs, Then both tests pass against the new tile-URL shape.

AC-9: Architecture gate stays green Given the static-only profile runs (scripts/run-tests.sh --static-only), When STC-ARCH-01 and STC-ARCH-02 execute, Then no new cross-component import violation is introduced.

Non-Functional Requirements

Performance

  • A cold pan over an uncached region must not block the UI thread: the SPA must continue to render placeholders while satellite-provider downloads upstream tiles.
  • The same tile URL viewed twice within a session MUST be served from the browser's HTTP cache (i.e., Cache-Control + ETag round-trip).

Compatibility

  • The MapContainer / TileLayer API surface in react-leaflet is unchanged. No version bump.
  • Production deploy MUST work behind the suite's nginx ingress on a single origin; cross-origin direct calls are explicitly NOT supported.

Reliability

  • A 401 from the tile endpoint MUST NOT crash the map; it must render a broken-tile placeholder and the rest of the SPA must remain functional.
  • A 503 from the tile endpoint (Google Maps upstream down) MUST be tolerated identically to 404.

Unit Tests

AC Ref What to Test Required Outcome
AC-1 Module-scope evaluation of TILE_URL with env mocked Equals mocked value
AC-2 Module-scope evaluation of TILE_URL with env unset Equals dev default
AC-5 TypeScript compilation against ImportMetaEnv Compiles; no VITE_OSM_TILE_URL reference remains

Blackbox Tests

AC Ref Initial Data/Conditions What to Test Expected Behavior NFR References
AC-1 / AC-2 env set / unset; FlightMap mounted Rendered <TileLayer> url prop Equals the resolved URL Compat
AC-3 FlightMap mounted Rendered tile <img> element's crossOrigin attribute use-credentials Reliability
AC-4 FlightMap mounted DOM scan for [data-testid="map-type-toggle"] AND absence of mapType references Toggle absent; MiniMap.Props has no mapType UX
AC-6 suite-e2e profile up GET http://<tile-host>:<port>/tiles/1/0/0 200 + image bytes E2E determinism
AC-7 Contract file present Contract shape matches implementation No Spec-Gap finding Docs
AC-8 tile_split_zoom tests updated Run against new URL shape Pass Compat

Constraints

  • Leaflet's <TileLayer> API surface MUST NOT change; only the url value, crossOrigin prop, and removal of the per-mode branching change.
  • Same-origin deployment via nginx is the production assumption. Any setup that requires cross-origin cookies on tile requests is out of scope.
  • No new third-party tile provider may be introduced as a fallback (would re-violate restriction E1).

Risks & Mitigation

Risk 1: Cross-workspace dependency (cookie auth on /tiles/{z}/{x}/{y})

  • Risk: Until the satellite-provider workspace adds cookie auth, the endpoint returns 401 to the SPA in production. Merging the UI side first results in a broken map.
  • Mitigation: The user files the satellite-provider-side ticket separately. This UI task is gated on that work landing. The task's deploy step (autodev Step 16) MUST verify both sides are in place before flipping prod traffic; suggested gate is a "tiles-render" smoke check in the deploy skill.

Risk 2: Dev environment cookie scope (localhost:5173localhost:5100)

  • Risk: Once cookie auth is enforced, devs running the SPA at localhost:5173 and satellite-provider at localhost:5100 cannot send the auth cookie cross-port. Tiles will 401 in dev.
  • Mitigation: Document the limitation in _docs/02_document/deployment/environment_strategy.md. Recommend local satellite-provider be run with auth disabled OR be reached through the suite's local nginx (same origin). This is an explicit trade-off the user accepted at cycle-2 assumption validation.

Risk 3: UX regression — losing the classic (street) view

  • Risk: Pilots accustomed to OSM road context for ground-reference lose that view.
  • Mitigation: Accepted by user choice (cycle-2 tile-scope = B). Tracked here so a future cycle can restore a street view via a different self-hosted source if demand arises.

Risk 4: E2E flake during the tile-stub repurpose

  • Risk: Repurposing e2e/stubs/tile/server.ts to the new path may cause AZ-456 / AZ-474 / AZ-479 / AZ-480 e2e tests to flap during the transition.
  • Mitigation: Land the suite-e2e compose change in the same PR as the source change so the harness is consistent in every commit. Add a short pre-flight check in infrastructure.e2e.ts that confirms the stub responds at the new path before downstream specs run.

Risk 5: Silent broken-image rendering on auth failure

  • Risk: If cookie auth fails post-deploy, Leaflet renders blank tiles without surfacing a user-facing error.
  • Mitigation: Add a tileerror listener on the <MapContainer> that, on the first error, logs a structured warning and (optionally) shows an inline banner ("Imagery unavailable; please re-sign-in"). This is a small follow-up; recommended as part of this task's deliverables but acceptable to defer to a follow-up if scope pressure builds.

Contract

This task consumes the contract at _docs/02_document/contracts/satellite-provider/tiles.md (v1.0.0, status: draft). The satellite-provider workspace owns producing/maintaining that contract. The UI MUST read that file — not this task spec — to discover the interface.

Document Dependencies

  • _docs/02_document/contracts/satellite-provider/tiles.md — slippy-tile API contract.
  • _docs/02_document/components/05_flights/description.md — owning component description.
  • _docs/02_document/modules/src__features__flights.md — module-layout mapping for the affected files.