Files
ui/_docs/01_solution/solution.md
Oleksandr Bezdieniezhnykh 510df68bcf [AZ-447] autodev Steps 1-4 baseline: docs, tests, refactor specs
Captures the full output of autodev existing-code Phase A through
Step 4 (Code Testability Revision) for the Azaion UI workspace:

- Step 1 Document: _docs/02_document/ (FINAL_report, architecture,
  glossary, components/, modules/, diagrams/, system-flows,
  module-layout) plus _docs/00_problem/ + _docs/01_solution/ +
  _docs/legacy/ + _docs/how_to_test + README.
- Step 2 Architecture Baseline: architecture_compliance_baseline.md.
- Step 3 Test Spec: _docs/02_document/tests/ (environment,
  test-data, blackbox/performance/resilience/security/
  resource-limit tests, traceability-matrix), enum_spec_snapshot,
  expected_results/results_report.md (98 rows), plus the
  run-tests.sh + run-performance-tests.sh runners.
- Step 4 Code Testability Revision: 01-testability-refactoring/
  run dir (list-of-changes C01-C07, deferred_to_refactor,
  analysis/research_findings + refactoring_roadmap) and the 7
  child task specs AZ-448..AZ-454 under _docs/02_tasks/todo/
  plus _dependencies_table.md.
- _docs/_autodev_state.md pins the cursor at Step 4 / refactor
  Phase 4 entry so /autodev resumes cleanly.

Epic AZ-447 (UI testability gates) tracks the 7 child tasks that
will land in subsequent commits.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 00:38:49 +03:00

295 lines
29 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 (F1F14): `_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 F1F14 (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