# Azaion UI — Retrospective Solution

> Output of `/document` Step 5. Synthesis of the **implemented** architecture
> and per-component choices, derived from the verified technical docs:
> `_docs/02_document/architecture.md` (Step 3a), `system-flows.md` (Step 3b),
> `data_model.md` (Step 3c), `deployment/*.md` (Step 3d),
> `components/*/description.md` (Step 2), `04_verification_log.md` (Step 4),
> `glossary.md` and `architecture.md` § Architecture Vision (Step 4.5).
>
> This is retrospective — it describes the solution **as it is**, with
> observed limitations called out per component. Future work (testability
> fixes, async-detect wiring, mission-planner convergence) is referenced
> by source and not re-stated as a plan here.

**Status**: synthesised-from-verified-docs (Step 5 — `/document`)
**Date**: 2026-05-10
**Project**: Azaion UI (operator-facing browser SPA)

---

## Product Solution Description

Azaion UI is a single-page React 19 application, statically built and served
by nginx inside an ARM64 container, that operates the browser-facing half of
the Azaion UAV operations suite. It lets an operator plan flights, browse and
annotate captured media, run AI object detection (synchronous on images;
asynchronous video detect is **target-only — not wired today**, see
`04_verification_log.md` F7), curate datasets, manage detection classes /
users / aircraft, and operate the GPS-Denied positioning workflow including a
planned Test Mode driven by `.tlog` + video pairs through SITL.

The solution communicates with the parent suite's microservices over **REST
and Server-Sent Events only** — no WebSocket, no GraphQL, no in-browser
persistence beyond a single bearer token in memory and a `Secure HttpOnly`
refresh cookie. State management is two React Contexts (`AuthContext` and
`FlightContext`); everything else is page-local.

A second React 18 + MUI 5 tree (`mission-planner/`) lives at the repo root as
the **port-source** for `05_flights` — it is **NOT deployed**, **NOT
compiled** by the production Vite build, and is on a multi-cycle path to
deletion as features migrate into `src/features/flights/` (Phase B feature
cycles per the convergence plan in `architecture.md` § Architecture Vision).

### Component interaction diagram

```mermaid
flowchart TB
  subgraph Browser SPA
    AppShell[10_app-shell]
    AppShell --> Auth[02_auth]
    AppShell --> Login[04_login]
    Auth --> Shared[03_shared-ui<br/>Header, FlightContext,<br/>ConfirmDialog, HelpModal]
    Shared --> Flights[05_flights]
    Shared --> Annotations[06_annotations]
    Shared --> Dataset[07_dataset]
    Shared --> Admin[08_admin]
    Shared --> Settings[09_settings]
    Foundation[00_foundation<br/>types, hooks, i18n] -.shared.-> Auth
    Foundation -.shared.-> Shared
    Foundation -.shared.-> Flights
    Foundation -.shared.-> Annotations
    Foundation -.shared.-> Dataset
    Foundation -.shared.-> Admin
    Foundation -.shared.-> Settings
    ClassColors[11_class-colors] -.shared.-> Shared
    ClassColors -.shared.-> Annotations
    ClassColors -.shared.-> Dataset
    Transport[01_api-transport<br/>fetch + EventSource]
    Auth --> Transport
    Flights --> Transport
    Annotations --> Transport
    Dataset --> Transport
    Admin --> Transport
    Settings --> Transport
  end

  subgraph nginx reverse-proxy
    Transport --> Nginx[nginx<br/>strip /api/svc/]
  end

  subgraph Suite services
    Nginx --> AdminSvc[admin/]
    Nginx --> FlightsSvc[flights/]
    Nginx --> AnnotSvc[annotations/]
    Nginx --> DetectSvc[detect/]
    Nginx --> GpsDenied[gps-denied-*/]
    Nginx --> Resource[resource/]
    Nginx --> Autopilot[autopilot/]
    Nginx --> Loader[loader/]
  end

  Flights --> OWM[OpenWeatherMap<br/>direct HTTPS<br/>hardcoded key — finding]
  Flights --> OSM[OSM tile servers<br/>direct HTTPS]
```

Detailed per-flow sequences (F1–F14): `_docs/02_document/system-flows.md`.

---

## Architecture

The solution is organised as **11 components** under a strict layering
(`_docs/02_document/module-layout.md`):

