# List of Changes

**Run**: 01-testability-refactoring
**Mode**: guided
**Source**: autodev-testability-analysis (autodev existing-code Step 4)
**Date**: 2026-05-10

## Summary

Minimal-surgical edits required so the suite of black-box tests in `_docs/02_document/tests/` can be exercised against a controlled environment. Five hardcoded externalities (OpenWeatherMap key + URL, OSM tile URL, Esri satellite tile URL, Leaflet marker icon URL on `unpkg.com`) and two thin accessors (API base, login navigation) are isolated into a Vite-env-driven configuration so the `fast` profile can mock them via MSW and the `e2e` profile can redirect them to the suite-internal stubs (`owm-stub`, `tile-stub`) without changing call sites. No business logic, no algorithm changes, no public API renames. The pre-existing module-level access-token accessor in `src/api/client.ts` (already a thin getter/setter) is documented for test override and left structurally unchanged.

## Changes

### C01: Externalize OpenWeatherMap API key
- **File(s)**: `src/features/flights/flightPlanUtils.ts`, `.env.example` (new), `vite-env.d.ts` (extend `ImportMetaEnv`)
- **Problem**: A literal OpenWeatherMap API key (`'335799082893fad97fa36118b131f919'`) is hardcoded at `flightPlanUtils.ts:60`. NFT-SEC-09 (security tests, results_report.md row 63) fails today as a quarantined source-string check. The literal also blocks the `e2e` profile from routing OWM calls through `owm-stub:8081` because the URL is baked in.
- **Change**: Move the key to `import.meta.env.VITE_OWM_API_KEY` (Vite-exposed env var). Read it at call time. Provide `.env.example` with the variable documented as required-at-build-time. If the env var is unset at runtime, the function returns `null` (matches the existing try/catch fallback) so the caller's `?? 0` paths still work.
- **Rationale**: Hardcoded credentials are forbidden by AC-O6, O7 of restrictions.md and by NFT-SEC-09. Externalizing them is the minimal change required to make NFT-SEC-09 pass without rewriting how weather is fetched. The change is at the line level — no caller signatures move.
- **Constraint Fit**: Preserves the `getWeatherData(lat, lon)` signature → no caller breakage. Adheres to AC-N4 (no signature library — env-driven config is plain `import.meta.env`). The build pipeline already supports `.env` files via Vite 6 (S3); no new tooling is introduced.
- **Risk**: low
- **Dependencies**: None

### C02: Externalize OpenWeatherMap base URL
- **File(s)**: `src/features/flights/flightPlanUtils.ts`, `.env.example`, `vite-env.d.ts`
- **Problem**: The OWM endpoint `https://api.openweathermap.org/data/2.5/weather` is hardcoded at the same line as C01. The `e2e` profile's `owm-stub:8081` cannot intercept calls unless this is parameterizable.
- **Change**: Add `VITE_OWM_BASE_URL` (default `https://api.openweathermap.org/data/2.5`). Read at call time. Compose the request URL as `${VITE_OWM_BASE_URL}/weather?lat=...&lon=...&appid=...&units=metric`.
- **Rationale**: Without this, `tile-stub` / `owm-stub` redirection requires either DNS-level hijacking inside the docker network (operationally heavier than a single env var) or a code branch — the env-var approach is the standard Vite pattern (per `coderule.mdc` "follow established project patterns").
- **Constraint Fit**: Preserves `getWeatherData` signature. Adheres to S3 (Vite 6) and existing project tooling. No new deps.
- **Risk**: low
- **Dependencies**: C01 (same file; should land in one commit to avoid mid-state mismatch)

### C03: Externalize map tile URLs (OSM + Esri satellite)
- **File(s)**: `src/features/flights/types.ts`, `.env.example`, `vite-env.d.ts`
- **Problem**: `TILE_URLS.classic = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'` and `TILE_URLS.satellite = 'https://server.arcgisonline.com/.../tile/{z}/{y}/{x}'` are hardcoded at `types.ts:56-57`. The `e2e` profile's `tile-stub:8082` cannot serve tiles unless these are overridable. AC-N3 (no offline mode) test (`NFT-RES-03`, row 93) also fails noisily because Leaflet tries to reach an external host on app boot.
- **Change**: Move both URLs to `VITE_OSM_TILE_URL` and `VITE_ESRI_TILE_URL` env vars with the current strings as defaults. Re-export `TILE_URLS` as a function or a const computed from `import.meta.env`. Call sites (`FlightMap.tsx`) unchanged.
- **Rationale**: Same as C02 — minimal-surgical, fits existing Vite env pattern, makes `e2e` profile reproducible.
- **Constraint Fit**: Preserves the `TILE_URLS.classic | TILE_URLS.satellite` consumer pattern. Adheres to S9 (Leaflet 1.9.4 + react-leaflet 5) — no API surface change for Leaflet.
- **Risk**: low
- **Dependencies**: None

