# 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 `` + HTML5 `