- **L0 (Foundation)**: `00_foundation`, `11_class-colors`
- **L1 (Transport)**: `01_api-transport`
- **L2 (Auth + Shared UI)**: `02_auth`, `03_shared-ui`
- **L3 (Feature pages)**: `04_login`, `05_flights`, `06_annotations`,
  `07_dataset`, `08_admin`, `09_settings`
- **L4 (App shell)**: `10_app-shell`

Component dependency graph: `_docs/02_document/diagrams/components.md`.

### Cross-cutting principles (binding constraints — `architecture.md` § Architecture Vision)

P1 REST + SSE only · P2 Static bundle + nginx · P3 Bearer in memory + refresh
in HttpOnly cookie · P4 Two-context state (Auth + Flight) · P5 ARM-first edge
deployment · P6 Bilingual (en + ua) · P7 Lift cross-cutting at 2+ touches ·
P8 WPF parity is a goal not a constraint · P9 Spec is source of truth for
numeric enums · P10 No hardcoded credentials in source · P11 Persist what you
type · P12 Admin can edit existing detection classes.

---

### Component: `00_foundation`

| Solution | Tools | Advantages | Limitations | Requirements | Security | Cost | Fit |
|----------|-------|-----------|-------------|--------------|----------|------|-----|
| Shared types + hooks + i18n bundles, no domain logic | `typescript@5.7 strict`, `i18next` + `react-i18next`, custom hooks (`useDebounce`, `useResizablePanel`) | Single source of truth for the suite's typed REST contract; zero runtime cost (types erased); all bilingual strings live here | `useResizablePanel` reads `UserSettings.panelWidths` but **never writes back** — violates principle P11 (`04_verification_log.md` finding #11). `i18next` `lng:'en'` is hardcoded — no detector / no persistence. Inline numeric-enum comments are required by P9 (already added 2026-05-10) | TypeScript strict mode; bilingual coverage; numeric-enum drift between `src/types/index.ts` and the suite spec must be resolved | None — shared types only | Negligible (transitive only) | **Selected — current solution.** Layer-0 placement keeps every other component dependency-free w.r.t. types/hooks. |

### Component: `01_api-transport`

| Solution | Tools | Advantages | Limitations | Requirements | Security | Cost | Fit |
|----------|-------|-----------|-------------|--------------|----------|------|-----|
| Native `fetch` wrapper + native `EventSource` wrapper | `src/api/client.ts` (fetch + 401-retry refresh), `src/api/sse.ts` (`createSSE` helper) | Zero added dependencies (no axios / TanStack / SWR); 401-retry is centralised — every authenticated request gets refresh-token rotation for free | Bearer for SSE goes in the **query string** (`?token=...`); EventSource cannot send headers (`ADR-008`). EventSource holds the bearer captured at create time — refresh-rotation breaks long-running subscriptions; reconnect logic is missing today (Step 8 hardening) | All authenticated `fetch` requests must include `credentials:'include'` for the HttpOnly refresh cookie to flow; SSE endpoints must accept the bearer in the URL | 401-retry path is **secure** (POST + cookie); bootstrap GET refresh in `AuthContext.tsx:24` is **broken** (no `credentials:'include'`) — Step 4 fix | Negligible | **Selected — current solution.** Fits P1 (REST + SSE only) and P3 (no localStorage). |

### Component: `02_auth`

| Solution | Tools | Advantages | Limitations | Requirements | Security | Cost | Fit |
|----------|-------|-----------|-------------|--------------|----------|------|-----|
| `AuthContext` + `ProtectedRoute` + login/logout/bootstrap-refresh | `react-router-dom@7`, React Context, `01_api-transport` | XSS-resistant (bearer in memory, never in storage); login UX matches WPF era; refresh-token rotation is server-driven | **Two refresh paths in code** (`F2`): bootstrap GET (`AuthContext.tsx:24` — broken, missing `credentials:'include'`) vs. 401-retry POST (`api/client.ts:44` — correct). Bootstrap path will fail on cross-origin and force a re-login on cold load — Step 4 fix priority. `ProtectedRoute` spinner has no `role='status'` / no timeout | RBAC is server-enforced; UI must NOT trust `AuthUser.role` for security — only for showing/hiding nav | Bearer never written to storage (P3); refresh cookie is `Secure HttpOnly SameSite=Strict` (issued server-side); WPF-era encrypted-creds command-line handoff intentionally NOT ported (P8) | Negligible | **Selected — current solution.** Refresh-path consolidation (single POST with `credentials:'include'`) is the planned fix; structurally sound. |

