Closes autodev existing-code Step 9 for cycle 2.
- Epic AZ-497 (Self-Hosted Satellite Tiles - SPA Integration) added
to _docs/02_tasks/_dependencies_table.md as the cycle-2 umbrella.
- AZ-498 (5 pts): self-hosted satellite tiles + drop map-type toggle.
Cross-workspace prereq: satellite-provider must add cookie-auth on
GET /tiles/{z}/{x}/{y} before merge (user files separately).
- AZ-499 (2 pts): mission-planner OWM env-var hardening + closes the
AZ-482 source-scan gap with a new owm_key_in_source banned-deps kind.
- Contract _docs/02_document/contracts/satellite-provider/tiles.md
v1.0.0 (draft): slippy-tile XYZ shape both sides commit to.
- _docs/_autodev_state.md: Step 9 closure note + advances pointer to
Step 10 (Implement) for cycle 2.
Co-authored-by: Cursor <cursoragent@cursor.com>
13 KiB
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-providerservice 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.mddocuments the contract both sides commit to.
Scope
Included
- Collapse
TILE_URLSinsrc/features/flights/types.tsto a single URL string read fromimport.meta.env.VITE_SATELLITE_TILE_URL. - Remove the classic/satellite toggle from
FlightMap.tsx: themapTypestate, the toggle<button>, and themapTypeprop passed toMiniMap. - Update
MiniMap.tsxto render a single<TileLayer>without amapTypeprop. - Both
<TileLayer>instances MUST includecrossOrigin="use-credentials"so the browser attaches the auth cookie on same-origin requests. - Update
.env.example: addVITE_SATELLITE_TILE_URL, removeVITE_OSM_TILE_URLandVITE_ESRI_TILE_URL, refresh the comment block. - Update
src/vite-env.d.ts: addVITE_SATELLITE_TILE_URL?: string, remove the two OSM/Esri declarations. - Update
_docs/02_document/contracts/satellite-provider/tiles.mdto reference this task in theConsumer tasksfield once the ticket ID is assigned. - Update
e2e/docker-compose.suite-e2e.yml: replacetile-stubwiring with either (a) a redirect of the SPA'sVITE_SATELLITE_TILE_URLto the actualsatellite-providerDocker service, or (b) repurposee2e/stubs/tile/server.tsto 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.tsAC-2 path assertion ande2e/tests/tile_split_zoom.e2e.tsto point at the new path/host. - Remove the i18n key
flights.planner.satellitefromsrc/i18n/en.jsonandsrc/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.mdand_docs/02_document/components/05_flights/description.mdto reflect the new tile source and the removed toggle. - New blackbox test that asserts the
<TileLayer>URL resolves to the env-var value AND thatcrossOrigin="use-credentials"is present on the rendered DOM element. - New blackbox test that asserts the toggle button and the
mapTypestate are absent from the renderedFlightMap.
Excluded
- The
satellite-providerserver-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 handlesmission-plannerseparately for OWM, andmission-planner's tile config is independent (its ownVITE_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-providerdownloads upstream tiles. - The same tile URL viewed twice within a session MUST be served from the browser's HTTP cache (i.e.,
Cache-Control+ETaground-trip).
Compatibility
- The
MapContainer/TileLayerAPI surface inreact-leafletis 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 theurlvalue,crossOriginprop, 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:5173 ↔ localhost:5100)
- Risk: Once cookie auth is enforced, devs running the SPA at
localhost:5173and satellite-provider atlocalhost:5100cannot 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.tsto 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.tsthat 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
tileerrorlistener 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.