### C04: Replace external Leaflet marker icon URL with bundled asset
- **File(s)**: `src/features/flights/mapIcons.ts`, `public/leaflet-marker.png` (new — copied from the leaflet npm package), `src/assets/` (alternative location)
- **Problem**: `mapIcons.ts:18` sets `iconUrl: 'https://unpkg.com/leaflet@1.7.1/dist/images/marker-icon.png'` — a hardcoded external CDN URL. This breaks:
  - **AC-N3 / NFT-RES-03 / E1 air-gap**: app boot fetches from `unpkg.com` even with no other internet egress permitted.
  - **Test determinism**: the `e2e` profile cannot serve the icon from any local source today.
  - **Version mismatch**: pinned `leaflet@1.7.1` URL while package.json pins `leaflet@^1.9.4` (S9 violation in the URL string only — the JS library version is correct).
- **Change**: Either copy the marker PNG into `public/` (Vite serves `/leaflet-marker.png` from root) or import it from the `leaflet` package as a Vite asset (`import markerIcon from 'leaflet/dist/images/marker-icon.png'`). Update `iconUrl` to the local path. No call-site change.
- **Rationale**: The icon is a 600-byte PNG already shipping inside the `leaflet` npm package — pulling it locally costs zero extra bytes and removes an external dependency entirely.
- **Constraint Fit**: Preserves the `customIcon` export shape. Adheres to E1 (air-gap-friendly bundle), S9 (Leaflet 1.9.4 pin — local copy aligns with the pinned version). No new deps.
- **Risk**: low
- **Dependencies**: None

