# 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
Header, FlightContext,
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
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
fetch + EventSource]
Auth --> Transport
Flights --> Transport
Annotations --> Transport
Dataset --> Transport
Admin --> Transport
Settings --> Transport
end
subgraph nginx reverse-proxy
Transport --> Nginx[nginx
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
direct HTTPS
hardcoded key — finding]
Flights --> OSM[OSM tile servers
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 `