### Component: `03_shared-ui`

| Solution | Tools | Advantages | Limitations | Requirements | Security | Cost | Fit |
|----------|-------|-----------|-------------|--------------|----------|------|-----|
| Header + flight dropdown + `FlightContext` + `ConfirmDialog` + `HelpModal` + `DetectionClasses` strip | React Context (`FlightContext`), Tailwind, `01_api-transport` | One place for cross-page chrome; `FlightContext` is the only flight-selection store (P4); `ConfirmDialog` reused by 4 components | `FlightContext` ceiling: `GET /api/flights?pageSize=1000` is a hardcoded magic number (finding B3). `selectFlight` is **fire-and-forget** PUT — no error path. Header dropdown lacks `role=combobox` / `aria-expanded` / Esc-to-close / focus-trap. `ConfirmDialog` lacks `aria-modal` / `role=dialog`. `HelpModal` does NOT close on Esc (inconsistent with `ConfirmDialog`); `GUIDELINES` are hardcoded English instead of i18n | Selected flight persists as a `UserSettings` field via `PUT /api/annotations/settings/user` (NOT `/api/flights/select` — `04_verification_log.md` F3) | None additional | Negligible | **Selected — current solution.** Flight-dropdown a11y + 1000-row pagination ceiling are Step 4 / Step 8 candidates. |

### Component: `04_login`

| Solution | Tools | Advantages | Limitations | Requirements | Security | Cost | Fit |
|----------|-------|-----------|-------------|--------------|----------|------|-----|
| Public `/login` route with username + password, calls `02_auth` login | React, Tailwind | Single dedicated public surface; clean separation from `ProtectedRoute` | `runUnlockSequence` 4×600 ms theatrical animation is decorative; document only (`finding B4`) | Receives bearer from server; cookie set server-side | Login form does NOT autocomplete sensitive values; only public route in the SPA | Negligible | **Selected — current solution.** No structural concerns. |

### Component: `05_flights`