### C05: Extract `getApiBase()` accessor for the `/api/...` path prefix
- **File(s)**: `src/api/client.ts`, `src/api/sse.ts`, `vite-env.d.ts`
- **Problem**: `api/client.ts:44` and every call site that calls `api.get('/api/...')` assumes the SPA and the suite are served by the same nginx (production layout). The `fast` test profile's MSW handlers also match on the literal `/api/...` prefix, so this is workable today. However, the `e2e` profile's standalone Playwright runner pointing at `http://azaion-ui:80` works only because the test runner runs inside the same network — there is no override path if the suite ever needs to expose the SPA on a sub-path (E2 already strips `/api/<service>/` server-side, but the SPA assumes the root mount). Adding a single accessor `getApiBase()` reading `import.meta.env.VITE_API_BASE_URL` (default `''`, preserving today's relative behavior) is the minimal change.
- **Change**: Introduce `getApiBase()` in `src/api/client.ts`. `request()` and `refreshToken()` prepend it to the URL. `createSSE()` does the same. Default `''` keeps every existing call site unchanged.
- **Rationale**: Tests can override via `VITE_API_BASE_URL=http://owm-stub:8081/api/owm` etc. for cross-service routing scenarios. Default-zero behavior means no production deployment changes.
- **Constraint Fit**: Preserves every `api.get` / `api.post` call signature. Adheres to E2 (nginx still strips `/api/<service>/`) and O3 (`credentials:'include'` semantics unchanged).
- **Risk**: low
- **Dependencies**: None

### C06: Wrap `window.location.href = '/login'` redirect in a thin accessor
- **File(s)**: `src/api/client.ts`
- **Problem**: `client.ts:34` performs a hard `window.location.href = '/login'` after a failed refresh. The DOM API is global; jsdom can intercept it but the test cannot easily distinguish "library code redirected" from "user code redirected". A thin `navigateToLogin()` accessor lets tests verify the redirect call without overriding `window.location` globally.
- **Change**: Introduce `let navigateToLoginImpl: () => void = () => { window.location.href = '/login' }` and `export function setNavigateToLogin(fn: () => void)`. `request()` calls `navigateToLoginImpl()`. Production behavior unchanged; tests override.
- **Rationale**: Allowed change per Step 4 list ("Wrap global singletons in thin accessors that tests can override (thread-local / context var / setter gate)"). Surgical — six lines net.
- **Constraint Fit**: Preserves the production redirect target and timing. Adheres to AC-23 (auth refresh transparency) — failed refresh still kicks the user to `/login`.
- **Risk**: low
- **Dependencies**: None

### C07: Document the existing module-level access-token accessor for test override
- **File(s)**: `src/api/client.ts` (comment-only edit)
- **Problem**: `src/api/client.ts:1-9` already exposes `setToken(token: string | null)` / `getToken()` as the test override hook for the module-level `accessToken` singleton (AC-02 — never write the bearer to client storage; AC-23 — refresh transparency). Tests in `_docs/02_document/tests/blackbox-tests.md` rely on calling `setToken('test-bearer-xyz')` before mounting components. The pattern is intentional but not documented — a future maintainer could "clean it up" thinking it's dead code.
- **Change**: Add a short JSDoc on `setToken` documenting the test-override intent and linking to `_docs/02_document/tests/test-data.md` "Stubbed bearer / cookie in test helpers". Zero behavioral change.
- **Rationale**: Comment-only adjustment that prevents future deletion (per `coderule.mdc` "Dead code rots — but before deletion verify reflection / DI / dynamic-dispatch usages"). The accessor IS used dynamically by tests; documenting that fact protects it.
- **Constraint Fit**: Preserves O2 (no bearer in localStorage/sessionStorage) — comment-only.
- **Risk**: low (comment-only)
- **Dependencies**: None

## Rejected entries (deferred to Step 8 — Refactor — or Phase B)

The following candidates were considered and rejected from this run because they fall outside the allowed list ("Replace hardcoded URLs / file paths / credentials / magic numbers; extract narrow interfaces for stubbing; add optional DI parameters; wrap global singletons; split only when required for isolation"). They are recorded in `deferred_to_refactor.md` so the next refactor pass picks them up.

- **D01 — Bootstrap refresh missing `credentials:'include'`** (FT-P-01 quarantine): a one-line product fix in the bootstrap path. Changes wire behavior, not testability surface. **Defer to Step 8** (or Step 9 New-Task ticket).
- **D02 — Numeric enum drift in `src/types/index.ts`** (FT-P-04/05/06; `AnnotationStatus`, `MediaStatus`, `Affiliation`, `CombatReadiness`, `MediaType` per `enum_spec_snapshot.json`): renumbering 5 enums is a wire-contract change with payload-shape implications across `annotations/`, `flights/`, and the SSE event handlers. **Defer to Step 8** (or a dedicated Phase B ticket gated on .NET-side `CombatReadiness` + `MediaType` numeric confirmation — see snapshot's `verification_pending: true`).
- **D03 — ProtectedRoute timeout** (FT-P-33 / NFT-RES-04 quarantine): missing UX — adding a timeout is new behavior. **Defer to Phase B feature cycle**.
- **D04 — i18n language detector + persistence** (FT-P-24 / FT-P-25 quarantine): missing feature. **Defer to Phase B**.
- **D05 — Panel-width persistence** (FT-P-37 / FT-P-38 / NFT-PERF-08 quarantine): missing writer in `useResizablePanel`. **Defer to Phase B**.
- **D06 — Settings save-error surfacing** (FT-N-13/14 / NFT-RES-05/06 / NFT-PERF-09 quarantine): missing error UI. **Defer to Phase B**.
- **D07 — Role-gates for `/admin` and `/settings`** (FT-N-03 / FT-N-05 / NFT-SEC-05 / NFT-SEC-06 quarantine): missing client-side RBAC. **Defer to Step 8** (it's a client-side defence-in-depth addition over server-side RBAC).
- **D08 — Numeric input hygiene in settings** (FT-N-11 / FT-N-12 quarantine): missing form validation. **Defer to Phase B**.
- **D09 — SSE refresh-rotation reconnect** (NFT-PERF-03 / NFT-RES-02 quarantine): missing reconnect-on-rotation logic. **Defer to Step 8 hardening**.
- **D10 — Tile-zoom UX + indicator** (AC-40, FT-P-54/55 quarantine): missing UX. **Defer to Phase B**.
- **D11 — Tile-splitting surface on dataset page** (FT-P-51 quarantine): missing UI. **Defer to Phase B**.
- **D12 — Async video detect (F7)** (FT-P-12/13 quarantine): target feature not wired. **Defer to Phase B**.

## Acceptance gates

This list is reviewed under the BLOCKING gate at refactor Phase 1 (Discovery). The user must confirm before any task file is created in `_docs/02_tasks/todo/`. Each entry C01–C07 maps to one atomic task at decompose time, and Phase 4 (Execution) of the refactor skill delegates to the implement skill via those task files.

After execution, the refactor skill writes `testability_changes_summary.md` listing every applied change with plain-language rationale and risk — that summary is the second BLOCKING gate before Step 5 (Decompose Tests).
