# 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 ` ` 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 ``, and the `mapType` prop passed to `MiniMap`.
- Update `MiniMap.tsx` to render a single `` without a `mapType` prop.
- Both `` 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 `` 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 `` `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 `` `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 `` issues a tile request via Leaflet,
Then the rendered ` ` 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://:/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 `` 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 `` `url` prop | Equals the resolved URL | Compat |
| AC-3 | `FlightMap` mounted | Rendered tile ` ` 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://:/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 `` 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:5173` ↔ `localhost: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 `` 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.