| Solution | Tools | Advantages | Limitations | Requirements | Security | Cost | Fit |
|----------|-------|-----------|-------------|--------------|----------|------|-----|
| Flight CRUD + waypoints + altitude profile + GPS-Denied (Operations + planned Test Mode) — currently being ported from `mission-planner/` (React 18 + MUI 5) into `src/features/flights/` (React 19 + Tailwind 4) | `leaflet@1.9.4` + `react-leaflet@5` + `leaflet-draw` + `leaflet-polylinedecorator`; `chart.js@4` + `react-chartjs-2`; `@hello-pangea/dnd@18`; native `fetch`; `EventSource` for `F13 live-GPS SSE`; OpenWeatherMap (direct HTTPS) | Replaces WPF `MapMatcher` with browser-native cartography; live-GPS telemetry is real (F13); altitude charts work; mission-planner port gives a high-fidelity reference UX | **Component spans two physical trees** (one component, two trees — `ADR-009`). `mission-planner/src/utils/flightPlanUtils.ts:60` carries a **hardcoded OpenWeatherMap API key** (P10 violation — Step 4 fix). Wind errors are silently swallowed; sequential per-segment `await` is a perf trap; battery-capacity unit ambiguous (Wh vs Ws); km vs m altitude mixing. `mapIcons.ts` defaultIcon CDN URL pinned to `leaflet@1.7.1` (drift). Waypoint POST shape **mismatches** the suite spec — UI sends `{name, latitude, longitude, order}`; spec wants `{Geopoint:{Lat,Lon,MGRS}, Source, Objective, OrderNum, Height}` (finding #20 — likely 400s on a strict server). Edit-cycle is **delete-then-recreate** today (finding #19). FlightsPage save is N+M round-trips (delete + recreate per waypoint). `MiniMap` licence/responsive concerns; `AltitudeDialog` / `JsonEditorDialog` modal a11y; `WaypointList` drag/touch a11y; `AltitudeChart` bundle bloat. **Test Mode (F12) is target-only** — `.tlog` + video upload, IMU sync, SITL feed — none wired today | Wind data fetch + map tile fetch require browser internet; field deployments need an offline tile cache (not implemented); `.tlog` parser must be available client-side or server-side once Test Mode lands | OpenWeatherMap key must move to `.env` per P10 (Step 4 testability fix); satellite tile URL is env-driven via `VITE_SATELLITE_TILE_URL` in mission-planner only (target: `src/` once port lands) | Direct browser → external HTTP costs for OWM + tiles; otherwise compute is client-side | **Selected — current solution; under active convergence.** Per `architecture.md` § Architecture Vision, the mission-planner tree is on a multi-cycle path to deletion (`mission-planner/` → `src/features/flights/`); convergence happens in Phase B feature cycles, not in Step 8. |

### Component: `06_annotations`

| Solution | Tools | Advantages | Limitations | Requirements | Security | Cost | Fit |
|----------|-------|-----------|-------------|--------------|----------|------|-----|
| Bounding-box editor (`CanvasEditor`), `VideoPlayer`, AI Detect (sync only today), `AnnotationsSidebar`, `MediaList` browser scoped to selected flight, `AnnotationsPage` orchestrator | HTML5 `<canvas>` + HTML5 `<video>` (replaces WPF LibVLC — `ADR-003`); native `fetch` + SSE; `react-dropzone` for upload; Tailwind | Frame-accurate-ish video review without LibVLC; doubly-prefixed `POST /api/annotations/annotations` save path verified at Step 4 (F5); `F14 annotation-status SSE` (admin-wide, client-side filtered) is real | **Async video detect (F7) is NOT wired** — no `/api/detect/video/{id}`, no `/api/detect/stream/{jobId}` calls anywhere; sync `POST /api/detect/${id}` is used for **both** images and videos today (silent UX hazard for long videos). `VideoPlayer` hardcoded `fps=30` (`ADR-003` consequence). `CanvasEditor` missing pan; wrong time-window (symmetric ±200 ms instead of asymmetric `[-50, +150]` ms — finding #6); missing affiliation icons; missing CombatReadiness indicator; dead `AFFILIATION_COLORS`. `AnnotationsSidebar` AI-detect doesn't stream progress; silent catches. `AnnotationsPage` no panel-width persistence (P11 violation); `handleDownload` tainted-canvas risk; `handleSave` fallback hides save loss; **annotation save body shape mismatches spec** — must add `Source`, `WaypointId`, rename `time→videoTime` (finding #32). `MediaList` uses `alert()`; blob: locals ignore filter. **Missing keyboard shortcuts** (R, V, PageUp/Down). Missing Camera config side panel (GSD computation — finding #17). Missing Tile zoom for `splitTile`. **Hardcoded English strings** in help / sidebar | Detect must be wired to `detect/` async pipeline once F7 ships (Phase B); `X-Refresh-Token` header required for long-running video detect (per `_docs/10_auth.md`); annotation overlay window must align to spec asymmetric `[-50, +150]` ms | XSS-safe canvas rendering; uploads filtered by `react-dropzone` MIME; server is authoritative for virus scan; tainted-canvas risk on download (finding) | Detect-pipeline cost is server-side; UI compute is client-side | **Selected — current solution.** Async-detect wiring + canvas-editor parity + a11y are Phase B targets; structurally sound. |

### Component: `07_dataset`

| Solution | Tools | Advantages | Limitations | Requirements | Security | Cost | Fit |
|----------|-------|-----------|-------------|--------------|----------|------|-----|
| Dataset Explorer with three tabs (annotations / editor / class-distribution), bulk-validate, status filters | React, Tailwind, `chart.js@4` for class-distribution chart (`DatasetPage.tsx:151`), reuses `CanvasEditor` from `06_annotations` (cross-feature edge — finding) | **Validate button is wired** (Step 4 correction — `04_verification_log.md` F9); class-distribution chart **is implemented** (Step 4 correction); "objectsOnly" checkbox **is implemented**; `objectsOnly` filter works | **`[V]` keyboard shortcut missing** (only the button works); `Refresh thumbnails` button missing; status-bar `StatusText` slots missing; `IsSeed` highlight missing (legacy 8 px IndianRed border — open question); editor tab **does not save** (finding #4); magic `mediaType=1` literal (finding #5); dead `ConfirmDialog` import; silent catches; status filter conflates `None` with `All`; `classNum=0` sentinel collides with real class 0 (finding #9); no virtualisation; no keyboard shortcuts at all | `AnnotationStatus` numeric drift (UI 0/1/2 vs spec 0/10/20/30) surfaces as wrong status filter values on the wire — Step 4 fix per P9; `DatasetItem.isSplit` not in spec response — parent-suite doc fix recorded in leftovers and applied | None additional | Class-distribution endpoint server-side cost; otherwise client-side | **Selected — current solution.** Bulk-validate + chart parity confirm the React port is closer to WPF parity than the original draft suggested; remaining gaps are surface-level + a11y. |

### Component: `08_admin`

| Solution | Tools | Advantages | Limitations | Requirements | Security | Cost | Fit |
|----------|-------|-----------|-------------|--------------|----------|------|-----|
| Class CRUD (add + delete; **edit to be re-introduced** per P12), user management, AI/GPS settings forms, aircraft default-toggle | React forms with `useState`; native `fetch`; reuses `ConfirmDialog` (only on user-deactivation) | Single page consolidates 4 admin surfaces; aircraft cross-service mutation works (`PATCH /api/flights/aircrafts/${id}`) | **AI Settings & GPS Settings forms render with `defaultValue` only** — no state, no submit handler, the Save button **does nothing** (PRIORITY — `Step 6` problem-extraction surface). Hardcoded GPS device default `'192.168.1.100'` / port `'5535'` shipped in production bundle. `handleDeleteClass` has **no `ConfirmDialog`** despite being destructive (finding B4). Detection-class read uses `/api/annotations/classes` but write uses `/api/admin/classes` (cross-service split — accepted but documented). `handleToggleDefault` duplicated in `09_settings` (aircraft default lives in two pages — surface intent at Step 6). Many hardcoded English strings (P6 violation — Step 4 fix). Admin **cannot edit existing classes** today (P12 violation; `PATCH /api/admin/classes/{id}` to introduce — Phase B task) | RBAC is server-enforced (UI MUST NOT trust `AuthUser.role` for security); `/admin` route lacks role-gate — security PRIORITY | RBAC server-enforced; class-delete bypasses ConfirmDialog (Step 4 fix) | Compute is server-side | **Selected — current solution; multiple Step 4 / Step 6 / Phase B fixes queued.** Structurally sound; the broken Save buttons are a P0 product-correctness defect. |

### Component: `09_settings`

| Solution | Tools | Advantages | Limitations | Requirements | Security | Cost | Fit |
|----------|-------|-----------|-------------|--------------|----------|------|-----|
| System / Directory / Camera / User settings forms; aircraft default-toggle | React forms, `useState`, native `fetch` | Settings persist via `annotations/` service (`/api/annotations/settings/system` + `/api/annotations/settings/directories`) — verified Step 4 (F11); aircraft default-toggle goes to `flights/` (cross-service — accepted) | `saveSystem` / `saveDirs` lack `try/finally` — PUT failure leaves `saving:true` permanently (finding B4). Numeric inputs use `parseInt(v) || 0` — clearing a field silently writes 0 (finding B4). No optimistic concurrency (Step 6 surface). `UserSettings.panelWidths` is typed but `useResizablePanel` doesn't write back (P11 violation — Step 4 fix). `/settings` route role-gate is more nuanced than `/admin` (server-enforced via 403; no SETTINGS permission code in spec) | `/settings` role-gate per `_docs/02_document/architecture.md` § Security; aircraft default is a global config today (finding) | RBAC server-enforced | Compute is server-side | **Selected — current solution.** Settings persistence path is correct; form-state hygiene is the main fix. |

### Component: `10_app-shell`

| Solution | Tools | Advantages | Limitations | Requirements | Security | Cost | Fit |
|----------|-------|-----------|-------------|--------------|----------|------|-----|
| `App.tsx` + `main.tsx` + routing tree + global CSS | `react-router-dom@7`, Tailwind 4 + `az-*` design tokens (`src/index.css`), React 19 root | Single composition root; clear `AuthProvider → ProtectedRoute → FlightProvider → Header + Routes` chain; `/flights` is the default authenticated route | `/admin` and `/settings` lack **role-based route guards** (PRIORITY — security finding; complements server-side RBAC). No `ErrorBoundary`. No lazy code-splitting / no chunked routes (bundle bloat finding). `index.html` body class hardcodes hex literals (`bg-[#1e1e1e] text-[#adb5bd]`) instead of `az-*` tokens (cosmetic — `ADR-005`) | Routing tree is the security surface for client-side navigation; server-side RBAC is authoritative | UI role-gate is **convenience, not security**; server-enforced 403 is the actual gate | Negligible | **Selected — current solution.** Adding `ErrorBoundary` + lazy routes + role-gate are Step 4 / Step 8 candidates. |

### Component: `11_class-colors`

| Solution | Tools | Advantages | Limitations | Requirements | Security | Cost | Fit |
|----------|-------|-----------|-------------|--------------|----------|------|-----|
| Class → color + text mapping; `getPhotoModeSuffix` helper; consumed by `03_shared-ui/DetectionClasses`, `06_annotations`, `07_dataset` | Pure TypeScript module; no external deps | Lifted out of `06_annotations` at Step 2 (P7); single source of truth for class color tokens; `yoloId = classId + photoModeOffset` mapping is centralised | Physical file still lives at `src/features/annotations/classColors.ts` — the layout-doc-only mapping pending a Step 4 file move (`module-layout.md` Verification Needed #1, #8). `getPhotoModeSuffix` may duplicate the typed `DetectionClass.photoMode` field — likely redundant; possible deletion after typed propagation | None | None | Negligible | **Selected — current solution.** Cleanest L0 component; the file-move is purely cosmetic / layering hygiene. |

---

## Testing Strategy

### Current state — verified at Step 4

- **Zero test coverage in `src/`** — `src/**/*.test.*` returns **zero matches** (Grep verified at Step 4, `04_verification_log.md` §1).
- **No test framework configured** in `package.json` (Vitest / Jest / Playwright are all absent).
- **No test step in CI** — `.woodpecker/build-arm.yml` runs `bun install` + `bun run build` only.
- The legacy WPF stack had an `Azaion.Test` xUnit project that tested utilities only; that test surface did NOT migrate.

### Target test pyramid (to be defined at autodev Step 5 — Decompose Tests)

The test-runner choice is **deferred to autodev Step 3 (Test Spec) → Step 5 (Decompose Tests)** per the Step 4.5 decision (`architecture.md` § Architecture Vision, item 7 of Open Questions). This document does not pre-empt that decision; it lists the **categories** the existing code already implies.

#### Unit / module-level

- **Foundation hooks**: `useDebounce`, `useResizablePanel` — pure timing + state behavior; testable with React Testing Library + fake timers.
- **Foundation utilities**: `flightPlanUtils.ts` (battery / distance / wind compute) — pure functions; testable in isolation once the OpenWeatherMap key is moved to `.env` (P10 / Step 4).
- **Class-colors module**: `getPhotoModeSuffix`, `yoloId` mapping — pure functions.
- **i18n bundles**: assert that every English key has a Ukrainian counterpart and vice versa (P6 mandatory check).
- **Numeric-enum coverage**: assert that `AnnotationStatus`, `MediaStatus`, `Affiliation`, `CombatReadiness` numeric values match the suite spec (`P9` enforcement — directly catches the enum drift that motivated Step 4 corrections).

#### Component / integration

- **`AuthContext`** — login, bootstrap-refresh (currently broken — fix first), 401-retry refresh, logout. Mock `01_api-transport` to assert request shape (`credentials:'include'` is the regression to lock down).
- **`FlightContext`** — list pagination ceiling, `selectFlight` round-trip, hydration of selected flight from `UserSettings`.
- **`CanvasEditor`** — bbox draw / 8-handle resize / Ctrl-multi-select; affiliation overlay; CombatReadiness indicator (once dead `AFFILIATION_COLORS` is wired).
- **`MediaList`** — pagination, filter, drag-drop upload, delete with `ConfirmDialog`.
- **`DatasetPage`** — bulk-validate flow (POST + UI state); class-distribution chart load; status filter (specifically that `None` and `All` are NOT conflated — finding fix).
- **`AdminPage`** — class add / delete (currently no edit; will gain edit per P12). User add / deactivate. **AI Settings / GPS Settings forms** (currently broken — the Save button must POST something — Step 6 product-correctness defect).
- **`SettingsPage`** — system / directory / camera saves; aircraft default toggle (cross-service `flights/` mutation).

#### End-to-end (browser)

- **Login → /flights default route → flight selection → MediaList → annotation save → bulk-validate** — the operator's primary loop. Already exercised manually at every release.
- **GPS-Denied Test Mode** (`F12`) — once implemented (Phase B target). Inputs: `.tlog` + video; assertion: SITL feed reaches the onboard service and produces a positioning trace.
- **Async video detect** (`F7`) — once implemented (Phase B target). Inputs: long video; assertion: SSE progress visible; final detections persisted.

#### Non-functional

- **Bundle size budget**: `vite build` artifact ≤ ~2 MB gzipped (currently no enforcement — CI gate to add).
- **Auth refresh transparency**: 401 → POST refresh → retry, **no UI re-render past `<ProtectedRoute>`** (regression test — locks the bootstrap-refresh fix).
- **Route guards**: unauth user → `/admin` → redirected to `/login` (locks the missing role-gate fix).
- **i18n coverage**: every visible string in both `en.json` and `ua.json` (P6 mandatory).
- **a11y smoke**: `ConfirmDialog` has `role=dialog` + `aria-modal`; Header dropdown has `role=combobox` + `aria-expanded` + Esc-to-close; spinner has `role=status`.
- **Endpoint contract**: every `api.*()` call URL matches the suite OpenAPI; specifically that the doubly-prefixed `/api/annotations/annotations` save path stays correct after any refactor.
- **Performance**: long video detect — UI stays responsive; progress visible (currently NOT met — `04_verification_log.md` F7 / finding #21).

### Test environment expectations

- **Test DB / services**: tests run against the suite docker-compose stack or a service-mock layer. The UI is HTTP-only — there is no UI-side database to bootstrap.
- **CI integration**: a test step must be added to `.woodpecker/build-arm.yml` (currently missing — `architecture.md` § Deployment Model "Missing from the pipeline today").
- **Coverage target**: TBD at Step 3 (Test Spec) — this document does not commit to a percentage.

---

## References

### Source docs (verified inputs)

- `_docs/02_document/00_discovery.md` — Step 0 codebase discovery, dep graph, topo order
- `_docs/02_document/architecture.md` — Step 3a system architecture (with Step 4.5 § Architecture Vision)
- `_docs/02_document/system-flows.md` — Step 3b 14 sequence flows F1–F14 (with Step 4 corrections)
- `_docs/02_document/data_model.md` — Step 3c entity-relationship + enum-drift map
- `_docs/02_document/deployment/containerization.md` — multi-stage Dockerfile, ARM64, nginx static serve
- `_docs/02_document/deployment/ci_cd_pipeline.md` — Woodpecker `.woodpecker/build-arm.yml`
- `_docs/02_document/deployment/environment_strategy.md` — dev / stage / production
- `_docs/02_document/deployment/observability.md` — current state (no centralized client telemetry — Step 6 surface)
- `_docs/02_document/04_verification_log.md` — Step 4 Verification Pass corrections
- `_docs/02_document/01_legacy_coverage_gaps.md` — WPF parity rollup
- `_docs/02_document/glossary.md` — Step 4.5 confirmed terminology
- `_docs/02_document/module-layout.md` — Step 2.5 file-ownership map
- `_docs/02_document/components/00_foundation/description.md` — through `11_class-colors/description.md`
- `_docs/02_document/modules/*.md` — 22 module docs covering all 77 modules
- `_docs/legacy/wpf-era.md` — legacy reference

### Configuration evidence

- `package.json` — React 19, Vite 6, TypeScript 5.7 strict, Bun 1.3.11
- `vite.config.ts` — dev `/api → http://localhost:8080` proxy
- `Dockerfile` — multi-stage `oven/bun:1.3.11-alpine` → `nginx:alpine`
- `nginx.conf` — 9 `/api/<service>/` reverse-proxy routes, `client_max_body_size 500M`
- `.woodpecker/build-arm.yml` — ARM64-only build pipeline, no test step today
- `src/index.css` — `az-*` Tailwind 4 design tokens
- `src/i18n/en.json`, `src/i18n/ua.json` — bilingual bundles
- `src/types/index.ts` — typed REST contract (with Step 4.5 inline numeric-enum comments per P9)

### External integrations

- OpenWeatherMap: `api.openweathermap.org/data/2.5/onecall` (hardcoded key in source — P10 violation, Step 4 fix)
- OpenStreetMap: tile servers via `react-leaflet` `TileLayer` defaults
- Suite services (via nginx): `admin/`, `flights/`, `annotations/`, `detect/`, `loader/`, `gps-denied-{desktop,onboard}/`, `autopilot/`, `resource/`

### Related artifacts (downstream)

- `_docs/00_problem/` — Step 6 retrospective problem extraction (problem.md, restrictions.md, acceptance_criteria.md, input_data/, security_approach.md) — to be produced next
- `_docs/02_document/FINAL_report.md` — Step 7 final integrated report — to be produced after Step 6
