[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>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-12 04:34:39 +03:00
parent 20a39d3d8a
commit b016fd8207
26 changed files with 739 additions and 85 deletions
+4 -1
View File
@@ -98,7 +98,10 @@ services:
environment:
VITE_API_BASE_URL: "/api"
VITE_OWM_BASE_URL: "http://owm-stub:8081"
VITE_TILE_BASE_URL: "http://tile-stub:8082"
# AZ-498 — single self-hosted satellite tile URL pointed at tile-stub.
# The {z}/{x}/{y} placeholders are passed through to Leaflet's
# TileLayer template; the stub serves /tiles/{z}/{x}/{y} (no .png).
VITE_SATELLITE_TILE_URL: "http://tile-stub:8082/tiles/{z}/{x}/{y}"
depends_on:
admin: { condition: service_started }
flights: { condition: service_started }
+22 -9
View File
@@ -1,6 +1,12 @@
// tile-stub — OSM + Esri tile stand-in for the e2e profile (AZ-456 AC-2).
// Always returns a deterministic 256×256 transparent PNG. Records every
// request so tile-coverage tests can assert on the access log.
// tile-stub — satellite-provider tile stand-in for the e2e profile.
// Always returns a deterministic 256×256 transparent PNG (Content-Type
// `image/jpeg` to mirror the real `satellite-provider` contract). Records
// every request so tile-coverage tests can assert on the access log.
//
// Contract: `_docs/02_document/contracts/satellite-provider/tiles.md`
// (v1.0.0). Path shape `/tiles/{z}/{x}/{y}` — no `.png` suffix. The
// pre-AZ-498 OSM (`/{z}/{x}/{y}.png`) and Esri (`/sat/{z}/{y}/{x}`)
// schemes were retired together with the classic/satellite map toggle.
const PORT = Number(process.env.PORT ?? 8082)
@@ -12,11 +18,12 @@ const TILE_PNG = new Uint8Array([
0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
])
const requestLog: Array<{ ts: string; method: string; url: string; scheme: 'osm' | 'esri' | 'other' }> = []
type Scheme = 'satellite-provider' | 'other'
function classify(pathname: string): 'osm' | 'esri' | 'other' {
if (/^\/sat\//.test(pathname)) return 'esri'
if (/^\/\d+\/\d+\/\d+\.png$/.test(pathname)) return 'osm'
const requestLog: Array<{ ts: string; method: string; url: string; scheme: Scheme }> = []
function classify(pathname: string): Scheme {
if (/^\/tiles\/\d+\/\d+\/\d+$/.test(pathname)) return 'satellite-provider'
return 'other'
}
@@ -33,8 +40,14 @@ const server = Bun.serve({
if (url.pathname === '/mock/log') {
return Response.json(requestLog)
}
if (scheme === 'osm' || scheme === 'esri') {
return new Response(TILE_PNG, { headers: { 'Content-Type': 'image/png' } })
if (scheme === 'satellite-provider') {
return new Response(TILE_PNG, {
headers: {
'Content-Type': 'image/jpeg',
'Cache-Control': 'public, max-age=86400',
'ETag': '"e2e-stub-fixture"',
},
})
}
return new Response('not found', { status: 404 })
},
+18 -5
View File
@@ -4,12 +4,18 @@ import { test, expect } from '@playwright/test'
// Every other test file under e2e/tests/ is owned by AZ-457..AZ-482; those
// tasks add the production-shaped assertions per group. This file MUST stay
// minimal so any flake here is unambiguously an infrastructure regression.
//
// AZ-498 update (2026-05-12):
// - The classic/satellite map toggle was removed; the SPA now consumes
// only `satellite-provider` tiles via `VITE_SATELLITE_TILE_URL`. The
// tile-stub serves `/tiles/{z}/{x}/{y}` (no `.png` suffix) per
// `_docs/02_document/contracts/satellite-provider/tiles.md`.
// - The dead OSM/Esri entries in EXTERNAL_HOSTS are removed; the SPA can
// no longer attempt those hosts. The OWM and unpkg defenses stay.
const EXTERNAL_HOSTS = [
/api\.openweathermap\.org/,
/unpkg\.com/,
/\.tile\.openstreetmap\.org$/,
/^tile\.openstreetmap\.org$/,
]
test.describe('AZ-456 e2e infrastructure', () => {
@@ -50,11 +56,18 @@ test.describe('AZ-456 e2e infrastructure', () => {
expect(body.wind).toEqual({ speed: 5.0, deg: 270 })
})
test('AC-2: tile-stub returns a 256x256 PNG', async ({ request }) => {
const res = await request.get('http://tile-stub:8082/1/0/0.png')
test('AC-2: tile-stub serves /tiles/{z}/{x}/{y} as a JPEG (AZ-498 contract)', async ({ request }) => {
const res = await request.get('http://tile-stub:8082/tiles/1/0/0')
expect(res.status()).toBe(200)
expect(res.headers()['content-type']).toBe('image/png')
expect(res.headers()['content-type']).toBe('image/jpeg')
// Cache-Control + ETag are part of the contract — assert they're present
// so a future tile-stub regression that drops them is caught here.
expect(res.headers()['cache-control']).toMatch(/max-age=/)
expect(res.headers()['etag']).toBeTruthy()
const body = await res.body()
// The stub serves a tiny PNG byte sequence; assert the PNG signature so
// we know SOME image came back even though the Content-Type header is
// jpeg-shaped. Production satellite-provider returns real JPEG bytes.
expect(body.subarray(0, 8)).toEqual(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))
})