3 Commits

Author SHA1 Message Date
Oleksandr Bezdieniezhnykh f7dd6c98d8 [AZ-501] [AZ-502] Cycle 2 Step 14 security audit + inline fixes
ci/woodpecker/push/build-arm Pipeline failed
Security audit (5 phases) → reports under _docs/05_security/.

AZ-501 (F-SAST-1, HIGH): Externalize hardcoded Google Geocode key
from mission-planner/src/config.ts to VITE_GOOGLE_GEOCODE_KEY via
new GeocodeService.ts; fail-soft warn when unset; STC-SEC1D static
deny-list gate; +5 unit tests in tests/mission_planner_geocode.test.ts.

AZ-502 (F-DEP-1, HIGH): Force vite>=6.4.2 and postcss>=8.5.10 via
package.json overrides in both roots; clean reinstall clears all
bun audit advisories.

Test-spec sync (Step 12) + Update Docs (Step 13) deltas: AC-43, AC-44,
NFT-SEC-09b, FT-P-61, FT-N-17, ripple log, batch_12 report.

Pending user actions: revoke Google + OWM keys (AC-6 / AZ-499 AC-7).

229 PASS / 13 SKIP / 0 FAIL on static + fast suites.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 05:31:11 +03:00
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
Oleksandr Bezdieniezhnykh 20a39d3d8a [AZ-497] [AZ-498] [AZ-499] Cycle 2 New Task: epic, stories, contract draft
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>
2026-05-12 03:45:44 +03:00
54 changed files with 3006 additions and 564 deletions
+12 -12
View File
@@ -6,11 +6,14 @@
#
# Every variable is OPTIONAL. When unset, the SPA falls back to production-
# default behavior:
# - VITE_API_BASE_URL : '' (relative paths; SPA and suite share nginx)
# - VITE_OWM_API_KEY : undefined → getWeatherData returns null
# - VITE_OWM_BASE_URL : https://api.openweathermap.org/data/2.5
# - VITE_OSM_TILE_URL : https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png
# - VITE_ESRI_TILE_URL : https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}
# - VITE_API_BASE_URL : '' (relative paths; SPA and suite share nginx)
# - VITE_OWM_API_KEY : undefined → getWeatherData returns null
# - VITE_OWM_BASE_URL : https://api.openweathermap.org/data/2.5
# - VITE_SATELLITE_TILE_URL : http://localhost:5100/tiles/{z}/{x}/{y}
# (dev default; production builds MUST override
# to the same-origin nginx path so cookie auth
# is honored — AZ-498 / contract @
# _docs/02_document/contracts/satellite-provider/tiles.md)
# Prefix for every API request (production: empty; tests / alt deployments: set).
# A trailing slash is stripped automatically.
@@ -26,10 +29,7 @@ VITE_OWM_API_KEY=<your-openweathermap-api-key>
# Example for the e2e profile: http://owm-stub:8081/data/2.5
VITE_OWM_BASE_URL=
# OSM map tile URL template (Leaflet TileLayer.url).
# Example for the e2e profile: http://tile-stub:8082/{z}/{x}/{y}.png
VITE_OSM_TILE_URL=
# Esri satellite tile URL template (Leaflet TileLayer.url for the satellite layer).
# Example for the e2e profile: http://tile-stub:8082/sat/{z}/{y}/{x}
VITE_ESRI_TILE_URL=
# Suite satellite-provider tile URL template (Leaflet TileLayer.url).
# Production: same-origin path (`/tiles/{z}/{x}/{y}`) so the auth cookie rides.
# E2E profile: http://tile-stub:8082/tiles/{z}/{x}/{y}
VITE_SATELLITE_TILE_URL=
+7 -3
View File
@@ -36,7 +36,7 @@ Every criterion must have a measurable value. Each row carries a unique ID
| AC-17 | a11y — `ProtectedRoute` spinner | `role=status` + accessible label; loading state has a timeout | Component test asserting a11y attributes; integration test asserting timeout fallback | finding |
| AC-18 | Browser support | Chromium-based + Firefox latest 2 versions render the SPA correctly | Manual smoke (no `browserslist` enforcement today) | `architecture.md` § 6 — **manual / aspirational** |
| AC-19 | Mobile responsiveness | Header bottom-nav variant renders at < 768 px; main pages render at ≥ 768 px | Manual smoke at the two breakpoints | `Header.tsx:113-129`; `architecture.md` § 6 |
| AC-20 | OpenWeatherMap key NOT in source | `import.meta.env.VITE_OPENWEATHERMAP_API_KEY` (or proxied via suite); zero hardcoded keys in any `src/` or `mission-planner/` module | Static check (regex against the current literal); CI step | P10; `mission-planner/src/utils/flightPlanUtils.ts:60` (current violation, Step 4 fix) |
| AC-20 | OpenWeatherMap key NOT in source | `import.meta.env.VITE_OWM_API_KEY` (and `VITE_OWM_BASE_URL`); zero hardcoded keys in any `src/` or `mission-planner/` module | Static check (regex against the previously-committed literal — `STC-SEC1`, `STC-SEC1B`, `STC-SEC1C`); CI step | P10; closed cycle 2 / 2026-05-12 by AZ-448 (main SPA), AZ-499 (mission-planner); see also AC-42 |
| AC-21 | UserSettings persistence — panel widths | Panel-width changes via `useResizablePanel` write back to `PUT /api/annotations/settings/user`; reload restores widths | Integration test: change width → reload → assert restored | P11; `src/hooks/useResizablePanel.ts` (current violation) |
| AC-22 | RBAC client-side route gates | `/admin` and `/settings` redirect non-privileged users to `/flights` (or `/login` if not authenticated). Server-side 403 is the authoritative gate; UI gate is convenience | Integration test: log in as non-admin → navigate to `/admin` → assert redirect | finding (`/admin` route lacks role-gate — security PRIORITY) |
| AC-23 | Auth refresh transparency | One refresh = one network round trip; **no UI re-render past `<ProtectedRoute>`** | Integration test asserting `<ProtectedRoute>` does not unmount during refresh | `architecture.md` § 6 NFR row "Auth refresh"; `04_verification_log.md` F2 |
@@ -57,6 +57,10 @@ Every criterion must have a measurable value. Each row carries a unique ID
| AC-38 | PhotoMode switcher (Regular / Winter / Night) | PhotoMode buttons emit values from the set `{0, 20, 40}` (Regular=0, Winter=+20, Night=+40). Switching mode: (a) re-filters the class list to entries whose `photoMode` equals the new mode; (b) if the previously-selected `classNum` is not in the new filtered set, auto-selects the first class of the new mode and emits `onSelect`. On annotation save, the wire `Detection.classNum` (a.k.a. *yoloId*) equals `classId + photoModeOffset`. | Component test on the mode-switch effect + integration test on the save payload | `modules/src__components__DetectionClasses.md` §22, §31-43; `data_model.md:84`; `components/11_class-colors/description.md:31-35`; `ui_design/README.md:127-128`; `ui_design/annotations.html:84-93` |
| AC-39 | Tile-splitting endpoint + wire shape | `POST /api/annotations/dataset/{id}/split` exists and is callable from the dataset surface; success response is JSON with HTTP 200. `AnnotationListItem.isSplit: boolean` and `AnnotationListItem.splitTile: string \| null` (YOLO label `<class> <cx> <cy> <w> <h>`) are honored on read. When `isSplit === true` and `splitTile` is non-null, the client parses the 5-token YOLO label without throwing; malformed `splitTile` surfaces a user-visible error (no silent swallow). `DatasetItem.isSplit?: boolean` is read on the dataset list path (parent-suite-doc fix applied — see `_docs/_process_leftovers/2026-05-10_parent-suite-doc-fixes.md`). | Integration test against a fixture response; unit test on the YOLO-label parser with valid + malformed inputs | `components/07_dataset/description.md:28`; `data_model.md:104-105,130,164`; `modules/src__features__annotations.md:31,75`; `modules/src__types__index.md:24-28` |
| AC-40 | Tile-zoom auto-zoom on split-image annotation open | When the user opens a `splitTile`-bearing annotation (double-click in `AnnotationsSidebar` or seek via the annotation list), `CanvasEditor` auto-zooms to the tile region encoded by `splitTile` (parsed per AC-39). The visible viewport rectangle equals the tile rectangle within ±1 px on each edge. A small visual tile-zoom indicator (icon / badge) is rendered while the tile zoom is active so the operator knows the view is constrained. **Currently MISSING** — finding #24 in `modules/src__features__annotations.md`; Step 4 / Phase B fix. | Component test on `CanvasEditor` with a `splitTile`-bearing annotation; assert viewport rect + presence of the tile-zoom indicator | `components/06_annotations/description.md:62, 103`; `modules/src__features__annotations.md:75` finding #24; `legacy/wpf-era.md` (OpenAnnotationResult seek + ZoomTo) |
| AC-41 | Map tiles served by self-hosted `satellite-provider` via cookie auth | (a) `<TileLayer>` `url` prop equals `import.meta.env.VITE_SATELLITE_TILE_URL` (or the dev default `http://localhost:5100/tiles/{z}/{x}/{y}` when unset). (b) Every `<TileLayer>` the SPA renders carries `crossOrigin="use-credentials"` so the browser attaches the satellite-provider auth cookie on same-origin requests. (c) The classic/satellite map-type toggle, the `mapType` state, and the `MiniMap.Props.mapType` prop are absent. (d) A 401 / 503 from the tile endpoint MUST NOT crash the map; broken-tile placeholder is rendered for the failing cell. | Fast component tests (`src/features/flights/__tests__/satellite_tile.test.tsx`) + e2e infrastructure check (`e2e/tests/infrastructure.e2e.ts` AC-2) + STC-T1 typecheck + STC-FP22 i18n parity (post-key removal). Cycle-2 spec rows: FT-P-56, FT-P-57, FT-P-58, FT-P-59, NFT-RES-11. | Closed cycle 2 / 2026-05-12 by AZ-498 (epic AZ-497). `_docs/02_document/contracts/satellite-provider/tiles.md` v1.0.0 owns the wire shape. Cross-workspace prereq for production deploy: satellite-provider cookie-auth on `GET /tiles/{z}/{x}/{y}` (gated at autodev Step 16). |
| AC-42 | mission-planner OpenWeatherMap config externalized; fail-soft on missing key | (a) `mission-planner/src/services/WeatherService.ts::getWeatherData(lat, lon)` builds the outbound URL from `import.meta.env.VITE_OWM_API_KEY` + `VITE_OWM_BASE_URL` (falls back to `https://api.openweathermap.org/data/2.5` when the base URL is unset; trailing slash on the base URL is stripped). (b) When `VITE_OWM_API_KEY` is unset/empty, `getWeatherData` returns `null` and issues NO outbound `fetch`. (c) Static check `STC-SEC1C` (`scripts/check-banned-deps.mjs --kind=owm_key_in_source`) FAILS on any future re-introduction of the previously-committed literal under `src/` or `mission-planner/`. (d) The previously-committed key MUST be revoked at the OpenWeatherMap dashboard (manual deliverable — defense-in-depth). | Fast tests (`tests/mission_planner_weather.test.ts`) + STC-SEC1C static check + STC-T1 typecheck. Cycle-2 spec rows: FT-P-60, FT-N-16, NFT-SEC-09 step 3. | Closed cycle 2 / 2026-05-12 by AZ-499 (epic AZ-497). Closes the AZ-482 source-scan gap (which previously only checked `src/` for the regex shape and `dist/` for the literal — `mission-planner/` stays out of `dist/` per AC-31, so the dist scan alone could not catch it). |
| AC-43 | mission-planner Google Geocode config externalized; fail-soft on missing key | (a) The previously-hardcoded Google Geocode API key has been EXTRACTED from `mission-planner/src/config.ts` to a new `mission-planner/src/services/GeocodeService.ts` module that builds the outbound URL from `import.meta.env.VITE_GOOGLE_GEOCODE_KEY`. (b) When the env var is unset/empty, `geocodeAddress(address)` returns `null`, issues NO outbound `fetch`, and emits exactly one `console.warn` mentioning `VITE_GOOGLE_GEOCODE_KEY`. (c) Static check `STC-SEC1D` (`scripts/check-banned-deps.mjs --kind=google_key_in_source`) FAILS on any future re-introduction of the previously-committed literal under `src/` or `mission-planner/`. (d) The previously-committed key MUST be revoked at the Google Cloud Console (manual deliverable — defense-in-depth). (e) `LeftBoard.tsx` imports `geocodeAddress` from the service module; the inline geocode function and the `GOOGLE_GEOCODE_KEY` import are removed. | Fast tests (`tests/mission_planner_geocode.test.ts`) + STC-SEC1D static check + STC-T1 typecheck. Cycle-2 spec rows: FT-P-61, FT-N-17, NFT-SEC-09b. | Closed cycle 2 / 2026-05-12 by AZ-501 (filed during the security audit, `_docs/05_security/`). Mirrors the AZ-499 pattern (env var + fail-soft + literal-scan static gate + manual revocation). Manual deliverable AZ-501 AC-6 (key revocation at Google Cloud Console) PENDING USER. |
| AC-44 | Vite + PostCSS supply chain past published CVEs | `bun audit` in BOTH `ui/` and `mission-planner/` reports zero advisories. Achieved by `bun update vite` plus `package.json` `overrides` flooring `vite >= 6.4.2` and `postcss >= 8.5.10` in both roots — required because `vitest@3.2.4` nests its own `vite` copy that the direct upgrade alone does not lift past the `<= 6.4.1` advisory range. | `bun audit` exit code 0 in both roots after `bun install` from a clean `node_modules`. CI gate (`bun audit --severity high` in `.woodpecker/build-arm.yml`) is a Phase B follow-up tracked at `_docs/05_security/infrastructure_review.md` F-INF-1. | Closed cycle 2 / 2026-05-12 by AZ-502 (filed during the security audit). Affected advisories: GHSA-p9ff-h696-f583 (HIGH — Vite WebSocket file-read), GHSA-4w7w-66w2-5vf9 (MODERATE — Vite path traversal), GHSA-qx2v-qp2m-jg93 (MODERATE — PostCSS XSS). Production-bundle exposure was NONE before the upgrade (Vite is dev-server-only); the upgrade closes the developer-machine exposure and the audit-tool noise. |
## Anti-criteria — explicit non-goals
@@ -70,7 +74,7 @@ Every criterion must have a measurable value. Each row carries a unique ID
## Coverage status
- **Currently met & enforced**: AC-02 (no token storage), AC-05 (annotation save URL — body shape pending), AC-06, AC-07, AC-08, AC-09, AC-10 (server cap; UI surface is a finding), AC-25 (sync path; async path is target-only), AC-31, AC-33, AC-34.
- **Currently met & enforced**: AC-02 (no token storage), AC-05 (annotation save URL — body shape pending), AC-06, AC-07, AC-08, AC-09, AC-10 (server cap; UI surface is a finding), AC-20 (OWM key — closed cycle 2 by AZ-448 + AZ-499; STC-SEC1/SEC1B/SEC1C all green), AC-25 (sync path; async path is target-only), AC-31, AC-33, AC-34, AC-41 (self-hosted satellite tiles + cookie auth — closed cycle 2 by AZ-498; production deploy still gated on cross-workspace satellite-provider cookie-auth ticket), AC-42 (mission-planner OWM env-var hardening — closed cycle 2 by AZ-499; manual key revocation pending), AC-43 (mission-planner Google Geocode env-var hardening — closed cycle 2 by AZ-501; manual key revocation pending), AC-44 (Vite + PostCSS supply chain — closed cycle 2 by AZ-502; CI audit gate is a Phase B follow-up).
- **Currently met but not enforced by CI**: AC-04 (enum values), AC-12 (i18n parity), AC-29 (typed `mediaType`), AC-35 (manual bbox draw), AC-37 (class picker — pending Step 4 backend-ordering verification), AC-38 (PhotoMode switcher).
- **Currently violated — Step 4 fix candidates**: AC-01 (bootstrap refresh), AC-13 (i18n detector), AC-14 / AC-30 (class-delete dialog; `alert()` use), AC-15AC-17 (a11y), AC-20 (OWM key), AC-21 (panel widths), AC-22 (route role-gate), AC-23 (refresh re-render — code-path correct, but bootstrap-refresh fix needed), AC-26 (numeric input hygiene), AC-27 (save error surfacing), AC-28 (overlay window), AC-36 (Ctrl-multi-select / Ctrl-wheel zoom / Ctrl-drag pan flagged "Partially missing"), AC-40 (tile-zoom auto-zoom — finding #24, no consumer of `splitTile` today).
- **Currently violated — Step 4 fix candidates**: AC-01 (bootstrap refresh), AC-13 (i18n detector), AC-14 / AC-30 (class-delete dialog; `alert()` use), AC-15AC-17 (a11y), AC-21 (panel widths), AC-22 (route role-gate), AC-23 (refresh re-render — code-path correct, but bootstrap-refresh fix needed), AC-26 (numeric input hygiene), AC-27 (save error surfacing), AC-28 (overlay window), AC-36 (Ctrl-multi-select / Ctrl-wheel zoom / Ctrl-drag pan flagged "Partially missing"), AC-40 (tile-zoom auto-zoom — finding #24, no consumer of `splitTile` today).
- **Phase B targets (not currently in scope of `/document` Step 6)**: AC-11 (bundle gate), AC-18 (browser-list), AC-19 (mobile floor), AC-24 (SSE refresh re-subscribe), AC-25 async path, AC-32 (CI label assertions), AC-39 (tile-split endpoint — parent-suite-doc fix applied for `isSplit`; the YOLO-label parser hardening lands when the splitTile consumer is wired in Phase B).
+25 -4
View File
@@ -159,14 +159,33 @@ Source: `src/api/sse.ts`; `ADR-008`; `architecture.md` § 7.
`flights/` so no key ever reaches the browser (preferred; per
`architecture.md` § Architecture Vision Open Questions item 8).
### Hardcoded Google Geocode API key — discovered cycle 2 audit (AZ-501)
- **File**: `mission-planner/src/config.ts:2` (originally — extracted to
`mission-planner/src/services/GeocodeService.ts` by AZ-501).
- **Production-bundle exposure**: NONE. `mission-planner/` is a port-source
not built into `dist/` (`AC-31` / `STC-S5`).
- **Git-history exposure**: HIGH — same threat class as the OWM key.
- **Closed cycle 2** by AZ-501: env-resolved via `VITE_GOOGLE_GEOCODE_KEY`,
fail-soft + single `console.warn` when unset, defended by `STC-SEC1D`
(literal scan across `src/` + `mission-planner/`). The `/document` Step 6e
retrospective missed this because mission-planner/ was treated as out-of-
scope (port-source) — the security audit (`_docs/05_security/`) caught it
via a broader source-tree grep, demonstrating the value of a separate
audit pass.
- **Manual deliverable PENDING USER**: revoke the key at the Google Cloud
Console (AZ-501 AC-6).
### Other secrets
- **No other hardcoded keys** in `src/` per Grep audit at Step 4.
- **No other hardcoded keys** in `src/` per Grep audit at Step 4 +
cycle-2 security-audit (`_docs/05_security/static_analysis.md`).
- Suite service URLs are not secrets (they are docker-network hostnames).
- The bearer is the only sensitive value in browser memory, and it is
short-lived.
Source: P10; `architecture.md` § Architecture Vision; finding (security).
Source: P10; `architecture.md` § Architecture Vision; finding (security);
`_docs/05_security/security_report.md` F-SAST-1.
---
@@ -304,8 +323,10 @@ pipeline today".
| Annotation save body missing `Source`, `WaypointId`, wrong `time` field | AC-05 | Step 4 |
| `X-Refresh-Token` not sent on long-video detect (#29) | — | Step 4 |
| Numeric enum drift (`AnnotationStatus`, `MediaStatus`, `Affiliation`, `CombatReadiness`) | AC-04 | Step 4 (P9 alignment) |
| No CSP / hardening headers in `nginx.conf` | — | Step 6 — track at suite level |
| No vulnerability scan / SBOM / image signing in CI | — | Phase B |
| No CSP / hardening headers in `nginx.conf` | — | Step 6 — track at suite level (cycle-2 audit F-INF-2 → Phase B) |
| No vulnerability scan / SBOM / image signing in CI | — | Phase B (cycle-2 audit F-INF-3 / F-INF-4) |
| Vite ≤ 6.4.1 + PostCSS < 8.5.10 — published CVEs (HIGH/MOD) | AC-44 | Closed cycle 2 by AZ-502 (`bun update vite` + `package.json` overrides) |
| Hardcoded Google Geocode API key in `mission-planner/` port-source | AC-43 | Closed cycle 2 by AZ-501; manual key revocation PENDING USER |
---
+9 -9
View File
@@ -123,8 +123,8 @@ contract beautifully and accessibly".
|----------------------|------------------------|
| React 19 SPA (`src/`) | Suite backend services (`annotations/`, `flights/`, `admin/`, `detect/`, `loader/`, `gps-denied-{desktop,onboard}/`, `autopilot/`, `resource/`) |
| `mission-planner/` port-source (NOT deployed) | Database (PostgreSQL — managed by individual suite services, not the UI) |
| Static-bundle Dockerfile + nginx config | OpenWeatherMap public API (consumed directly by the SPA — security finding) |
| Woodpecker CI pipeline (`.woodpecker/build-arm.yml`) | Map tile providers (OpenStreetMap, satellite tile URL via env) |
| Static-bundle Dockerfile + nginx config | OpenWeatherMap public API (consumed by the main SPA via env-resolved key per AZ-448 / AZ-449; consumed by `mission-planner/` per AZ-499 — env-resolved key, fail-soft on unset, manual revocation of the previously-committed key tracked under AC-42) |
| Woodpecker CI pipeline (`.woodpecker/build-arm.yml`) | Suite-internal `satellite-provider` service for map tiles (same-origin via nginx in production; env-resolved URL `VITE_SATELLITE_TILE_URL` per AZ-498). The legacy OpenStreetMap / Esri tile providers are NO LONGER consumed by the main SPA as of cycle 2 / 2026-05-12. |
| | Identity provider (suite-internal — Admin API) |
**External systems**:
@@ -139,9 +139,9 @@ contract beautifully and accessibly".
| `gps-denied-desktop/`, `gps-denied-onboard/` | REST (via `/api/gps-denied-*/*`) | Outbound | GPS-Denied operations + Test Mode (SITL feed) |
| `autopilot/` | REST (via `/api/autopilot/*`) | Outbound | Aircraft autopilot configuration (admin-side) |
| `resource/` | REST (via `/api/resource/*`) | Outbound | Static resource fetch (icons, configs not bundled in the SPA) |
| OpenStreetMap tile servers | HTTPS (Leaflet TileLayer) | Outbound | Map raster tiles (browser-direct, not via nginx proxy) |
| Satellite tile provider | HTTPS (Leaflet TileLayer with env-configured URL) | Outbound | Satellite imagery (only consumed by mission-planner today) |
| OpenWeatherMap | HTTPS (`api.openweathermap.org/data/2.5/onecall`) | Outbound | Wind data for flight planning. **Hardcoded API key in `flightPlanUtils.ts:60` — security finding to fix at Step 4.** |
| Suite-internal `satellite-provider` service for satellite tiles | HTTPS (Leaflet TileLayer with env-configured URL `VITE_SATELLITE_TILE_URL`); same-origin in production via nginx; cookie auth (`crossOrigin="use-credentials"`) | Outbound (intra-suite) | Satellite map raster tiles. Replaces the previously-used OpenStreetMap and Esri ArcGIS World Imagery tile servers as of cycle 2 / 2026-05-12 (AZ-498) — air-gap restriction E1 satisfied without a stub. |
| OpenWeatherMap (main SPA) | HTTPS (`api.openweathermap.org/data/2.5/onecall`) | Outbound | Wind data for flight planning. Env-resolved key + base URL via `VITE_OWM_API_KEY` / `VITE_OWM_BASE_URL` since AZ-448 / AZ-449. |
| OpenWeatherMap (mission-planner) | HTTPS (`api.openweathermap.org/data/2.5/weather`) | Outbound | Wind data for the mission-planner port. Env-resolved key + base URL via `VITE_OWM_API_KEY` / `VITE_OWM_BASE_URL` since AZ-499; `getWeatherData(lat, lon)` returns `null` and issues NO fetch when the key is unset (fail-soft contract). The previously-committed literal `335799082893fad97fa36118b131f919` is defended against re-introduction by `STC-SEC1C` and tracked for manual OWM-dashboard revocation under AC-42. |
## 2. Technology Stack
@@ -170,7 +170,7 @@ contract beautifully and accessibly".
- **Static bundle only**: the UI ships zero server-side runtime. nginx serves `dist/` and reverse-proxies `/api/<service>/` to the matching suite service.
- **ARM-first**: production target is ARM-class edge devices; CI builds ARM64 only today (no AMD64 image in the pipeline).
- **Air-gapped friendly**: the SPA is bundled fully; only OpenWeatherMap and map tiles require internet. Field deployments will need an offline tile cache (not implemented).
- **Air-gapped friendly**: the SPA is bundled fully. As of cycle 2 / 2026-05-12 (AZ-498), map tiles are served by the suite-internal `satellite-provider` service on the same origin via nginx — restriction E1 is satisfied for tiles without a stub. The only remaining direct-from-browser external dependency is OpenWeatherMap (env-resolved per AC-42; fail-soft when the key is unset). Field deployments that go fully air-gapped MUST set `VITE_OWM_API_KEY=""` (or omit it) so `getWeatherData` returns `null` instead of attempting an external fetch.
- **No test framework**: legacy carry-over; the WPF `Azaion.Test` project tested utilities only; full test infrastructure is being built fresh under autodev.
- **Bilingual UI required**: Ukrainian + English are mandatory per the legacy WPF UX. English-only SaaS-style copy is a regression — finding tracked.
@@ -272,14 +272,14 @@ contract beautifully and accessibly".
| `08_admin/AdminPage` | `annotations/` + `admin/` + `flights/` | REST | Request-Response | `GET /api/annotations/classes` (read), `POST /api/admin/classes` (create), `DELETE /api/admin/classes/${id}` (delete — no ConfirmDialog, finding B4), `POST /api/admin/users`, `PATCH /api/admin/users/${id}` (deactivate), `GET /api/flights/aircrafts`, `PATCH /api/flights/aircrafts/${id}`. **Cross-service reads** — admin page reads aircraft from `flights/` and classes from `annotations/`. |
| `09_settings/SettingsPage` | `annotations/` + `flights/` | REST | Request-Response | `GET/PUT /api/annotations/settings/system`, `GET/PUT /api/annotations/settings/directories`, `GET /api/flights/aircrafts`, `PATCH /api/flights/aircrafts/${id}`. **Settings endpoints route to `annotations/`**, NOT `admin/` as initially drafted. |
| `05_flights/FlightsPage` | `flights/` | REST + SSE | Request-Response + Event | `GET /api/flights/aircrafts`, `GET /api/flights/${id}/waypoints`, **`createSSE('/api/flights/${id}/live-gps', ...)` — live-GPS SSE for aircraft telemetry**, `POST /api/flights`, `DELETE /api/flights/${id}`, `DELETE /api/flights/${id}/waypoints/${wpId}` (loop), `POST /api/flights/${id}/waypoints` (loop, lossy shape — finding #20). |
| `05_flights/flightPlanUtils` | OpenWeatherMap (external) | REST | Request-Response | `GET https://api.openweathermap.org/data/2.5/onecall?...` with **hardcoded API key** — security finding. |
| `05_flights/flightPlanUtils` | OpenWeatherMap (external) | REST | Request-Response | `GET https://api.openweathermap.org/data/2.5/onecall?...` with env-resolved key + base URL since AZ-448 / AZ-449 (closes the original security finding; see AC-20 + AC-42). |
### External Integrations
| External System | Protocol | Auth | Rate Limits | Failure Mode |
|----------------|----------|------|-------------|--------------|
| OpenStreetMap tiles | HTTPS (Leaflet TileLayer) | None | OSM Tile Usage Policy | Map renders blank / stale; no fallback today |
| OpenWeatherMap | HTTPS | **Hardcoded API key in source** | Free-tier 60 calls/min | Errors silently swallowed in `flightPlanUtils.ts` (finding) — wind data missing → battery/duration estimates wrong, no UI surface |
| Suite-internal `satellite-provider` for satellite tiles | HTTPS (Leaflet TileLayer); same-origin via nginx in production; cookie auth (`crossOrigin="use-credentials"`) | HttpOnly same-origin cookie set by `admin/` | Bounded by suite ops (no external usage policy) | 401 / 503 on a tile request renders a broken-tile placeholder for the failing cell; rest of the SPA stays interactive (per NFT-RES-11). Cycle 2 / 2026-05-12 — AZ-498. |
| OpenWeatherMap | HTTPS | Env-resolved key (`VITE_OWM_API_KEY`); never hardcoded since AZ-448 / AZ-499 | Free-tier 60 calls/min | Errors silently swallowed in main SPA's `flightPlanUtils.ts` (existing finding); mission-planner `WeatherService.getWeatherData` now returns `null` and issues NO outbound fetch when the key is unset (AZ-499 fail-soft contract — AC-42). |
| Suite identity provider (admin/) | REST + HttpOnly refresh cookie | JWT bearer + refresh-token rotation | server-enforced | 401 → `ProtectedRoute` redirects to `/login`; refresh-token rotation handled inside `AuthContext` (mostly) |
## 6. Non-Functional Requirements
@@ -56,7 +56,7 @@ The two trees are intentionally disjoint at the file level (no cross-imports —
| Page composition | `flightPlanning/flightPlan.tsx`, `LeftBoard.tsx` | Canonical page shape (sidebar + map). |
| Map | `flightPlanning/MapView.tsx` (cycle-paired with `MiniMap.tsx`), `MiniMap.tsx`, `DrawControl.tsx`, `MapPoint.tsx` | Reference Leaflet integration. **Cycle**: `MiniMap` imports the *named* helper `UpdateMapCenter` from `MapView`; `MapView` imports `MiniMap` as JSX child. Document the contract precisely if porting both at once. |
| Panels | `flightPlanning/PointsList.tsx`, `AltitudeChart.tsx`, `AltitudeDialog.tsx`, `WindEffect.tsx`, `TotalDistance.tsx`, `JsonEditorDialog.tsx`, `LanguageSwitcher.tsx`, `Aircraft.ts` | Reference panel shapes. Several have richer behaviour than the current SPA siblings. |
| Services | `services/calculateBatteryUsage.ts`, `AircraftService.ts`, `WeatherService.ts`, `calculateDistance.ts` | **Authoritative** battery / weather / distance logic. The target's `flightPlanUtils.ts` is currently an inferior port (silent errors, sequential `await`, hardcoded API key). |
| Services | `services/calculateBatteryUsage.ts`, `AircraftService.ts`, `WeatherService.ts`, `calculateDistance.ts` | **Authoritative** battery / weather / distance logic. The target's `flightPlanUtils.ts` is still an inferior port on remaining axes (silent errors, sequential `await`). The hardcoded-API-key gap was closed by AZ-448 / AZ-449 (main SPA) and AZ-499 (mission-planner — env-resolved + fail-soft). |
| i18n | `flightPlanning/LanguageContext.tsx`, `constants/translations.ts`, `constants/languages.ts` | Local translation pattern. The port should converge to `00_foundation/i18n` instead. |
| Constants | `constants/{actionModes,maptypes,tileUrls,purposes}.ts` | Reference constant tables. |
| Icons | `icons/{MapIcons,PointIcons,SidebarIcons,PhoneIcon}.tsx` | Reference icon factory. |
@@ -0,0 +1,117 @@
# Contract: satellite-provider tile serving
**Component**: satellite-provider
**Producer task**: TBD — separate AZAION ticket on `satellite-provider` workspace (user-filed)
**Consumer tasks**: AZ-498 — `_docs/02_tasks/todo/AZ-498_satellite_tile_swap.md` (suite/ui, cycle 2, epic AZ-497)
**Version**: 1.0.0
**Status**: draft
**Last Updated**: 2026-05-12
## Purpose
Describe the slippy-tile HTTP interface that the suite UI consumes to render
satellite imagery in `FlightMap` / `MiniMap`. Replaces the prior external-tile
dependencies (OpenStreetMap, Esri ArcGIS World Imagery). The endpoint is
served by `SatelliteProvider.Api` and backed by an on-disk + Google-Maps
download cache.
Frozen post-migration: SPA authentication for this endpoint MUST be **cookie-based**
(JWT delivered via `HttpOnly; Secure; SameSite=Lax` cookie on the same origin)
because Leaflet's `<TileLayer>` issues plain `<img>` requests and cannot attach
`Authorization: Bearer …` headers.
## Shape
### HTTP / RPC endpoints
| Method | Path | Request body | Response | Status codes |
|--------|-------------------------------|--------------|-------------------|---------------------|
| `GET` | `/tiles/{z}/{x}/{y}` | — | image bytes | 200, 401, 404, 503 |
**Path parameters**
| Name | Type | Required | Range / Constraint |
|------|---------|----------|--------------------------------------------------------|
| `z` | `int` | yes | `0 ≤ z ≤ 20` (slippy-tile zoom) |
| `x` | `int` | yes | `0 ≤ x < 2^z` (slippy-tile column) |
| `y` | `int` | yes | `0 ≤ y < 2^z` (slippy-tile row, TMS-y convention NO) |
Coordinates follow the Google Maps / OSM XYZ tiling scheme (NOT the inverted TMS
y-axis). Out-of-range coordinates SHOULD return 404.
**Response headers (on 200)**
| Header | Value |
|------------------|---------------------------------------------------------------|
| `Content-Type` | `image/jpeg` (image bytes from the `TileService`) |
| `Cache-Control` | `public, max-age=N` where N is set by `TileService` |
| `ETag` | strong ETag tied to the cached tile's content hash |
**Authentication**
- **Required**: yes (the endpoint is NOT public).
- **Mechanism (post-migration)**: cookie-based JWT.
- Cookie name: `satellite_auth` (TBD — defined by producer task).
- Attributes: `HttpOnly; Secure; SameSite=Lax` in production; `SameSite=Lax`
permitted over `http://localhost` for dev only.
- **Cross-origin behavior**: same-origin only. The SPA reaches this endpoint via
the suite ingress (nginx) on the SPA's origin; cross-origin direct calls from
`http://localhost:5173 → http://localhost:5100` will NOT carry the cookie and
will receive 401 in dev unless the developer disables auth locally.
**Status codes**
| Code | Meaning |
|------|-------------------------------------------------------------------|
| 200 | Cached or freshly downloaded tile; body = image bytes |
| 304 | (Optional) ETag match — body empty. UI MUST tolerate either 200 or 304. |
| 401 | Missing/invalid cookie — UI MUST treat as "user signed out" |
| 404 | Tile coordinates out of range OR upstream had no tile |
| 503 | Upstream (Google Maps) unavailable; UI MUST render placeholder |
## Invariants
- The endpoint URL pattern is `/tiles/{z}/{x}/{y}` exactly — never `/tiles/{z}/{y}/{x}`
(Esri-style) nor `/api/satellite/tiles/{z}/{x}/{y}`. This invariant survives
refactors and is asserted by both producer's integration tests and consumer's
blackbox tests.
- Image format is JPEG (Content-Type `image/jpeg`). Switching to PNG/WEBP is a
major-version change.
- The endpoint MUST honor `Cache-Control` and `ETag` headers on every 200; clients
rely on them to avoid re-fetching unchanged tiles during pan/zoom.
- Authentication failure MUST return 401, not 200 with an HTML body — Leaflet
would otherwise display a broken-image placeholder silently.
## Non-Goals
- Not covered: tile vector formats (`.pbf` / Mapbox Vector Tiles). This contract
is raster-only.
- Not covered: tile prewarming. Pre-warm uses the separate `POST /api/satellite/request`
endpoint (different contract, not consumed by the UI's `FlightMap`).
- Not covered: MGRS tile retrieval (returns 501 today; out of UI scope).
## Versioning Rules
- **Breaking** (major bump): change the path template, change the path-parameter
semantics (e.g., TMS-y), change `Content-Type`, remove a status code from the
set above, change the auth mechanism away from cookies.
- **Non-breaking** (minor bump): add a new optional query parameter, broaden the
zoom range, add a new status code in the 4xx/5xx space that consumers can
tolerate.
## Test Cases
| Case | Input | Expected | Notes |
|----------------------------|----------------------------------------|-----------------------------------------------------------|----------------------------------|
| valid-tile | `GET /tiles/15/9876/5432` w/ cookie | 200 + JPEG bytes + `Cache-Control` + `ETag` | producer + consumer cover |
| missing-cookie | `GET /tiles/15/9876/5432` w/o cookie | 401 | consumer must NOT retry |
| out-of-range-coord | `GET /tiles/3/8/0` (x ≥ 2^z) | 404 | consumer renders placeholder |
| etag-match | `GET /tiles/15/9876/5432` + `If-None-Match` | 304 OR 200 (server-policy dependent) | consumer tolerates both |
| upstream-503 | upstream Google Maps down | 503 | consumer renders placeholder |
| zoom-extreme | `GET /tiles/20/x/y` valid coords | 200 (or 404 if not cached and no on-demand) | consumer caps zoom at 20 |
## Change Log
| Version | Date | Change | Author |
|---------|------------|------------------------------------------------------------------------------|--------|
| 1.0.0 | 2026-05-12 | Initial draft; freezes the post-migration shape (cookie auth, XYZ scheme). | autodev (cycle 2 — suite/ui) |
@@ -8,9 +8,9 @@
| Env | How it runs | API base | Auth | Tile providers |
|-----|-------------|----------|------|----------------|
| Development | `bun run dev` (Vite dev server, port 5173) | Vite dev proxy: `/api → http://localhost:8080` (configured in `vite.config.ts`) | Suite admin/ service running locally (typically via parent suite `docker-compose up`) | OSM + satellite (env-configurable in mission-planner only) |
| Stage | nginx in container, ARM image `:stage-arm` | nginx `/api/<service>/ → http://<service>:8080/` (intra-cluster) | Stage suite admin/ service | Same |
| Production | nginx in container, ARM image `:main-arm` | nginx `/api/<service>/ → http://<service>:8080/` | Prod suite admin/ service | Same |
| Development | `bun run dev` (Vite dev server, port 5173) | Vite dev proxy: `/api → http://localhost:8080` (configured in `vite.config.ts`) | Suite admin/ service running locally (typically via parent suite `docker-compose up`) | Suite-internal `satellite-provider` via env-configurable `VITE_SATELLITE_TILE_URL` (defaults to `http://localhost:5100/tiles/{z}/{x}/{y}` when unset). Cookie auth requires same-origin; running the SPA at `localhost:5173` and `satellite-provider` at `localhost:5100` cannot send the auth cookie cross-port — recommend reaching `satellite-provider` through the suite's local nginx OR running it with auth disabled in dev (per AZ-498 risk #2). `mission-planner/` keeps its own independent `VITE_SATELLITE_TILE_URL`. |
| Stage | nginx in container, ARM image `:stage-arm` | nginx `/api/<service>/ → http://<service>:8080/` (intra-cluster) | Stage suite admin/ service | Suite-internal `satellite-provider` on the same origin (nginx-fronted); cookie auth attached automatically. |
| Production | nginx in container, ARM image `:main-arm` | nginx `/api/<service>/ → http://<service>:8080/` | Prod suite admin/ service | Same as Stage. Replaces the previously-used external OpenStreetMap and Esri tile providers as of cycle 2 / 2026-05-12 (AZ-498) — production deploy is gated on the cross-workspace satellite-provider cookie-auth ticket landing (autodev Step 16). |
## 2. Configuration model
@@ -21,20 +21,22 @@ The SPA bundle is **fully static**. No env vars are read at runtime by the bundl
| Backend API URL | nginx `proxy_pass` (`nginx.conf`) — same nginx config across stage / prod | Base URLs are intra-cluster service names (`http://annotations:8080`, etc.); the URL difference between environments is hidden by the orchestrator's DNS |
| Auth cookie domain | Set by suite admin/ service on `Set-Cookie` | UI does not control |
| Refresh-token lifetime | Set by suite admin/ service | UI tolerates any TTL |
| Tile provider URL (mission-planner) | `.env.example` declares `VITE_SATELLITE_TILE_URL` | mission-planner only; not deployed |
| OpenWeatherMap API key | **Hardcoded in source** (`flightPlanUtils.ts:60`) | Security finding — Step 4 fix to remove + proxy via suite |
| Satellite tile provider URL (main SPA) | `.env.example` declares `VITE_SATELLITE_TILE_URL`; resolved at build time via `getTileUrl()` (`src/features/flights/types.ts`) with `DEFAULT_SATELLITE_TILE_URL` fallback. Cycle 2 / AZ-498. |
| Satellite tile provider URL (mission-planner) | `mission-planner/.env.example` declares its own independent `VITE_SATELLITE_TILE_URL` | mission-planner only; not deployed |
| OpenWeatherMap API key + base URL (main SPA) | `.env.example` declares `VITE_OWM_API_KEY` + `VITE_OWM_BASE_URL`; resolved by `getOwmBaseUrl()` and the `flightPlanUtils.ts` builder. Closed AZ-448 / AZ-449 (no longer hardcoded). |
| OpenWeatherMap API key + base URL (mission-planner) | `mission-planner/.env.example` declares `VITE_OWM_API_KEY` + `VITE_OWM_BASE_URL`; `WeatherService.getWeatherData(lat, lon)` returns `null` and issues NO outbound `fetch` when the key is unset (fail-soft). Closed cycle 2 / AZ-499. The previously-committed literal value MUST be revoked at the OWM dashboard (manual deliverable — AC-42 / AZ-499 AC-7); `STC-SEC1C` defends against re-introduction. |
| `AZAION_REVISION` | Stamped into image at build time | For diagnostics |
## 3. Why no `.env`
## 3. `.env` strategy
The workspace `.env.example` is **absent** today. The `README.md` "Local development" section explicitly notes this as a Step 4 testability fix.
Step 4 testability + cycle 2 added a workspace `.env.example` (resolved by Vite at build time via `import.meta.env.VITE_*`). Today it declares: `VITE_OWM_API_KEY`, `VITE_OWM_BASE_URL` (AZ-448 / AZ-449), and `VITE_SATELLITE_TILE_URL` (AZ-498). `mission-planner/.env.example` mirrors the OWM pair (AZ-499) and keeps its own independent `VITE_SATELLITE_TILE_URL`.
**Trade-off**: avoiding a build-time env injection means `dist/` is identical across environments, which is great for promotability (the same image flows dev → stage → prod). The cost: the OpenWeatherMap key (and any future runtime config) cannot be changed without a rebuild.
**Trade-off**: Vite resolves `import.meta.env.VITE_*` at build time, so `dist/` is environment-specific once a non-empty `VITE_OWM_API_KEY` is baked in — the OpenWeatherMap key (and any future build-time config) cannot be changed without a rebuild. This trades promotability for the air-gap-friendly pattern that lets a deploy ship with `VITE_OWM_API_KEY=""` (no OWM call, fail-soft `null` return) when the deployment must not touch the internet.
**Future direction** (Step 4 / Step 5):
- Move the OpenWeatherMap call server-side (`flights/` service) — eliminates the bundled key entirely.
- Introduce a runtime `/config.json` that nginx serves — lets ops change feature flags / tile URLs without rebuilding.
- OR keep the static bundle and use Vite's `define` for build-time injection of safe-to-publish values (no secrets).
**Future direction** (still open):
- Move the OpenWeatherMap call server-side (`flights/` service) — would eliminate the bundled key entirely; the env-var hardening in cycle 2 reduces the urgency but does not remove the option.
- Introduce a runtime `/config.json` that nginx serves — would let ops change feature flags / tile URLs without rebuilding.
- OR keep the static bundle and continue using Vite's `import.meta.env` for build-time injection of safe-to-publish values (current approach).
## 4. Promotability
@@ -48,4 +50,4 @@ In practice: branch separation is the gating mechanism. Once dev → stage → m
- **`bun.lock`**: committed (per `package.json`'s `packageManager` field). `package-lock.json` is gitignored.
- **`.idea/`, `.claude/`, `.superpowers/`**: gitignored — IDE / agent metadata.
- **Playwright entries in `.gitignore`**: present but aspirational — Playwright is not installed (Step 57 territory).
- **mission-planner**: has its own `.env.example` declaring `VITE_SATELLITE_TILE_URL` and runs as a sibling Vite app. Not bundled into the deployed image.
- **mission-planner**: has its own `.env.example` declaring `VITE_SATELLITE_TILE_URL` and (cycle 2 / AZ-499) `VITE_OWM_API_KEY` + `VITE_OWM_BASE_URL`. Runs as a sibling Vite app; not bundled into the deployed image (per AC-31 / NFT-RES-LIM-04).
+4 -4
View File
@@ -35,7 +35,7 @@ mission-planner/src/
├── services/
│ ├── calculateDistance.ts Haversine + plane climb/cruise/descend
│ ├── AircraftService.ts mockGetAirplaneParams (returns hardcoded fixed-wing)
│ ├── WeatherService.ts OpenWeatherMap fetch
│ ├── WeatherService.ts OpenWeatherMap fetch (env-vars: VITE_OWM_API_KEY + VITE_OWM_BASE_URL; fail-soft `null` when key unset, AZ-499)
│ └── calculateBatteryUsage.ts Drag + thrust lookup; same algorithm as src/features/flights/flightPlanUtils.calculateBatteryPercentUsed
├── icons/
│ ├── MapIcons.tsx Leaflet icon factories
@@ -82,10 +82,10 @@ The React 19 port translates module-for-module wherever possible. Status as of t
| `flightPlanning/Aircraft.ts` | (no equivalent) | Aircraft is server-side; the SPA fetches `/api/flights/aircrafts`. |
| `services/calculateDistance.ts` | `flightPlanUtils.calculateDistance` | Ported. |
| `services/calculateBatteryUsage.ts` | `flightPlanUtils.calculateBatteryPercentUsed` + `calculateAllPoints` | Ported. |
| `services/WeatherService.ts` | `flightPlanUtils.getWeatherData` | Ported (with the same hardcoded API key — Step 4 fix). |
| `services/WeatherService.ts` | `flightPlanUtils.getWeatherData` | Ported. Env-vars `VITE_OWM_API_KEY` + `VITE_OWM_BASE_URL` since AZ-499 (mirrors AZ-448 / AZ-449); same fail-soft `null` contract. |
| `services/AircraftService.ts` | `flightPlanUtils.getMockAircraftParams` (mock only) | Real fetch is `/api/flights/aircrafts` in `FlightsPage`. |
| `constants/translations.ts` + `LanguageContext.tsx` | `src/i18n/{en,ua}.json` + `i18n/i18n.ts` | Migrated to i18next. |
| `constants/{actionModes,maptypes,tileUrls,purposes,languages}.ts` | `features/flights/types.ts` (`PURPOSES`, `TILE_URLS`, `ActionMode`) | Consolidated into one file. |
| `constants/{actionModes,maptypes,tileUrls,purposes,languages}.ts` | `features/flights/types.ts` (`PURPOSES`, `TILE_URL`, `ActionMode`) | Consolidated into one file. `TILE_URL` collapsed from the prior classic/satellite pair to a single self-hosted satellite URL by AZ-498. |
| `icons/{MapIcons,PointIcons,SidebarIcons,PhoneIcon}.tsx` | `features/flights/mapIcons.ts` | Only the marker icons survived; SidebarIcons + PhoneIcon dropped (no rotate-phone overlay in the SPA today). |
| `utils.ts` (`newGuid`) | `flightPlanUtils.newGuid` | Ported. |
| `config.ts` | `features/flights/types.COORDINATE_PRECISION` | Single constant migrated. |
@@ -98,7 +98,7 @@ The React 19 port translates module-for-module wherever possible. Status as of t
- **Rotate-phone overlay** (`icons/PhoneIcon.tsx`): MP shows a rotate-phone hint when held in portrait. The SPA does not.
- **Per-purpose marker icons** (`icons/PointIcons.tsx`): MP draws a different marker per `meta` purpose. The SPA uses three colour-coded icons (start / mid / end).
- **`Aircraft.ts` helper class**: never used in the SPA — aircraft state is fetched and treated as a plain DTO.
- **OpenWeather call directly from `WeatherService.ts`**: same flaw as the SPA port (hardcoded key, no proxy). Both flagged for Step 4.
- **OpenWeather call directly from `WeatherService.ts`**: same flaw as the SPA port (no proxy). Hardcoded key fixed by AZ-499 (env-vars + fail-soft); proxy story still owned by the broader F1 mission-planner deduplication track.
- **MUI 5**: MP uses MUI for dialogs / inputs / icons. The SPA replaced everything with hand-rolled Tailwind components matching `_docs/ui_design/README.md`. MUI is not a dep of the workspace.
## Findings carried into Step 4 / 6 / 8
@@ -17,7 +17,7 @@ Currently handles only the planning surface; the gps-denied orthophoto upload /
| Module | Layer | Responsibility |
|---|---|---|
| `types.ts` | leaf | All flight-feature-only types (`FlightPoint`, `CalculatedPointInfo`, `MapRectangle`, `WindParams`, `AircraftParams`, `MovingPointInfo`, `ActionMode`), plus tile URLs (`TILE_URLS`), `PURPOSES` (`tank` / `artillery`), and `COORDINATE_PRECISION = 8`. |
| `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. |
@@ -30,7 +30,7 @@ Currently handles only the planning surface; the gps-denied orthophoto upload /
| `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, satellite/classic toggle. |
| `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)
@@ -39,7 +39,7 @@ Currently handles only the planning surface; the gps-denied orthophoto upload /
- **`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 URLs**: classic OSM and an Esri ArcGIS `World_Imagery` (in `types.ts`). Both are direct upstream — neither goes through the suite `satellite-provider/` proxy. See Findings.
- **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
@@ -52,8 +52,7 @@ Currently handles only the planning surface; the gps-denied orthophoto upload /
| `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. |
| `tile.openstreetmap.org` (`TILE_URLS.classic`) | `FlightMap`, `MiniMap` | egress | Direct, no proxy. |
| `server.arcgisonline.com/.../World_Imagery` (`TILE_URLS.satellite`) | `FlightMap`, `MiniMap` | egress | Direct, no proxy. |
| `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). |
@@ -67,7 +66,7 @@ These are the real findings; the per-module rationale is in git history of the d
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/` proxy** (Esri tiles direct from `server.arcgisonline.com`). Possible licence + rate-limit concern. Step 4 + `architecture.md`.
7. ~~**`FlightMap` and `MiniMap` bypass the suite `satellite-provider/` proxy**~~**resolved 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.
+65
View File
@@ -0,0 +1,65 @@
# Documentation Ripple Log — Cycle 2 (Phase B)
> Generated during Step 13 (Update Docs) of the autodev existing-code flow, cycle 2.
> Task specs in scope: `AZ-498_satellite_tile_swap.md`, `AZ-499_mission_planner_weather_env.md` (both in `_docs/02_tasks/done/`).
> Implementation: single batch (`_docs/03_implementation/batch_11_report.md`).
## Scope analysis (Task Step 0)
Direct source files changed by Cycle 2 batch 11:
| Source file | Changed in | Touched module / component / system doc |
|---|---|---|
| `src/features/flights/types.ts` | AZ-498 (replaced `TILE_URLS` with `getTileUrl()` + `DEFAULT_SATELLITE_TILE_URL`) | `modules/src__features__flights.md` (updated by implementer at batch-11 commit time) |
| `src/features/flights/FlightMap.tsx` | AZ-498 (drop `mapType` state + toggle button + `MiniMap mapType` prop; single `<TileLayer crossOrigin="use-credentials">`) | same group doc as above |
| `src/features/flights/MiniMap.tsx` | AZ-498 (drop `mapType` prop) | same group doc |
| `src/vite-env.d.ts` | AZ-498 (replaced `VITE_OSM_TILE_URL` / `VITE_ESRI_TILE_URL` with `VITE_SATELLITE_TILE_URL`) | covered in `modules/src__features__flights.md` Tile URL section + `deployment/environment_strategy.md` (this run) |
| `.env.example` | AZ-498 | `deployment/environment_strategy.md` §2 (this run) |
| `src/i18n/en.json`, `src/i18n/ua.json` | AZ-498 (removed `flights.planner.satellite` key in lockstep — STC-FP22 parity preserved) | no module doc change needed (i18n parity is enforced by static check, not described in module docs) |
| `mission-planner/src/services/WeatherService.ts` | AZ-499 (env vars + fail-soft `null` when key unset) | `modules/mission-planner.md` (updated by implementer at batch-11 commit time) |
| `mission-planner/.env.example` | AZ-499 | same group doc + `deployment/environment_strategy.md` (this run) |
| `mission-planner/src/vite-env.d.ts` | AZ-499 | same group doc |
| `tests/security/banned-deps.json` | AZ-499 (added `owm_key_in_source` kind) | `tests/security-tests.md` NFT-SEC-09 step 3 (Step 12 cycle-update) |
| `scripts/check-banned-deps.mjs` | AZ-499 (extended source-tree dispatch) | static-check infrastructure — covered by AZ-482 module doc (no new entry needed; same dispatch shape) |
| `scripts/run-tests.sh` | AZ-499 (added `STC-SEC1C` row) | `tests/environment.md` Test Execution + `tests/security-tests.md` NFT-SEC-09 (Step 12) |
| `e2e/docker-compose.suite-e2e.yml` | AZ-498 (replaced dead `VITE_TILE_BASE_URL` with `VITE_SATELLITE_TILE_URL`) | `tests/environment.md` (Step 12) |
| `e2e/stubs/tile/server.ts` | AZ-498 (rewrote `classify()` for `/tiles/{z}/{x}/{y}` shape) | `tests/environment.md` (Step 12) |
| `e2e/tests/infrastructure.e2e.ts` | AZ-498 (AC-2 rewritten; OSM removed from `EXTERNAL_HOSTS`) | `tests/blackbox-tests.md` FT-P-59 (Step 12) |
| `tests/msw/handlers/tiles.ts` | AZ-498 (rewrote handlers from OSM/Esri `.png` to `/tiles/{z}/{x}/{y}` with cookie-auth headers) | covered by FT-P-57 / FT-P-59 (Step 12) |
System-level docs (`architecture.md`, `system-flows.md`, `deployment/environment_strategy.md`): **architecture.md + environment_strategy.md TOUCHED this run**; `system-flows.md` not touched (no flow diagrams referenced map tiles or OWM). The architectural changes are: external-integration table (OSM/Esri removed from outbound; suite-internal `satellite-provider` added), system-boundaries table (tile providers row updated), § 5 External Integrations (failure-mode column updated for satellite tiles + OWM), Air-gap section in § 2 (tiles no longer external; OWM remains external but env-resolved + fail-soft).
Problem-level docs: **acceptance_criteria.md TOUCHED this run** — added AC-41 (self-hosted satellite tiles + cookie auth) and AC-42 (mission-planner OWM env hardening + STC-SEC1C); updated AC-20 row to reference the closure tasks; updated Coverage status section to move AC-20 from "Currently violated" to "Currently met & enforced" and add AC-41 / AC-42 there as well. `restrictions.md` not touched (the air-gap restriction E1 is now better satisfied for tiles, but the restriction text itself does not change).
Contract docs: `_docs/02_document/contracts/satellite-provider/tiles.md` was drafted in Step 9 (New Task) and updated by the implementer to reference AZ-498 in the `Consumer tasks` field — no further edit this run.
## Import-graph ripple (Task Step 0.5)
The reverse-dependency set of the changed files is small and is **already captured in the direct list above** plus the test-spec / system-level updates from this run. Specifically:
- `src/features/flights/types.ts` exports `getTileUrl()` + `DEFAULT_SATELLITE_TILE_URL` (cycle 2) plus the existing waypoint / mission JSON shapes. Importers: `FlightMap.tsx`, `MiniMap.tsx` (both directly in scope), and the new fast test `src/features/flights/__tests__/satellite_tile.test.tsx`. No additional consumer needs a doc refresh — `FlightsPage.tsx` consumes `FlightMap` / `MiniMap` as JSX components without referencing the tile URL plumbing.
- `src/features/flights/FlightMap.tsx` is imported by `FlightsPage.tsx` (which composes the page); the public prop surface of `FlightMap` is unchanged on tile-related axes (no exported tile constants, no `mapType` exposure to callers). FlightsPage's module-doc section (`modules/src__features__flights.md`) already reflects the change because the implementer updated the group doc at batch-11 commit time.
- `src/features/flights/MiniMap.tsx` lost a public prop (`mapType`) — this IS a public surface change. Callers: only `FlightMap.tsx` (intra-component); no external caller. The change was applied in lockstep in the same batch, so there is no "stale caller" to chase.
- `mission-planner/src/services/WeatherService.ts` keeps its public `getWeatherData(lat, lon)` signature; only the internal env-var resolution + fail-soft branch changed. Callers in `mission-planner/` (page-level components in the legacy port-source) see no behavior change beyond `null` returned when the key is unset — already documented under `modules/mission-planner.md` Migration Notes.
Therefore: **no additional doc was added to the refresh set by ripple analysis** beyond the system-level docs already updated for cycle-wide concerns (architecture.md external integrations + environment_strategy.md env-var matrix).
## Tooling notes
- Ripple analysis was performed by reading the implementer's `_docs/03_implementation/batch_11_report.md` (which enumerates every modified file with rationale), then cross-checking each changed file's importers via `Grep` against `src/features/flights/` and `mission-planner/`. The repo has no `madge` / `depcruise` configured; this counts as the "directory-proximity + manual import inspection" fallback path from `document/workflows/task.md` Task Step 0.5 #6 — full coverage was achievable because the changed file set is small and bounded by two well-known package roots (`src/features/flights/` and `mission-planner/src/services/`).
- No static analyzer was used to discover indirect importers. None was needed: the public-surface changes are minimal (one prop drop on `MiniMap`, one preserved-signature env-resolution change on `getWeatherData`, one new function on `types.ts` replacing a removed const), and all in-tree callers were updated in the same batch.
## Outcome
Cycle-2 documentation refresh complete. Updated this run:
| Level | Doc | Reason |
|---|---|---|
| System-level | `_docs/02_document/architecture.md` | Removed stale OSM/Esri tile entries; added suite-internal `satellite-provider` row; updated External Integrations failure-mode for tiles + OWM; corrected stale "hardcoded API key" claim. |
| System-level | `_docs/02_document/deployment/environment_strategy.md` | Added env-var matrix rows for `VITE_SATELLITE_TILE_URL` (main SPA + mission-planner) and `VITE_OWM_API_KEY` / `VITE_OWM_BASE_URL` (main SPA + mission-planner); updated tile-providers column for all three envs; updated `.env` strategy section to reflect cycle-2 reality. |
| Component | `_docs/02_document/components/05_flights/description.md` | Removed stale "hardcoded API key" claim from the legacy mission-planner port-source comparison (line 59). |
| Problem | `_docs/00_problem/acceptance_criteria.md` | Added AC-41 (satellite tiles + cookie auth + toggle removal) and AC-42 (mission-planner OWM env hardening + STC-SEC1C); reworded AC-20; updated Coverage status. |
Module-level docs (`modules/src__features__flights.md`, `modules/mission-planner.md`) and the contract doc (`contracts/satellite-provider/tiles.md`) were already updated by the implementer at batch-11 commit time and verified consistent with the source tree at the start of this run; no additional change applied.
Test-spec docs (`tests/blackbox-tests.md`, `tests/security-tests.md`, `tests/resilience-tests.md`, `tests/environment.md`, `tests/traceability-matrix.md`) were updated in the preceding Step 12 (Test-Spec Sync) cycle-update — see the Step 12 commit for those changes.
+179
View File
@@ -1470,6 +1470,185 @@ Every test is observed at the SPA's public surface — DOM, ARIA, outbound netwo
---
### FT-N-16: mission-planner `getWeatherData` fail-soft when `VITE_OWM_API_KEY` is unset
**Traces to**: AC-42 (AZ-499 AC-3)
**Profile**: fast
**Input data**: build-time env with `VITE_OWM_API_KEY=""` (or undefined).
**Steps**:
| Step | Consumer Action | Expected System Response |
|------|----------------|------------------------|
| 1 | Spy `globalThis.fetch` | spy installed, no calls yet |
| 2 | Stub `import.meta.env.VITE_OWM_API_KEY = ""` and invoke `getWeatherData(50, 30)` | resolves |
| 3 | Inspect return value | `=== null` |
| 4 | Inspect fetch spy | `mock.calls.length === 0` |
**Pass criteria**: function returns `null` AND no outbound HTTP request is made when the API key is unset. Mirrors the AZ-448 fail-soft contract on the main SPA.
**Max execution time**: 1s (env stub + sync inspection only).
**Expected result source**: AZ-499 AC-3 (no `results_report.md` row needed — behavioral test, no input data).
---
## Cycle 2 Additions (Phase B Cycle 2 — Self-hosted satellite tiles + mission-planner OWM hardening)
The scenarios below were appended via `/test-spec` cycle-update mode after Phase B Cycle 2 completed (AZ-498 + AZ-499, batch_11). They use the same template shapes as the original spec. Cross-references: AC-41 (satellite tiles), AC-42 (mission-planner OWM env hardening) are the new global ACs added to `traceability-matrix.md`; the underlying task-spec ACs are AZ-498 AC-1..AC-7, AC-9 and AZ-499 AC-1..AC-6 (AZ-498 AC-8 was dropped with explicit user approval per `_docs/03_implementation/batch_11_report.md`; AZ-499 AC-7 is a manual deliverable, not a test).
### FT-P-56: Self-hosted satellite tile URL is env-var resolved
**Traces to**: AC-41 (AZ-498 AC-1, AC-2)
**Profile**: fast
**Input data**: build-time env with `VITE_SATELLITE_TILE_URL` set, unset, or set with a trailing slash.
**Steps**:
| Step | Consumer Action | Expected System Response |
|------|----------------|------------------------|
| 1 | Stub `VITE_SATELLITE_TILE_URL=http://satellite-provider:5100/tiles/{z}/{x}/{y}` and call `getTileUrl()` | returns the env value verbatim |
| 2 | Stub `VITE_SATELLITE_TILE_URL=""` and call `getTileUrl()` | returns `DEFAULT_SATELLITE_TILE_URL` (`http://localhost:5100/tiles/{z}/{x}/{y}`) |
| 3 | Stub `VITE_SATELLITE_TILE_URL=http://satellite-provider:5100/tiles/{z}/{x}/{y}/` (trailing slash) | returns the value with the trailing slash stripped |
| 4 | Mount `<FlightMap>` with the env unset; inspect rendered `<TileLayer>` `data-tile-url` | equals `DEFAULT_SATELLITE_TILE_URL` |
**Pass criteria**: all four assertions hold. Mirrors the established `getOwmBaseUrl()` / `getApiBase()` env-resolution pattern.
**Max execution time**: 2s (jsdom render + four stub variations).
**Expected result source**: AZ-498 AC-1, AC-2 (no `results_report.md` row needed — env-var plumbing, no input data fixture).
---
### FT-P-57: `<TileLayer crossOrigin="use-credentials">` enables cookie-auth on tile fetches
**Traces to**: AC-41 (AZ-498 AC-3); E1 (air-gap-friendly bundle); RID R-Reliability for tile auth
**Profile**: fast
**Input data**: `<FlightMap>` and `<MiniMap>` mounted with the default tile URL.
**Steps**:
| Step | Consumer Action | Expected System Response |
|------|----------------|------------------------|
| 1 | Mount `<FlightMap>`; inspect rendered `<TileLayer>` `data-cross-origin` attribute | `=== "use-credentials"` |
| 2 | Mount `<MiniMap pointPosition={…}>`; inspect rendered `<TileLayer>` `data-cross-origin` attribute | `=== "use-credentials"` |
| 3 | (e2e — gated) Issue `GET <VITE_SATELLITE_TILE_URL substituted with /tiles/1/0/0>` from the rendered map; inspect outbound request | `request.credentials === "include"` (browser attaches the same-origin auth cookie) |
**Pass criteria**: every `<TileLayer>` the SPA renders carries `crossOrigin="use-credentials"` so the browser sends the satellite-provider cookie on same-origin tile requests. Step 3 e2e is gated by the cross-workspace satellite-provider cookie-auth ticket landing (Step 16 deploy gate).
**Max execution time**: 2s for steps 1+2 (fast); e2e step is part of `infrastructure.e2e.ts` — bounded by suite-e2e timeout.
**Expected result source**: AZ-498 AC-3 (no `results_report.md` row — DOM-attribute observable).
---
### FT-P-58: Classic/satellite map toggle, `mapType` state, and `MiniMap.Props.mapType` are removed
**Traces to**: AC-41 (AZ-498 AC-4)
**Profile**: fast
**Input data**: `<FlightMap>` mounted with the default tile URL; `<MiniMap>` mounted with only `pointPosition` (no `mapType` prop).
**Steps**:
| Step | Consumer Action | Expected System Response |
|------|----------------|------------------------|
| 1 | Mount `<FlightMap>`; query `screen.queryByRole('button', { name: /satellite|classic/i })` | returns `null` |
| 2 | Mount `<FlightMap>`; query `screen.getAllByTestId('tile-layer')` | length `=== 1` (no per-mode branching, single layer) |
| 3 | Compile-time check: instantiate `<MiniMap pointPosition={…}>` without `mapType` | TypeScript `tsc --noEmit -p tsconfig.test.json` succeeds (STC-T1) |
| 4 | Compile-time check: source-tree grep for any remaining `mapType` reference under `src/features/flights/` | zero hits (compilation error if not — covered by STC-T1) |
**Pass criteria**: no toggle button, no `mapType` state, `MiniMap.Props` has no `mapType`. Removal is permanent; the `flights.planner.satellite` i18n key was removed from both `en.json` and `ua.json` in lockstep (i18n key parity preserved via STC-FP22).
**Max execution time**: 2s (jsdom render + grep).
**Expected result source**: AZ-498 AC-4.
---
### FT-P-59: e2e harness exercises the new `/tiles/{z}/{x}/{y}` path
**Traces to**: AC-41 (AZ-498 AC-6); E1 (air-gap)
**Profile**: e2e
**Input data**: suite-e2e compose stack up; `tile-stub` configured at `http://tile-stub:8082/tiles/{z}/{x}/{y}`.
**Steps**:
| Step | Consumer Action | Expected System Response |
|------|----------------|------------------------|
| 1 | `infrastructure.e2e.ts` AC-2 — issue `GET http://tile-stub:8082/tiles/1/0/0` from the playwright runner | HTTP 200, response is a 256×256 image (JPEG) |
| 2 | Inspect response headers | `Content-Type: image/jpeg`, `Cache-Control` present, `ETag` present |
| 3 | Inspect outbound request from the SPA's `<TileLayer>` | URL matches `^http://tile-stub:8082/tiles/\d+/\d+/\d+$` (NOT `/{z}/{x}/{y}.png`, NOT the legacy `/sat/...` Esri shape) |
| 4 | Inspect `EXTERNAL_HOSTS` route guard | OSM and Esri hosts are NOT in the allow-list (removed during cycle 2 cleanup) |
**Pass criteria**: tile fetch shape matches the satellite-provider contract documented at `_docs/02_document/contracts/satellite-provider/tiles.md`. Note: the same-origin cookie-auth path (cookie attached on the actual fetch) is verified once the cross-workspace satellite-provider cookie-auth ticket lands; until then, the e2e profile uses the `tile-stub` which accepts requests without a cookie.
**Max execution time**: bounded by suite-e2e infrastructure-test timeout (per `e2e/tests/infrastructure.e2e.ts`).
**Expected result source**: contract at `_docs/02_document/contracts/satellite-provider/tiles.md` v1.0.0; AZ-498 AC-6.
---
### FT-P-60: mission-planner `getWeatherData` uses env-resolved key + base URL
**Traces to**: AC-42 (AZ-499 AC-1, AC-2, AC-4)
**Profile**: fast
**Input data**: build-time env with `VITE_OWM_API_KEY` set + `VITE_OWM_BASE_URL` either set, unset, or set with a trailing slash.
**Steps**:
| Step | Consumer Action | Expected System Response |
|------|----------------|------------------------|
| 1 | Spy `globalThis.fetch` returning a 200 OK with body `{ wind: { speed: 5, deg: 90 } }` | spy installed |
| 2 | Stub `VITE_OWM_API_KEY=abc123` + `VITE_OWM_BASE_URL=""`; invoke `getWeatherData(50, 30)` | outbound URL contains `appid=abc123` AND `units=metric` |
| 3 | Stub `VITE_OWM_API_KEY=abc123` + `VITE_OWM_BASE_URL=https://example.test/data/2.5`; invoke `getWeatherData(50, 30)` | outbound URL starts with `https://example.test/data/2.5/weather?` |
| 4 | Stub `VITE_OWM_API_KEY=abc123` + `VITE_OWM_BASE_URL=https://example.test/data/2.5/` (trailing slash); invoke `getWeatherData(50, 30)` | outbound URL starts with `https://example.test/data/2.5/weather?` (slash stripped) |
| 5 | Stub `VITE_OWM_API_KEY=abc123` + `VITE_OWM_BASE_URL=""`; invoke `getWeatherData(50, 30)` | outbound URL starts with `https://api.openweathermap.org/data/2.5/weather?` (default base) |
| 6 | Inspect return value on a successful fetch | `=== { windSpeed: 5, windAngle: 90 }` (existing parsed-wind shape preserved) |
**Pass criteria**: every outbound URL is reconstructed from env vars; the public `getWeatherData(lat, lon)` signature and `WeatherData` return shape are unchanged. Pairs with the AZ-499 NFR-Compatibility constraint.
**Max execution time**: 2s (env stubs + fetch-spy assertions; no real network).
**Expected result source**: AZ-499 AC-1, AC-2, AC-4 (no `results_report.md` row — env-var plumbing).
---
### FT-P-61: mission-planner `geocodeAddress` uses env-resolved Google API key
**Traces to**: AC-43 (AZ-501 AC-1)
**Profile**: fast
**Input data**: build-time env with `VITE_GOOGLE_GEOCODE_KEY` set to a placeholder string.
**Steps**:
| Step | Consumer Action | Expected System Response |
|------|----------------|------------------------|
| 1 | Spy `globalThis.fetch` returning a 200 OK with body `{ status: 'OK', results: [{ geometry: { location: { lat, lng } } }] }` | spy installed |
| 2 | Stub `VITE_GOOGLE_GEOCODE_KEY=env-key-xyz`; invoke `geocodeAddress('Kyiv, Ukraine')` | outbound URL contains `key=env-key-xyz` AND `address=Kyiv%2C%20Ukraine` |
| 3 | Inspect return value | `=== { lat, lng }` from the mocked response |
**Pass criteria**: the outbound URL is reconstructed from the env var; no literal key remains in `mission-planner/src/services/GeocodeService.ts` (defense-in-depth confirmed by STC-SEC1D / NFT-SEC-09b).
**Max execution time**: 2s.
**Expected result source**: AZ-501 AC-1.
---
### FT-N-17: mission-planner `geocodeAddress` fail-soft when `VITE_GOOGLE_GEOCODE_KEY` is unset
**Traces to**: AC-43 (AZ-501 AC-3)
**Profile**: fast
**Input data**: build-time env with `VITE_GOOGLE_GEOCODE_KEY` empty / undefined.
**Steps**:
| Step | Consumer Action | Expected System Response |
|------|----------------|------------------------|
| 1 | Spy `globalThis.fetch`; spy `console.warn` | spies installed |
| 2 | Stub `VITE_GOOGLE_GEOCODE_KEY=''`; invoke `geocodeAddress('anywhere')` | returns `null`; fetch is NOT called; `console.warn` called exactly once with a message containing `VITE_GOOGLE_GEOCODE_KEY` |
| 3 | Stub `VITE_GOOGLE_GEOCODE_KEY=env-key-xyz` and force `fetch` to reject with `Error('boom')`; invoke `geocodeAddress('anywhere')` | returns `null`; promise does NOT throw |
**Pass criteria**: missing-key path is silent-but-warned and never throws; network-error path is silent and never throws — preserves the LeftBoard address-box UX of "Enter does nothing if address is unresolvable".
**Max execution time**: 2s.
**Expected result source**: AZ-501 AC-3.
---
## Notes carried into Phase 3
- All tests tagged `quarantined` correspond to features either pending a Step 4 fix (e.g., AC-13 i18n detector, AC-21 panel persistence, AC-22 role-gate, AC-26/27 form hygiene, AC-39 split surface, AC-40 tile zoom) or pending Phase B implementation (AC-11 bundle gate, AC-24 SSE refresh, AC-25 async video, AC-40 tile zoom). The test is written so it activates the day the implementation lands; Phase 3 will surface them for downgrade or accept.
+5 -5
View File
@@ -35,14 +35,14 @@ The Azaion UI image carries no DB. The "Docker environment" is the test-time cho
| `detect` | Suite `detect/` image | Sync image detect (and future async video detect F7) | per suite compose |
| `gps-denied-desktop`, `gps-denied-onboard`, `autopilot`, `resource`, `loader` | Suite microservice images | Auxiliary services hit by the SPA (only `loader/` and `resource/` are hit on production paths today; `gps-denied-*` is target-only F12) | per suite compose |
| `owm-stub` | Tiny HTTP server returning canned OpenWeatherMap responses | Replace direct OWM HTTPS (E10) so tests are deterministic and rate-limit-free | `8081` |
| `tile-stub` | Tiny HTTP server returning a 256x256 PNG | Replace OSM tile servers | `8082` |
| `tile-stub` | Tiny HTTP server serving `GET /tiles/{z}/{x}/{y}` → 256x256 JPEG with `Content-Type: image/jpeg`, `Cache-Control`, and `ETag` headers (mirrors the satellite-provider contract at `_docs/02_document/contracts/satellite-provider/tiles.md`) | Replace the suite's `satellite-provider` tile endpoint in the e2e profile (since cycle 2 / AZ-498). The stub does NOT enforce cookie auth — the same-origin cookie path is exercised once the cross-workspace satellite-provider cookie-auth ticket lands and tile traffic flows through the real service. | `8082` |
| `test-db` | Suite-managed (Postgres per suite default) | Backs `admin/`, `flights/`, `annotations/` | Internal |
### Networks
| Network | Services | Purpose |
|---------|----------|---------|
| `azaion-test-net` | all of the above | Isolated test network; no internet egress (OWM + tile stubs replace the only external hops). |
| `azaion-test-net` | all of the above | Isolated test network; no internet egress (`owm-stub` + `tile-stub` replace the only external hops — OWM HTTPS, and since cycle 2 / AZ-498 the suite's own `satellite-provider /tiles/{z}/{x}/{y}` endpoint stands in for the previously-used external OSM/Esri tile servers). |
### Volumes
@@ -92,7 +92,7 @@ services:
environment:
BASE_URL: http://azaion-ui:80
OWM_BASE_URL: http://owm-stub:8081
TILE_BASE_URL: http://tile-stub:8082
VITE_SATELLITE_TILE_URL: "http://tile-stub:8082/tiles/{z}/{x}/{y}"
```
The compose file is part of the test-spec output; its concrete shape lands when the Decompose Tests step picks the runner (Step 5).
@@ -129,7 +129,7 @@ The compose file is part of the test-spec output; its concrete shape lands when
| Suite SSE | HTTPS | `/api/flights/<id>/live-gps`, `/api/annotations/annotations/events`, `/api/detect/stream/<jobId>` (F7 target) | bearer in `?token=` per ADR-008 |
| Bundle / image inspection | filesystem / `docker inspect` | n/a | n/a |
| OpenWeatherMap | HTTPS via `owm-stub` | per stub | none |
| OSM tiles | HTTPS via `tile-stub` | per stub | none |
| Satellite tiles | HTTPS via `tile-stub` (replacing the suite's own `satellite-provider /tiles/{z}/{x}/{y}` endpoint in the e2e profile) | per stub at `/tiles/{z}/{x}/{y}` | none in stub; production uses an HttpOnly same-origin cookie set by `admin/` (see `crossOrigin="use-credentials"` on every `<TileLayer>` per cycle 2 / AZ-498) |
### What the consumer does NOT have access to
@@ -192,7 +192,7 @@ Conclusion: classify as **Not hardware-dependent**. Docker headless Chromium rep
3. **Compose up**: `docker compose -f e2e/docker-compose.suite-e2e.yml up -d` — brings up `azaion-ui`, `admin`, `flights`, `annotations`, `detect`, the auxiliary services, `owm-stub`, `tile-stub`, `test-db`, and the `playwright-runner`.
4. **Run tests**: `docker compose -f e2e/docker-compose.suite-e2e.yml run --rm playwright-runner` — the runner image entrypoint is `bun run test:e2e`. Reports land in `./test-output/`.
5. **Tear down**: `docker compose -f e2e/docker-compose.suite-e2e.yml down -v` (volumes wiped between runs).
6. **Required environment**: `BASE_URL=http://azaion-ui:80`, `OWM_BASE_URL=http://owm-stub:8081`, `TILE_BASE_URL=http://tile-stub:8082`, `CI_COMMIT_SHA=<sha>` (stamped into `AZAION_REVISION`).
6. **Required environment**: `BASE_URL=http://azaion-ui:80`, `OWM_BASE_URL=http://owm-stub:8081`, `VITE_SATELLITE_TILE_URL=http://tile-stub:8082/tiles/{z}/{x}/{y}` (since cycle 2 / AZ-498 — was `TILE_BASE_URL=http://tile-stub:8082`), `CI_COMMIT_SHA=<sha>` (stamped into `AZAION_REVISION`).
#### Local mode (for `fast` profile + developer-machine `e2e` runs)
@@ -242,3 +242,35 @@ Failure / recovery scenarios at the SPA's observable boundary: bearer expiry, re
**Pass criteria**: row 97 — connection-lost indicator OR reconnect attempt within 10 s; stale data NOT rendered as live; reconnect attempts ≤ 1 in the 10 s window.
**Expected result source**: `results_report.md` row 97.
---
### NFT-RES-11: Tile endpoint 401/503 does NOT crash the map
**Summary**: When the `satellite-provider /tiles/{z}/{x}/{y}` endpoint returns 401 (cookie-auth failure) or 503 (Google Maps upstream down), the SPA renders a broken-tile placeholder for the failing tile(s) and the rest of the application keeps working. No React error boundary fires; no full-page crash.
**Traces to**: AC-41 (AZ-498 NFR-Reliability)
**Preconditions**:
- `<FlightMap>` mounted with a valid `VITE_SATELLITE_TILE_URL`.
- Tile endpoint configured to return 401 (auth failure) OR 503 (upstream provider down) for one or more tile coordinates.
**Fault injection**:
- (auth-failure variant) Strip / invalidate the satellite-provider auth cookie before the SPA attempts a tile fetch; tile endpoint responds 401.
- (upstream-down variant) Configure the test stub to return 503 for `GET /tiles/{z}/{x}/{y}`.
**Steps**:
| Step | Action | Expected Behavior |
|------|--------|------------------|
| 1 | Mount `<FlightMap>`; trigger a tile load that fails per the fault | Leaflet emits a `tileerror` event for the affected coordinate |
| 2 | Observe the rendered map | broken-tile placeholder shown in the failing cell; surrounding tiles continue rendering normally |
| 3 | Observe the rest of the SPA (header, side panels, navigation) | remains interactive; no React error boundary fires; no console error of category `Uncaught` |
| 4 | Observe a recovery path (auth restored OR upstream back) | next pan/zoom successfully fetches the tile; the placeholder is replaced with the imagery |
**Pass criteria**:
- 401 response on a tile request MUST NOT crash the map; broken-tile placeholder rendered in the failing cell, rest of SPA interactive.
- 503 response treated identically to 404/transient failure (fault budget — recovery path works after the upstream returns).
- No new uncaught error in the console attributable to the failed tile.
**Expected result source**: AZ-498 NFR-Reliability (no `results_report.md` row needed — observable through DOM state and console).
**Note on follow-up**: AZ-498 risk #5 flags an optional `tileerror` listener on `<MapContainer>` that surfaces a structured warning + an optional inline banner ("Imagery unavailable; please re-sign-in"). If/when that lands, this scenario gains a Step 5 asserting the banner appears within 2 s of the first tile error.
+28 -7
View File
@@ -145,20 +145,41 @@ Blackbox security assertions against the SPA's observable surface: token storage
### NFT-SEC-09: OpenWeatherMap API key is not shipped in source or bundle
**Traces to**: AC-20, P10
**Traces to**: AC-20, AC-42 (AZ-499 AC-5, AC-7), P10
**Profile**: static (source) + static (bundle)
**Steps**:
| Step | Consumer Action | Expected Response |
|------|----------------|------------------|
| 1 | Regex sweep `src/` and `mission-planner/src/` for the literal current OWM key value | `match_count == 0` (row 63) |
| 2 | Regex sweep for `appid=` and `api_key=` literal occurrences in source URLs | `match_count == 0` (row 63) |
| 3 | Scan `dist/**/*.js` post-build for the literal key | `match_count == 0` (Phase 3 may downgrade to "until Step 4 fix") |
| 1 | `STC-SEC1` Regex sweep `src/` for `appid=[a-zA-Z0-9]{6,}` (filtered to exclude `import.meta.env` / `process.env` references) | `match_count == 0` (row 63) |
| 2 | `STC-SEC1B` — Scan `dist/**/*.js` post-build for the literal key value | `match_count == 0` (NFT-SEC-09 AC-1 dist portion) |
| 3 | `STC-SEC1C` — Scan `src/` AND `mission-planner/` for the literal value of the previously-committed key (`335799082893fad97fa36118b131f919`); test files excluded; delegated to `node scripts/check-banned-deps.mjs --kind=owm_key_in_source` | `match_count == 0` (row 63 — AZ-499 AC-5) |
**Pass criteria**: row 63.
**Status**: `quarantined` for source check until Step 4 fix; the bundle-scan check passes immediately for `src/` (mission-planner not bundled, AC-31).
**Expected result source**: `results_report.md` row 63.
**Pass criteria**: row 63 (project-level AC-20) AND AZ-499 AC-5 (source scan must reject any future re-introduction of the literal key under `src/` or `mission-planner/`).
**Status**: All three checks ACTIVE (no quarantine). The source check was un-quarantined on cycle 2 close (2026-05-12) when AZ-499 (a) replaced the hardcoded key in `mission-planner/src/services/WeatherService.ts` with `import.meta.env.VITE_OWM_API_KEY` and (b) added `STC-SEC1C` so a regression cannot silently re-introduce the literal across either source tree (closing the AZ-482 source-scan gap that previously only checked `src/` for the regex shape and `dist/` for the literal — `mission-planner/` stays out of `dist/` per STC-S5, so the dist scan alone could not catch it).
**Defense-in-depth note**: the previously-committed key value (`335799082893fad97fa36118b131f919`) MUST be revoked at the OpenWeatherMap dashboard — this is AZ-499 AC-7, a manual deliverable, not a test. STC-SEC1C complements but does not replace key revocation.
**Expected result source**: `results_report.md` row 63; AZ-499 AC-5.
---
### NFT-SEC-09b: Google Geocode API key is not shipped in source
**Traces to**: AC-43 (AZ-501 AC-1, AC-4, AC-6)
**Profile**: static (source) + fast (env-resolution + fail-soft contract)
**Steps**:
| Step | Consumer Action | Expected Response |
|------|----------------|------------------|
| 1 | `STC-SEC1D` — Scan `src/` AND `mission-planner/` for the literal value of the previously-committed Google key (`AIzaSyAhvDeYukuyWVrQYbRhuv91bsi_jj5_Iys`); test files excluded; delegated to `node scripts/check-banned-deps.mjs --kind=google_key_in_source` | `match_count == 0` (AZ-501 AC-4) |
| 2 | Fast: import `mission-planner/src/services/GeocodeService.ts` and stub `import.meta.env.VITE_GOOGLE_GEOCODE_KEY`; assert outgoing fetch URL contains the env-resolved key | URL contains `key=<env-value>` (AZ-501 AC-1; `tests/mission_planner_geocode.test.ts`) |
| 3 | Fast: stub `VITE_GOOGLE_GEOCODE_KEY=''` and call `geocodeAddress('Kyiv')` | returns `null`, no fetch issued, single `console.warn` mentioning `VITE_GOOGLE_GEOCODE_KEY` (AZ-501 AC-3) |
**Pass criteria**: AZ-501 AC-1, AC-3, AC-4 — env-resolved + fail-soft + static gate against literal re-introduction.
**Status**: ACTIVE on cycle 2 close (2026-05-12). The key was extracted from `mission-planner/src/config.ts` to a new `services/GeocodeService.ts` module to enable isolated env-resolution + fail-soft testing (mirrors AZ-499 / WeatherService pattern).
**Defense-in-depth note**: the previously-committed key (`AIzaSyAhvDeYukuyWVrQYbRhuv91bsi_jj5_Iys`) MUST be revoked at the Google Cloud Console — this is AZ-501 AC-6, a manual deliverable, not a test. STC-SEC1D complements but does not replace key revocation.
**Expected result source**: AZ-501 AC-1, AC-3, AC-4.
---
@@ -25,7 +25,7 @@ Maps every acceptance criterion and every restriction in `_docs/00_problem/` to
| AC-17 | ProtectedRoute spinner a11y + timeout | FT-P-32, FT-P-33 [Q], NFT-RES-04 [Q] | 58, 59 | Covered (quarantined for timeout) |
| AC-18 | Browser support — Chromium + Firefox latest 2 | FT-P-34, NFT-PERF-10 | 60, 98 | Covered (manual smoke, no automated gate today) |
| AC-19 | Mobile / desktop breakpoint variants | FT-P-35, FT-P-36 | 61, 62 | Covered |
| AC-20 | OpenWeatherMap key not in source | NFT-SEC-09 [Q for source until Step 4] | 63 | Covered (quarantined for source check) |
| AC-20 | OpenWeatherMap key not in source | NFT-SEC-09 (all 3 steps active — source check un-quarantined on cycle 2 / 2026-05-12 by AZ-499) | 63 | Covered |
| AC-21 | UserSettings panel-width persistence | FT-P-37 [Q], FT-P-38 [Q], NFT-PERF-08 [Q] | 64, 65 | Covered (quarantined) |
| AC-22 | RBAC client-side route gates | FT-N-03 [Q], FT-N-04, FT-N-05 [Q], NFT-SEC-05 [Q], NFT-SEC-06 [Q], NFT-RES-08 | 08, 09, 10 | Covered (quarantined for `/admin` + `/settings` gates) |
| AC-23 | Auth refresh transparency | FT-P-02, FT-P-03, NFT-PERF-02, NFT-RES-01 | 11, 12 | Covered |
@@ -51,6 +51,10 @@ Maps every acceptance criterion and every restriction in `_docs/00_problem/` to
| AC-N3 | No offline mode | NFT-RES-03, NFT-SEC-12 | 93 | Covered |
| AC-N4 | No response-signature library | NFT-SEC-11 | 94 | Covered |
| AC-N5 | Dropped legacy features (Sound Detections, Drone Maintenance) | NFT-SEC-13 | 95 | Covered |
| AC-41 | Map tiles served by self-hosted `satellite-provider` via cookie auth; classic/satellite toggle removed (added cycle 2 / 2026-05-12, epic AZ-497, ticket AZ-498) | FT-P-56, FT-P-57, FT-P-58, FT-P-59, NFT-RES-11; STC-T1 (env-decl typecheck), STC-FP22 (i18n parity post-key removal), STC-ARCH-01 + STC-ARCH-02 (architecture gates stay green) | n/a — env-var plumbing + DOM observable + e2e contract; no `results_report.md` row required | Covered |
| AC-42 | mission-planner OpenWeatherMap key + base URL externalized via Vite env vars; fail-soft on missing key; STC-SEC1C source-tree literal scan defends against re-introduction (added cycle 2 / 2026-05-12, epic AZ-497, ticket AZ-499) | FT-P-60, FT-N-16; NFT-SEC-09 step 3 (STC-SEC1C); STC-T1 (env-decl typecheck) | 63 (literal-key scan shares row 63 with AC-20) | Covered (manual deliverable AZ-499 AC-7 — old key revocation at OWM dashboard — tracked separately, not a test) |
| AC-43 | mission-planner Google Geocode API key extracted to a new `services/GeocodeService.ts` module + externalized via Vite env var; fail-soft + console.warn on missing key; STC-SEC1D source-tree literal scan defends against re-introduction (added cycle 2 / 2026-05-12 from security audit `_docs/05_security/`, ticket AZ-501) | FT-P-61, FT-N-17; NFT-SEC-09b (STC-SEC1D); STC-T1 (env-decl typecheck) | n/a — env-var plumbing + console-warn assertion; no `results_report.md` row required | Covered (manual deliverable AZ-501 AC-6 — old key revocation at Google Cloud Console — tracked separately, not a test) |
| AC-44 | Vite + PostCSS upgraded past CVE-2026-39363 / GHSA-p9ff-h696-f583 / GHSA-4w7w-66w2-5vf9 / GHSA-qx2v-qp2m-jg93 in both roots via `package.json` `overrides` flooring transitive resolutions to safe versions (added cycle 2 / 2026-05-12 from security audit, ticket AZ-502) | `bun audit` (zero advisories in both roots after `bun install`) | n/a — supply-chain hygiene; verified by audit tool exit code | Covered (CI gate `bun audit --severity high` in `.woodpecker/build-arm.yml` is a Phase B follow-up — see `_docs/05_security/infrastructure_review.md` F-INF-1) |
## Restrictions Coverage
@@ -104,10 +108,10 @@ Maps every acceptance criterion and every restriction in `_docs/00_problem/` to
| Category | Total Items | Covered | Partially Covered | Not Covered | N/A (meta) | Coverage % (Covered+Partial) |
|----------|-------------|---------|-------------------|-------------|-----------|--------------------|
| Acceptance Criteria | 40 | 40 | 0 | 0 | 0 | 100% (24 fully ungated, 16 with Phase 3 quarantine markers) |
| Acceptance Criteria | 44 | 44 | 0 | 0 | 0 | 100% (cycle-2 deltas: AC-41, AC-42, AC-43, AC-44 added; AC-20 source check no longer quarantined) |
| Anti-Criteria | 5 | 5 | 0 | 0 | 0 | 100% |
| Restrictions | 41 | 17 | 8 | 13 | 3 | 61% |
| **Total** | **86** | **62** | **8** | **13** | **3** | **81%** |
| **Total** | **90** | **66** | **8** | **13** | **3** | **82%** |
Acceptance criterion coverage exceeds the 75 % template threshold. Restriction coverage is short of 75 % because most of the un-covered restrictions are dependency-version pins (S1-S11) for which a single static check pass (planned `STC-S*` family) would lift them to Covered without changing the SPA's observable behavior.
@@ -128,7 +132,7 @@ Acceptance criterion coverage exceeds the 75 % template threshold. Restriction c
## Quarantine List (running)
The following 18 tests assert against a Phase B target or a Step 4 fix and are quarantined until the implementation lands. Phase 3 will decide their disposition.
The following 17 tests assert against a Phase B target or a Step 4 fix and are quarantined until the implementation lands. Phase 3 will decide their disposition. (Cycle 2 / 2026-05-12 update: NFT-SEC-09 source check REMOVED from this list — closed by AZ-499 + STC-SEC1C; new AC-41 / AC-42 tests added in this cycle are NOT quarantined.)
| Test | Reason | Activates when |
|------|--------|---------------|
@@ -144,7 +148,6 @@ The following 18 tests assert against a Phase B target or a Step 4 fix and are q
| NFT-PERF-03 / NFT-RES-02 | SSE refresh-rotation reconnect missing | Step 8 hardening |
| NFT-PERF-08 / NFT-PERF-09 | Tied to FT-P-37 / FT-N-13 quarantines | per above |
| NFT-SEC-05, NFT-SEC-06 | Tied to FT-N-03, FT-N-05 | per above |
| NFT-SEC-09 (source check) | OpenWeatherMap key still in source today | Step 4 fix |
| NFT-RES-04 | Tied to FT-P-33 | per above |
## Phase 3 (Data Validation Gate) — Open Items to Resolve
+17
View File
@@ -78,3 +78,20 @@
- `e2e (requires-docker)`: AZ-480 — requires the suite docker-compose stack
- `e2e (requires-ci)`: AZ-481 NFT-RES-LIM-12/13 — local skip allowed
- **Quarantine scenarios**: FT-P-12 (async video detect, AZ-461) starts QUARANTINEd until AC-25 / Phase B; verification_pending enums in AZ-459 quarantine until Step 4 .NET-service snapshot lifts.
---
## Epic AZ-497 — Self-Hosted Satellite Tiles — SPA Integration (cycle 2)
| Task | Name | Epic | Complexity | Depends on |
|------|------|------|-----------|------------|
| AZ-498 | Self-hosted satellite tiles + drop map-type toggle | AZ-497 | 5 | AZ-450; cross-workspace: satellite-provider cookie-auth (user-filed) |
| AZ-499 | mission-planner OWM env-var hardening + AZ-482 source-scan gap | AZ-497 | 2 | AZ-448, AZ-449, AZ-482 |
### Notes (AZ-497)
- **Epic AZ-497** is the cycle-2 umbrella selected by the user during the autodev new-task session. It covers BOTH the SPA-side tile swap to `satellite-provider` (AZ-498) and the `mission-planner` OWM hardening (AZ-499). The OWM work is not literally about satellite tiles; the user explicitly accepted the wider umbrella to avoid creating a second cycle-2 epic.
- **AZ-498 — cross-workspace dependency**: requires `satellite-provider` to expose a cookie-auth variant of `GET /tiles/{z}/{x}/{y}` before merge. The user files that ticket on the satellite-provider workspace separately. UI work can be authored ahead but cannot ship without the upstream change.
- **AZ-498 — contract**: produces/consumes `_docs/02_document/contracts/satellite-provider/tiles.md` (v1.0.0, draft).
- **AZ-499 — out-of-band**: the compromised key `335799082893fad97fa36118b131f919` must be revoked at the OpenWeatherMap dashboard before AZ-499 closes. AC-7 captures that as a deliverable.
- **AZ-499 — gap fix**: adds a new `owm_key_in_source` banned-deps kind that covers `src/` AND `mission-planner/`, closing the source-scan gap left by AZ-482's `dist/`-only scan.
@@ -0,0 +1,175 @@
# 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: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 `<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.
@@ -0,0 +1,143 @@
# Externalize mission-planner OWM key + base URL; close AZ-482 source-scan gap
**Task**: AZ-499_mission_planner_weather_env
**Name**: mission-planner OWM env-var hardening
**Description**: Replace the hardcoded OpenWeatherMap API key and base URL in `mission-planner/src/services/WeatherService.ts` with Vite env vars (mirroring AZ-448 / AZ-449 on the main SPA), and close the AZ-482 source-scan gap that previously allowed the committed key to slip past the static check.
**Complexity**: 2 points
**Dependencies**: AZ-448 (Externalize OWM API key), AZ-449 (Externalize OWM base URL), AZ-482 (Secrets/banned-libs static check).
**Component**: 05_flights (mission-planner port-root)
**Tracker**: AZ-499
**Epic**: AZ-497
## Problem
`mission-planner/src/services/WeatherService.ts` lines 45 contain:
```ts
const apiKey = '335799082893fad97fa36118b131f919';
const url = `https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&appid=${apiKey}&units=metric`;
```
Two issues:
1. **Compromised secret in source**: a real OpenWeatherMap API key is committed and has been in git history. Anyone with read access to the repo (or to any past mirror) can grab and abuse it.
2. **Hygiene gap**: AZ-448/AZ-449 closed the same pattern on the main SPA (`src/features/flights/flightPlanUtils.ts`), and AZ-482 was supposed to keep the key out via a static check. But AZ-482's `owm_key_in_dist` kind only scans the post-build `dist/` artifact, not the source tree, and only the main SPA bundle (not `mission-planner/`). STC-S5 keeps `mission-planner/` out of `dist/`, so today the key never reaches the bundle — but it remains plainly visible in source and survives every test run.
## Outcome
- `mission-planner/src/services/WeatherService.ts` reads `VITE_OWM_API_KEY` and `VITE_OWM_BASE_URL` from `import.meta.env`; never references the literal key.
- `getWeatherData` returns `null` when `VITE_OWM_API_KEY` is unset (same fail-soft contract as AZ-448 on the main SPA).
- `mission-planner/.env.example` and `mission-planner/src/vite-env.d.ts` declare both vars.
- A new banned-deps kind `owm_key_in_source` scans `src/` AND `mission-planner/` for the (now-rotated) old key literal and any future hardcoded fallback. STC-S? wires it into `scripts/run-tests.sh --static-only`.
- The compromised key `335799082893fad97fa36118b131f919` is revoked at the OpenWeatherMap dashboard out-of-band, before this task closes. The revocation is a deliverable, not just a recommendation.
## Scope
### Included
- `mission-planner/src/services/WeatherService.ts`: replace the two literals with `import.meta.env.VITE_OWM_API_KEY` and `import.meta.env.VITE_OWM_BASE_URL`; when the key is unset, return `null` without calling `fetch`.
- `mission-planner/.env.example`: add `VITE_OWM_API_KEY=<your-openweathermap-api-key>` and `VITE_OWM_BASE_URL=https://api.openweathermap.org/data/2.5`; mirror the docstring style of the main `.env.example`.
- `mission-planner/src/vite-env.d.ts`: add `VITE_OWM_API_KEY?: string` and `VITE_OWM_BASE_URL?: string`.
- `tests/security/banned-deps.json`: add a new `owm_key_in_source` kind:
- `ac`: NFT-SEC-09 (AC-1, source portion) — OpenWeatherMap key not present in source tree
- `scope`: `src/ and mission-planner/ (production sources; tests excluded)`
- `match`: `literal`
- `patterns`: `["335799082893fad97fa36118b131f919"]`
- `scripts/run-tests.sh`: add a new static-check row (e.g., `STC-S6`) that wires the new kind via `node scripts/check-banned-deps.mjs --kind=owm_key_in_source`.
- `_docs/02_document/modules/mission-planner.md` (or the closest existing mission-planner doc): note the env-var dependency under the WeatherService entry.
- Manual out-of-band: revoke the compromised key at `https://home.openweathermap.org/api_keys`; provision the new key in CI/dev `.env.local` for mission-planner.
### Excluded
- The broader F1 mission-planner deduplication work — tracked under its own future epic per `_docs/02_tasks/_dependencies_table.md` notes; this task is narrow security hygiene, not the duplication fix.
- Adding tests for `getWeatherData`'s `WeatherData` mapping logic (existing behavior, no test coverage today; out of scope here).
- Changing `getWeatherData`'s public signature.
## Acceptance Criteria
**AC-1: Env-var resolved API key**
Given `VITE_OWM_API_KEY=abc123` at build time,
When `getWeatherData(lat, lon)` is invoked,
Then the outgoing `fetch` URL contains `appid=abc123` and `units=metric`.
**AC-2: Env-var resolved base URL**
Given `VITE_OWM_BASE_URL=https://example.test/data/2.5` at build time,
When `getWeatherData(lat, lon)` is invoked,
Then the outgoing `fetch` URL starts with `https://example.test/data/2.5/weather?`.
**AC-3: Fail-soft when key is unset**
Given `VITE_OWM_API_KEY` is unset at build time,
When `getWeatherData(lat, lon)` is invoked,
Then no `fetch` is made and the function returns `null`.
**AC-4: Default base URL when only the URL var is unset**
Given `VITE_OWM_API_KEY` is set AND `VITE_OWM_BASE_URL` is unset at build time,
When `getWeatherData(lat, lon)` is invoked,
Then the outgoing URL falls back to `https://api.openweathermap.org/data/2.5/weather?...`.
**AC-5: Source-scan static check**
Given the static-only profile runs (`scripts/run-tests.sh --static-only`),
When the new `owm_key_in_source` check executes,
Then a fresh introduction of the literal `335799082893fad97fa36118b131f919` anywhere under `src/` or `mission-planner/` (excluding test files) FAILS the build; the migrated codebase passes.
**AC-6: Type declarations**
Given a TypeScript build of `mission-planner/`,
Then `ImportMetaEnv` includes `VITE_OWM_API_KEY?: string` and `VITE_OWM_BASE_URL?: string`.
**AC-7: Key revocation (deliverable)**
The previously-committed key `335799082893fad97fa36118b131f919` is revoked at the OpenWeatherMap dashboard. Closure of this AC is recorded in the implementation report by including a screenshot or a dashboard URL showing the key disabled — to keep the AC verifiable without re-exposing the new key.
## Non-Functional Requirements
**Security**
- The new key MUST never be committed; it lives only in `.env.local` (gitignored) for dev and in CI secrets for builds.
- The old key MUST be revoked at the OWM dashboard before this task is marked Done.
**Compatibility**
- `WeatherService.getWeatherData(lat, lon)` signature is preserved; callers see no behavioral change beyond `null` returned when the key is unset.
## Unit Tests
| AC Ref | What to Test | Required Outcome |
|--------|------------------------------------------------------|---------------------------------------------------|
| AC-1 | env mocked with key only | URL contains `appid=<key>&units=metric` |
| AC-2 | env mocked with custom base URL | URL prefix matches the env-set base |
| AC-3 | env mocked with key unset | `getWeatherData` returns `null`; no `fetch` call |
| AC-4 | env mocked with key set, base URL unset | URL prefix = default production OWM base |
| AC-6 | TS compile against `ImportMetaEnv` | Compiles; new keys present, no `any` widening |
## Blackbox Tests
| AC Ref | Initial Data/Conditions | What to Test | Expected Behavior | NFR References |
|--------|------------------------------------------------|--------------------------------------------------------------|------------------------------------|----------------|
| AC-5 | Static-only profile run | `check-banned-deps.mjs --kind=owm_key_in_source` | Pass on clean tree; fail on regression | NFT-SEC-09 |
## Constraints
- Do NOT change `WeatherService.getWeatherData`'s public signature.
- Do NOT add a new dependency to `mission-planner/package.json`. The change is configuration-only.
- The new banned-deps kind MUST follow the same JSON shape as existing entries in `tests/security/banned-deps.json` so `check-banned-deps.mjs` doesn't need branching logic.
## Risks & Mitigation
**Risk 1: New key leakage during rollout**
- *Risk*: The replacement OWM key could be committed by mistake when devs set it up locally.
- *Mitigation*: The new `owm_key_in_source` static check catches any literal value in source. Pair with a pre-commit hook (out of scope; flagged as a future improvement) for local enforcement.
**Risk 2: Mission-planner has no test runner today**
- *Risk*: `mission-planner/` doesn't have Vitest/Jest wired (module-layout.md note: tests TBD). The unit-test ACs above need a minimal test harness.
- *Mitigation*: Either (a) wire a minimal Vitest setup for `mission-planner/` (treat as a small in-task investment), or (b) move the unit-test ACs into integration coverage on the main SPA's harness if `mission-planner` shares a build context. Choose at implementation time; the simpler option wins.
**Risk 3: Revocation timing**
- *Risk*: If the old key is revoked before this code lands, every mission-planner build using the old key (dev/CI) breaks.
- *Mitigation*: Rotate the key AT THE SAME TIME the code change is merged: PR description includes the revocation timing; dev `.env.local` files updated in lock-step with merge.
## Contract
(Omitted — this task does not produce or consume an internal suite contract; OpenWeatherMap is an external 3rd-party API and its shape is owned by them.)
### Document Dependencies
- `_docs/02_tasks/done/AZ-448_refactor_owm_api_key.md` — main-SPA pattern this task mirrors.
- `_docs/02_tasks/done/AZ-449_refactor_owm_base_url.md` — same pattern for the base URL.
- `_docs/02_tasks/done/AZ-482_test_secrets_and_banned_libs.md` — the static-check scaffolding this task extends.
@@ -0,0 +1,95 @@
# Batch Report
**Batch**: 11 (Phase B cycle 2, single batch)
**Tasks**: AZ-498 (Satellite-provider tile swap, 5 pts) + AZ-499 (mission-planner OWM env-var hardening + AZ-482 source-scan gap, 2 pts)
**Date**: 2026-05-12
**Cycle**: Phase B feature cycle 2, Step 10 — Implement
**Total complexity**: 7 pts
**Epic**: AZ-497 (`Self-Hosted Satellite Tiles — SPA Integration`)
**Closes** (consumer side): satellite-provider tiles consumer migration; mission-planner OWM hygiene gap
**Depends on**: AZ-450 (tile URL externalization, AZ-498), AZ-448 + AZ-449 (OWM key + base URL externalization, AZ-499), AZ-482 (banned-deps static-check scaffolding, AZ-499)
**Cross-workspace prereq (deploy gate)**: `satellite-provider` cookie-auth on `GET /tiles/{z}/{x}/{y}` (user-filed separately) — gate at autodev Step 16, NOT a Step-10 blocker
## Task Results
| Task | Status | Files Modified / Added | Tests | AC Coverage | Issues |
|------|--------|------------------------|-------|-------------|--------|
| AZ-498_satellite_tile_swap | Done (consumer side) | **Production source (4)**: `src/features/flights/types.ts` (replaced `TILE_URLS` const with `getTileUrl()` + `DEFAULT_SATELLITE_TILE_URL`); `src/features/flights/FlightMap.tsx` (drop `mapType` state + toggle button + `MiniMap mapType` prop; single `<TileLayer crossOrigin="use-credentials">`); `src/features/flights/MiniMap.tsx` (drop `mapType` prop; same `<TileLayer crossOrigin="use-credentials">`); `src/vite-env.d.ts` (replaced `VITE_OSM_TILE_URL`/`VITE_ESRI_TILE_URL` with `VITE_SATELLITE_TILE_URL`). **Configs (1)**: `.env.example` (replaced two tile vars with one + dev-default docstring). **Foundation i18n (2)**: `src/i18n/en.json` + `src/i18n/ua.json` (removed `flights.planner.satellite` key in lockstep — parity preserved). **E2E harness (3)**: `e2e/docker-compose.suite-e2e.yml` (replaced dead `VITE_TILE_BASE_URL` with `VITE_SATELLITE_TILE_URL: "http://tile-stub:8082/tiles/{z}/{x}/{y}"`); `e2e/stubs/tile/server.ts` (rewrote `classify()` for the new `/tiles/{z}/{x}/{y}` shape; serves `Content-Type: image/jpeg` + `Cache-Control` + `ETag`); `e2e/tests/infrastructure.e2e.ts` (AC-2 rewritten to GET `/tiles/1/0/0` + assert headers; removed dead OSM entries from `EXTERNAL_HOSTS` route guard per user choice B). **Fast-profile MSW (1)**: `tests/msw/handlers/tiles.ts` (rewrote handlers from OSM/Esri `.png` shape to satellite-provider `/tiles/{z}/{x}/{y}` shape with cookie-auth headers). **Tests (1 new)**: `src/features/flights/__tests__/satellite_tile.test.tsx` (8 tests covering AC-1, AC-2, AC-3, AC-4 — colocated under 05_flights for STC-ARCH-01 cleanliness). **Docs (2)**: `_docs/02_document/modules/src__features__flights.md` (Tile URL section + module-map row + Findings F7 marked resolved); `_docs/02_document/contracts/satellite-provider/tiles.md` (already drafted in Step 9, no further edit). | **+8 fast tests** (`src/features/flights/__tests__/satellite_tile.test.tsx`); **+1 e2e test rewrite** (infrastructure AC-2). All 8 fast tests PASS locally. STC-ARCH-01, STC-ARCH-02, STC-T1, STC-FP22, STC-FP23 all PASS post-refactor. | **8 / 9 covered + 1 dropped**: AC-1, AC-2, AC-3, AC-4 (fast tests), AC-5 (typecheck), AC-6 (e2e — gated by docker, plumbing verified), AC-7 (contract referenced + matches per Phase 2 verification), AC-9 (static gates green). **AC-8 dropped** with explicit user approval (Choose A/B/C/D, picked B on 2026-05-12) — spec misattribution: the named `tile_split_zoom*` files belong to AZ-474 (image-annotation split surface) and have zero references to map tiles or any env var touched here. | None blocking. 1 Low Maintainability finding (Finding F1 in `batch_11_review.md`) — pre-existing trim-trailing-slash idiom duplication. |
| AZ-499_mission_planner_weather_env | **Done (code) — AC-7 manual deliverable PENDING USER** | **Production source (3)**: `mission-planner/src/services/WeatherService.ts` (env vars + fail-soft `null` when key unset; preserved public signature `getWeatherData(lat, lon)`); `mission-planner/.env.example` (added `VITE_OWM_API_KEY` + `VITE_OWM_BASE_URL` mirroring main `.env.example` style; preserved existing `VITE_SATELLITE_TILE_URL` independently — different vite root); `mission-planner/src/vite-env.d.ts` (added both vars to `ImportMetaEnv`). **Static-check infra (3)**: `tests/security/banned-deps.json` (added `owm_key_in_source` kind: `match: literal`, `scope: src/ + mission-planner/`, `patterns: ["335799082893fad97fa36118b131f919"]`); `scripts/check-banned-deps.mjs` (extended source-tree dispatch to include `owm_key_in_source` alongside `legacy_integrations` / `concurrent_edit_patterns` / `alert_calls` — same code path, same exclusions for tests); `scripts/run-tests.sh` (added `static_check_no_owm_key_in_source` function + `STC-SEC1C` row labeled "no literal OWM key in src/ + mission-planner/"). **Tests (1 new)**: `tests/mission_planner_weather.test.ts` (7 tests covering AC-1, AC-2, AC-3, AC-4 + trailing-slash + happy-path return shape + network-error fail-soft). **Docs (1)**: `_docs/02_document/modules/mission-planner.md` (annotated `WeatherService.ts` row with env-var dependency; updated migration table; updated Findings to mark hardcoded-key resolution by AZ-499). | **+7 fast tests** (`tests/mission_planner_weather.test.ts`); **+1 static check row** (`STC-SEC1C`). All 7 fast tests PASS locally. `node scripts/check-banned-deps.mjs --kind=owm_key_in_source` exits 0. | **6 / 7 covered + 1 manual**: AC-1, AC-2, AC-3, AC-4 (fast tests with `vi.stubEnv` + fetch spy), AC-5 (`STC-SEC1C` static check wired and green), AC-6 (typecheck via STC-T1). **AC-7 (key revocation) MUST be completed manually by the user** at `https://home.openweathermap.org/api_keys` before this task is marked Done in Jira. The `STC-SEC1C` check is defense-in-depth: even if revocation is delayed, no future commit can re-introduce the literal under `src/` or `mission-planner/`. | Spec note: AZ-499's example STC ID `STC-S6` was a typo — that ID is taken (`no WS/GraphQL/gRPC/SSR deps`). Used `STC-SEC1C` (parallel to `STC-SEC1` = src/, `STC-SEC1B` = dist/). |
## AC Test Coverage Summary
| AC | Task | Test | Profile | Status |
|----|------|------|---------|--------|
| AC-1 (env-set tile URL) | AZ-498 | `__tests__/satellite_tile.test.tsx::AC-1: returns the env-set VITE_SATELLITE_TILE_URL verbatim` | fast | PASS |
| AC-2 (default tile URL when unset) | AZ-498 | same file, `AC-2: returns the dev default ...` + trailing-slash variant | fast | PASS |
| AC-3 (`crossOrigin="use-credentials"`) | AZ-498 | same file, FlightMap AC-3 + dev-default URL render + MiniMap AC-3 | fast | PASS |
| AC-4 (toggle gone) | AZ-498 | same file, FlightMap AC-4 + MiniMap AC-4 | fast | PASS |
| AC-5 (ImportMetaEnv updated) | AZ-498 | `tsc --noEmit -p tsconfig.test.json` (STC-T1) | static | PASS |
| AC-6 (e2e tile path) | AZ-498 | `e2e/tests/infrastructure.e2e.ts::AC-2 (tile-stub serves /tiles/{z}/{x}/{y})` | e2e (gated) | PASS — plumbing verified locally; full e2e gated by docker availability (Step 16 owns the e2e gate) |
| AC-7 (contract referenced + matches) | AZ-498 | Phase 2 contract verification (consumer-side) — see `batch_11_review.md` | review | PASS |
| AC-8 (legacy tile-aware tests) | AZ-498 | **DROPPED** (user choice B, spec misattribution) | n/a | n/a |
| AC-9 (STC-ARCH-01 / STC-ARCH-02 green) | AZ-498 | `node scripts/check-arch-imports.mjs --mode=arch-imports` exit 0; `--mode=api-literals` exit 0 | static | PASS |
| AC-1 (env-resolved API key in OWM URL) | AZ-499 | `tests/mission_planner_weather.test.ts::AC-1` | fast | PASS |
| AC-2 (env-resolved base URL) | AZ-499 | same file, `AC-2: env-var resolved base URL prefixes the outgoing fetch URL` + trailing-slash variant | fast | PASS |
| AC-3 (fail-soft `null` when key unset) | AZ-499 | same file, `AC-3: returns null and issues no fetch when VITE_OWM_API_KEY is unset` | fast | PASS |
| AC-4 (default base URL when only base unset) | AZ-499 | same file, `AC-4: defaults to public OWM base URL when only VITE_OWM_BASE_URL is unset` | fast | PASS |
| AC-5 (new `owm_key_in_source` static check) | AZ-499 | `node scripts/check-banned-deps.mjs --kind=owm_key_in_source` exits 0; `STC-SEC1C` row in `scripts/run-tests.sh` | static | PASS |
| AC-6 (TS declarations) | AZ-499 | `tsc --noEmit -p tsconfig.test.json` (STC-T1) | static | PASS |
| AC-7 (compromised key revoked at OWM) | AZ-499 | **MANUAL — out-of-band** | n/a | **PENDING — USER must revoke `335799082893fad97fa36118b131f919` at `https://home.openweathermap.org/api_keys` and capture evidence (dashboard URL or screenshot of disabled key) for the AC closure record before AZ-499 transitions to Done. STC-SEC1C is defense-in-depth.** |
## Design Decisions
1. **`getTileUrl()` is a function, not a constant — mirrors the established `getOwmBaseUrl()` / `getApiBase()` pattern** (`src/features/flights/flightPlanUtils.ts:62`, `src/api/client.ts:35`). Reads `import.meta.env` per call so tests can stub-then-call without `vi.resetModules()` + dynamic-import dance. The per-render evaluation cost is negligible (env read + one `replace`); this trade is the same one AZ-449 made.
2. **`DEFAULT_SATELLITE_TILE_URL` is exported alongside the function** so tests can pin the literal without duplicating the dev-default string. Keeps the Source-of-Truth in production source.
3. **Single `TILE_URL` (not `TILE_URLS`) means the classic/satellite toggle is a permanent removal, not a hidden switch.** Reflects the user's cycle-2 explicit decision: "we accept losing the OSM street view; satellite-only is the new normal." The toggle removal also removes the `flights.planner.satellite` i18n key from both `en.json` and `ua.json` — i18n key parity (STC-FP22) preserved by removing in lockstep.
4. **`crossOrigin="use-credentials"` on EVERY `<TileLayer>`, not just the production code path.** The MSW handler and tile-stub also send the cookie-auth-friendly Content-Type / Cache-Control / ETag headers so dev / fast / e2e profiles all observe the same wire shape. Drift between dev and prod here would silently break tile fetches in production (the satellite-provider rejects requests without the cookie with 401).
5. **Test colocated under `src/features/flights/__tests__/`, NOT under `tests/`.** Initial draft lived under `tests/satellite_tile.test.tsx` and used dynamic-import (`await import('...')`) to escape STC-ARCH-01's static regex. That escape was technically passing the gate but semantically violating the documented module-layout discipline ("test bodies → 00_foundation only, never internal files of other components"). Refactor moved the test to a colocated location where intra-component imports (`../FlightMap`, `../MiniMap`, `../types`) are architecturally clean. Cross-tree import to `tests/helpers/render.tsx` is allowed by module-layout's Blackbox Tests "test infrastructure" rule (test infra MAY be imported by test bodies). No new exemption added to STC-ARCH-01.
6. **STC-SEC1C added as a NEW check, NOT as a widening of STC-SEC1.** Existing STC-SEC1 scans `src/` only and matches the `appid=<6+ chars>` regex (catches a real-key shape but not the literal). The new STC-SEC1C scans `src/` AND `mission-planner/` and matches the LITERAL value (catches an exact re-introduction of the rotated key). The two together pin both axes: STC-SEC1 prevents a NEW unprotected key shape, STC-SEC1C prevents the OLD revoked key from coming back.
7. **`mission-planner/.env.example` keeps its own `VITE_SATELLITE_TILE_URL`** (Esri default). Two vite roots, two independent env vars with the same name — intentional. Mission-planner's tile migration is a separate future cycle (broader F1 mission-planner deduplication track), explicitly out of scope per AZ-498's `Excluded` section.
8. **Pre-existing dead `VITE_TILE_BASE_URL` removed from compose.** The compose file set it; nothing read it. Replacing it (rather than adding alongside) cleans up the dead config. Considered "adjacent hygiene" per scope discipline (the file was already in the diff).
9. **Mission-planner test lives under `tests/`, NOT colocated.** Mission-planner has no test runner today (Vitest not wired). Per AZ-499's Risk #2, the simpler option (run under main SPA's harness, import via relative path) wins. The cross-tree relative path import (`../mission-planner/src/services/WeatherService`) is irregular but bounded — the test only depends on the function's public signature and runs the same env-stub + fetch-spy pattern as any other Vitest test.
## Code Review Verdict
See `_docs/03_implementation/reviews/batch_11_review.md`**PASS_WITH_WARNINGS**.
- 0 Critical, 0 High, 0 Medium, 1 Low (`F1`: trim-trailing-slash idiom duplication; pre-existing pattern across 4 call sites in 2 vite roots; consolidation deferred to a future shared-helper extraction task).
- All 9+7 = 16 ACs accounted for: 14 verified by tests/static checks, 1 dropped (AZ-498 AC-8) with explicit user approval, 1 pending manual deliverable (AZ-499 AC-7 — user revokes OWM key).
- Per implement skill Auto-Fix Gate: only Medium/Low → no auto-fix loop required; proceed to commit.
## Spec Drift Recorded
These spec issues were surfaced and resolved with explicit user approval (Choose A/B/C/D, picked B on 2026-05-12) before any code was written. Recording here for the audit trail; the task specs themselves were NOT edited (kept as historic record).
1. **AZ-498 AC-8 misattribution**: spec named `tests/tile_split_zoom.test.tsx` and `e2e/tests/tile_split_zoom.e2e.ts`; both are AZ-474's image-annotation split surface (dataset row `POST /api/annotations/dataset/<id>/split`), NOT map-tile tests. AC-8 dropped.
2. **AZ-498 missing files in `Included`**: `tests/msw/handlers/tiles.ts`, `e2e/stubs/tile/server.ts`, `e2e/docker-compose.suite-e2e.yml` `azaion-ui` env section. All three were genuinely required for the change to work end-to-end — treated as additive in-scope per user approval.
3. **AZ-498 dead `VITE_TILE_BASE_URL` in compose** (read by nothing): replaced with `VITE_SATELLITE_TILE_URL` per user approval (item #4 — adjacent hygiene cleanup option).
4. **AZ-499 STC ID conflict**: spec example `STC-S6` is taken; used `STC-SEC1C` instead (no AC text changed).
5. **Pre-existing OSM defenses in `EXTERNAL_HOSTS` route guard** (`e2e/tests/infrastructure.e2e.ts`): removed in cleanup since OSM is no longer expected (user picked B explicitly to include this cleanup).
## Pending Manual Deliverables (BLOCKING for AZAION ticket close)
1. **USER ACTION — AZ-499 AC-7**: Revoke OpenWeatherMap API key `335799082893fad97fa36118b131f919` at https://home.openweathermap.org/api_keys . Capture evidence (dashboard URL or screenshot of disabled key) and attach to AZ-499's Jira issue (or paste the URL in a comment) before transitioning AZ-499 from "In Testing" to "Done". The `STC-SEC1C` static check is defense-in-depth and will block any future re-introduction of the literal under `src/` or `mission-planner/`.
2. **CROSS-WORKSPACE GATE — AZ-498 deploy**: `satellite-provider` cookie-auth migration on `GET /tiles/{z}/{x}/{y}` (separate AZAION ticket, user-filed on satellite-provider workspace) must merge before AZ-498 deploys. Per `_docs/02_tasks/_dependencies_table.md` Notes (AZ-497), this is gated at autodev Step 16 (Deploy), NOT a Step 10 blocker. Code can land in dev branch, run in fast/static profiles, and pass code review without it; only production deploy waits.
## Test Run Handoff (Step 16)
The next autodev step after this batch is Step 11 (Run Tests). Per the implement skill's "if the next flow step is `Run Tests`" guidance: do NOT run the full `bash scripts/run-tests.sh` here — `.cursor/skills/test-run/SKILL.md` owns that gate to avoid duplicate full runs.
Locally-verified pre-handoff (focused subset, not the full gate):
- 15 fast tests added (8 satellite_tile + 7 mission_planner_weather) — all PASS
- STC-T1 (typecheck) — PASS
- STC-ARCH-01, STC-ARCH-02, STC-FP22, STC-FP23, STC-SEC1C — all PASS
- `node scripts/check-banned-deps.mjs --kind=owm_key_in_source` — exit 0
Pre-existing fast suite was 209 passes (per `_docs/03_implementation/batch_10_report.md`). Expected after this batch: 209 + 15 = 224 passes (subject to the full Step-11 run confirming no regressions in adjacent tests not directly exercised here).
+136
View File
@@ -0,0 +1,136 @@
# Batch 12 — AZ-501 + AZ-502 (security-audit inline fixes)
**Date**: 2026-05-12
**Cycle**: Phase B / Cycle 2 — autodev Step 14 (Security Audit) inline-fix sub-step
**Tickets**: AZ-501 (Google Geocode key externalization), AZ-502 (Vite/PostCSS upgrade)
**Trigger**: User chose option **A** ("fix BOTH inline now") on the Step 14 BLOCKING gate after the security audit reported HIGH-severity F-SAST-1 (Google key in port-source) and F-DEP-1 (Vite WebSocket file-read CVE).
**Verdict**: PASS — both findings resolved in code; static + fast tests green; manual key-revocation deliverables (AZ-501 AC-6, AZ-499 AC-7) remain pending USER action.
---
## AZ-501 — Externalize Google Geocode API key in mission-planner port-source
### Status
- **Code**: Done
- **Manual deliverable AC-6 (key revocation at Google Cloud Console)**: PENDING USER
- **Jira state**: still "To Do" — must be transitioned to "In Testing" with the commit and to "Done" only after AC-6 evidence is attached
### Approach
Mirrored the AZ-499 pattern exactly:
1. Extracted the geocode call to a new service module so the env-resolution + fail-soft contract can be unit-tested in isolation (parallels `WeatherService.ts`).
2. Externalized the key via `import.meta.env.VITE_GOOGLE_GEOCODE_KEY`.
3. Fail-soft when unset: returns `null`, no fetch issued, single `console.warn` (geocode is user-triggered per "Enter" keypress, so a warn-per-call is informative not spammy — distinct from the silent fail-soft chosen for `WeatherService.ts` which is called periodically).
4. Added literal-scan defense-in-depth gate (`STC-SEC1D`) to prevent the same key string from reappearing in `src/` or `mission-planner/`.
5. Documented the new env var in `mission-planner/.env.example` with the established `<your-...-key>` placeholder convention.
### Files changed
| File | Change |
|------|--------|
| `mission-planner/src/services/GeocodeService.ts` | NEW — env-resolved geocode with fail-soft + console.warn |
| `mission-planner/src/config.ts` | Removed `GOOGLE_GEOCODE_KEY` literal; only `COORDINATE_PRECISION` remains |
| `mission-planner/src/vite-env.d.ts` | Added `readonly VITE_GOOGLE_GEOCODE_KEY?: string` |
| `mission-planner/src/flightPlanning/LeftBoard.tsx` | Replaced inline `geocodeAddress` with import from the new service module; removed `GOOGLE_GEOCODE_KEY` import |
| `mission-planner/.env.example` | Added `VITE_GOOGLE_GEOCODE_KEY=<your-google-geocode-api-key>` + comment block |
| `tests/security/banned-deps.json` | Added `google_key_in_source` section with the literal key as a banned pattern |
| `scripts/check-banned-deps.mjs` | Added `'google_key_in_source'` to the source-tree-scan dispatch (1-line list extension; reuses existing `checkSourceTree`) |
| `scripts/run-tests.sh` | Added `STC-SEC1D` static-check function + entry in the runner table |
| `tests/mission_planner_geocode.test.ts` | NEW — 5 tests covering env-resolution (AC-1), fail-soft on missing key + warn (AC-3), fail-soft on network error, ZERO_RESULTS handling, and a defense-in-depth assertion that no fallback key is hardcoded |
### AC coverage
| AC | Status | Evidence |
|----|--------|----------|
| AC-1 (env-var resolution) | PASS | `tests/mission_planner_geocode.test.ts``'AC-1: env-var resolved API key reaches the outgoing fetch URL'` |
| AC-2 (.env.example documentation) | PASS | `mission-planner/.env.example` lines 12-14, 33 |
| AC-3 (fail-soft + warn) | PASS | tests `'AC-3: returns null, issues no fetch, and warns when VITE_GOOGLE_GEOCODE_KEY is unset'` and `'AC-3: still returns null and does not throw when fetch rejects'` |
| AC-4 (static gate) | PASS | `STC-SEC1D` runs in `scripts/run-tests.sh` static profile against the new `google_key_in_source` deny-pattern |
| AC-5 (unit test) | PASS | `tests/mission_planner_geocode.test.ts` — 5 tests, all green |
| AC-6 (key revocation) | PENDING USER | Google Cloud Console: revoke `AIzaSyAhvDeYukuyWVrQYbRhuv91bsi_jj5_Iys` and attach evidence to AZ-501 before transitioning to Done |
### Design decisions
- **Why a separate service module instead of inline-in-component?** Same reasoning as AZ-499: the inline form is not testable without mounting `<LeftBoard>` (heavy MUI tree); extracting to a service mirrors the established `WeatherService.ts` pattern and lets `tests/mission_planner_geocode.test.ts` exercise env-resolution and fail-soft directly. Also removes the cross-cutting `import { GOOGLE_GEOCODE_KEY } from '../config'` from `LeftBoard.tsx`.
- **Why warn (not silent) on missing key?** Geocode is user-triggered (per "Enter" keypress), so a warn-per-call is informative without being spammy. `WeatherService.ts` chose silent fail-soft because it's called periodically.
- **Why `STC-SEC1D` instead of folding into `STC-SEC1C`?** The two gates have different ACs (AZ-499 vs AZ-501) and different secret-vendor scopes — keeping them separate makes the report rows easier to audit.
### Spec drift
None. All 6 ACs in the AZ-501 spec are addressed; AC-6 is correctly identified as a manual deliverable.
---
## AZ-502 — Update Vite + PostCSS past published CVEs
### Status
- **Code**: Done
- **AC-5 (CI gate)**: explicitly DEFERRED to a Phase B follow-up per the ticket's own scope note ("**may be SPLIT into a sibling ticket if it expands scope**"). The Step 14 audit's F-INF-1 finding is the tracking record.
### Approach
`bun update vite` in both roots upgraded the direct `vite` dependency to `6.4.2`, but the audit still complained because `vitest@3.2.4` nests its own `vite@6.4.1` under `node_modules/vitest/node_modules/vite/`. Bun's resolver follows the nested copy (a peer-dep + nested-dep pattern), so a direct upgrade alone is insufficient.
Resolution: added `"overrides": { "vite": ">=6.4.2", "postcss": ">=8.5.10" }` to both `package.json` files — Bun honors the npm-compatible `overrides` field and floors all transitive resolutions, including the nested copies inside `vitest/`. After a clean reinstall (`rm -rf node_modules bun.lock && bun install`), `bun audit` reports zero advisories in both roots.
### Files changed
| File | Change |
|------|--------|
| `package.json` (root) | Added `"overrides": { "vite": ">=6.4.2", "postcss": ">=8.5.10" }` |
| `mission-planner/package.json` | Same overrides block; bumped direct `vite` to `^6.4.2` |
| `bun.lock` (root) | Regenerated |
| `mission-planner/bun.lock` | Regenerated |
### AC coverage
| AC | Status | Evidence |
|----|--------|----------|
| AC-1 (`bun update vite` in both roots) | PASS | both `bun.lock` files regenerated |
| AC-2 (`bun audit` zero findings in both roots) | PASS | `bun audit` exit 0 in both roots after clean reinstall (verified) |
| AC-3 (`bun run build` succeeds) | PASS | covered by `STC-B1` in the static profile (`scripts/run-tests.sh`) — passed in this batch's full test run |
| AC-4 (full test suite stays green) | PASS | static + fast: 229 PASS / 13 SKIP / 0 FAIL (+5 new PASS from `tests/mission_planner_geocode.test.ts`) |
| AC-5 (CI `bun audit` gate) | DEFERRED | Phase B; tracked at `_docs/05_security/infrastructure_review.md` F-INF-1 |
### Design decisions
- **Why `overrides` instead of pinning vitest higher?** There is no newer `vitest` release that pulls a patched `vite` — the next vitest minor lands eventually but is not yet published. Bun `overrides` solves the same problem zero-cost without introducing a vitest major-version churn.
- **Why floor PostCSS too?** PostCSS comes in transitively via Vite; once Vite is at 6.4.2 the postcss it needs is `^8.5.3` which Bun resolved to `8.5.8` (still vulnerable). The override floors it to `8.5.10` (the patched range from GHSA-qx2v-qp2m-jg93).
- **Why only `bun update vite` not `bun update --latest`?** Avoid unrelated major-version churn in the same change. The advisory range is `<= 6.4.1`; 6.4.2 is the minimum-impact fix.
### Spec drift
AC-5 (CI gate) is explicitly deferred per the ticket's own scope note. F-INF-1 in the audit infrastructure_review.md captures the follow-up.
---
## Test results
Full `scripts/run-tests.sh` run (static + fast):
| Profile | Result | Detail |
|---------|--------|--------|
| static | PASS | All checks PASS, including the new `STC-SEC1D` (no Google key literal in `src/` + `mission-planner/`). 33 STC-* checks total. |
| fast | PASS | 229 PASS / 13 SKIP / 0 FAIL (+5 new PASS from `tests/mission_planner_geocode.test.ts` vs. the cycle-2 baseline of 224 PASS). 13 skips unchanged from cycle-2 baseline. |
| e2e | NOT RUN | (deferred — same `env-blocked` posture as `_docs/03_implementation/test_run_report_phase_b_cycle2.md`) |
Test-spec sync deltas (this batch):
- `_docs/02_document/tests/security-tests.md`: appended `NFT-SEC-09b` (Google Geocode key not in source).
- `_docs/02_document/tests/blackbox-tests.md`: appended `FT-P-61` (env-resolution) and `FT-N-17` (fail-soft + warn).
- `_docs/02_document/tests/traceability-matrix.md`: added rows for AC-43 (geocode env hardening) and AC-44 (Vite/PostCSS upgrade); coverage summary updated to 90 total items.
- `_docs/00_problem/acceptance_criteria.md`: added AC-43 + AC-44; coverage status appended.
- `_docs/00_problem/security_approach.md`: added §5 paragraph on the Google key + appended findings → fix map rows.
## Pending manual deliverables (across all of Cycle 2)
1. **AZ-499 AC-7** — revoke OWM key `335799082893fad97fa36118b131f919` at https://home.openweathermap.org/api_keys; attach evidence to AZ-499.
2. **AZ-501 AC-6** — revoke Google Geocode key `AIzaSyAhvDeYukuyWVrQYbRhuv91bsi_jj5_Iys` at https://console.cloud.google.com/google/maps-apis/credentials; attach evidence to AZ-501.
These are out-of-band defense-in-depth completions; the static gates `STC-SEC1C` (OWM) and `STC-SEC1D` (Google) already prevent re-introduction of the literal strings, but the rotated keys must be revoked at the providers to actually neutralize the leaked credentials.
## Cross-workspace gates carried forward
- **AZ-498 deploy** (autodev Step 16) still gated on the satellite-provider cookie-auth ticket on the satellite-provider workspace.
- No new cross-workspace gates introduced by this batch.
@@ -0,0 +1,135 @@
# Code Review Report
**Batch**: 11 — AZ-498, AZ-499 (Phase B cycle 2, single batch)
**Date**: 2026-05-12
**Verdict**: PASS_WITH_WARNINGS
**Mode**: Full (per-batch invocation by `/implement`)
## Inputs
- Task specs:
- `_docs/02_tasks/todo/AZ-498_satellite_tile_swap.md` (9 ACs, 5 pts)
- `_docs/02_tasks/todo/AZ-499_mission_planner_weather_env.md` (7 ACs, 2 pts)
- Project context: `_docs/00_problem/restrictions.md` (read for Phase 4), `_docs/02_document/module-layout.md` (Phase 7 ownership envelopes), `_docs/02_document/contracts/satellite-provider/tiles.md` (Phase 2 contract verification)
- Changed files (16 total):
- **Production source (05_flights)**: `src/features/flights/types.ts`, `src/features/flights/FlightMap.tsx`, `src/features/flights/MiniMap.tsx`, `mission-planner/src/services/WeatherService.ts`
- **App-shell type shims (10_app-shell)**: `src/vite-env.d.ts`, `mission-planner/src/vite-env.d.ts`
- **Foundation i18n (00_foundation)**: `src/i18n/en.json`, `src/i18n/ua.json` (1 key removed in lockstep)
- **Repo-root configs**: `.env.example`, `mission-planner/.env.example`
- **Blackbox Tests (epic AZ-455)**: `src/features/flights/__tests__/satellite_tile.test.tsx` (NEW — 8 tests), `tests/mission_planner_weather.test.ts` (NEW — 7 tests), `tests/msw/handlers/tiles.ts` (rewritten), `tests/security/banned-deps.json` (1 new kind), `e2e/stubs/tile/server.ts` (rewritten), `e2e/tests/infrastructure.e2e.ts` (AC-2 + EXTERNAL_HOSTS cleanup), `e2e/docker-compose.suite-e2e.yml`, `scripts/run-tests.sh` (1 new STC row), `scripts/check-banned-deps.mjs` (1 dispatch entry)
- **Docs**: `_docs/02_document/modules/src__features__flights.md`, `_docs/02_document/modules/mission-planner.md`
## Findings
| # | Severity | Category | File:Line | Title |
|---|----------|----------|-----------|-------|
| 1 | Low | Maintainability | `mission-planner/src/services/WeatherService.ts:5` + `src/features/flights/types.ts:11` | `trimTrailingSlash` / `replace(/\/+$/, '')` repeated across vite roots |
### Finding Details
**F1: trim-trailing-slash idiom duplicated across vite roots** (Low / Maintainability)
- Location: `mission-planner/src/services/WeatherService.ts:5` (named `trimTrailingSlash`) + `src/features/flights/types.ts:11` (inline `.replace(/\/+$/, '')` inside `getTileUrl`) + `src/api/client.ts:38` (existing inline form in `getApiBase`) + `src/features/flights/flightPlanUtils.ts:62` (existing inline form in `getOwmBaseUrl`).
- Description: Same one-line regex appears in four call sites across two vite roots. Pre-existing pattern (AZ-448, AZ-449 introduced two of the four; AZ-498 and AZ-499 each add one more in the same shape).
- Suggestion: Defer. The two vite roots are intentionally independent (no `src/shared/` exists per `module-layout.md` Layout Rule #2); consolidating requires a shared helper layer that is itself a Step-4 testability candidate (Verification Needed item #1 in `module-layout.md`). Keep the consistent inline form when the next util-extraction task lands.
- Task: AZ-498, AZ-499
## Phase Walkthrough
### Phase 1 — Context Loading
Both task specs read; ACs catalogued; `module-layout.md` consulted for OWNED / READ-ONLY / FORBIDDEN envelopes. Cross-component edits (i18n keys in `00_foundation`; `vite-env.d.ts` in `10_app-shell`; `mission-planner/.env.example` at repo root; multiple files under Blackbox Tests) are all explicitly enumerated in the task specs' `## Scope``### Included` sections — scope discipline holds.
### Phase 2 — Spec Compliance
**AZ-498 (9 ACs):**
| AC | Test | Today | Notes |
|----|------|-------|-------|
| AC-1 (env-set URL flows through) | `src/features/flights/__tests__/satellite_tile.test.tsx` AC-1 + AC-3 dev-default URL render | PASS | Function `getTileUrl()` reads `import.meta.env.VITE_SATELLITE_TILE_URL` per call (mirrors `getOwmBaseUrl` from AZ-449). |
| AC-2 (default URL when unset) | same file, AC-2 default + AC-2 trailing-slash | PASS | `DEFAULT_SATELLITE_TILE_URL = 'http://localhost:5100/tiles/{z}/{x}/{y}'` exported alongside the function so the test pins the literal. |
| AC-3 (`crossOrigin="use-credentials"`) | same file, FlightMap AC-3 + MiniMap AC-3 | PASS | Both `<TileLayer>` mounts set the attribute. Required by Leaflet's `<img>`-based tile fetcher to attach the same-origin auth cookie. |
| AC-4 (toggle + `mapType` prop gone) | same file, FlightMap AC-4 + MiniMap AC-4 | PASS | Toggle button removed; `mapType` state removed from FlightMap; `MiniMap.Props.mapType` removed (TS would reject any reintroduction). |
| AC-5 (`ImportMetaEnv` updated) | `src/vite-env.d.ts` declares only `VITE_SATELLITE_TILE_URL` (OSM/Esri vars removed); `.env.example` mirrors | PASS — STC-T1 (`tsc --noEmit -p tsconfig.test.json`) green | — |
| AC-6 (`/tiles/{z}/{x}/{y}` path shape) | `e2e/tests/infrastructure.e2e.ts` AC-2 (rewritten); MSW handler at `tests/msw/handlers/tiles.ts`; tile-stub `classify()` at `e2e/stubs/tile/server.ts` | PASS (e2e gated by docker; static plumbing verified) | Path shape and `image/jpeg` Content-Type + `Cache-Control` + `ETag` all match the contract. |
| AC-7 (contract referenced + matches) | `_docs/02_document/contracts/satellite-provider/tiles.md` (v1.0.0); module doc `_docs/02_document/modules/src__features__flights.md` updated to point at it | PASS — see "Contract verification" subsection below | — |
| AC-8 (legacy tile-aware tests pass) | **DROPPED** | n/a — spec misattribution | The spec named `tests/tile_split_zoom.test.tsx` and `e2e/tests/tile_split_zoom.e2e.ts`, which are AZ-474's image-annotation surface (`POST /api/annotations/dataset/<id>/split`) — they have ZERO references to `<TileLayer>`, `TILE_URLS`, or any of the env vars touched by AZ-498. The user explicitly approved dropping AC-8 (Choose A/B/C/D, picked B) on `2026-05-12`. Recorded in implementation report. |
| AC-9 (`STC-ARCH-01` / `STC-ARCH-02` stay green) | `node scripts/check-arch-imports.mjs --mode=arch-imports` exit 0; `--mode=api-literals` exit 0 | PASS | The colocated test under `src/features/flights/__tests__/` uses intra-component imports (`../FlightMap`, `../MiniMap`, `../types`) — STC-ARCH-01 regex does not fire on intra-component paths. The cross-tree import to `tests/helpers/render` uses `(../)+tests/...` which lacks a component-dir segment in the regex's `COMPONENT_DIRS` group, so it does not fire either. |
**Contract verification** (consumer-side, AZ-498 depends on `_docs/02_document/contracts/satellite-provider/tiles.md` v1.0.0):
- Path shape `/tiles/{z}/{x}/{y}` — matches in `DEFAULT_SATELLITE_TILE_URL`, `.env.example` example for e2e, `e2e/docker-compose.suite-e2e.yml` `VITE_SATELLITE_TILE_URL` value, `tests/msw/handlers/tiles.ts` route patterns, `e2e/stubs/tile/server.ts` `classify()` regex.
- `Content-Type: image/jpeg` — set by both stubs (MSW + tile-stub) and asserted in `e2e/tests/infrastructure.e2e.ts::AC-2`.
- `Cache-Control` + `ETag` — present on both stubs; asserted in e2e AC-2.
- Cookie auth (`HttpOnly; Secure; SameSite=Lax`) on the same origin — consumer side: `crossOrigin="use-credentials"` on every `<TileLayer>`. Producer side is the cross-workspace `satellite-provider` ticket the user filed separately; gate is at autodev Step 16 (Deploy), NOT a blocker for Step 10 (Implement) per `_docs/02_tasks/_dependencies_table.md` Notes (AZ-497).
- No drift: every consumer-side touch matches the contract's Shape section.
**AZ-499 (7 ACs):**
| AC | Test | Today | Notes |
|----|------|-------|-------|
| AC-1 (env-resolved API key in URL) | `tests/mission_planner_weather.test.ts` AC-1 | PASS | `vi.stubEnv` → spy on `globalThis.fetch` → assert URL contains `appid=<key>&units=metric`. |
| AC-2 (env-resolved base URL) | same file, AC-2 + trailing-slash variant | PASS | Both env-set base and the trailing-slash strip behavior pinned. |
| AC-3 (fail-soft `null` when key unset) | same file, AC-3 | PASS | `expect(result).toBeNull()` + `expect(fetchMock).not.toHaveBeenCalled()`. |
| AC-4 (default base URL when only base unset) | same file, AC-4 | PASS | URL prefix asserted to be `https://api.openweathermap.org/data/2.5/weather?...`. |
| AC-5 (new `owm_key_in_source` static check) | `tests/security/banned-deps.json` adds `owm_key_in_source` kind; `scripts/check-banned-deps.mjs` extends source-tree dispatch with the new kind; `scripts/run-tests.sh` adds `STC-SEC1C` row; `node scripts/check-banned-deps.mjs --kind=owm_key_in_source` exit 0 | PASS | Wired through the same path as `legacy_integrations` / `concurrent_edit_patterns` / `alert_calls` (all also scan src/ + mission-planner/). |
| AC-6 (TS declarations) | `mission-planner/src/vite-env.d.ts` declares both vars; STC-T1 (typecheck) green | PASS | — |
| AC-7 (key revocation deliverable) | **NOT YET COMPLETE** — manual out-of-band | flagged | The compromised key `335799082893fad97fa36118b131f919` MUST be revoked at `https://home.openweathermap.org/api_keys` before the task is marked Done. Implementation cannot self-complete this AC. The `STC-SEC1C` static check ensures any future re-introduction in source fails the build, providing defense in depth even if revocation is delayed. The implementation report records this as a pending deliverable for the user. |
**Spec deviation (recorded once)**: AZ-499's task spec illustrative example used `STC-S6` for the new check ID, but `STC-S6` is already taken by `no WS/GraphQL/gRPC/SSR deps` (run-tests.sh line 533). Used `STC-SEC1C` (parallel to `STC-SEC1` = src/, `STC-SEC1B` = dist/) — same severity-class, naturally adjacent in the report listing. No AC text was changed; only the suggested ID was substituted.
### Phase 3 — Code Quality
- **SRP**: `getTileUrl()` is one concept (URL resolution, mirroring `getOwmBaseUrl` / `getApiBase`). `getWeatherData()` keeps its single public signature unchanged. FlightMap loses two responsibilities (mode state + toggle button); both removals are clean. MiniMap loses `mapType` prop; the rest of its code is untouched.
- **Error handling**: `WeatherService.ts` keeps its `catch { return null }` block (existing fail-soft contract from AZ-448). No new bare catches anywhere.
- **Naming**: `TILE_URL` was renamed to `getTileUrl()` to match the established function-form pattern (`getOwmBaseUrl`, `getApiBase`); also added `DEFAULT_SATELLITE_TILE_URL` exported for tests so the literal isn't duplicated.
- **Complexity**: longest changed function is `FlightMap.tsx` (~95 lines, unchanged from before — net `-3` lines after removing toggle).
- **Test quality**: every AC test asserts behavior, not just non-throw. The colocated test mocks `react-leaflet` to lightweight stand-ins so jsdom doesn't need to satisfy Leaflet's map-init lifecycle — the standard pattern for component tests around Leaflet.
- **Dead code**: removed `flights.planner.satellite` i18n key (only call site was the toggle button), removed `mapType` state, removed `mapType` prop from MiniMap.
### Phase 4 — Security Quick-Scan
- No SQL / command injection surface.
- No new hardcoded secrets — AZ-499 explicitly removes one (`335799082893fad97fa36118b131f919`); the new `STC-SEC1C` check ensures it cannot be reintroduced under either `src/` or `mission-planner/`.
- AZ-498's `crossOrigin="use-credentials"` is the contractually-required cookie-auth ride, not a security loosening — the satellite-provider endpoint is same-origin in production via nginx and rejects unauthenticated requests with 401 (per contract).
- No sensitive data in logs.
### Phase 5 — Performance Scan
- `getTileUrl()` evaluated per render of `<TileLayer>`. Negligible cost (`import.meta.env` lookup + one regex replace). Same shape as `getOwmBaseUrl()`.
- One `<TileLayer>` instead of two (FlightMap previously branched on `mapType`); minor render-time win.
- No N+1 / unbounded fetch / blocking-async regressions.
### Phase 6 — Cross-Task Consistency
- Both tasks add env vars in the same shape (`VITE_*` + `.env.example` mirror + `vite-env.d.ts` declaration).
- Both tasks extend the static profile via the shared `tests/security/banned-deps.json` + `scripts/check-banned-deps.mjs` infrastructure (AZ-499) or via the same `scripts/run-tests.sh` `run_static` row mechanism (AZ-498 makes no STC additions; e2e AC-2 row in `infrastructure.e2e.ts` is the equivalent).
- No interface conflicts; no shared file mutated by both tasks.
- The two tasks are independent in the dependency graph (`AZ-498` deps: `AZ-450`; `AZ-499` deps: `AZ-448, AZ-449, AZ-482`); ordering inside the batch was AZ-499 first (smaller, no cross-workspace dep) then AZ-498.
### Phase 7 — Architecture Compliance
Phase-7 pass after the colocation refactor:
1. **Layer direction** — every changed file's imports respect the Allowed Dependencies table:
- `FlightMap.tsx`, `MiniMap.tsx` (Layer 3 / Application — `05_flights`) → `./types`, `./MiniMap`, `./mapIcons`, `./DrawControl`, `./MapPoint` (intra-component, allowed).
- `WeatherService.ts` (Layer 3 / Application — `05_flights` port-source) → `../types` (intra-component, allowed).
- `src/features/flights/__tests__/satellite_tile.test.tsx` (Blackbox Tests, intra-component) → `../FlightMap`, `../MiniMap`, `../types` (intra-component, ALLOWED) + `../../../../tests/helpers/render` (test-infra → test-body, ALLOWED per module-layout's Blackbox Tests "Imports from" rule).
- `tests/mission_planner_weather.test.ts` (Blackbox Tests) → `../mission-planner/src/services/WeatherService` (test bodies MAY import from `00_foundation` only; mission-planner is `05_flights` port-source. Carve-out: `tsconfig.test.json` already includes `mission-planner/src/test/**/*` — colocated mission-planner tests would be the architecturally-clean home, but mission-planner has NO running test harness today (per `module-layout.md` "Test layout is therefore TBD"). The pragmatic exception: this test file lives under `tests/` so it runs in the main SPA's Vitest environment. Documented in the test file header. STC-ARCH-01's regex does not flag this path because `mission-planner` is not in `COMPONENT_DIRS`; the carve-out is accepted as part of AZ-499's narrow scope and tracked under the broader F1 mission-planner deduplication track. **No new exemption added to STC-ARCH-01.**
2. **Public API respect** — no cross-component reach into another component's internal files via static `import` statements. STC-ARCH-01 PASS.
3. **No new cyclic module dependencies**`getTileUrl` is leaf-level (no imports); `FlightMap`/`MiniMap` already imported `./types`; no new cycles.
4. **Duplicate symbols across components**`getTileUrl` is unique. The trimTrailingSlash idiom (Finding F1) is duplicated but the duplication is structural (independent vite roots, no shared layer).
5. **Cross-cutting concerns not locally re-implemented** — env-var resolution + URL trimming follows the established repo pattern.
**Static gates re-run (post-refactor)**:
- STC-ARCH-01 (no cross-component deep imports) — PASS
- STC-ARCH-02 (no `/api/<service>/` literals in production source) — PASS
- STC-SEC1C (no literal OWM key in src/ + mission-planner/) — PASS
- STC-T1 (`tsc --noEmit -p tsconfig.test.json`) — PASS
- STC-FP22 (i18n key parity en vs ua) — PASS
- STC-FP23 (no raw user strings outside `t()`) — PASS
## Verdict
**PASS_WITH_WARNINGS** — 0 Critical, 0 High, 0 Medium, 1 Low (F1 maintainability, pre-existing pattern not new debt).
Per the implement skill's Auto-Fix Gate (Step 10): only Medium/Low → no auto-fix loop required; proceed to commit.
@@ -0,0 +1,61 @@
# Test Run Report — Phase B Cycle 2 (Step 11)
**Date**: 2026-05-12
**Mode**: functional
**Runner**: `scripts/run-tests.sh` (default profiles: static + fast; e2e env-blocked, see Step 7 / cycle-1 reports)
**Verdict**: PASS_WITH_DOCUMENTED_GATE
**Cycle scope**: AZ-498 (self-hosted satellite tiles) + AZ-499 (mission-planner OWM env hardening) — implemented in batch 11.
## Profile Outcomes
| Profile | Status | Counts | Wall-clock | Report file |
|---------|--------|--------|------------|-------------|
| static | PASS | **32 / 32** including new `STC-SEC1C` (AZ-499) | ~14 s | `test-output/static-report.csv` |
| fast | PASS | 30 files / **224 PASS / 13 SKIP / 0 FAIL** | ~17 s | `test-output/fast-report.xml` |
| e2e | env-blocked (deferred — same registry-access block as Step 7 / cycle 1) | n/a | n/a | n/a |
## Delta vs Cycle 1 (Step 11) baseline
- Fast: 209 / 13 → **224 / 13** (+15 new tests, 0 new skips):
- +8 in `src/features/flights/__tests__/satellite_tile.test.tsx` (AZ-498 AC-1..AC-4 — env-resolved tile URL, cookie-credentialed `<TileLayer>`, classic/satellite toggle removal).
- +7 in `tests/mission_planner_weather.test.ts` (AZ-499 AC-1..AC-4 + happy-path + fail-soft for `getWeatherData()`).
- Static: 31 / 31 → **32 / 32** (+1 new gate):
- `STC-SEC1C` (AZ-499) — no literal OWM key in `src/` + `mission-planner/`. Complements `STC-SEC1` (which scans `src/` for `appid=<chars>`) and `STC-SEC1B` (dist scan) by catching the rotated literal value across both source trees.
- Skip count unchanged at 13 — no new skips introduced this cycle.
## System-Under-Test Reality Gate
PASS:
- `_docs/00_problem/input_data/expected_results/results_report.md` still exists; `_docs/02_document/tests/traceability-matrix.md` still maps every AC. No internal product module was faked, monkeypatched, or replaced with a deterministic fallback by this cycle's batch — verified by review for batch 11.
- The cycle-2 surface is pure config/wire hardening:
- AZ-498: removes the classic/satellite toggle, swaps the OSM tile URL for an env-resolved `getTileUrl()` returning the satellite-provider endpoint, and adds `crossOrigin="use-credentials"` so the browser attaches the satellite-provider auth cookie. The product code under test (`getTileUrl`, `<TileLayer>` props, removed `mapType` state) is real; only `react-leaflet` and `leaflet` (external deps) and `globalThis.fetch` (network boundary) are stubbed — same boundary discipline as the rest of the suite.
- AZ-499: replaces the hardcoded OWM key + base URL in `mission-planner/src/services/WeatherService.ts` with `import.meta.env.VITE_OWM_*` accessors and a fail-soft return when the key is unset. The product code under test (`getWeatherData()` body) is real; only `globalThis.fetch` is stubbed.
- CSV report inspected — all 32 static rows PASS; fast profile rolled-up PASS row points at the JUnit XML.
## Skipped Tests — Same 13 As Cycle 1, Still Legitimate
The 13 skips are byte-for-byte the same set documented in `test_run_report.md` (Step 7) and re-affirmed in `test_run_report_phase_b_cycle1.md`. None of this cycle's two tasks (AZ-498, AZ-499) touched any of the skip conditions:
- AZ-498 changes the tile-base URL plumbing and `<TileLayer>` `crossOrigin` attribute — orthogonal to the auth bootstrap, canvas-editor, i18n, sse-lifecycle, and wire-contract skip surfaces.
- AZ-499 changes only the `mission-planner` weather service — orthogonal to every skip in the SPA surface.
The user-approved acceptance from Step 7 still applies; carried forward without re-prompting.
## Environment Block — e2e Profile
Same as Step 7 / cycle 1: registry-access block on `azaion/{admin,flights,annotations,detect,loader,resource}:test` images. AZ-498 / AZ-499 do not affect Docker images or compose configuration directly — but note: AZ-498 introduced a new `e2e/stubs/tile/` server (already wired into `e2e/docker-compose.suite-e2e.yml` per the staged changes, plus a new `tests/msw/handlers/tiles.ts` and an updated `e2e/tests/infrastructure.e2e.ts`) for the satellite-tile e2e path. These will exercise once the registry-access block clears in the merge-lane CI.
Defer e2e to the merge-lane CI per Step 7's user-approved option A.
## Outcome
Step 11 **passes**. Auto-chain to Step 12 (Test-Spec Sync).
## Open Items
Unchanged from cycle 1:
- F-CUM-5 production-drift backlog — feature cycles continue.
- F-CUM-4 long-running-soak Playwright config tag — recommended fold-in to merge-lane config.
Cycle-2 specific (carried forward, not gating Step 11):
- AZ-499 AC-7 — OWM key revocation at OWM dashboard (pending USER ACTION).
- AZ-498 — satellite-provider cookie-auth (pending CROSS-WORKSPACE; gates Step 16 Deploy).
+105
View File
@@ -0,0 +1,105 @@
# Dependency Scan — Azaion UI
**Date**: 2026-05-12
**Scope**: `package.json` + `bun.lock` (root) and `mission-planner/package.json` + `mission-planner/bun.lock`
**Tool**: `bun audit v1.3.11` (the project's pinned package manager)
**Cycle**: Phase B / Cycle 2 (post AZ-498, AZ-499)
---
## Summary
| Severity | Count | Packages |
|----------|-------|----------|
| Critical | 0 | — |
| High | 1 | `vite` (dev-server only) |
| Moderate | 2 | `vite` (dev-server only), `postcss` (build-time, low surface) |
| Low | 0 | — |
**Both roots (main `ui/` and `mission-planner/`) report the SAME advisory set** — they share the same Vite 6.x + PostCSS 8.5.x major versions.
## Findings
### F-DEP-1 — Vite Arbitrary File Read via Dev Server WebSocket — HIGH
| Field | Value |
|-------|-------|
| Advisory | [GHSA-p9ff-h696-f583](https://github.com/advisories/GHSA-p9ff-h696-f583) |
| Package | `vite` |
| Installed | `6.4.1` (resolved in `bun.lock`) |
| Affected | `vite <= 6.4.1` |
| Fix | Upgrade to `vite >= 6.4.2` (or latest 6.x), or `bun update` |
| Found via | `bun audit` |
| Roots | `ui/` (direct), `mission-planner/` (direct) |
**Production impact**: **NONE.** The Vite dev server is only used during `bun run dev` and `vitest` (test). Production runs `nginx:alpine` serving pre-built static assets from `dist/` (`Dockerfile:8-12`). The Vite WebSocket endpoint does not exist in production.
**Developer-machine impact**: **HIGH** in dev. An attacker on the same network as a developer running `bun run dev` (default `--host` exposes `0.0.0.0`) can read arbitrary files from the developer's filesystem via the WebSocket path traversal. Mitigation: bind dev server to `localhost` only (Vite default unless `--host` is passed).
**Remediation**:
1. `bun update vite` in both roots (drops in-range to `6.4.2+`).
2. Verify build passes (`bun run build`) and fast tests stay green (`scripts/run-tests.sh fast`).
3. CI would-have-blocked check: add `bun audit --high` exit-code gate to `.woodpecker/build-arm.yml` (Phase B follow-up — see infrastructure review).
### F-DEP-2 — Vite Path Traversal in Optimized Deps `.map` Handling — MODERATE
| Field | Value |
|-------|-------|
| Advisory | [GHSA-4w7w-66w2-5vf9](https://github.com/advisories/GHSA-4w7w-66w2-5vf9) |
| Package | `vite` |
| Installed | `6.4.1` |
| Affected | `vite <= 6.4.1` |
| Fix | Upgrade to `vite >= 6.4.2` (same upgrade as F-DEP-1) |
| Found via | `bun audit` |
| Roots | `ui/` (direct), `mission-planner/` (direct) |
**Production impact**: **NONE** — same reason as F-DEP-1; production has no Vite dev server.
**Developer-machine impact**: **MODERATE** — path traversal on `/optimized-deps/<…>.map` paths during dev sessions.
**Remediation**: same upgrade as F-DEP-1 (single `bun update vite` resolves both).
### F-DEP-3 — PostCSS XSS via Unescaped `</style>` in CSS Stringify Output — MODERATE
| Field | Value |
|-------|-------|
| Advisory | [GHSA-qx2v-qp2m-jg93](https://github.com/advisories/GHSA-qx2v-qp2m-jg93) |
| Package | `postcss` (transitive: `vite > postcss`) |
| Installed | `8.5.8` (resolved in `bun.lock`) |
| Affected | `postcss < 8.5.10` |
| Fix | Upgrade to `postcss >= 8.5.10` (transitive — flows through `vite >= 6.4.2`) |
| Found via | `bun audit` |
| Roots | `ui/` (transitive), `mission-planner/` (transitive) |
**Production impact**: **LOW.** The advisory affects code that takes UNTRUSTED CSS as input and feeds it to PostCSS to stringify; the result is then injected into a page, allowing `</style>` breakout → XSS. In this project PostCSS only processes:
- `src/index.css` (controlled, in-repo)
- Tailwind-generated CSS (via `@tailwindcss/vite` 4.2.2, controlled inputs)
- No user-supplied CSS is ever processed.
There is no exploit path in this codebase today. Treat as a hygiene upgrade.
**Build-time impact**: PostCSS runs at build time. The vulnerability surfaces only with attacker-controlled CSS input, which does not occur in this build.
**Remediation**: same upgrade as F-DEP-1/F-DEP-2 (transitive resolution lifts `postcss` to `>= 8.5.10`).
## Combined Remediation
A single command fixes all three findings in both roots:
```bash
bun update vite # in ui/
cd mission-planner && bun update vite
```
Then re-run `bun audit` in both roots to confirm zero findings.
## CI Coverage Gap
`.woodpecker/build-arm.yml` does NOT run `bun audit` today (confirmed by file inspection). The current pipeline catches only static-analysis regressions (`scripts/run-tests.sh static`), not new CVEs entering the lockfile. This is a **MEDIUM** infrastructure finding — see `infrastructure_review.md` F-INF-1.
## Self-verification
- [x] Both `package.json` manifests scanned (`ui/` + `mission-planner/`)
- [x] Each finding has a GHSA advisory ID
- [x] Upgrade paths identified for the High and Moderate findings (single `bun update vite`)
- [x] Production vs. dev impact distinguished for every finding
+236
View File
@@ -0,0 +1,236 @@
# Infrastructure & Configuration Review — Azaion UI
**Date**: 2026-05-12
**Scope**: `Dockerfile`, `nginx.conf`, `.woodpecker/build-arm.yml`, `e2e/docker-compose.suite-e2e.yml`, `.env.example` files, `.gitignore`
**Cycle**: Phase B / Cycle 2
---
## Summary
| Severity | Count |
|----------|-------|
| Critical | 0 |
| High | 0 |
| Medium | 4 (F-INF-1 .. F-INF-4) |
| Low | 1 (F-INF-5) |
All findings are pre-existing infrastructure hardening gaps — no new findings introduced by Cycle 2. Several findings here overlap with `owasp_review.md` A05/A08 entries and are cross-referenced.
---
## Container Security
### `Dockerfile`
```dockerfile
FROM --platform=$BUILDPLATFORM oven/bun:1.3.11-alpine AS build
WORKDIR /app
COPY package.json bun.lock* ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun run build
FROM nginx:alpine
ARG CI_COMMIT_SHA=unknown
ENV AZAION_REVISION=$CI_COMMIT_SHA
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
```
**Verified controls**:
- Multi-stage build → only static assets land in the runtime image; no Bun, no node_modules, no source.
- Alpine base images → minimal attack surface.
- `--frozen-lockfile` → no transitive drift between `bun install` and what was tested.
- `CI_COMMIT_SHA` baked in for traceability.
- `EXPOSE 80` — port surface limited to nginx HTTP.
**Findings**:
- **F-INF-5** — runs as `nginx` default `root` for the master process. The `nginx:alpine` image's default config drops worker processes to `nginx` user; the master remains `root`. Consider switching to `nginxinc/nginx-unprivileged` if the suite ingress permits a non-80 listen port. **Severity: LOW** (industry-standard pattern; minor improvement).
- No `HEALTHCHECK` directive in the Dockerfile (the e2e compose adds one externally). For Kubernetes / external orchestration, add `HEALTHCHECK CMD wget -qO- http://localhost:80/ || exit 1`. **Severity: LOW** (operational, not security).
**No HIGH/MEDIUM issues with the Dockerfile itself.**
---
## CI/CD Security
### `.woodpecker/build-arm.yml`
**Verified controls**:
- Registry credentials sourced from secrets (`from_secret: registry_token` etc.) — never in repo.
- `docker login --password-stdin` — token not in argv (would otherwise leak via `ps`).
- Branch-restricted (`when.branch: [dev, stage, main]`) — feature branches do NOT push to registry.
- OCI labels (`org.opencontainers.image.revision`, `created`, `source`) stamped at build time.
- The image tag is branch-derived (`${CI_COMMIT_BRANCH}-arm`) — production deployments pin to the SHA via OCI label.
**Findings**:
### F-INF-1 — `bun audit` not gated in CI — MEDIUM
**Evidence**: `.woodpecker/build-arm.yml` runs only `docker build` + `docker push`. The static-test pipeline runs in the developer's `scripts/run-tests.sh`, NOT in CI. The Cycle 2 dependency findings (F-DEP-1 vite High, F-DEP-2/F-DEP-3 Moderate) would not have failed CI.
**Risk**: a future dependency upgrade introducing a Critical/High CVE could ship to `dev-arm` undetected.
**Remediation**: insert a step before `build-push`:
```yaml
- name: dep-audit
image: oven/bun:1.3.11-alpine
commands:
- bun audit --severity high # exits non-zero if any High/Critical
```
**Severity**: MEDIUM (visible CVE exposure, easy to fix).
### F-INF-3 — No vulnerability scan on the produced image — MEDIUM
**Evidence**: `.woodpecker/build-arm.yml` does not invoke Trivy, Grype, or any image scanner. Base-image CVEs in `nginx:alpine` are invisible to CI.
**Risk**: nginx alpine releases ship with periodic CVEs (latest run-time vulns in the OS packages). Without a scan, the image can ship vulnerable.
**Remediation**:
```yaml
- name: image-scan
image: aquasec/trivy:latest
commands:
- trivy image --severity HIGH,CRITICAL --exit-code 1 \
$REGISTRY_HOST/azaion/ui:$TAG
```
**Severity**: MEDIUM.
### F-INF-4 — No SBOM emission and no image signing — MEDIUM
**Evidence**: pipeline produces and pushes images but does not emit an SBOM (Syft/cyclonedx) and does not sign images (cosign).
**Risk**: registry compromise or MITM during pull cannot be detected. Post-deploy SBOM-based vulnerability triage is impossible.
**Remediation**: best owned at the suite level — coordinate with the registry team. Adding cosign requires a key management decision (KMS vs. file-based). Typical ordering:
1. Add `syft packages docker:$image -o cyclonedx-json > sbom.json` — emit and store SBOM as a build artifact.
2. Configure cosign keyless via OIDC (if Woodpecker integrates) OR file-based key from secrets.
3. `cosign sign --key cosign.key $image` step + `cosign verify` step in the deploy pipeline.
**Severity**: MEDIUM (supply-chain integrity).
---
## Network Security & Headers
### `nginx.conf`
**Verified controls**:
- Strict `proxy_pass` to fixed upstream service names (no user-controlled URL routing).
- `client_max_body_size 500M` — bounded.
- SPA fallback `try_files $uri $uri/ /index.html` — safe (no upstream rewrite).
- `proxy_read_timeout 86400` on `/api/annotations/` (SSE) and `600` on `/api/detect/` (long video) — explicit per-route limits, not a global config.
**Findings**:
### F-INF-2 — Missing security response headers — MEDIUM
**Evidence**: zero `add_header` directives in `nginx.conf`. None of the standard hardening headers are emitted to the browser:
- `Content-Security-Policy`
- `X-Frame-Options` / CSP `frame-ancestors`
- `Strict-Transport-Security` (depends on suite ingress decision — coordinate)
- `Referrer-Policy: strict-origin-when-cross-origin`
- `X-Content-Type-Options: nosniff`
**Bearer-redaction**: SSE URLs include `?access_token=…` → currently logged in plaintext to nginx access logs. No redaction directive.
**Risk**:
- Without CSP, any future XSS (we have none today, but the surface evolves) gets unrestricted execution.
- Without `frame-ancestors`/`X-Frame-Options`, the SPA can be framed → clickjacking on the operator's session.
- Without `Referrer-Policy`, internal SPA URLs leak to external sites if the operator clicks an outbound link.
- Bearers persist in nginx access logs (operator-internal but still — log retention compounds).
**Remediation** (one PR; recommended starting point per `_docs/00_problem/security_approach.md` §9):
```nginx
add_header Content-Security-Policy "default-src 'self'; img-src 'self' https: data:; connect-src 'self' https://api.openweathermap.org/; frame-ancestors 'none'; object-src 'none'" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header X-Content-Type-Options "nosniff" always;
# add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; # decide with suite ingress
# In the SSE location (e.g. /api/annotations/):
log_format azaion_redact '$remote_addr - $remote_user [$time_local] '
'"$request_method $uri ' # drop ?args from the access log
'$server_protocol" $status $body_bytes_sent';
access_log /var/log/nginx/access.log azaion_redact;
```
**Cycle 2 specific**: the new `<TileLayer crossOrigin="use-credentials">` (AZ-498) needs the production env to point at the same-origin nginx path. The CSP `connect-src 'self'` above already permits this; if the suite-internal `satellite-provider` lives on a different origin it must be added explicitly. This is captured in the AZ-498 deploy gate (Step 16).
**Severity**: MEDIUM.
---
## Environment Configuration
### `.env.example` files
| File | Status |
|------|--------|
| `.env.example` (ui/) | Clean — only documentation comments and empty/placeholder values. Cycle 2 added `VITE_OWM_API_KEY=<your-openweathermap-api-key>`, `VITE_OWM_BASE_URL=`, `VITE_SATELLITE_TILE_URL=` placeholders. |
| `mission-planner/.env.example` | Clean — same pattern. Includes `VITE_SATELLITE_TILE_URL=https://server.arcgisonline.com/...` (legacy default; does NOT contain a real auth key). |
**Verified**: no real secrets committed to either `.env.example`. Both use the `<your-...>` convention.
### `.gitignore`
**Verified** (`grep` against root `.gitignore`): excludes `.env.local`, `.env.development.local`, `.env.test.local`, `.env.production.local`. Real secrets are properly kept out of git.
**Recommendation**: add `mission-planner/.env.local` and equivalents to `mission-planner/.gitignore` (or a root-level pattern that catches both roots) for symmetry. Verify by grep — not part of this audit's automated checks.
---
## E2E Harness Security
### `e2e/docker-compose.suite-e2e.yml`
**Verified controls**:
- Isolated `azaion-test-net` bridge network — no host network access for the runner.
- Stubbed external endpoints (`owm-stub`, `tile-stub`) — Playwright tests cannot accidentally hit production OWM or external tile providers.
- Test DB password is `azaion`/`azaion` — visible in plaintext, but acceptable: the DB is bound to the isolated network and lives only for the e2e run.
- `ENABLE_TEST_ONLY_ENDPOINTS: "true"` is gated to the e2e profile.
- The `azaion-ui` image build wires `VITE_SATELLITE_TILE_URL` to the in-cluster `tile-stub` — confirming AZ-498's env-driven design works end-to-end with no real-world tile auth required.
**No findings.**
---
## Cycle-2 specific infrastructure review (AZ-498, AZ-499)
| Change | Infra review |
|--------|--------------|
| `VITE_SATELLITE_TILE_URL` introduction | OK. `.env.example` documents prod requirement (same-origin nginx path) explicitly. E2E compose wires the test value. No infra regression. |
| Cookie-credentialed tile fetch (`crossOrigin="use-credentials"`) | OK conditional on prod env override. The default (`http://localhost:5100/...`) only works in local dev; misconfiguration in stage/prod (forgetting to set the env var) results in tile failure (UX impact, no security regression). The Step 16 deploy gate covers this. |
| `STC-SEC1C` static check (OWM key in `mission-planner/`) | OK — added to `scripts/run-tests.sh`. No CI integration today (see F-INF-1) — same gap that affects everything else. |
| `mission-planner/.env.example` updated | OK — placeholder convention preserved, no real secrets. |
---
## Recommendations roll-up
| ID | Severity | Effort | Owner | Recommendation |
|----|----------|--------|-------|----------------|
| F-INF-1 | MEDIUM | 1 SP | UI | Add `bun audit --severity high` step to `.woodpecker/build-arm.yml` |
| F-INF-2 | MEDIUM | 2 SP | UI | Add CSP / X-Frame-Options / Referrer-Policy / X-Content-Type-Options + bearer-redaction log format to `nginx.conf` |
| F-INF-3 | MEDIUM | 2 SP | UI / DevOps | Add Trivy image scan step to `.woodpecker/build-arm.yml` |
| F-INF-4 | MEDIUM | 3-5 SP | Suite-wide | SBOM + cosign — coordinate registry decision suite-wide |
| F-INF-5 | LOW | 1 SP | UI | Switch to `nginxinc/nginx-unprivileged` and add `HEALTHCHECK` directive |
---
## Self-verification
- [x] `Dockerfile` reviewed
- [x] `.woodpecker/build-arm.yml` reviewed
- [x] `nginx.conf` reviewed
- [x] `e2e/docker-compose.suite-e2e.yml` reviewed
- [x] `.env.example` files reviewed (root + `mission-planner/`)
- [x] `.gitignore` reviewed
- [x] Cycle 2 deltas individually reviewed
+184
View File
@@ -0,0 +1,184 @@
# OWASP Top 10 Review — Azaion UI
**Date**: 2026-05-12
**Framework**: [OWASP Top 10 — 2021](https://owasp.org/www-project-top-ten/) (current edition; 2024 revision is in draft and not yet final at time of audit)
**Scope**: Browser SPA (`src/`) + nginx + supporting infrastructure
**Cycle**: Phase B / Cycle 2
---
## Summary
| # | Category | Status | Notes |
|---|----------|--------|-------|
| A01 | Broken Access Control | PASS_WITH_KNOWN | Server-authoritative; 1 known UX-only client gap (`/admin` route) |
| A02 | Cryptographic Failures | PASS_WITH_KNOWN | Bearer in memory; refresh in HttpOnly cookie; 1 accepted trade-off (SSE bearer-in-query) |
| A03 | Injection | PASS | No eval/Function; React JSX escapes; URL params encoded |
| A04 | Insecure Design | PASS | Same-origin nginx + bearer-header + SameSite=Strict cookie pattern |
| A05 | Security Misconfiguration | FAIL | nginx missing CSP, X-Frame-Options, HSTS, Referrer-Policy, X-Content-Type-Options, log redaction |
| A06 | Vulnerable & Outdated Components | FAIL | 1 High vite (dev-server only) + 2 Moderate (vite, postcss) — see `dependency_scan.md` |
| A07 | Identification & Authentication Failures | PASS_WITH_KNOWN | 1 known cold-load refresh bug (F2 in `security_approach.md`) |
| A08 | Software & Data Integrity Failures | FAIL | No SBOM, no image signing, no `bun audit` in CI |
| A09 | Security Logging & Monitoring Failures | N/A | Server-side concern; SPA is operator-internal with no client telemetry |
| A10 | Server-Side Request Forgery | N/A | Browser SPA has no server-side request surface |
**Overall posture**: PASS_WITH_WARNINGS. No exploitable vulnerabilities in the production browser bundle. Multiple infrastructure-level hardening gaps are tracked at the suite level (nginx headers, CI scanning).
---
## A01 — Broken Access Control — PASS_WITH_KNOWN
**Server-authoritative model.** Per `_docs/00_problem/security_approach.md` §2 and `_docs/02_document/architecture.md` §7: every authenticated endpoint validates `User.role` and `permissions[]` server-side. The browser is treated as untrusted; the UI inspects `AuthUser.role` only to render or hide nav elements.
**Verified controls**:
- The 401-recovery path in `src/api/client.ts:90` attempts a server-side refresh and surfaces 401/403 to the user — there is no path that "promotes" a denied request client-side.
- IDOR / horizontal-escalation surface: every API URL embeds the suite-side resource ID; the UI does not assemble paths from user input that could be substituted to access another tenant's data. The server is the gate.
- CORS misconfiguration: same-origin via nginx (`nginx.conf:6-72`) — no `Access-Control-Allow-Origin: *` headers anywhere in nginx config.
- Directory traversal: nginx serves `dist/` only with the SPA fallback `try_files $uri $uri/ /index.html` (`nginx.conf:73-75`); proxy_pass directives are scoped to fixed upstream prefixes.
**Known gap (UX-only, not exploitable)**:
- `/admin` route lacks a client-side role-gate. Non-admin users navigating to `/admin` see the broken admin UI flicker before server-side 403s reject the API calls. Tracked as **finding F2 / AC-22** in `security_approach.md`. Server is authoritative — no privilege escalation. Step 4 hardening was paused; remains a Phase B candidate.
---
## A02 — Cryptographic Failures — PASS_WITH_KNOWN
**Verified controls** (per `security_approach.md` §3 + Cycle 2 audit):
- Bearer JWT held only in `AuthContext` React state (memory). Verified by manual grep + `STC-SEC3` static check + `NFT-SEC-01` test (`src/auth/AuthContext.test.tsx`).
- Refresh token in `Secure HttpOnly SameSite=Strict` cookie — never readable by JS. Verified by `STC-SEC4` static check.
- TLS termination at suite ingress (out-of-band of this workspace; documented in `security_approach.md` §9).
- No symmetric encryption performed client-side (no banned crypto libs per `tests/security/banned-deps.json` `signature_libs`).
- No localStorage/sessionStorage/IndexedDB persistence of secrets (`O2` anti-criterion, `persistence_libs` deny-list).
**Accepted trade-off (ADR-008)**:
- SSE bearer-in-query-string (`src/api/sse.ts:11`). `EventSource` cannot send headers; the bearer rides in `?access_token=…`. HTTPS encrypts the URL on the wire, but it appears in nginx access logs and (low risk) browser history. Mitigation: log redaction at the nginx layer is **NOT yet configured** (tracked under A05 below). Acknowledged in `security_approach.md` §4.
**Cycle 2 cryptography review**:
- `<TileLayer crossOrigin="use-credentials">` (AZ-498) — sends cookies on tile requests when same-origin. **Does NOT** send the bearer (bearer travels via `Authorization` header, which Leaflet does not set). Cookie is `SameSite=Strict`, so cross-site is impossible. Clean.
---
## A03 — Injection — PASS
**Browser-side surfaces**:
- **No SQL/NoSQL** — the SPA never builds SQL queries; all DB access is server-side.
- **No eval / Function constructor / setTimeout-of-string** — verified by manual grep this audit (zero matches in `src/` or `mission-planner/`).
- **No template injection** — React JSX escapes string children by default; no `dangerouslySetInnerHTML` anywhere (verified this audit + grep).
- **URL parameter construction** — searched `src/` for hand-built query strings; all observed cases use `encodeURIComponent` or template literals over typed values (e.g. `src/api/sse.ts:11`).
- **OS command injection** — N/A (browser has no shell).
**Output encoding** (XSS):
- React 19 default escaping handles all string content.
- The `HelpModal` ships hardcoded English strings inline (P6 violation — i18n only; XSS-safe).
- Annotation download tainted-canvas issue (`AnnotationsPage.handleDownload`) is a UX bug, not a security defect — image data may taint the canvas, the download silently fails. Already documented in `security_approach.md` §8.
**Cycle 2 review**: `getTileUrl()` reads `import.meta.env.VITE_SATELLITE_TILE_URL` (a build-time-frozen string) and passes it to Leaflet's `TileLayer.url` template. There is no user-controlled input on this path — no template-injection or URL-injection surface.
---
## A04 — Insecure Design — PASS
**Architectural pattern**:
- Same-origin via nginx → cookies scope cleanly; no cross-origin CSRF surface.
- Bearer in `Authorization` header (cannot be auto-attached by a cross-origin form).
- `SameSite=Strict` refresh cookie — no CSRF on the refresh endpoint either.
- Bearer-in-memory + short TTL + 401-retry → minimises window of compromise after XSS.
- No client-side persistence of mutable state — server is source of truth.
This pattern is the recommended approach for an internal-operator SPA per current OWASP cheatsheet guidance.
**No design-level violations identified.** The only design decision flagged in past reviews — the SSE bearer-in-query-string — is an `EventSource`-protocol limitation, not a design choice.
---
## A05 — Security Misconfiguration — FAIL
**Failures (confirmed by `nginx.conf` inspection)**:
- **NO** `Content-Security-Policy` header (recommended starting point in `security_approach.md` §9).
- **NO** `X-Frame-Options: DENY` (or CSP `frame-ancestors`). Clickjacking surface.
- **NO** `Referrer-Policy: strict-origin-when-cross-origin`.
- **NO** `Strict-Transport-Security` (TLS terminated at suite ingress; HSTS should be set there or here — needs decision).
- **NO** `X-Content-Type-Options: nosniff`.
- **NO** bearer-redaction in nginx access logs for SSE URLs (acknowledged in `security_approach.md` §4 and §9).
**Other**:
- Default credentials: not applicable — auth is server-side via the `admin/` service.
- Unnecessary features enabled: nginx config is minimal (only `client_max_body_size 500M` + per-service proxy_pass).
- Verbose error messages in production: not applicable — Vite production build strips dev banners.
**Remediation**: track all six header/redaction items as a single Phase B ticket against the SPA's `nginx.conf` (low risk, code-only change). See `infrastructure_review.md` F-INF-2.
---
## A06 — Vulnerable & Outdated Components — FAIL
**Findings** (full detail in `dependency_scan.md`):
- F-DEP-1: vite `<= 6.4.1` — Arbitrary File Read via Dev Server WebSocket — HIGH (dev-server only).
- F-DEP-2: vite `<= 6.4.1` — Path Traversal in Optimized Deps `.map` — MODERATE (dev-server only).
- F-DEP-3: postcss `< 8.5.10` — XSS via Unescaped `</style>` — MODERATE (low surface — no untrusted CSS in this build).
**Production-bundle exploitability**: NONE — all three findings are dev-time only. Production runtime is `nginx:alpine` serving pre-built static assets.
**Verdict**: FAIL on the audit category because `bun audit` reports a HIGH advisory and the remediation is trivially available (`bun update vite`). Lifting these immediately is straightforward.
---
## A07 — Identification & Authentication Failures — PASS_WITH_KNOWN
**Verified controls**:
- Bearer JWT signed and validated server-side; UI never inspects token contents.
- Refresh-token rotation on 401 (`src/api/client.ts:88-99`).
- Server is authoritative on lockout, brute-force, and MFA enforcement.
- Logout: `POST /api/admin/auth/logout` clears bearer in memory; server invalidates the refresh cookie.
**Known gap**:
- Bootstrap (cold-load) refresh missing `credentials:'include'` (`src/auth/AuthContext.tsx:24`). Effect: even with a valid refresh cookie, cold-load refresh fails → user is bounced to `/login`. Tracked as **F2 / AC-01** with a `it.fails` quarantined test that flips when the fix lands. Documented in `security_approach.md` §1. Functional/UX bug, not a security regression — server still rejects unauthenticated requests properly.
---
## A08 — Software & Data Integrity Failures — FAIL
**Verified controls**:
- `bun install --frozen-lockfile` in `Dockerfile:4` enforces lockfile fidelity (no in-build dependency drift).
- `AZAION_REVISION=$CI_COMMIT_SHA` baked into the image (`Dockerfile:9-10`); OCI labels stamped at push time (`.woodpecker/build-arm.yml:23-28`).
**Failures**:
- **NO SBOM emission** (Syft / cyclonedx-bom). Cannot audit the produced image's bill of materials post-hoc.
- **NO image signing** (cosign / docker content trust). The registry has no integrity guarantee on `ui:dev-arm` / `ui:stage-arm` / `ui:main-arm`.
- **NO vulnerability scan** (Trivy / Grype) on the produced image. Base-image CVEs (e.g. in `nginx:alpine`) are invisible to CI.
- **NO `bun audit` step** in `.woodpecker/build-arm.yml` — Cycle 2 dependency findings would not have failed CI.
**Remediation priorities**:
1. Quick: add `bun audit --high` exit-code gate to the pipeline (catches future regressions).
2. Medium: add Trivy scan on the produced image (surfaces base-image and OS-package CVEs).
3. Long: SBOM + cosign signing — coordinate at the suite level (depends on registry capabilities).
See `infrastructure_review.md` F-INF-1, F-INF-3, F-INF-4.
---
## A09 — Security Logging & Monitoring Failures — N/A
The SPA does not emit audit logs. All audit events are emitted by the server-side suite services (`admin/`, `flights/`, `annotations/`, `detect/`, `loader/`, `resource/`, `gps-denied-*`, `autopilot/`). The browser console is the only client-side log surface; no centralized client telemetry exists today.
**Justification for N/A** (per `security_approach.md` §10 + `_docs/00_problem/anti_criteria.md`): the SPA is internal/operator-only — observability is a suite-level concern intentionally NOT duplicated client-side.
---
## A10 — Server-Side Request Forgery — N/A
The codebase under audit is a browser SPA. There is no server-side request surface that accepts URLs from user input. The SPA's outbound calls are:
- Same-origin nginx proxies (`/api/<service>/*`) — fixed paths, server-authoritative routing.
- Build-time-fixed env URLs: `VITE_OWM_BASE_URL` (defaults to `https://api.openweathermap.org/data/2.5`), `VITE_SATELLITE_TILE_URL` (defaults to `http://localhost:5100/...`).
Neither URL is user-controllable at runtime. The only browser endpoint resembling SSRF — passing the user's address through to Google Geocode in `mission-planner/` — is in the port-source which is NOT shipped (see `static_analysis.md` F-SAST-1). The suite-level recommendation is to proxy any future geocoding through a server-side endpoint to remove the client-visible API key, which would naturally introduce real SSRF surface; that future ticket should explicitly validate URL inputs.
---
## Self-verification
- [x] All ten OWASP Top 10 (2021) categories assessed
- [x] Every FAIL has at least one specific finding with file path / line
- [x] N/A categories have explicit justification
- [x] `security_approach.md` cross-referenced — every existing known-gap is reflected here, not hidden
- [x] Cycle 2 changes (AZ-498, AZ-499) reviewed under each applicable category
+154
View File
@@ -0,0 +1,154 @@
# Security Audit Report — Azaion UI
**Date**: 2026-05-12
**Scope**: `src/` (production SPA), `mission-planner/src/` (port-source — in git history but NOT in production bundle), `nginx.conf`, `Dockerfile`, `.woodpecker/build-arm.yml`, `e2e/` harness, `.env.example` files
**Cycle**: Phase B / Cycle 2 (post AZ-498, AZ-499)
**Verdict**: **FAIL** — 1 HIGH-severity secret leak in port-source (`F-SAST-1` Google Geocode API key) plus 1 HIGH-severity dependency advisory (`F-DEP-1` vite — dev-server only, no prod exposure)
---
## Summary
| Severity | Count | Notes |
|----------|-------|-------|
| Critical | 0 | — |
| High | 2 | F-SAST-1 (production-bundle exposure: NONE today; git-history exposure: HIGH); F-DEP-1 (production exposure: NONE; dev-server: HIGH) |
| Medium | 7 | F-SAST-2, F-SAST-3, F-DEP-2, F-DEP-3, F-INF-1, F-INF-2, F-INF-3, F-INF-4 |
| Low | 2 | F-SAST-4, F-INF-5 |
**Production browser bundle is clean** — no exploitable findings. All HIGH-severity items are concentrated in (a) port-source code that does not ship and (b) dev-time tooling (Vite dev server). The audit's FAIL verdict reflects:
1. The port-source key is a real secret in real git history → must be revoked + externalized following the AZ-499 pattern.
2. CI does not run `bun audit`, so the High Vite advisory shipped through Cycle 2 unflagged → procedural gap to close.
## OWASP Top 10 (2021) Assessment
| # | Category | Status | Findings |
|---|----------|--------|----------|
| A01 | Broken Access Control | PASS_WITH_KNOWN | 1 known UX gap (`/admin` route, F2/AC-22 — pre-existing) |
| A02 | Cryptographic Failures | PASS_WITH_KNOWN | 1 accepted trade-off (SSE bearer-in-query, ADR-008) |
| A03 | Injection | PASS | — |
| A04 | Insecure Design | PASS | — |
| A05 | Security Misconfiguration | FAIL | F-INF-2 (nginx headers + log redaction missing) |
| A06 | Vulnerable & Outdated Components | FAIL | F-DEP-1, F-DEP-2, F-DEP-3 |
| A07 | Identification & Authentication Failures | PASS_WITH_KNOWN | 1 known cold-load refresh bug (F2 — pre-existing) |
| A08 | Software & Data Integrity Failures | FAIL | F-INF-1, F-INF-3, F-INF-4 |
| A09 | Security Logging & Monitoring Failures | N/A | Server-side concern (operator-internal SPA) |
| A10 | Server-Side Request Forgery | N/A | Browser SPA has no server-side request surface |
## Findings (severity-ranked)
| # | Severity | Category | Location | Title |
|---|----------|----------|----------|-------|
| F-SAST-1 | **HIGH** | Secrets in code | `mission-planner/src/config.ts:2` | Hardcoded Google Geocode API key in port-source |
| F-DEP-1 | **HIGH** | Vulnerable component | `vite@6.4.1` (both roots) | Vite Arbitrary File Read via Dev Server WebSocket (GHSA-p9ff-h696-f583) — dev-server only |
| F-INF-1 | MEDIUM | CI/CD | `.woodpecker/build-arm.yml` | `bun audit` not gated in CI pipeline |
| F-INF-2 | MEDIUM | Misconfiguration | `nginx.conf` | Missing CSP, X-Frame-Options, HSTS, Referrer-Policy, X-Content-Type-Options, log-redaction |
| F-INF-3 | MEDIUM | Supply chain | `.woodpecker/build-arm.yml` | No image vulnerability scan (Trivy/Grype) |
| F-INF-4 | MEDIUM | Supply chain | `.woodpecker/build-arm.yml` | No SBOM emission, no image signing (cosign) |
| F-DEP-2 | MEDIUM | Vulnerable component | `vite@6.4.1` | Vite Path Traversal in Optimized Deps `.map` (GHSA-4w7w-66w2-5vf9) — dev-server only |
| F-DEP-3 | MEDIUM | Vulnerable component | `postcss@8.5.8` (transitive) | PostCSS XSS via Unescaped `</style>` (GHSA-qx2v-qp2m-jg93) — low surface |
| F-SAST-2 | MEDIUM | Supply chain | `mission-planner/src/icons/PointIcons.tsx:7` | `unpkg.com` CDN reference in port-source |
| F-SAST-3 | MEDIUM | Coverage gap | `scripts/run-tests.sh` (`STC-SEC2`) | No-CDN gate does not scan `mission-planner/` |
| F-SAST-4 | LOW | Future risk | `mission-planner/src/constants/tileUrls.ts:2-3` | Port-source still uses third-party tile fallbacks |
| F-INF-5 | LOW | Container hardening | `Dockerfile` | nginx runs as root master process; no `HEALTHCHECK` directive |
### Finding Details
#### F-SAST-1 — Hardcoded Google Geocode API key — HIGH
- **Location**: `mission-planner/src/config.ts:2`
- **Value**: `AIzaSyAhvDeYukuyWVrQYbRhuv91bsi_jj5_Iys`
- **Description**: The Google Geocode API key is committed in `mission-planner/` (port-source). Used by `mission-planner/src/flightPlanning/LeftBoard.tsx:114` for address-to-coords lookups.
- **Production-bundle exposure**: NONE today. `src/` does not import from `mission-planner/`; `Dockerfile` builds only `src/`-rooted Vite. The key is NOT in `dist/`.
- **Git-history exposure**: HIGH. Anyone with repo read access can extract the key. Same threat class as the OWM key resolved by AZ-499.
- **Impact**: Quota theft, billing-account abuse, accelerated risk if `mission-planner/` is later ported into the SPA without remediation.
- **Remediation** (mirror AZ-499 / AC-42 pattern):
1. **Revoke** the key at https://console.cloud.google.com/google/maps-apis/credentials (manual, OUT-OF-BAND, USER ACTION). Capture evidence.
2. Externalize: `import.meta.env.VITE_GOOGLE_GEOCODE_KEY` in `mission-planner/src/config.ts` with fail-soft if unset.
3. Update `mission-planner/.env.example` with placeholder.
4. Extend `tests/security/banned-deps.json` `owm_key_in_source` (or add a sibling `google_key_in_source`) section to also block the literal Google key.
5. Long-term: route geocoding via suite-side proxy when the SPA needs it.
- See: `static_analysis.md` F-SAST-1.
#### F-DEP-1 — Vite Arbitrary File Read via Dev Server WebSocket — HIGH
- **Location**: `vite@6.4.1` (resolved in `bun.lock`, both `ui/` and `mission-planner/` roots)
- **Advisory**: [GHSA-p9ff-h696-f583](https://github.com/advisories/GHSA-p9ff-h696-f583)
- **Description**: WebSocket endpoint exposed by `vite dev` allows arbitrary local-file read via path traversal.
- **Production-bundle exposure**: NONE. The Vite dev server is never present in production (`Dockerfile` final stage is `nginx:alpine` serving static `dist/`).
- **Developer-machine exposure**: HIGH if `bun run dev --host` is ever used (binding to `0.0.0.0`); MODERATE for the default `localhost` binding (still a browser-side script attack vector via DNS rebinding).
- **Remediation**: `bun update vite` in both roots → `vite >= 6.4.2`. Verify build + fast tests still pass.
- See: `dependency_scan.md` F-DEP-1.
(Full detail for F-INF-1 .. F-INF-5 in `infrastructure_review.md`; for F-DEP-2/F-DEP-3 in `dependency_scan.md`; for F-SAST-2/F-SAST-3/F-SAST-4 in `static_analysis.md`. Not duplicated here.)
---
## Dependency Vulnerabilities
| Package | GHSA / Advisory | Severity | Installed | Fix |
|---------|-----------------|----------|-----------|-----|
| `vite` | [GHSA-p9ff-h696-f583](https://github.com/advisories/GHSA-p9ff-h696-f583) | HIGH | 6.4.1 | `>= 6.4.2` (bun update vite) |
| `vite` | [GHSA-4w7w-66w2-5vf9](https://github.com/advisories/GHSA-4w7w-66w2-5vf9) | MODERATE | 6.4.1 | `>= 6.4.2` (same upgrade) |
| `postcss` | [GHSA-qx2v-qp2m-jg93](https://github.com/advisories/GHSA-qx2v-qp2m-jg93) | MODERATE | 8.5.8 | `>= 8.5.10` (transitive — flows through Vite upgrade) |
A single `bun update vite` in each root fixes all three.
---
## Recommendations
### Immediate (HIGH — block deploys until done)
- [ ] **F-SAST-1 (USER ACTION + CODE)**: Revoke the Google Geocode API key at the Google Cloud Console, then externalize per AZ-499 pattern. Mirror the manual evidence-capture protocol used for AZ-499 AC-7. Recommended ticket: `AZ-NEW — Externalize Google Geocode key in mission-planner port-source` (3 SP — same shape as AZ-499 minus AC-8 misattribution).
- [ ] **F-DEP-1 / F-DEP-2 / F-DEP-3 (CODE)**: `bun update vite` in `ui/` and `mission-planner/`. Re-run `bun audit` to confirm zero findings. Recommended ticket: `AZ-NEW — Update Vite to fix CVE-2026 advisories` (1 SP).
### Short-term (MEDIUM — Phase B)
- [ ] **F-INF-1**: Add `bun audit --severity high` step to `.woodpecker/build-arm.yml` so future advisory regressions fail CI (1 SP).
- [ ] **F-INF-2**: Add CSP, X-Frame-Options, Referrer-Policy, X-Content-Type-Options + bearer-redaction log format to `nginx.conf` (2 SP). Coordinate HSTS decision with suite ingress.
- [ ] **F-INF-3**: Add Trivy image-scan step to `.woodpecker/build-arm.yml` after `docker build` (2 SP).
- [ ] **F-SAST-2**: Bundle Leaflet marker icon locally instead of `unpkg.com` CDN reference (covered by the same port-source cleanup as F-SAST-1).
- [ ] **F-SAST-3**: Widen no-CDN static gate to scan `mission-planner/` — move pattern into `tests/security/banned-deps.json` and use the existing `check-banned-deps.mjs` widening (2 SP).
### Long-term (Suite-wide / Hardening)
- [ ] **F-INF-4**: SBOM (Syft/cyclonedx) + cosign image signing — coordinate registry capability with suite team (3-5 SP).
- [ ] **F-SAST-4**: Mission-planner port-source modernization will resolve the third-party tile fallbacks naturally — no separate ticket needed.
- [ ] **F-INF-5**: `nginxinc/nginx-unprivileged` migration + `HEALTHCHECK` directive (1 SP, low priority).
### Pre-existing (not introduced by this audit; tracked elsewhere)
- F2 / AC-01 — bootstrap refresh missing `credentials:'include'` (`src/auth/AuthContext.tsx:24`). Quarantined-test acknowledged. Phase B fix.
- AC-22 — `/admin` route lacks client-side role-gate. Server-authoritative, no exploit. Phase B UX fix.
- ADR-008 — SSE bearer-in-query-string. Accepted trade-off; mitigation lives in F-INF-2 (nginx log redaction).
- AZ-499 AC-7 — OWM key revocation manual deliverable. **Pending USER action.**
---
## Cycle 2 — security regression check
No security regressions introduced by AZ-498 or AZ-499. Both changes pass static + fast test suites; the cookie-credentialed tile fetch is correctly scoped to `SameSite=Strict` and same-origin; the OWM env hardening closes the previously quarantined `NFT-SEC-09` source check.
`STC-SEC1C` is now part of the static gate and would catch any future re-introduction of the literal OWM key in either `src/` or `mission-planner/`.
---
## Verdict justification
The verdict is **FAIL** because:
1. F-SAST-1 is a real third-party API key in real git history. The same finding class as AZ-499 — same remediation pattern, same urgency, same need for out-of-band revocation.
2. F-DEP-1 is a HIGH advisory against a current direct dependency. Even with no production exposure, OWASP A06 categorically fails on any actionable HIGH advisory.
Both findings have one-line remediations. Once F-SAST-1 is revoked + externalized and F-DEP-1 is upgraded, a follow-up audit cycle should re-rate the verdict to PASS_WITH_WARNINGS pending the MEDIUM infrastructure tickets.
The production browser bundle itself is **not vulnerable** — the SPA is well-architected (server-authoritative auth, bearer-in-memory + HttpOnly cookie, no eval/injection surface, no client-side persistence). The deficiencies are at the supply-chain, infrastructure, and port-source layers.
## Self-verification
- [x] All findings from Phases 14 included
- [x] No duplicate findings (cross-references used instead)
- [x] Every finding has remediation guidance
- [x] Verdict matches severity logic (FAIL on any HIGH)
- [x] Production-vs-dev impact distinguished for each HIGH finding
- [x] Cycle 2 deltas (AZ-498, AZ-499) explicitly reviewed for regressions
+159
View File
@@ -0,0 +1,159 @@
# Static Analysis — Azaion UI
**Date**: 2026-05-12
**Scope**: `src/` (production SPA), `mission-planner/src/` (port-source — NOT shipped in production bundle but in git history), `nginx.conf`, `.env.example` files
**Method**: targeted ripgrep patterns + manual review, complementing the 32 existing static checks in `scripts/run-tests.sh` (STC-SEC*, STC-N*, STC-S*, STC-ARCH-*)
**Cycle**: Phase B / Cycle 2
---
## Summary
| Severity | Count | New in this audit |
|----------|-------|-------------------|
| Critical | 0 | — |
| High | 1 | F-SAST-1 (Google Geocode API key in `mission-planner/`) |
| Medium | 2 | F-SAST-2 (`unpkg.com` CDN ref in `mission-planner/`), F-SAST-3 (`mission-planner` not covered by `STC-SEC2`) |
| Low | 1 | F-SAST-4 (port-source still uses third-party tile fallbacks) |
**No NEW Critical or High findings in `src/` (production bundle).** All High-severity findings are confined to `mission-planner/` — the inferior port-source documented in `_docs/02_document/components/05_flights/description.md` as "not built; manual reference for porting work".
The 32 existing static checks (run-tests.sh) cover: no eval/Function, no `dangerouslySetInnerHTML`, no token logging, no `innerHTML=` writes, no banned ML/crypto/persistence libs, no hardcoded `/api` literals, TS strict mode, no `target=_blank`, no OWM key in `src/` or `mission-planner/`. All passed in the Cycle 2 test run (`_docs/03_implementation/test_run_report_phase_b_cycle2.md`).
---
## Findings
### F-SAST-1 — Hardcoded Google Geocode API key in `mission-planner/src/config.ts` — HIGH
**Location**: `mission-planner/src/config.ts:2`
```ts
export const GOOGLE_GEOCODE_KEY = 'AIzaSyAhvDeYukuyWVrQYbRhuv91bsi_jj5_Iys';
```
**Used by**: `mission-planner/src/flightPlanning/LeftBoard.tsx:114-115`
```ts
`https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(address)}&key=${GOOGLE_GEOCODE_KEY}`
```
**Production-bundle exposure**: NONE today. `src/` does NOT import from `mission-planner/` (verified via grep: zero matches for `from '.*mission-planner` in `src/`). The `Dockerfile` builds only the main project (`bun run build` produces `dist/` from the `src/` Vite root). `mission-planner/` is a port-source kept around for reference per `_docs/02_document/components/05_flights/description.md` line 59.
**Git-history exposure**: HIGH. The key is committed and visible to anyone who clones the repository, has read access to the upstream remote, or reads any historical revision. Same threat class as the OpenWeatherMap key resolved in **AZ-499** (`_docs/00_problem/security_approach.md` §5).
**Risk**:
- Quota/rate-limit theft (Google charges per geocode call past the free tier).
- Provider account abuse — whoever owns the Google Cloud billing account is liable.
- Accelerated risk if `mission-planner/` is ever ported into the production SPA without this finding being remediated first.
**Remediation** (mirrors AZ-499 / AC-42 pattern):
1. **Revoke the key** at https://console.cloud.google.com/google/maps-apis/credentials (manual, OUT-OF-BAND, USER ACTION). Capture evidence per the AZ-499 AC-7 protocol.
2. Externalize: `import.meta.env.VITE_GOOGLE_GEOCODE_KEY` in `mission-planner/src/config.ts`. Fail-soft if unset (mirror `WeatherService.ts` pattern from AZ-499).
3. Update `mission-planner/.env.example` to advertise the new variable + the `<your-google-geocode-api-key>` placeholder.
4. Extend the `owm_key_in_source` static-check pattern in `tests/security/banned-deps.json` to also block the literal `AIzaSyAhvDeYukuyWVrQYbRhuv91bsi_jj5_Iys` (defense-in-depth — does not replace revocation).
5. Long-term: when geocoding lands in the production SPA, route via a suite-side proxy (no client-visible key — same architecture decision noted in `security_approach.md` §5 for OWM).
**Recommended ticket**: `AZ-NEW (Phase B) — Externalize Google Geocode key in mission-planner port-source` (mirror AZ-499 structure).
---
### F-SAST-2 — `unpkg.com` CDN reference in `mission-planner/` — MEDIUM
**Location**: `mission-planner/src/icons/PointIcons.tsx:7`
```ts
iconUrl: 'https://unpkg.com/leaflet@1.7.1/dist/images/marker-icon.png',
```
**Production-bundle exposure**: NONE today (same reason as F-SAST-1).
**Risk class**:
- Supply-chain: a compromised unpkg.com or a take-over of `leaflet@1.7.1` could replace the icon with a tracking pixel or attack payload.
- Privacy: the browser leaks the user's IP + referer to a third-party CDN on every page load that uses these icons.
- Air-gap incompatibility: the suite is documented as "air-gapped friendly" (`_docs/02_document/architecture.md`); a CDN dependency violates that.
The main `src/` is already protected: `STC-SEC2` (`scripts/run-tests.sh`) blocks `unpkg.com` in `src/`. **`mission-planner/` is currently NOT scanned by STC-SEC2** — see F-SAST-3.
**Remediation**:
- Replace with a relative import (the `leaflet` package is already a dependency; bundling the marker icon locally is one line).
- OR move this asset into the same-origin nginx static path during the eventual port.
**Recommended ticket**: bundle into the same Phase B port-source cleanup task as F-SAST-1.
---
### F-SAST-3 — `STC-SEC2` (no-CDN gate) does NOT scan `mission-planner/` — MEDIUM
**Location**: `scripts/run-tests.sh` (the `src_grep` helper passes `src` only for STC-SEC2)
**Evidence**: `STC-SEC2` is currently `src/`-scoped only; the `owm_key_in_source` and `alert_calls` checks were widened in AZ-499/AZ-466 to scan both `src/` and `mission-planner/` (see `scripts/check-banned-deps.mjs:204`), but the `unpkg.com`/CDN deny-pattern was not.
**Risk**: a future port that copies more `mission-planner/` code into `src/` could re-introduce CDN URLs that the current static gate would not catch on the source side.
**Remediation**:
- Move the no-CDN check into `tests/security/banned-deps.json` as a new section (e.g. `cdn_in_source`) and let `check-banned-deps.mjs` apply it to both roots, mirroring the AZ-499 widening pattern.
- Add the new STC-ID to `_docs/02_document/tests/security-tests.md`.
**Recommended ticket**: `AZ-NEW (Phase B) — Widen no-CDN static gate to cover mission-planner/` (small, 2-3 SP).
---
### F-SAST-4 — Port-source still uses third-party tile fallbacks — LOW
**Location**: `mission-planner/src/constants/tileUrls.ts:2-3`, `mission-planner/.env.example:25`
```ts
export const TILE_URLS = {
classic: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
satellite: import.meta.env.VITE_SATELLITE_TILE_URL || 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
};
```
**Production-bundle exposure**: NONE today (port-source is not built).
**Risk**: if `mission-planner/` is ever shipped, AZ-498's gains are partially undone — the classic tile path goes straight to OSM with NO env override path, and the satellite fallback hits ArcGIS unauthenticated.
**Remediation**: deferred to whichever ticket consumes / replaces the `mission-planner/` port-source in the SPA. Do NOT fix in-place — `mission-planner/` is documented as inferior and slated for removal once the port is complete.
---
## Negative findings (clean — explicitly verified)
| Pattern | `src/` | `mission-planner/src/` | Coverage |
|---------|--------|------------------------|----------|
| `eval(`, `new Function(`, `setTimeout('...')` | clean | clean | manual grep this audit |
| `dangerouslySetInnerHTML`, `innerHTML=`, `outerHTML=`, `document.write` | clean | clean | manual grep this audit |
| `target='_blank'` (without `rel='noopener'`) | clean | clean | manual grep this audit |
| `console.log`/`console.error` of `token`/`bearer`/`password`/`secret`/`key`/`cookie`/`auth` | clean | clean | manual grep this audit |
| `__proto__`, `constructor[…]`, `prototype[…]` (prototype-pollution patterns) | clean | clean | manual grep this audit |
| `localStorage`/`sessionStorage`/`indexedDB` writes of bearer | clean (only test-fixture reads in `auth/AuthContext.test.tsx`) | clean | STC-SEC3 + manual grep |
| `credentials: 'include'` on every authed fetch | present on the 401-recovery path (`src/api/client.ts:90`); KNOWN MISSING on bootstrap refresh (`src/auth/AuthContext.tsx:24`, quarantined test acknowledges Step 4 fix) | n/a (no auth in port-source) | KNOWN — `security_approach.md` §1 finding F2 |
| Hardcoded OWM key `335799082893fad97fa36118b131f919` | clean | clean | STC-SEC1 + STC-SEC1B + STC-SEC1C (AZ-499) |
| Hardcoded URLs other than the OWM endpoint | clean (only `flightPlanUtils.ts:59` `DEFAULT_OWM_BASE_URL` — env-overridable fallback) | F-SAST-1, F-SAST-2, F-SAST-4 above | manual grep this audit |
| Other API-key formats: `AIza…`, `sk_live_`, `pk_live_`, `xox*`, `ghp_`, `AKIA…`, generic 32+ hex | clean | F-SAST-1 only | manual grep this audit |
| `password = '...'`/`secret = '...'`/`api_key = '...'` literals | clean (only `password` field labels and `AdminPage.tsx` form bindings) | clean | manual grep this audit |
---
## Cycle-2 delta — security review of AZ-498 + AZ-499 changes
| Change | Security review |
|--------|----------------|
| `src/features/flights/FlightMap.tsx`, `MiniMap.tsx``<TileLayer crossOrigin="use-credentials" url={getTileUrl()}/>` | OK. `crossOrigin="use-credentials"` only sends cookies to the SAME origin (`/tiles/{z}/{x}/{y}`) when production env points at the same-origin nginx path. Dev default `http://localhost:5100/...` is HTTP and DEV-ONLY (acknowledged in `.env.example:12-16`). Confirms cookie ride for tile auth without exposing the bearer. |
| `src/features/flights/types.ts:63``DEFAULT_SATELLITE_TILE_URL = 'http://localhost:5100/tiles/{z}/{x}/{y}'` | OK. Dev default; production `.env` MUST override. The `.env.example` documentation is explicit. No bearer leakage path. |
| `mission-planner/src/services/WeatherService.ts` — env-resolved key + base URL + fail-soft | OK — matches AZ-499 spec. Key never re-introduced in source (verified by STC-SEC1C). |
| `STC-SEC1C` static gate in `scripts/run-tests.sh` | OK. Defense-in-depth as designed; widens the `STC-SEC1*` family to scan `mission-planner/` for the literal OWM key. |
| Tests `src/features/flights/__tests__/satellite_tile.test.tsx`, `tests/mission_planner_weather.test.ts` | OK. Tests do not contain real secrets; they `vi.stubEnv` with placeholder strings. |
No security regressions introduced by Cycle 2.
---
## Self-verification
- [x] Both source trees scanned (`src/`, `mission-planner/src/`)
- [x] Each finding has a file path + line number + extract
- [x] Test files explicitly excluded from finding lines (only quoted as evidence of negative results)
- [x] Cycle 2 deltas individually reviewed
- [x] Existing static checks not duplicated — only NEW findings or coverage gaps reported
+11 -6
View File
@@ -2,8 +2,8 @@
## Current Step
flow: existing-code
step: 9
name: New Task
step: 15
name: Performance Test
status: not_started
sub_step:
phase: 0
@@ -14,7 +14,12 @@ cycle: 2
tracker: jira
## Notes
- Cycle 1 (Phase B) CLOSED. Retro: `_docs/06_metrics/retro_2026-05-12.md`
(+ `structure_2026-05-12.md`). Lessons appended to `_docs/LESSONS.md`.
Steps 14, 15, 16 SKIPPED (character-identical structural refactor); no
auth/wire/perf/deploy-relevant surface changed.
- Cycle 2 Step 14 CLOSED. Audit: `_docs/05_security/` (5 reports). Verdict:
FAIL (1 HIGH F-SAST-1, 1 HIGH F-DEP-1, 7 MED, 2 LOW). User chose A —
fixed both HIGH inline (AZ-501 Google key, AZ-502 Vite/PostCSS).
Implementation report: `_docs/03_implementation/batch_12_report.md`.
Static + fast: 229 PASS / 13 SKIP / 0 FAIL. Both tickets transitioned to
"In Progress" in Jira. PENDING USER: AZ-501 AC-6 (Google key revocation
at Google Cloud Console) + AZ-499 AC-7 (OWM key revocation, carried from
earlier). PENDING CROSS-WORKSPACE: AZ-498 deploy gate (Step 16).
Phase B follow-ups deferred: F-INF-1..F-INF-5 in security audit report.
+138 -286
View File
@@ -1,6 +1,6 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "azaion-ui",
@@ -40,11 +40,15 @@
"msw": "^2.7.0",
"tailwindcss": "^4.1.1",
"typescript": "~5.7.2",
"vite": "^6.2.0",
"vitest": "^3.0.0",
"vite": "^6.4.2",
"vitest": "^3.2.4",
},
},
},
"overrides": {
"postcss": ">=8.5.10",
"vite": ">=6.4.2",
},
"packages": {
"@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="],
@@ -52,45 +56,45 @@
"@asamuzakjp/css-color": ["@asamuzakjp/css-color@3.2.0", "", { "dependencies": { "@csstools/css-calc": "^2.1.3", "@csstools/css-color-parser": "^3.0.9", "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "lru-cache": "^10.4.3" } }, "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw=="],
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, ""],
"@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
"@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, ""],
"@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
"@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, ""],
"@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
"@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, ""],
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, ""],
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, ""],
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, ""],
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, ""],
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="],
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, ""],
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, ""],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, ""],
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, ""],
"@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="],
"@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, ""],
"@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="],
"@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" } }, ""],
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, ""],
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, ""],
"@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
"@babel/runtime": ["@babel/runtime@7.29.2", "", {}, ""],
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, ""],
"@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
"@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, ""],
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, ""],
"@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="],
@@ -104,59 +108,9 @@
"@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, ""],
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
"@hello-pangea/dnd": ["@hello-pangea/dnd@18.0.1", "", { "dependencies": { "@babel/runtime": "^7.26.7", "css-box-model": "^1.2.1", "raf-schd": "^4.0.3", "react-redux": "^9.2.0", "redux": "^5.0.1" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-xojVWG8s/TGrKT1fC8K2tIWeejJYTAeJuj36zM//yEm/ZrnZUSFGS15BpO+jGZT1ybWvyXmeDJwPYb4dhWlbZQ=="],
"@hello-pangea/dnd": ["@hello-pangea/dnd@18.0.1", "", { "dependencies": { "@babel/runtime": "^7.26.7", "css-box-model": "^1.2.1", "raf-schd": "^4.0.3", "react-redux": "^9.2.0", "redux": "^5.0.1" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, ""],
"@inquirer/ansi": ["@inquirer/ansi@2.0.5", "", {}, "sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw=="],
@@ -172,19 +126,19 @@
"@istanbuljs/schema": ["@istanbuljs/schema@0.1.6", "", {}, "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, ""],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, ""],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, ""],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, ""],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, ""],
"@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="],
"@kurkle/color": ["@kurkle/color@0.3.4", "", {}, ""],
"@mswjs/interceptors": ["@mswjs/interceptors@0.41.8", "", { "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" } }, "sha512-pRLMNKTSGRoLq+KnEB/7OY5vijw1XmcheAAOiv6pj7W1FG32kAGqj1C/RK/cqxRGr1Fh+zBi8sDur8kj3EQv6A=="],
"@mswjs/interceptors": ["@mswjs/interceptors@0.41.9", "", { "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" } }, "sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w=="],
"@open-draft/deferred-promise": ["@open-draft/deferred-promise@3.0.0", "", {}, "sha512-XW375UK8/9SqUVNVa6M0yEy8+iTi4QN5VZ7aZuRFQmy76LRwI9wy5F4YIBU6T+eTe2/DNDo8tqu8RHlwLHM6RA=="],
@@ -194,91 +148,21 @@
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
"@playwright/test": ["@playwright/test@1.59.1", "", { "dependencies": { "playwright": "1.59.1" }, "bin": { "playwright": "cli.js" } }, "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg=="],
"@playwright/test": ["@playwright/test@1.60.0", "", { "dependencies": { "playwright": "1.60.0" }, "bin": { "playwright": "cli.js" } }, "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag=="],
"@react-leaflet/core": ["@react-leaflet/core@3.0.0", "", { "peerDependencies": { "leaflet": "^1.9.0", "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ=="],
"@react-leaflet/core": ["@react-leaflet/core@3.0.0", "", { "peerDependencies": { "leaflet": "^1.9.0", "react": "^19.0.0", "react-dom": "^19.0.0" } }, ""],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, ""],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.0", "", { "os": "android", "cpu": "arm" }, "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.0", "", { "os": "darwin", "cpu": "arm64" }, ""],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.0", "", { "os": "android", "cpu": "arm64" }, "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw=="],
"@tailwindcss/node": ["@tailwindcss/node@4.2.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.2" } }, ""],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.2", "", { "optionalDependencies": { "@tailwindcss/oxide-darwin-arm64": "4.2.2" } }, ""],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.2", "", { "os": "darwin", "cpu": "arm64" }, ""],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.0", "", { "os": "linux", "cpu": "arm" }, "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw=="],
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ=="],
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.0", "", { "os": "linux", "cpu": "x64" }, "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.0", "", { "os": "linux", "cpu": "x64" }, "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw=="],
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.0", "", { "os": "none", "cpu": "arm64" }, "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.0", "", { "os": "win32", "cpu": "x64" }, "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.0", "", { "os": "win32", "cpu": "x64" }, "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w=="],
"@tailwindcss/node": ["@tailwindcss/node@4.2.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.2" } }, "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.2", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.2", "@tailwindcss/oxide-darwin-arm64": "4.2.2", "@tailwindcss/oxide-darwin-x64": "4.2.2", "@tailwindcss/oxide-freebsd-x64": "4.2.2", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", "@tailwindcss/oxide-linux-x64-musl": "4.2.2", "@tailwindcss/oxide-wasm32-wasi": "4.2.2", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" } }, "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.2", "", { "os": "android", "cpu": "arm64" }, "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2", "", { "os": "linux", "cpu": "arm" }, "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.2", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA=="],
"@tailwindcss/vite": ["@tailwindcss/vite@4.2.2", "", { "dependencies": { "@tailwindcss/node": "4.2.2", "@tailwindcss/oxide": "4.2.2", "tailwindcss": "4.2.2" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w=="],
"@tailwindcss/vite": ["@tailwindcss/vite@4.2.2", "", { "dependencies": { "@tailwindcss/node": "4.2.2", "@tailwindcss/oxide": "4.2.2", "tailwindcss": "4.2.2" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, ""],
"@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="],
@@ -290,41 +174,41 @@
"@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, ""],
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, ""],
"@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
"@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, ""],
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, ""],
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, ""],
"@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="],
"@types/geojson": ["@types/geojson@7946.0.16", "", {}, ""],
"@types/leaflet": ["@types/leaflet@1.9.21", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w=="],
"@types/leaflet": ["@types/leaflet@1.9.21", "", { "dependencies": { "@types/geojson": "*" } }, ""],
"@types/leaflet-draw": ["@types/leaflet-draw@1.0.13", "", { "dependencies": { "@types/leaflet": "^1.9" } }, "sha512-YU82kilOaU+wPNbqKCCDfHH3hqepN6XilrBwG/mSeZ/z4ewumaRCOah44s3FMxSu/Aa0SVa3PPJvhIZDUA09mw=="],
"@types/leaflet-draw": ["@types/leaflet-draw@1.0.13", "", { "dependencies": { "@types/leaflet": "^1.9" } }, ""],
"@types/leaflet-polylinedecorator": ["@types/leaflet-polylinedecorator@1.6.5", "", { "dependencies": { "@types/leaflet": "^1.9" } }, "sha512-m3hMuCyii8t7N/t1xc9aMzpA/tTnc/WFq63yR334Fgbw4jDytTCUcTNvACmod6bnZl5oCigqyTd7Pbb+VQtGZQ=="],
"@types/leaflet-polylinedecorator": ["@types/leaflet-polylinedecorator@1.6.5", "", { "dependencies": { "@types/leaflet": "^1.9" } }, ""],
"@types/node": ["@types/node@22.19.18", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-9v00a+dn2yWVsYDEunWC4g/TcRKVq3r8N5FuZp7u0SGrPvdN9c2yXI9bBuf5Fl0hNCb+QTIePTn5pJs2pwBOQQ=="],
"@types/node": ["@types/node@22.19.19", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew=="],
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, ""],
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, ""],
"@types/set-cookie-parser": ["@types/set-cookie-parser@2.4.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw=="],
"@types/statuses": ["@types/statuses@2.0.6", "", {}, "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA=="],
"@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="],
"@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, ""],
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="],
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, ""],
"@vitest/coverage-v8": ["@vitest/coverage-v8@3.2.4", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^1.0.2", "ast-v8-to-istanbul": "^0.3.3", "debug": "^4.4.1", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", "istanbul-reports": "^3.1.7", "magic-string": "^0.30.17", "magicast": "^0.3.5", "std-env": "^3.9.0", "test-exclude": "^7.0.1", "tinyrainbow": "^2.0.0" }, "peerDependencies": { "@vitest/browser": "3.2.4", "vitest": "3.2.4" }, "optionalPeers": ["@vitest/browser"] }, "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ=="],
@@ -356,25 +240,25 @@
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
"attr-accept": ["attr-accept@2.2.5", "", {}, "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ=="],
"attr-accept": ["attr-accept@2.2.5", "", {}, ""],
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.10", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.10", "", { "bin": "dist/cli.cjs" }, ""],
"brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": "cli.js" }, ""],
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"caniuse-lite": ["caniuse-lite@1.0.30001781", "", {}, "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw=="],
"caniuse-lite": ["caniuse-lite@1.0.30001781", "", {}, ""],
"chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="],
"chart.js": ["chart.js@4.5.1", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw=="],
"chart.js": ["chart.js@4.5.1", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, ""],
"check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="],
@@ -388,23 +272,23 @@
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, ""],
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
"cookie": ["cookie@1.1.1", "", {}, ""],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"css-box-model": ["css-box-model@1.2.1", "", { "dependencies": { "tiny-invariant": "^1.0.6" } }, "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw=="],
"css-box-model": ["css-box-model@1.2.1", "", { "dependencies": { "tiny-invariant": "^1.0.6" } }, ""],
"css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="],
"cssstyle": ["cssstyle@4.6.0", "", { "dependencies": { "@asamuzakjp/css-color": "^3.2.0", "rrweb-cssom": "^0.8.0" } }, "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg=="],
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"csstype": ["csstype@3.2.3", "", {}, ""],
"data-urls": ["data-urls@5.0.0", "", { "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0" } }, "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, ""],
"decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="],
@@ -414,7 +298,7 @@
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, ""],
"dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="],
@@ -422,11 +306,11 @@
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
"electron-to-chromium": ["electron-to-chromium@1.5.325", "", {}, "sha512-PwfIw7WQSt3xX7yOf5OE/unLzsK9CaN2f/FvV3WjPR1Knoc1T9vePRVV4W1EM301JzzysK51K7FNKcusCr0zYA=="],
"electron-to-chromium": ["electron-to-chromium@1.5.325", "", {}, ""],
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"enhanced-resolve": ["enhanced-resolve@5.20.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA=="],
"enhanced-resolve": ["enhanced-resolve@5.20.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, ""],
"entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
@@ -440,15 +324,15 @@
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
"esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
"esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/darwin-arm64": "0.25.12" }, "bin": "bin/esbuild" }, ""],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"escalade": ["escalade@3.2.0", "", {}, ""],
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
"expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, ""],
"fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="],
@@ -456,19 +340,19 @@
"fast-wrap-ansi": ["fast-wrap-ansi@0.2.0", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" } }, ""],
"file-selector": ["file-selector@2.1.2", "", { "dependencies": { "tslib": "^2.7.0" } }, "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig=="],
"file-selector": ["file-selector@2.1.2", "", { "dependencies": { "tslib": "^2.7.0" } }, ""],
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, ""],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
"gensync": ["gensync@1.0.0-beta.2", "", {}, ""],
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
@@ -480,7 +364,7 @@
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, ""],
"graphql": ["graphql@16.14.0", "", {}, "sha512-BBvQ/406p+4CZbTpCbVPSxfzrZrbnuWSP1ELYgyS6B+hNeKzgrdB4JczCa5VZUBQrDa9hUngm0KnexY6pJRN5Q=="],
@@ -498,13 +382,13 @@
"html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="],
"html-parse-stringify": ["html-parse-stringify@3.0.1", "", { "dependencies": { "void-elements": "3.1.0" } }, "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg=="],
"html-parse-stringify": ["html-parse-stringify@3.0.1", "", { "dependencies": { "void-elements": "3.1.0" } }, ""],
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"i18next": ["i18next@24.2.3", "", { "dependencies": { "@babel/runtime": "^7.26.10" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-lfbf80OzkocvX7nmZtu7nSTNbrTYR52sLWxPtlXX1zAhVw8WEnFk4puUkCR4B1dNQwbSpEHHHemcZu//7EcB7A=="],
"i18next": ["i18next@24.2.3", "", { "dependencies": { "@babel/runtime": "^7.26.10" }, "peerDependencies": { "typescript": "^5" } }, ""],
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
@@ -528,59 +412,39 @@
"jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"jiti": ["jiti@2.6.1", "", { "bin": "lib/jiti-cli.mjs" }, ""],
"js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="],
"jsdom": ["jsdom@25.0.1", "", { "dependencies": { "cssstyle": "^4.1.0", "data-urls": "^5.0.0", "decimal.js": "^10.4.3", "form-data": "^4.0.0", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.5", "is-potential-custom-element-name": "^1.0.1", "nwsapi": "^2.2.12", "parse5": "^7.1.2", "rrweb-cssom": "^0.7.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^5.0.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0", "ws": "^8.18.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^2.11.2" }, "optionalPeers": ["canvas"] }, "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": "bin/jsesc" }, ""],
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"json5": ["json5@2.2.3", "", { "bin": "lib/cli.js" }, ""],
"leaflet": ["leaflet@1.9.4", "", {}, "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA=="],
"leaflet": ["leaflet@1.9.4", "", {}, ""],
"leaflet-draw": ["leaflet-draw@1.0.4", "", {}, "sha512-rsQ6saQO5ST5Aj6XRFylr5zvarWgzWnrg46zQ1MEOEIHsppdC/8hnN8qMoFvACsPvTioAuysya/TVtog15tyAQ=="],
"leaflet-draw": ["leaflet-draw@1.0.4", "", {}, ""],
"leaflet-polylinedecorator": ["leaflet-polylinedecorator@1.6.0", "", { "dependencies": { "leaflet-rotatedmarker": "^0.2.0" } }, "sha512-kn3krmZRetgvN0wjhgYL8kvyLS0tUogAl0vtHuXQnwlYNjbl7aLQpkoFUo8UB8gVZoB0dhI4Tb55VdTJAcYzzQ=="],
"leaflet-polylinedecorator": ["leaflet-polylinedecorator@1.6.0", "", { "dependencies": { "leaflet-rotatedmarker": "^0.2.0" } }, ""],
"leaflet-rotatedmarker": ["leaflet-rotatedmarker@0.2.0", "", {}, "sha512-yc97gxLXwbZa+Gk9VCcqI0CkvIBC9oNTTjFsHqq4EQvANrvaboib4UdeQLyTnEqDpaXHCqzwwVIDHtvz2mUiDg=="],
"leaflet-rotatedmarker": ["leaflet-rotatedmarker@0.2.0", "", {}, ""],
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.32.0" } }, ""],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, ""],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
"lodash-es": ["lodash-es@4.18.1", "", {}, ""],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
"lodash-es": ["lodash-es@4.18.1", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="],
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": "cli.js" }, ""],
"loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="],
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, ""],
"lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, ""],
"magicast": ["magicast@0.3.5", "", { "dependencies": { "@babel/parser": "^7.25.4", "@babel/types": "^7.25.4", "source-map-js": "^1.2.0" } }, "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ=="],
@@ -598,19 +462,19 @@
"minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"ms": ["ms@2.1.3", "", {}, ""],
"msw": ["msw@2.14.5", "", { "dependencies": { "@inquirer/confirm": "^6.0.11", "@mswjs/interceptors": "^0.41.3", "@open-draft/deferred-promise": "^3.0.0", "@types/statuses": "^2.0.6", "cookie": "^1.1.1", "graphql": "^16.13.2", "headers-polyfill": "^5.0.1", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.11.11", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.1", "type-fest": "^5.5.0", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-X6G05oX4x0e+CNI55KMdhMmwHCBKf2iwazGr+iwsdoJ94JA1ED7wSXb6V+lLPdqFkmIlPiGYvayqnaNcOzobDA=="],
"msw": ["msw@2.14.6", "", { "dependencies": { "@inquirer/confirm": "^6.0.11", "@mswjs/interceptors": "^0.41.3", "@open-draft/deferred-promise": "^3.0.0", "@types/statuses": "^2.0.6", "cookie": "^1.1.1", "graphql": "^16.13.2", "headers-polyfill": "^5.0.1", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.11.11", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.1", "type-fest": "^5.5.0", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-ALe+N10S72cyx94cMcy3Zs4HhXCj35sgeAL4c+WTvKi0zWnbd8/h0lcFqv0mb2P+aSgAdD7p9HzvA0DiUPxsyg=="],
"mute-stream": ["mute-stream@3.0.0", "", {}, "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": "bin/nanoid.cjs" }, ""],
"node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="],
"node-releases": ["node-releases@2.0.36", "", {}, ""],
"nwsapi": ["nwsapi@2.2.23", "", {}, "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"object-assign": ["object-assign@4.1.1", "", {}, ""],
"outvariant": ["outvariant@1.4.3", "", {}, "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA=="],
@@ -628,59 +492,59 @@
"pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picocolors": ["picocolors@1.1.1", "", {}, ""],
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
"picomatch": ["picomatch@4.0.4", "", {}, ""],
"playwright": ["playwright@1.59.1", "", { "dependencies": { "playwright-core": "1.59.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw=="],
"playwright": ["playwright@1.60.0", "", { "dependencies": { "playwright-core": "1.60.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA=="],
"playwright-core": ["playwright-core@1.59.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg=="],
"playwright-core": ["playwright-core@1.60.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA=="],
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
"postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="],
"pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, ""],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"raf-schd": ["raf-schd@4.0.3", "", {}, "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ=="],
"raf-schd": ["raf-schd@4.0.3", "", {}, ""],
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
"react": ["react@19.2.4", "", {}, ""],
"react-chartjs-2": ["react-chartjs-2@5.3.1", "", { "peerDependencies": { "chart.js": "^4.1.1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A=="],
"react-chartjs-2": ["react-chartjs-2@5.3.1", "", { "peerDependencies": { "chart.js": "^4.1.1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, ""],
"react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
"react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, ""],
"react-dropzone": ["react-dropzone@15.0.0", "", { "dependencies": { "attr-accept": "^2.2.4", "file-selector": "^2.1.0", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">= 16.8 || 18.0.0" } }, "sha512-lGjYV/EoqEjEWPnmiSvH4v5IoIAwQM2W4Z1C0Q/Pw2xD0eVzKPS359BQTUMum+1fa0kH2nrKjuavmTPOGhpLPg=="],
"react-dropzone": ["react-dropzone@15.0.0", "", { "dependencies": { "attr-accept": "^2.2.4", "file-selector": "^2.1.0", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">= 16.8 || 18.0.0" } }, ""],
"react-i18next": ["react-i18next@15.7.4", "", { "dependencies": { "@babel/runtime": "^7.27.6", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 23.4.0", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-nyU8iKNrI5uDJch0z9+Y5XEr34b0wkyYj3Rp+tfbahxtlswxSCjcUL9H0nqXo9IR3/t5Y5PKIA3fx3MfUyR9Xw=="],
"react-i18next": ["react-i18next@15.7.4", "", { "dependencies": { "@babel/runtime": "^7.27.6", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 23.4.0", "react": ">= 16.8.0", "typescript": "^5" } }, ""],
"react-icons": ["react-icons@5.6.0", "", { "peerDependencies": { "react": "*" } }, "sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA=="],
"react-icons": ["react-icons@5.6.0", "", { "peerDependencies": { "react": "*" } }, ""],
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
"react-is": ["react-is@16.13.1", "", {}, ""],
"react-leaflet": ["react-leaflet@5.0.0", "", { "dependencies": { "@react-leaflet/core": "^3.0.0" }, "peerDependencies": { "leaflet": "^1.9.0", "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw=="],
"react-leaflet": ["react-leaflet@5.0.0", "", { "dependencies": { "@react-leaflet/core": "^3.0.0" }, "peerDependencies": { "leaflet": "^1.9.0", "react": "^19.0.0", "react-dom": "^19.0.0" } }, ""],
"react-leaflet-draw": ["react-leaflet-draw@0.21.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "lodash-es": "^4.17.15" }, "peerDependencies": { "leaflet": "^1.8.0", "leaflet-draw": "^1.0.4", "prop-types": "^15.5.2", "react": "^19.1.0", "react-leaflet": "^5.0.0" } }, "sha512-MhfGKxh+qT4ngkb616AH6393SPc9zKo3uaVupg6mrZgXM+D8eqadpf5AKTaVseeOnGx7ZmV0j9ZFbvOlIEIojQ=="],
"react-leaflet-draw": ["react-leaflet-draw@0.21.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "lodash-es": "^4.17.15" }, "peerDependencies": { "leaflet": "^1.8.0", "leaflet-draw": "^1.0.4", "prop-types": "^15.5.2", "react": "^19.1.0", "react-leaflet": "^5.0.0" } }, ""],
"react-redux": ["react-redux@9.2.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="],
"react-redux": ["react-redux@9.2.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" } }, ""],
"react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
"react-refresh": ["react-refresh@0.17.0", "", {}, ""],
"react-router": ["react-router@7.13.2", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-tX1Aee+ArlKQP+NIUd7SE6Li+CiGKwQtbS+FfRxPX6Pe4vHOo6nr9d++u5cwg+Z8K/x8tP+7qLmujDtfrAoUJA=="],
"react-router": ["react-router@7.13.2", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, ""],
"react-router-dom": ["react-router-dom@7.13.2", "", { "dependencies": { "react-router": "7.13.2" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-aR7SUORwTqAW0JDeiWF07e9SBE9qGpByR9I8kJT5h/FrBKxPMS6TiC7rmVO+gC0q52Bx7JnjWe8Z1sR9faN4YA=="],
"react-router-dom": ["react-router-dom@7.13.2", "", { "dependencies": { "react-router": "7.13.2" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, ""],
"redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="],
"redux": ["redux@5.0.1", "", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="],
"redux": ["redux@5.0.1", "", {}, ""],
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
"rettime": ["rettime@0.11.11", "", {}, "sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ=="],
"rollup": ["rollup@4.60.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.0", "@rollup/rollup-android-arm64": "4.60.0", "@rollup/rollup-darwin-arm64": "4.60.0", "@rollup/rollup-darwin-x64": "4.60.0", "@rollup/rollup-freebsd-arm64": "4.60.0", "@rollup/rollup-freebsd-x64": "4.60.0", "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", "@rollup/rollup-linux-arm-musleabihf": "4.60.0", "@rollup/rollup-linux-arm64-gnu": "4.60.0", "@rollup/rollup-linux-arm64-musl": "4.60.0", "@rollup/rollup-linux-loong64-gnu": "4.60.0", "@rollup/rollup-linux-loong64-musl": "4.60.0", "@rollup/rollup-linux-ppc64-gnu": "4.60.0", "@rollup/rollup-linux-ppc64-musl": "4.60.0", "@rollup/rollup-linux-riscv64-gnu": "4.60.0", "@rollup/rollup-linux-riscv64-musl": "4.60.0", "@rollup/rollup-linux-s390x-gnu": "4.60.0", "@rollup/rollup-linux-x64-gnu": "4.60.0", "@rollup/rollup-linux-x64-musl": "4.60.0", "@rollup/rollup-openbsd-x64": "4.60.0", "@rollup/rollup-openharmony-arm64": "4.60.0", "@rollup/rollup-win32-arm64-msvc": "4.60.0", "@rollup/rollup-win32-ia32-msvc": "4.60.0", "@rollup/rollup-win32-x64-gnu": "4.60.0", "@rollup/rollup-win32-x64-msvc": "4.60.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ=="],
"rollup": ["rollup@4.60.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-darwin-arm64": "4.60.0", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, ""],
"rrweb-cssom": ["rrweb-cssom@0.7.1", "", {}, "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg=="],
@@ -688,9 +552,9 @@
"saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="],
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"scheduler": ["scheduler@0.27.0", "", {}, ""],
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, ""],
"set-cookie-parser": ["set-cookie-parser@3.1.0", "", {}, "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw=="],
@@ -702,7 +566,7 @@
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, ""],
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
@@ -730,19 +594,19 @@
"tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="],
"tailwindcss": ["tailwindcss@4.2.2", "", {}, "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q=="],
"tailwindcss": ["tailwindcss@4.2.2", "", {}, ""],
"tapable": ["tapable@2.3.2", "", {}, "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA=="],
"tapable": ["tapable@2.3.2", "", {}, ""],
"test-exclude": ["test-exclude@7.0.2", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^10.4.1", "minimatch": "^10.2.2" } }, "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw=="],
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, ""],
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
"tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, ""],
"tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="],
@@ -758,27 +622,27 @@
"tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"tslib": ["tslib@2.8.1", "", {}, ""],
"type-fest": ["type-fest@5.6.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA=="],
"typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
"typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, ""],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"until-async": ["until-async@3.0.2", "", {}, "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw=="],
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, ""],
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, ""],
"vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
"vite": ["vite@6.4.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": "bin/vite.js" }, "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ=="],
"vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
"vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="],
"void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="],
"void-elements": ["void-elements@3.1.0", "", {}, ""],
"w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="],
@@ -806,7 +670,7 @@
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"yallist": ["yallist@3.1.1", "", {}, ""],
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
@@ -814,7 +678,7 @@
"@asamuzakjp/css-color/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, ""],
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
@@ -824,18 +688,6 @@
"@mswjs/interceptors/@open-draft/deferred-promise": ["@open-draft/deferred-promise@2.2.0", "", {}, "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="],
"@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
@@ -844,7 +696,7 @@
"glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
"loose-envify/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"loose-envify/js-tokens": ["js-tokens@4.0.0", "", {}, ""],
"make-dir/semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="],
@@ -856,7 +708,7 @@
"pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
"react-router/set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
"react-router/set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, ""],
"strip-literal/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
+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]))
})
+32
View File
@@ -1 +1,33 @@
# mission-planner — Vite build-time environment variables.
#
# Copy to `.env.local` (gitignored) for local dev. CI / Docker pass the same
# variables through the build environment. Mirrors the main SPA's `.env.example`
# style so devs can keep both roots in sync.
#
# Every variable is OPTIONAL. When unset, the app falls back to:
# - VITE_OWM_API_KEY : undefined → getWeatherData returns null (no fetch)
# - VITE_OWM_BASE_URL : https://api.openweathermap.org/data/2.5
# - VITE_SATELLITE_TILE_URL : Esri ArcGIS World Imagery (legacy default; will
# be migrated in a future cycle to mirror AZ-498)
# - VITE_GOOGLE_GEOCODE_KEY : undefined → geocodeAddress logs a warn and
# returns null (LeftBoard's address box becomes
# a no-op for non-coordinate inputs)
# OpenWeatherMap API key. Required for the wind-effect overlay. Leave unset for
# CI / dry runs — `getWeatherData` returns `null` and the overlay hides itself.
VITE_OWM_API_KEY=<your-openweathermap-api-key>
# OpenWeatherMap REST base URL. Default targets the public endpoint; tests or
# alt deployments may override.
# Example for the suite e2e profile: http://owm-stub:8081/data/2.5
VITE_OWM_BASE_URL=
# Satellite tile URL template. Independent of the main SPA's same-named var
# (different vite root). Today defaults to Esri; AZ-498's swap to the suite's
# own satellite-provider only covers the main SPA.
VITE_SATELLITE_TILE_URL=https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}
# Google Geocode API key. Required by LeftBoard's address-search box (AZ-501).
# Leave unset for CI / dry runs — `geocodeAddress` returns `null` and logs a
# single warning. Coordinate-form input ("lat, lng") still works without a key.
VITE_GOOGLE_GEOCODE_KEY=<your-google-geocode-api-key>
+93 -121
View File
@@ -29,14 +29,18 @@
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"typescript": "^5.7.2",
"vite": "^6.0.5",
"vite": "^6.4.2",
},
},
},
"overrides": {
"postcss": ">=8.5.10",
"vite": ">=6.4.2",
},
"packages": {
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
"@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
"@babel/compat-data": ["@babel/compat-data@7.29.3", "", {}, "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg=="],
"@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
@@ -60,7 +64,7 @@
"@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="],
"@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="],
"@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
@@ -74,6 +78,12 @@
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
"@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
"@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
"@emotion/babel-plugin": ["@emotion/babel-plugin@11.13.5", "", { "dependencies": { "@babel/helper-module-imports": "^7.16.7", "@babel/runtime": "^7.18.3", "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", "@emotion/serialize": "^1.3.3", "babel-plugin-macros": "^3.1.0", "convert-source-map": "^1.5.0", "escape-string-regexp": "^4.0.0", "find-root": "^1.1.0", "source-map": "^0.5.7", "stylis": "4.2.0" } }, "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ=="],
"@emotion/cache": ["@emotion/cache@11.14.0", "", { "dependencies": { "@emotion/memoize": "^0.9.0", "@emotion/sheet": "^1.4.0", "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "stylis": "4.2.0" } }, "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA=="],
@@ -100,58 +110,6 @@
"@emotion/weak-memoize": ["@emotion/weak-memoize@0.4.0", "", {}, "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
"@hello-pangea/dnd": ["@hello-pangea/dnd@16.6.0", "", { "dependencies": { "@babel/runtime": "^7.24.1", "css-box-model": "^1.2.1", "memoize-one": "^6.0.0", "raf-schd": "^4.0.3", "react-redux": "^8.1.3", "redux": "^4.2.1", "use-memo-one": "^1.1.3" }, "peerDependencies": { "react": "^16.8.5 || ^17.0.0 || ^18.0.0", "react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0" } }, "sha512-vfZ4GydqbtUPXSLfAvKvXQ6xwRzIjUSjVU0Sx+70VOhc2xx6CdmJXJ8YhH70RpbTUGjxctslQTHul9sIOxCfFQ=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
@@ -182,61 +140,47 @@
"@mui/utils": ["@mui/utils@5.17.1", "", { "dependencies": { "@babel/runtime": "^7.23.9", "@mui/types": "~7.2.15", "@types/prop-types": "^15.7.12", "clsx": "^2.1.1", "prop-types": "^15.8.1", "react-is": "^19.0.0" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-jEZ8FTqInt2WzxDV8bhImWBqeQRD99c/id/fq83H0ER9tFl+sfZlaAoCdznGvbSQQ9ividMxqSV2c7cC1vBcQg=="],
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
"@oxc-project/types": ["@oxc-project/types@0.129.0", "", {}, "sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg=="],
"@popperjs/core": ["@popperjs/core@2.11.8", "", {}, "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="],
"@react-leaflet/core": ["@react-leaflet/core@2.1.0", "", { "peerDependencies": { "leaflet": "^1.9.0", "react": "^18.0.0", "react-dom": "^18.0.0" } }, "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0", "", { "os": "android", "cpu": "arm64" }, "sha512-TWMZnRLMe63C2Lhyicviu7ZHaU4kxa6PS3rofvc9GmcvptzNN11BcfQ4Sl7MwTOsisQoa2keB/EBdNCAnUo8vA=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-6XcD+8k0gPVItNagEw78/qqcBDwKcwDYS8V2hRmVsfUSIrd8cWe/CBvRDI5toqFyPfj+FJr6t8U6Xj2P2prEew=="],
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-iN/tWVXRQDWvmZlKdceP1Dwug9GDpEymhb9p4xnEe6zvCg5lFmzVljl+1qR1NVx3yfGpr2Na+CuLmv5IU8uzfQ=="],
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jjQMDvvwSOuhOwMszD/klSOjyWMM3zI64hWTj9KT5x4MxRbZAf+7vLQ6qouRhtsLVFHr3f0ILaJAfgENPiQdAQ=="],
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0", "", { "os": "linux", "cpu": "arm" }, "sha512-d//Dtg2x6/m3mbV64yUGNnDGNZaDGRpDLLNGerHQUVObuNaIQaaDp25yUiqGXtHEXX+NP2d0wAlmKgpYgIAJ2A=="],
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-n7Ofp0mx+aB2cC+Sdy5YtMnXtY9lchnHbY+3Yt0uq9JsWQExf4f5Whu0tK0R8Jdc9S6RchTHjIFY7uc92puOVQ=="],
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-EIVjy2cgd7uuMMo94FVkBp7F6DhcZAUwNURkSG3RwUmvAXR6s0ISxM81U+IydcZByPG0pZIHsf1b6kTxoFDgJA=="],
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-JEwwOPcwTLAcpDQlqSmjEmfs63xJnSiUNIGvLcDLUHCWK4XowpS/7c7tUsUH6uT/ct6bMUTdXKfI8967FYj6mg=="],
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-0wjCFhLrihtAubnT9iA0N++0pSV0z5Hg7tNGdNJ4RFaINceHadoF+kiFGyY1qSSNVIAZtLotG8Ju1bgDPkjnFA=="],
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Dfn7iak9BcMMePxcoJfpSbWqnEyrp/dRF63/8qW/eHBdOZov6x5aShLLEYGYdIeSJ6vMLK/XCVB+lGIxm41bQA=="],
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0", "", { "os": "linux", "cpu": "x64" }, "sha512-5/utzzDmD/pD/bmuaUcbTf/sZYy0aztwIVlfpoW1fTjCZ0BaPOMVWGZL1zvgxyi7ZIVYWlxKONHmSbHuiOh8Jw=="],
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0", "", { "os": "none", "cpu": "arm64" }, "sha512-ouJs8VcUomfLfpbUECqFMRqdV4x6aeAK3MA4m6vTrJJjKyWTV5KnxZx7Jd9G+GlDaQQxubcba00x16OyJ1meig=="],
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-E+oHKGiDA+lsKMmFtffDDw91EryDT7uJocrIuCHqhm6bCTM6xFK+3gaCkYOHfPwQr0cCNarSM2xaELoQDz9jJg=="],
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-yYK02n8Rngo+gbm1y6G0+7jk1sJ/2Wt7K0me0Y7k/ErBpyf+LJ2gFpqWVTcRV1rUepBlQRmpgWkTQCiiwrK0Ow=="],
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0", "", { "os": "win32", "cpu": "x64" }, "sha512-14bpChMahXRRXiTwahSl+zzHPW6qQTXtkMuJBFlbo+pqSAews2d4BdCSHfrJ/MBsCZtpmTafsY+1QhBzitcmdg=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.0", "", { "os": "android", "cpu": "arm" }, "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.0", "", { "os": "android", "cpu": "arm64" }, "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.0", "", { "os": "linux", "cpu": "arm" }, "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw=="],
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ=="],
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.0", "", { "os": "linux", "cpu": "x64" }, "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.0", "", { "os": "linux", "cpu": "x64" }, "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw=="],
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.0", "", { "os": "none", "cpu": "arm64" }, "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.0", "", { "os": "win32", "cpu": "x64" }, "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.0", "", { "os": "win32", "cpu": "x64" }, "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
@@ -246,8 +190,6 @@
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="],
"@types/hoist-non-react-statics": ["@types/hoist-non-react-statics@3.3.7", "", { "dependencies": { "hoist-non-react-statics": "^3.3.0" }, "peerDependencies": { "@types/react": "*" } }, "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g=="],
@@ -274,15 +216,15 @@
"babel-plugin-macros": ["babel-plugin-macros@3.1.0", "", { "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", "resolve": "^1.19.0" } }, "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.10", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.29", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ=="],
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
"browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
"caniuse-lite": ["caniuse-lite@1.0.30001781", "", {}, "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw=="],
"caniuse-lite": ["caniuse-lite@1.0.30001792", "", {}, "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw=="],
"chart.js": ["chart.js@4.5.1", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw=="],
@@ -308,6 +250,8 @@
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="],
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
@@ -318,13 +262,13 @@
"domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
"electron-to-chromium": ["electron-to-chromium@1.5.325", "", {}, "sha512-PwfIw7WQSt3xX7yOf5OE/unLzsK9CaN2f/FvV3WjPR1Knoc1T9vePRVV4W1EM301JzzysK51K7FNKcusCr0zYA=="],
"electron-to-chromium": ["electron-to-chromium@1.5.353", "", {}, "sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w=="],
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="],
"esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
@@ -340,7 +284,7 @@
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="],
"hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="],
@@ -348,7 +292,7 @@
"is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="],
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
"is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
@@ -366,6 +310,30 @@
"leaflet-rotatedmarker": ["leaflet-rotatedmarker@0.2.0", "", {}, "sha512-yc97gxLXwbZa+Gk9VCcqI0CkvIBC9oNTTjFsHqq4EQvANrvaboib4UdeQLyTnEqDpaXHCqzwwVIDHtvz2mUiDg=="],
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
@@ -378,9 +346,9 @@
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="],
"node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="],
"node-releases": ["node-releases@2.0.38", "", {}, "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw=="],
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
@@ -398,7 +366,7 @@
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
"postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="],
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
@@ -412,7 +380,7 @@
"react-icons": ["react-icons@5.6.0", "", { "peerDependencies": { "react": "*" } }, "sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA=="],
"react-is": ["react-is@19.2.4", "", {}, "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA=="],
"react-is": ["react-is@19.2.6", "", {}, "sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw=="],
"react-leaflet": ["react-leaflet@4.2.1", "", { "dependencies": { "@react-leaflet/core": "^2.1.0" }, "peerDependencies": { "leaflet": "^1.9.0", "react": "^18.0.0", "react-dom": "^18.0.0" } }, "sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q=="],
@@ -426,11 +394,11 @@
"redux": ["redux@4.2.1", "", { "dependencies": { "@babel/runtime": "^7.9.2" } }, "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w=="],
"resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
"resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="],
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
"rollup": ["rollup@4.60.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.0", "@rollup/rollup-android-arm64": "4.60.0", "@rollup/rollup-darwin-arm64": "4.60.0", "@rollup/rollup-darwin-x64": "4.60.0", "@rollup/rollup-freebsd-arm64": "4.60.0", "@rollup/rollup-freebsd-x64": "4.60.0", "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", "@rollup/rollup-linux-arm-musleabihf": "4.60.0", "@rollup/rollup-linux-arm64-gnu": "4.60.0", "@rollup/rollup-linux-arm64-musl": "4.60.0", "@rollup/rollup-linux-loong64-gnu": "4.60.0", "@rollup/rollup-linux-loong64-musl": "4.60.0", "@rollup/rollup-linux-ppc64-gnu": "4.60.0", "@rollup/rollup-linux-ppc64-musl": "4.60.0", "@rollup/rollup-linux-riscv64-gnu": "4.60.0", "@rollup/rollup-linux-riscv64-musl": "4.60.0", "@rollup/rollup-linux-s390x-gnu": "4.60.0", "@rollup/rollup-linux-x64-gnu": "4.60.0", "@rollup/rollup-linux-x64-musl": "4.60.0", "@rollup/rollup-openbsd-x64": "4.60.0", "@rollup/rollup-openharmony-arm64": "4.60.0", "@rollup/rollup-win32-arm64-msvc": "4.60.0", "@rollup/rollup-win32-ia32-msvc": "4.60.0", "@rollup/rollup-win32-x64-gnu": "4.60.0", "@rollup/rollup-win32-x64-msvc": "4.60.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ=="],
"rolldown": ["rolldown@1.0.0", "", { "dependencies": { "@oxc-project/types": "=0.129.0", "@rolldown/pluginutils": "1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0", "@rolldown/binding-darwin-arm64": "1.0.0", "@rolldown/binding-darwin-x64": "1.0.0", "@rolldown/binding-freebsd-x64": "1.0.0", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0", "@rolldown/binding-linux-arm64-gnu": "1.0.0", "@rolldown/binding-linux-arm64-musl": "1.0.0", "@rolldown/binding-linux-ppc64-gnu": "1.0.0", "@rolldown/binding-linux-s390x-gnu": "1.0.0", "@rolldown/binding-linux-x64-gnu": "1.0.0", "@rolldown/binding-linux-x64-musl": "1.0.0", "@rolldown/binding-openharmony-arm64": "1.0.0", "@rolldown/binding-wasm32-wasi": "1.0.0", "@rolldown/binding-win32-arm64-msvc": "1.0.0", "@rolldown/binding-win32-x64-msvc": "1.0.0" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-yD986aXDESFGS95spT1LAv0jssywP4npMEjmMHyN2/5+eE8qQJUype2AaKkRiLgBgyD0LFlubwAht7VmY8rGoA=="],
"sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="],
@@ -452,7 +420,9 @@
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
@@ -462,7 +432,7 @@
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
"vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
"vite": ["vite@8.0.12", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.14", "rolldown": "1.0.0", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w2dDofOWv2QB09ZITZBsvKTVAlYvPR4IAmrY/v0ir9KvLs0xybR7i48wxhM1/oyBWO34wPns+bPGw5ZrZqDpZg=="],
"world-countries": ["world-countries@5.1.0", "", {}, "sha512-CXR6EBvTbArDlDDIWU3gfKb7Qk0ck2WNZ234b/A0vuecPzIfzzxH+O6Ejnvg1sT8XuiZjVlzOH0h08ZtaO7g0w=="],
@@ -480,6 +450,8 @@
"react-redux/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
"rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0", "", {}, "sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ=="],
"csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
}
}
+5 -1
View File
@@ -8,6 +8,10 @@
"build": "tsc -b && vite build",
"preview": "vite preview"
},
"overrides": {
"vite": ">=6.4.2",
"postcss": ">=8.5.10"
},
"dependencies": {
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
@@ -33,6 +37,6 @@
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"typescript": "^5.7.2",
"vite": "^6.0.5"
"vite": "^6.4.2"
}
}
-1
View File
@@ -1,2 +1 @@
export const COORDINATE_PRECISION = 8;
export const GOOGLE_GEOCODE_KEY = 'AIzaSyAhvDeYukuyWVrQYbRhuv91bsi_jj5_Iys';
@@ -12,7 +12,7 @@ import './LeftBoard.css';
import { actionModes } from '../constants/actionModes';
import { DashedAreaIcon, HideSidebarIcon, ShowSidebarIcon } from '../icons/SidebarIcons';
import { FaLocationDot } from 'react-icons/fa6';
import { GOOGLE_GEOCODE_KEY } from '../config';
import { geocodeAddress } from '../services/GeocodeService';
import type { FlightPoint, CalculatedPointInfo, AircraftParams, LatLngPosition } from '../types';
interface LeftBoardProps {
@@ -108,23 +108,6 @@ export default function LeftBoard({
return null;
};
const geocodeAddress = async (address: string): Promise<LatLngPosition | null> => {
try {
const response = await fetch(
`https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(address)}&key=${GOOGLE_GEOCODE_KEY}`
);
const data = await response.json();
if (data.status === 'OK' && data.results.length > 0) {
const location = data.results[0].geometry.location;
return { lat: location.lat, lng: location.lng };
}
} catch {
return null;
}
return null;
};
return (
<>
{isShowed ?
@@ -0,0 +1,36 @@
import type { LatLngPosition } from '../types';
// AZ-501 — Google Geocode lookup, mirrors the WeatherService pattern from
// AZ-499: build-time env var, fail-soft when unset, fail-soft on network errors.
// Extracted from LeftBoard.tsx so the env-resolution and fail-soft contract
// can be unit-tested in isolation.
const ENDPOINT = 'https://maps.googleapis.com/maps/api/geocode/json';
export const geocodeAddress = async (
address: string,
): Promise<LatLngPosition | null> => {
const apiKey = import.meta.env.VITE_GOOGLE_GEOCODE_KEY;
if (!apiKey) {
// Surface the misconfiguration — geocode is user-triggered (per "Enter"
// keypress) so a single warn per call is informative, not spammy.
console.warn(
'[geocodeAddress] VITE_GOOGLE_GEOCODE_KEY is not set; skipping lookup',
);
return null;
}
const url = `${ENDPOINT}?address=${encodeURIComponent(address)}&key=${apiKey}`;
try {
const response = await fetch(url);
const data = await response.json();
if (data.status === 'OK' && data.results.length > 0) {
const location = data.results[0].geometry.location;
return { lat: location.lat, lng: location.lng };
}
} catch {
return null;
}
return null;
};
@@ -1,8 +1,15 @@
import type { WeatherData } from '../types';
const DEFAULT_BASE_URL = 'https://api.openweathermap.org/data/2.5';
const trimTrailingSlash = (s: string) => s.replace(/\/+$/, '');
export const getWeatherData = async (lat: number, lon: number): Promise<WeatherData | null> => {
const apiKey = '335799082893fad97fa36118b131f919';
const url = `https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&appid=${apiKey}&units=metric`;
const apiKey = import.meta.env.VITE_OWM_API_KEY;
if (!apiKey) return null;
const baseUrl = trimTrailingSlash(import.meta.env.VITE_OWM_BASE_URL ?? '') || DEFAULT_BASE_URL;
const url = `${baseUrl}/weather?lat=${lat}&lon=${lon}&appid=${apiKey}&units=metric`;
try {
const response = await fetch(url);
+3
View File
@@ -1,7 +1,10 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_OWM_API_KEY?: string;
readonly VITE_OWM_BASE_URL?: string;
readonly VITE_SATELLITE_TILE_URL?: string;
readonly VITE_GOOGLE_GEOCODE_KEY?: string;
}
interface ImportMeta {
+6 -2
View File
@@ -33,6 +33,10 @@
"react-leaflet-draw": "^0.21.0",
"react-router-dom": "^7.4.0"
},
"overrides": {
"vite": ">=6.4.2",
"postcss": ">=8.5.10"
},
"devDependencies": {
"@playwright/test": "^1.49.0",
"@tailwindcss/vite": "^4.1.1",
@@ -51,7 +55,7 @@
"msw": "^2.7.0",
"tailwindcss": "^4.1.1",
"typescript": "~5.7.2",
"vite": "^6.2.0",
"vitest": "^3.0.0"
"vite": "^6.4.2",
"vitest": "^3.2.4"
}
}
+3 -1
View File
@@ -198,7 +198,9 @@ function main() {
} else if (
kind === 'legacy_integrations' ||
kind === 'concurrent_edit_patterns' ||
kind === 'alert_calls'
kind === 'alert_calls' ||
kind === 'owm_key_in_source' ||
kind === 'google_key_in_source'
) {
hits = checkSourceTree(section, root, ['src', 'mission-planner'])
} else if (kind === 'destructive_surfaces') {
+21
View File
@@ -214,6 +214,25 @@ if [ "$RUN_STATIC" = "true" ]; then
node "$PROJECT_ROOT/scripts/check-banned-deps.mjs" --kind=owm_key_in_dist
}
# AZ-499 — NFT-SEC-09 AC-1 source-tree portion. Complements STC-SEC1
# (which scans src/ for the `appid=<chars>` pattern only) by catching the
# exact rotated literal value across BOTH src/ AND mission-planner/. This
# closes the AZ-482 gap where mission-planner/'s hardcoded key survived
# because mission-planner/ stays out of dist/ (STC-S5) and src_grep here
# didn't include it.
static_check_no_owm_key_in_source() {
node "$PROJECT_ROOT/scripts/check-banned-deps.mjs" --kind=owm_key_in_source
}
# AZ-501 — F-SAST-1 — defense-in-depth gate that the literal Google Geocode
# API key cannot reappear in src/ or mission-planner/. The user revokes the
# key out-of-band (AZ-501 AC-6); this static check guards against an
# accidental git-history-paste reintroducing the same string. Mirrors the
# STC-SEC1C pattern (literal-string scan across both source trees).
static_check_no_google_key_in_source() {
node "$PROJECT_ROOT/scripts/check-banned-deps.mjs" --kind=google_key_in_source
}
# Source-tree text search. Prefer ripgrep when available (much faster on
# large trees), fall back to POSIX grep -r so the CI runner doesn't need rg.
# Test files (*.test.{ts,tsx}, *.spec.{ts,tsx}) are EXCLUDED — production
@@ -556,6 +575,8 @@ if [ "$RUN_STATIC" = "true" ]; then
run_static "STC-RES09" "nginx exactly 9 /api/* location blocks" "NFT-RES-LIM-09" "n/a" static_check_nginx_route_count
run_static "STC-RES10" "nginx prefix-strip on every /api/<S>/ route" "NFT-RES-LIM-10" "n/a" static_check_nginx_prefix_strip
run_static "STC-SEC1B" "no literal OWM key in dist/" "SEC-09" "63" static_check_no_owm_key_in_dist
run_static "STC-SEC1C" "no literal OWM key in src/ + mission-planner/" "SEC-09" "AZ-499" static_check_no_owm_key_in_source
run_static "STC-SEC1D" "no literal Google Geocode key in src/ + mission-planner/" "F-SAST-1" "AZ-501" static_check_no_google_key_in_source
if [ "$STATIC_FAIL" = "1" ]; then
echo "[run-tests] static profile FAILED — see $STATIC_REPORT"
+5 -12
View File
@@ -8,7 +8,7 @@ import DrawControl from './DrawControl'
import MapPoint from './MapPoint'
import MiniMap from './MiniMap'
import { defaultIcon } from './mapIcons'
import { TILE_URLS } from './types'
import { getTileUrl } from './types'
import type { FlightPoint, CalculatedPointInfo, MapRectangle, ActionMode, MovingPointInfo } from './types'
interface MapEventsProps {
@@ -86,7 +86,6 @@ export default function FlightMap({
}: Props) {
const { t } = useTranslation()
const containerRef = useRef<HTMLDivElement>(null)
const [mapType, setMapType] = useState<'classic' | 'satellite'>('satellite')
const [movingPoint, setMovingPoint] = useState<MovingPointInfo | null>(null)
const [draggablePoints, setDraggablePoints] = useState(points)
const polylineClickRef = useRef(false)
@@ -123,13 +122,14 @@ export default function FlightMap({
<MapContainer center={currentPosition} zoom={15} className="h-full w-full">
<ClickHandler />
<TileLayer
url={mapType === 'classic' ? TILE_URLS.classic : TILE_URLS.satellite}
attribution={mapType === 'classic' ? '&copy; <a href="https://www.openstreetmap.org/copyright">OSM</a>' : 'Satellite'}
url={getTileUrl()}
crossOrigin="use-credentials"
attribution="Satellite"
/>
<MapEvents points={draggablePoints} handlePolylineClick={handlePolylineClick} containerRef={containerRef} onMapMove={onMapMove} />
<SetView center={currentPosition} />
{movingPoint && <MiniMap pointPosition={movingPoint} mapType={mapType} />}
{movingPoint && <MiniMap pointPosition={movingPoint} />}
{draggablePoints.map((point, index) => (
<MapPoint key={point.id}
@@ -171,13 +171,6 @@ export default function FlightMap({
Click and drag on the map to draw a {actionMode === 'workArea' ? 'work area' : 'no-go zone'}
</div>
)}
<button onClick={() => setMapType(m => m === 'classic' ? 'satellite' : 'classic')}
className={`absolute top-2 right-2 z-[400] px-2 py-1 text-xs rounded border ${
mapType === 'satellite' ? 'bg-az-panel border-az-orange text-white' : 'bg-az-panel border-az-border text-az-text'
}`}>
{t('flights.planner.satellite')}
</button>
</div>
)
}
+3 -4
View File
@@ -1,7 +1,7 @@
import { MapContainer, TileLayer, CircleMarker, useMap } from 'react-leaflet'
import { useEffect } from 'react'
import type L from 'leaflet'
import { TILE_URLS } from './types'
import { getTileUrl } from './types'
import type { MovingPointInfo } from './types'
function UpdateCenter({ latlng }: { latlng: L.LatLng }) {
@@ -12,10 +12,9 @@ function UpdateCenter({ latlng }: { latlng: L.LatLng }) {
interface Props {
pointPosition: MovingPointInfo
mapType: 'classic' | 'satellite'
}
export default function MiniMap({ pointPosition, mapType }: Props) {
export default function MiniMap({ pointPosition }: Props) {
return (
<div
className="absolute w-[240px] h-[180px] border border-az-border rounded shadow-lg z-[1000] overflow-hidden pointer-events-none"
@@ -23,7 +22,7 @@ export default function MiniMap({ pointPosition, mapType }: Props) {
>
<MapContainer center={pointPosition.latlng} zoom={18} zoomControl={false}
className="w-full h-full" attributionControl={false}>
<TileLayer url={mapType === 'classic' ? TILE_URLS.classic : TILE_URLS.satellite} />
<TileLayer url={getTileUrl()} crossOrigin="use-credentials" />
<CircleMarker center={pointPosition.latlng} radius={3} color="#fa5252" />
<UpdateCenter latlng={pointPosition.latlng} />
</MapContainer>
@@ -0,0 +1,217 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import type L from 'leaflet'
import { renderWithProviders, screen } from '../../../../tests/helpers/render'
// AZ-498 — self-hosted satellite tiles + drop classic/satellite toggle.
//
// Colocated under src/features/flights/__tests__/ per module-layout's "Tests"
// guidance: keeps the cross-component import surface clean (these tests
// reach into 05_flights internals — `./FlightMap`, `./MiniMap`, `./types` —
// which is intra-component access). Tests/ is reserved for cross-cutting
// black-box suites whose imports must go through public-API barrels.
//
// Covers the spec's fast-profile ACs:
// AC-1 — env-resolved getTileUrl() returns the env var verbatim.
// AC-2 — when the env var is unset, getTileUrl() returns the dev default
// `http://localhost:5100/tiles/{z}/{x}/{y}` (cycle-2 assumption).
// AC-3 — every <TileLayer> the SPA renders sets crossOrigin="use-credentials"
// so the browser attaches the satellite-provider auth cookie.
// AC-4 — the classic/satellite toggle, the `mapType` state, and the
// `MiniMap.Props.mapType` prop are all gone.
//
// Notes:
// - AC-5 is statically enforced by tsc on the new ImportMetaEnv shape +
// the `.env.example` audit; no runtime test needed.
// - AC-6, AC-7 are e2e/contract; AC-8 in the original spec misattributed
// `tile_split_zoom*` (image-annotation surface) — see implementation
// report. AC-9 is enforced by STC-ARCH-01 / STC-ARCH-02.
interface TileLayerProps {
url?: string
crossOrigin?: string
attribution?: string
}
interface MapContainerProps {
children?: React.ReactNode
className?: string
}
vi.mock('react-leaflet', () => ({
MapContainer: ({ children, className }: MapContainerProps) => (
<div data-testid="map-container" className={className}>{children}</div>
),
TileLayer: (props: TileLayerProps) => (
<img
data-testid="tile-layer"
data-tile-url={props.url ?? ''}
data-cross-origin={props.crossOrigin ?? ''}
data-attribution={props.attribution ?? ''}
alt=""
/>
),
Marker: () => null,
Popup: () => null,
Polyline: () => null,
Rectangle: () => null,
CircleMarker: () => null,
useMap: () => ({
on: () => undefined,
off: () => undefined,
setView: () => undefined,
removeLayer: () => undefined,
getCenter: () => ({ lat: 0, lng: 0 }),
invalidateSize: () => undefined,
}),
useMapEvents: () => null,
}))
// Leaflet itself is touched at import time by FlightMap (`L.polyline`,
// `L.Symbol.arrowHead`). Mock the bits the component reaches for so the
// import doesn't blow up under jsdom.
vi.mock('leaflet', () => {
const Lstub = {
polyline: () => ({ addTo: () => Lstub.polyline(), on: () => undefined }),
polylineDecorator: () => ({ addTo: () => undefined }),
Symbol: { arrowHead: () => ({}) },
Icon: { Default: class { mergeOptions() {} } },
Marker: class {},
Layer: class {},
LatLngBounds: class {},
}
return { default: Lstub }
})
vi.mock('leaflet/dist/leaflet.css', () => ({}))
vi.mock('leaflet-polylinedecorator', () => ({}))
vi.mock('../DrawControl', () => ({ default: () => null }))
vi.mock('../MapPoint', () => ({ default: () => null }))
vi.mock('../mapIcons', () => ({ defaultIcon: {} }))
import FlightMap from '../FlightMap'
import MiniMap from '../MiniMap'
import { getTileUrl, DEFAULT_SATELLITE_TILE_URL } from '../types'
const stubLatLng = { lat: 0, lng: 0 } as unknown as L.LatLng
const fixedPosition = { lat: 50, lng: 30 }
const baseFlightMapProps = {
points: [],
calculatedPointInfo: [],
currentPosition: fixedPosition,
rectangles: [],
setRectangles: () => undefined,
rectangleColor: 'red',
actionMode: 'points' as const,
onAddPoint: () => undefined,
onUpdatePoint: () => undefined,
onRemovePoint: () => undefined,
onAltitudeChange: () => undefined,
onMetaChange: () => undefined,
onPolylineClick: () => undefined,
onPositionChange: () => undefined,
onMapMove: () => undefined,
}
describe('AZ-498 — getTileUrl() env resolution', () => {
afterEach(() => {
vi.unstubAllEnvs()
})
it('AC-1: returns the env-set VITE_SATELLITE_TILE_URL verbatim', () => {
// Arrange
vi.stubEnv('VITE_SATELLITE_TILE_URL', 'http://satellite-provider:5100/tiles/{z}/{x}/{y}')
// Assert
expect(getTileUrl()).toBe('http://satellite-provider:5100/tiles/{z}/{x}/{y}')
})
it('AC-2: returns the dev default when VITE_SATELLITE_TILE_URL is unset', () => {
// Arrange
vi.stubEnv('VITE_SATELLITE_TILE_URL', '')
// Assert
expect(getTileUrl()).toBe(DEFAULT_SATELLITE_TILE_URL)
expect(DEFAULT_SATELLITE_TILE_URL).toBe('http://localhost:5100/tiles/{z}/{x}/{y}')
})
it('AC-2: strips trailing slashes off the env-set URL', () => {
// Arrange
vi.stubEnv('VITE_SATELLITE_TILE_URL', 'http://satellite-provider:5100/tiles/{z}/{x}/{y}/')
// Assert
expect(getTileUrl()).toBe('http://satellite-provider:5100/tiles/{z}/{x}/{y}')
})
})
describe('AZ-498 — FlightMap satellite-only TileLayer', () => {
beforeEach(() => {
vi.stubEnv('VITE_SATELLITE_TILE_URL', '')
})
afterEach(() => {
vi.unstubAllEnvs()
})
it('AC-3: <TileLayer> declares crossOrigin="use-credentials"', () => {
// Act
renderWithProviders(<FlightMap {...baseFlightMapProps} />, { withoutAuth: true })
// Assert
const tile = screen.getByTestId('tile-layer')
expect(tile.getAttribute('data-cross-origin')).toBe('use-credentials')
})
it('AC-3: <TileLayer> renders the dev-default URL when env is unset', () => {
// Act
renderWithProviders(<FlightMap {...baseFlightMapProps} />, { withoutAuth: true })
// Assert
const tile = screen.getByTestId('tile-layer')
expect(tile.getAttribute('data-tile-url')).toBe(DEFAULT_SATELLITE_TILE_URL)
})
it('AC-4: the classic/satellite toggle button is gone', () => {
// Act
renderWithProviders(<FlightMap {...baseFlightMapProps} />, { withoutAuth: true })
// Assert
expect(screen.queryByRole('button', { name: /satellite|classic/i })).toBeNull()
// Only one <TileLayer> is mounted (no per-mode branching).
expect(screen.getAllByTestId('tile-layer')).toHaveLength(1)
})
})
describe('AZ-498 — MiniMap satellite-only TileLayer', () => {
beforeEach(() => {
vi.stubEnv('VITE_SATELLITE_TILE_URL', '')
})
afterEach(() => {
vi.unstubAllEnvs()
})
it('AC-3: MiniMap <TileLayer> declares crossOrigin="use-credentials"', () => {
// Act
renderWithProviders(
<MiniMap pointPosition={{ x: 0, y: 0, latlng: stubLatLng }} />,
{ withoutAuth: true },
)
// Assert
const tile = screen.getByTestId('tile-layer')
expect(tile.getAttribute('data-cross-origin')).toBe('use-credentials')
})
it('AC-4: MiniMap mounts with only `pointPosition` prop (no `mapType`)', () => {
// Act — explicitly omit mapType; if MiniMap still required it, TS would
// error at compile time. The runtime render also confirms the component
// mounts with just the position prop.
renderWithProviders(
<MiniMap pointPosition={{ x: 0, y: 0, latlng: stubLatLng }} />,
{ withoutAuth: true },
)
// Assert
expect(screen.getByTestId('tile-layer')).toBeInTheDocument()
})
})
+14 -9
View File
@@ -52,13 +52,18 @@ export const PURPOSES = [
export const COORDINATE_PRECISION = 8
const trimTrailingSlash = (s: string) => s.replace(/\/+$/, '')
// AZ-498 — single self-hosted satellite tile URL. The previous classic/satellite
// pair (OSM + Esri) was retired so the SPA only consumes the suite's own
// satellite-provider service. Production builds MUST set VITE_SATELLITE_TILE_URL
// to the same-origin nginx path (e.g. `/tiles/{z}/{x}/{y}`); the dev default
// targets the satellite-provider container on its conventional dev port.
//
// Read via a function (mirrors `getOwmBaseUrl` in flightPlanUtils.ts) so tests
// can stub `import.meta.env` per-case without module-reload tricks.
export const DEFAULT_SATELLITE_TILE_URL = 'http://localhost:5100/tiles/{z}/{x}/{y}'
export const TILE_URLS = {
classic:
trimTrailingSlash(import.meta.env.VITE_OSM_TILE_URL ?? '') ||
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
satellite:
trimTrailingSlash(import.meta.env.VITE_ESRI_TILE_URL ?? '') ||
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
} as const
export function getTileUrl(): string {
const raw = import.meta.env.VITE_SATELLITE_TILE_URL
if (!raw) return DEFAULT_SATELLITE_TILE_URL
return raw.replace(/\/+$/, '')
}
-1
View File
@@ -77,7 +77,6 @@
},
"invalidJson": "Invalid JSON format",
"editJsonHint": "Edit the JSON data as needed.",
"satellite": "Satellite",
"cameraFov": "Camera FOV / Length / Field",
"cameraFovPlaceholder": "FOV parameters",
"commAddr": "Communication Addr / Port",
-1
View File
@@ -77,7 +77,6 @@
},
"invalidJson": "Невірний JSON формат",
"editJsonHint": "Відредагуйте JSON дані за потреби.",
"satellite": "Супутник",
"cameraFov": "Камера FOV / Фокус",
"cameraFovPlaceholder": "Параметри FOV",
"commAddr": "Адреса / Порт",
+1 -2
View File
@@ -7,8 +7,7 @@ interface ImportMetaEnv {
readonly VITE_API_BASE_URL?: string
readonly VITE_OWM_API_KEY?: string
readonly VITE_OWM_BASE_URL?: string
readonly VITE_OSM_TILE_URL?: string
readonly VITE_ESRI_TILE_URL?: string
readonly VITE_SATELLITE_TILE_URL?: string
}
interface ImportMeta {
+101
View File
@@ -0,0 +1,101 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { geocodeAddress } from '../mission-planner/src/services/GeocodeService'
// AZ-501 — mission-planner GeocodeService env-var hardening (mirrors AZ-499).
//
// Lives under tests/ rather than colocated under mission-planner/ for the
// same reason as mission_planner_weather.test.ts: mission-planner has no
// runner of its own; the suite Vitest config covers mission-planner/src.
type FetchMock = ReturnType<typeof vi.fn>
const okResponse = (lat: number, lng: number) =>
new Response(
JSON.stringify({
status: 'OK',
results: [{ geometry: { location: { lat, lng } } }],
}),
{ status: 200 },
)
describe('AZ-501 — mission-planner geocodeAddress (env vars + fail-soft)', () => {
let fetchMock: FetchMock
let warnSpy: ReturnType<typeof vi.spyOn>
beforeEach(() => {
fetchMock = vi.fn(async () => okResponse(50.45, 30.52))
vi.spyOn(globalThis, 'fetch').mockImplementation(fetchMock)
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
})
afterEach(() => {
vi.restoreAllMocks()
vi.unstubAllEnvs()
})
it('AC-1: env-var resolved API key reaches the outgoing fetch URL', async () => {
// Arrange
vi.stubEnv('VITE_GOOGLE_GEOCODE_KEY', 'env-key-xyz')
// Act
const result = await geocodeAddress('Kyiv, Ukraine')
// Assert
expect(fetchMock).toHaveBeenCalledTimes(1)
const url = String(fetchMock.mock.calls[0][0])
expect(url).toContain('key=env-key-xyz')
expect(url).toContain('address=Kyiv%2C%20Ukraine')
expect(result).toEqual({ lat: 50.45, lng: 30.52 })
})
it('AC-3: returns null, issues no fetch, and warns when VITE_GOOGLE_GEOCODE_KEY is unset', async () => {
// Arrange
vi.stubEnv('VITE_GOOGLE_GEOCODE_KEY', '')
// Act
const result = await geocodeAddress('Kyiv, Ukraine')
// Assert
expect(result).toBeNull()
expect(fetchMock).not.toHaveBeenCalled()
expect(warnSpy).toHaveBeenCalledTimes(1)
expect(String(warnSpy.mock.calls[0][0])).toContain('VITE_GOOGLE_GEOCODE_KEY')
})
it('AC-3: still returns null and does not throw when fetch rejects (network error fail-soft)', async () => {
// Arrange
vi.stubEnv('VITE_GOOGLE_GEOCODE_KEY', 'env-key-xyz')
fetchMock.mockRejectedValueOnce(new Error('boom'))
// Act
const result = await geocodeAddress('Kyiv, Ukraine')
// Assert
expect(result).toBeNull()
})
it('returns null when the response status is non-OK (e.g. ZERO_RESULTS)', async () => {
// Arrange
vi.stubEnv('VITE_GOOGLE_GEOCODE_KEY', 'env-key-xyz')
fetchMock.mockResolvedValueOnce(
new Response(JSON.stringify({ status: 'ZERO_RESULTS', results: [] }), {
status: 200,
}),
)
// Act
const result = await geocodeAddress('nowhere-place-12345')
// Assert
expect(result).toBeNull()
})
it('AC-4 (defense-in-depth): no live key string is hardcoded in the service module', async () => {
// Assert: importing GeocodeService.ts must NOT bring along the previously
// hardcoded literal. Done as a sanity assertion on the resolved URL when
// no env var is present — ensures we cannot accidentally leak a fallback.
vi.stubEnv('VITE_GOOGLE_GEOCODE_KEY', '')
await geocodeAddress('test')
expect(fetchMock).not.toHaveBeenCalled()
})
})
+116
View File
@@ -0,0 +1,116 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { getWeatherData } from '../mission-planner/src/services/WeatherService'
// AZ-499 — mission-planner WeatherService env-var hardening.
//
// Lives under tests/ (Blackbox-Tests-owned) rather than colocated under
// mission-planner/ because mission-planner does not have its own runner;
// the suite Vitest config already includes mission-planner/src in coverage
// and tsconfig.test.json picks up tests/** for type-check (STC-T1).
type FetchMock = ReturnType<typeof vi.fn>
describe('AZ-499 — mission-planner getWeatherData (env vars + fail-soft)', () => {
let fetchMock: FetchMock
beforeEach(() => {
fetchMock = vi.fn(async () =>
new Response(JSON.stringify({ wind: { speed: 5, deg: 90 } }), { status: 200 }),
)
vi.spyOn(globalThis, 'fetch').mockImplementation(fetchMock)
})
afterEach(() => {
vi.restoreAllMocks()
vi.unstubAllEnvs()
})
it('AC-1: env-var resolved API key reaches the outgoing fetch URL', async () => {
// Arrange
vi.stubEnv('VITE_OWM_API_KEY', 'abc123')
vi.stubEnv('VITE_OWM_BASE_URL', '')
// Act
await getWeatherData(50, 30)
// Assert
expect(fetchMock).toHaveBeenCalledTimes(1)
const url = String(fetchMock.mock.calls[0][0])
expect(url).toContain('appid=abc123')
expect(url).toContain('units=metric')
})
it('AC-2: env-var resolved base URL prefixes the outgoing fetch URL', async () => {
// Arrange
vi.stubEnv('VITE_OWM_API_KEY', 'abc123')
vi.stubEnv('VITE_OWM_BASE_URL', 'https://example.test/data/2.5')
// Act
await getWeatherData(50, 30)
// Assert
const url = String(fetchMock.mock.calls[0][0])
expect(url.startsWith('https://example.test/data/2.5/weather?')).toBe(true)
})
it('AC-2: trailing slash on env base URL is stripped', async () => {
// Arrange
vi.stubEnv('VITE_OWM_API_KEY', 'abc123')
vi.stubEnv('VITE_OWM_BASE_URL', 'https://example.test/data/2.5/')
// Act
await getWeatherData(50, 30)
// Assert
const url = String(fetchMock.mock.calls[0][0])
expect(url.startsWith('https://example.test/data/2.5/weather?')).toBe(true)
})
it('AC-3: returns null and issues no fetch when VITE_OWM_API_KEY is unset', async () => {
// Arrange
vi.stubEnv('VITE_OWM_API_KEY', '')
// Act
const result = await getWeatherData(50, 30)
// Assert
expect(result).toBeNull()
expect(fetchMock).not.toHaveBeenCalled()
})
it('AC-4: defaults to public OWM base URL when only VITE_OWM_BASE_URL is unset', async () => {
// Arrange
vi.stubEnv('VITE_OWM_API_KEY', 'abc123')
vi.stubEnv('VITE_OWM_BASE_URL', '')
// Act
await getWeatherData(50, 30)
// Assert
const url = String(fetchMock.mock.calls[0][0])
expect(url.startsWith('https://api.openweathermap.org/data/2.5/weather?')).toBe(true)
})
it('returns the parsed wind shape on a successful response', async () => {
// Arrange
vi.stubEnv('VITE_OWM_API_KEY', 'abc123')
// Act
const result = await getWeatherData(50, 30)
// Assert
expect(result).toEqual({ windSpeed: 5, windAngle: 90 })
})
it('returns null when fetch rejects (network error fail-soft)', async () => {
// Arrange
vi.stubEnv('VITE_OWM_API_KEY', 'abc123')
fetchMock.mockRejectedValueOnce(new Error('boom'))
// Act
const result = await getWeatherData(50, 30)
// Assert
expect(result).toBeNull()
})
})
+19 -13
View File
@@ -1,8 +1,13 @@
import { http, HttpResponse } from 'msw'
// OSM/Esri tile stand-in for the fast profile. Returns a tiny transparent
// PNG so `<img>` / Leaflet tile loads succeed in jsdom without exiting the
// process.
// Satellite-provider tile stand-in for the fast profile (AZ-498).
// Returns a tiny transparent PNG so `<img>` / Leaflet tile loads succeed in
// jsdom without exiting the process.
//
// The contract `_docs/02_document/contracts/satellite-provider/tiles.md`
// (v1.0.0) freezes the path shape `/tiles/{z}/{x}/{y}` (no `.png` suffix,
// `image/jpeg` Content-Type, cookie auth on the same origin). The handler
// matches that exact shape and the dev default URL the SPA falls back to.
const TILE_PNG = Uint8Array.from([
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4,
@@ -11,16 +16,17 @@ const TILE_PNG = Uint8Array.from([
0x42, 0x60, 0x82,
])
const tile = () => new HttpResponse(TILE_PNG, { headers: { 'Content-Type': 'image/png' } })
const tile = () =>
new HttpResponse(TILE_PNG, {
headers: {
'Content-Type': 'image/jpeg',
'Cache-Control': 'public, max-age=86400',
'ETag': '"fast-profile-fixture"',
},
})
export const tilesHandlers = [
// OSM XYZ scheme: {z}/{x}/{y}
http.get('https://*.tile.openstreetmap.org/:z/:x/:y.png', tile),
http.get('https://tile.openstreetmap.org/:z/:x/:y.png', tile),
// Esri ArcGIS satellite scheme: {z}/{y}/{x}
http.get('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/:z/:y/:x', tile),
// Local tile-stub aliases (e2e parity)
http.get('http://tile-stub:8082/:z/:x/:y.png', tile),
http.get('http://tile-stub:8082/sat/:z/:y/:x', tile),
http.get('/tiles/:z/:x/:y.png', tile),
http.get('/tiles/:z/:x/:y', tile),
http.get('http://localhost:5100/tiles/:z/:x/:y', tile),
http.get('http://tile-stub:8082/tiles/:z/:x/:y', tile),
]
+16
View File
@@ -82,6 +82,22 @@
"335799082893fad97fa36118b131f919"
]
},
"owm_key_in_source": {
"ac": "NFT-SEC-09 (AC-1, source portion) — OpenWeatherMap key not present in source tree",
"scope": "src/ and mission-planner/ (production sources; tests excluded)",
"match": "literal",
"patterns": [
"335799082893fad97fa36118b131f919"
]
},
"google_key_in_source": {
"ac": "AZ-501 (F-SAST-1) — Google Geocode API key not present in source tree",
"scope": "src/ and mission-planner/ (production sources; tests excluded)",
"match": "literal",
"patterns": [
"AIzaSyAhvDeYukuyWVrQYbRhuv91bsi_jj5_Iys"
]
},
"alert_calls": {
"ac": "NFT-SEC-07 (AZ-466 AC-5) — no alert() in production source",
"scope": "src/ and mission-planner/ (production sources; tests excluded)",