mirror of
https://github.com/azaion/ui.git
synced 2026-06-21 13:01:10 +00:00
f7dd6c98d8
ci/woodpecker/push/build-arm Pipeline failed
Security audit (5 phases) → reports under _docs/05_security/. AZ-501 (F-SAST-1, HIGH): Externalize hardcoded Google Geocode key from mission-planner/src/config.ts to VITE_GOOGLE_GEOCODE_KEY via new GeocodeService.ts; fail-soft warn when unset; STC-SEC1D static deny-list gate; +5 unit tests in tests/mission_planner_geocode.test.ts. AZ-502 (F-DEP-1, HIGH): Force vite>=6.4.2 and postcss>=8.5.10 via package.json overrides in both roots; clean reinstall clears all bun audit advisories. Test-spec sync (Step 12) + Update Docs (Step 13) deltas: AC-43, AC-44, NFT-SEC-09b, FT-P-61, FT-N-17, ripple log, batch_12 report. Pending user actions: revoke Google + OWM keys (AC-6 / AZ-499 AC-7). 229 PASS / 13 SKIP / 0 FAIL on static + fast suites. Co-authored-by: Cursor <cursoragent@cursor.com>
444 lines
42 KiB
Markdown
444 lines
42 KiB
Markdown
# Azaion UI — Architecture
|
||
|
||
> Synthesis output of `/document` Step 3a. Derived from Step 0 (`00_discovery.md`),
|
||
> Step 1 (`modules/*.md`), Step 2 (`components/*/description.md`), Step 2.5
|
||
> (`module-layout.md`), and the legacy reference `_docs/legacy/wpf-era.md`. No
|
||
> new code is read in this step; if anything is missing, the gap belongs upstream.
|
||
|
||
## Architecture Vision
|
||
|
||
> Status: **confirmed-by-user** (Step 4.5 — 2026-05-10). Source of truth for the
|
||
> system's structural intent and non-negotiables. Reconciles the verified
|
||
> code-grounded view (Steps 1–4) with operator/product intent the code alone
|
||
> cannot convey. Glossary at `_docs/02_document/glossary.md`.
|
||
|
||
### What this system is
|
||
|
||
The Azaion UI is a React 19 single-page application, statically built and served by nginx inside an ARM64 container, that operates the browser-facing half of the Azaion UAV operations suite. It is the **in-progress rewrite of the legacy WPF stack** (`Azaion.Annotator` + `Azaion.Dataset` + a `MapMatcher` mission planner) and communicates with the parent suite's microservices over **REST + SSE only** — no SSR, no GraphQL, no in-browser persistence beyond a single bearer token in memory and a `Secure HttpOnly` refresh cookie.
|
||
|
||
The dominant pattern is "**thin client over a typed REST contract**": React Context for two cross-cutting concerns (auth, selected flight), no global state library, feature pages that fetch + cache locally. A second React 18 + MUI 5 tree (`mission-planner/`) lives in the repo as the **port source** for the flights component — it is **NOT deployed**; its features migrate into `src/features/flights/` across Phase B feature cycles per the convergence plan below.
|
||
|
||
### Components / responsibilities
|
||
|
||
| ID | Component | One-line responsibility |
|
||
|----|-----------|--------------------------|
|
||
| 00 | `00_foundation` | Types, hooks (`useDebounce`, `useResizablePanel`), i18n (en + ua) — the data-vocabulary of the SPA |
|
||
| 01 | `01_api-transport` | Native `fetch` wrapper (`api/client.ts`) + `EventSource` wrapper (`api/sse.ts`); 401-retry refresh lives here |
|
||
| 02 | `02_auth` | `AuthContext`, login / logout, bootstrap refresh (broken — finding B3) + 401-retry refresh (works) |
|
||
| 03 | `03_shared-ui` | Header (top nav + flight dropdown), `FlightContext`, `ConfirmDialog`, `HelpModal`, `DetectionClasses` strip |
|
||
| 04 | `04_login` | Public `/login` route |
|
||
| 05 | `05_flights` | Flight CRUD + waypoints + GPS-Denied + planned Test Mode; `mission-planner/` is the port source |
|
||
| 06 | `06_annotations` | Bounding-box editor, AI Detect (sync today; async not wired), `MediaList` browser scoped to the selected flight |
|
||
| 07 | `07_dataset` | Dataset Explorer with three tabs (annotations / editor / class distribution); bulk-validate works |
|
||
| 08 | `08_admin` | Class CRUD (add + delete + **edit, to be re-introduced**), user management, AI/GPS settings forms (broken save) |
|
||
| 09 | `09_settings` | System / Directory / Camera / User settings |
|
||
| 10 | `10_app-shell` | `App.tsx` + `main.tsx` + routing tree |
|
||
| 11 | `11_class-colors` | Class → color + text mapping (lifted out of `06_annotations` in Step 2 — its own component) |
|
||
|
||
### Major data flows
|
||
|
||
- **F1 Login** → bearer in memory + refresh cookie set.
|
||
- **F2 Refresh** has TWO paths in code: broken bootstrap GET in `AuthContext.tsx:24` vs. working 401-retry POST in `api/client.ts:44` — Step 4 fix candidate.
|
||
- **F3 Flight selection** persists as a `UserSettings` field on `annotations/` (NOT `/api/flights/select`).
|
||
- **F5 Annotation save** POSTs the doubly-prefixed `/api/annotations/annotations`.
|
||
- **F6 AI Detect (sync)** hits `/api/detect/${id}` for **both** images and videos today (silent UX hazard for long videos — bridge until F7 ships).
|
||
- **F7 Async video detect** is target-only; not wired today. Comes via Phase B cycles.
|
||
- **F9 Bulk-validate** works via the Validate button (only `[V]` shortcut missing).
|
||
- **F12 GPS-Denied Test Mode** is target-only; planned per `_docs/how_to_test.md`.
|
||
- **F13 Live-GPS SSE** stream per selected flight.
|
||
- **F14 Annotation-status SSE** (admin-wide, client-side filtered).
|
||
|
||
Full sequence diagrams + error scenarios: `_docs/02_document/system-flows.md`.
|
||
|
||
### Architectural principles / non-negotiables
|
||
|
||
These are **inferred from code or user-confirmed at Step 4.5**. Downstream skills (refactor, decompose, new-task) treat them as binding constraints.
|
||
|
||
| # | Principle | Status | Inferred-from / source |
|
||
|---|-----------|--------|------------------------|
|
||
| P1 | **REST + SSE only** — no WebSocket, no GraphQL | inferred-from-code | `src/api/client.ts`, `src/api/sse.ts` |
|
||
| P2 | **Static bundle + nginx reverse-proxy** — zero UI runtime, no SSR, no React Server Components | inferred-from-code | `Dockerfile`, `nginx.conf` |
|
||
| P3 | **Bearer in memory; refresh in HttpOnly cookie** — tokens are never written to localStorage / sessionStorage | inferred-from-code | `src/auth/AuthContext.tsx` (no `storage.*` calls) |
|
||
| P4 | **Two-context state** — `AuthContext` + `FlightContext` are the only cross-cutting state stores; everything else is local | inferred-from-code | `src/auth/AuthContext.tsx`, `src/components/FlightContext.tsx` |
|
||
| P5 | **ARM-first edge deployment** — Woodpecker builds ARM64 only | inferred-from-code | `.woodpecker/build-arm.yml` |
|
||
| P6 | **Bilingual UI (en + ua)** is mandatory — English-only UX is a regression | inferred-from-code + WPF era | `src/i18n/i18n.ts`, `_docs/legacy/wpf-era.md` |
|
||
| P7 | **Lift cross-cutting concerns to their own component as soon as 2+ features touch them** — class colors did this at Step 2 | inferred-from-code | `components/11_class-colors/description.md` emergence |
|
||
| P8 | **WPF parity is a goal, not a constraint** — features that don't make sense in a browser (DI host, LibVLC, ZeroMQ, binary-split key fragments, Cython sidecars) are intentionally NOT ported | inferred-from-WPF-source | `_docs/legacy/wpf-era.md` §11 |
|
||
| P9 | **Spec is the source of truth for numeric enums** (`AnnotationStatus`, `MediaStatus`, `Affiliation`, `CombatReadiness`) — UI types file matches the spec verbatim and adds inline comments per numeric value | confirmed-by-user (Step 4.5) | `src/types/index.ts`; `04_verification_log.md` enum drift |
|
||
| P10 | **No hardcoded credentials in source** — third-party keys (e.g., OpenWeatherMap) live in `.env` and are read via `import.meta.env.*` | confirmed-by-user (Step 4.5) | `mission-planner/src/utils/flightPlanUtils.ts:60` (current violation, Step 4 fix) |
|
||
| P11 | **Persist what you type** — fields declared in `UserSettings` (incl. resizable-panel widths) are actually persisted; the typed shape is the contract | confirmed-by-user (Step 4.5) | `src/hooks/useResizablePanel.ts` (current violation, Step 4 fix); `src/types/index.ts` `UserSettings` |
|
||
| P12 | **Admin can edit existing detection classes** — add + edit + delete is the full CRUD surface; current code has add + delete only | confirmed-by-user (Step 4.5) | `04_verification_log.md` F10; new `PATCH /api/admin/classes/{id}` to introduce |
|
||
|
||
### Mission-planner convergence plan
|
||
|
||
`mission-planner/` (37 modules, React 18 + MUI 5) is the port source for `src/features/flights/` (15 modules, React 19 + Tailwind 4). They are **one component, two trees** — convergence is the active migration. Per Step 4.5 decision the timing across the autodev `existing-code` flow is:
|
||
|
||
| Autodev step | Role in convergence |
|
||
|--------------|---------------------|
|
||
| Step 1 (Document) — done | Both trees documented as one component (`05_flights`) |
|
||
| Step 2 (Architecture Baseline Scan) | **Flag the convergence as a Critical Architecture finding**; produces the work-list of port targets |
|
||
| Step 3 (Test Spec) | Write ACs for the **converged target** — incl. mission-planner-only behaviors (camera-config side panel, mission JSON I/O, satellite tile provider, richer waypoint-altitude UX) |
|
||
| Steps 5–7 (Decompose Tests / Implement Tests / Run Tests) | Build the test safety net for the **current** `src/features/flights/` |
|
||
| Step 8 (Refactor — optional) | Reserved for **mechanical / no-behavior** consolidation only (shared leaflet helpers, type aliases). Skip if convergence is all behavior-bearing. |
|
||
| Phase B feature cycles (Steps 9–17 looping) | **Primary home** for convergence. One Phase-B cycle per ported feature group: New Task → Implement → Run Tests → Test-Spec Sync → Update Docs → Security → Deploy → Retrospective. |
|
||
| Phase B final cycle | Once all port targets land, a final cycle **deletes `mission-planner/`** (its only consumer became zero). |
|
||
|
||
Rationale: 37 modules with new behaviors is too big for a single Step 8 refactor. Phase B cycles give a per-feature test safety net, per-feature doc updates, and per-feature shippable deploys. *source: `flows/existing-code.md` Steps 2 / 3 / 8 / 9–17*.
|
||
|
||
### Open questions / drift signals (deferred — for downstream skills)
|
||
|
||
These are surfaces the verified code can't answer alone; they will be picked up by Step 2 (Architecture Baseline) or by individual Phase B cycles. They are **not** blocking Step 4.5 — they are recorded here so downstream skills don't re-discover them.
|
||
|
||
1. **Sync `/api/detect/${id}` used for video** is the deliberate bridge until F7 (async video detect) ships. Plan: keep the bridge, time-bound it to "before the first Phase B cycle that ports the async-detect flow".
|
||
2. **OpenWeatherMap key** is currently hardcoded in `mission-planner/src/utils/flightPlanUtils.ts:60`. Step 4 (Code Testability Revision) extracts it to `import.meta.env.VITE_OPENWEATHERMAP_API_KEY` per principle P10.
|
||
3. **Resizable-panel widths** typed in `UserSettings` but not persisted by `useResizablePanel` — Step 4 fix per principle P11.
|
||
4. **Admin can no longer edit detection classes** — Step 4 fix candidate (or Phase B task) re-introduces `PATCH /api/admin/classes/{id}` plus the in-place edit form per principle P12.
|
||
5. **AnnotationStatus / MediaStatus / Affiliation / CombatReadiness numeric drift** between `src/types/index.ts` and the suite spec — Step 4 fix per principle P9: align values to the spec, add inline comments per numeric value.
|
||
6. **`IsSeed` annotation visual** (legacy 8 px IndianRed border) — does the modern API still expose `isSeed`? Defer to a future Phase B cycle.
|
||
7. **Test framework selection** — deferred to Step 5 (Decompose Tests) per Step 4.5 decision; the autodev flow chooses the runner there.
|
||
8. **Sound Detections + Drone Maintenance** (legacy WPF) — **dropped** per Step 4.5 decision; recorded in `_docs/02_document/01_legacy_coverage_gaps.md` and not ported.
|
||
|
||
---
|
||
|
||
## 1. System Context
|
||
|
||
**Problem being solved**: Azaion is a UAV / aerial-imagery operations suite for military and
|
||
defense use cases. The piece of the system documented here is the **operator-facing browser
|
||
UI** — a single-page React 19 application that lets an operator plan flights, browse and
|
||
annotate captured media, run AI object detection (synchronous on images, asynchronous via
|
||
SSE on video), curate datasets, manage detection classes / users / aircraft, and operate a
|
||
GPS-Denied positioning workflow including a Test Mode that drives SITL simulation from
|
||
a pre-recorded `.tlog` + video pair.
|
||
|
||
This UI is the **React rewrite of the front-end half** of the legacy WPF stack
|
||
(`_docs/legacy/wpf-era.md`). The Cython sidecars, the SQLite outbox, the LibVLC
|
||
playback, the per-app DI host, and the binary-split key-fragment loader handoff
|
||
all moved server-side into the parent suite (`suite/`) as separate .NET / Python /
|
||
Cython submodules. The UI's job is now narrowed to "render the suite's REST + SSE
|
||
contract beautifully and accessibly".
|
||
|
||
**System boundaries**:
|
||
|
||
| Inside this workspace | Outside this workspace |
|
||
|----------------------|------------------------|
|
||
| React 19 SPA (`src/`) | Suite backend services (`annotations/`, `flights/`, `admin/`, `detect/`, `loader/`, `gps-denied-{desktop,onboard}/`, `autopilot/`, `resource/`) |
|
||
| `mission-planner/` port-source (NOT deployed) | Database (PostgreSQL — managed by individual suite services, not the UI) |
|
||
| Static-bundle Dockerfile + nginx config | OpenWeatherMap public API (consumed by the main SPA via env-resolved key per AZ-448 / AZ-449; consumed by `mission-planner/` per AZ-499 — env-resolved key, fail-soft on unset, manual revocation of the previously-committed key tracked under AC-42) |
|
||
| Woodpecker CI pipeline (`.woodpecker/build-arm.yml`) | Suite-internal `satellite-provider` service for map tiles (same-origin via nginx in production; env-resolved URL `VITE_SATELLITE_TILE_URL` per AZ-498). The legacy OpenStreetMap / Esri tile providers are NO LONGER consumed by the main SPA as of cycle 2 / 2026-05-12. |
|
||
| | Identity provider (suite-internal — Admin API) |
|
||
|
||
**External systems**:
|
||
|
||
| System | Integration Type | Direction | Purpose |
|
||
|--------|------------------|-----------|---------|
|
||
| `annotations/` service | REST + SSE (via `/api/annotations/*` and `/api/detect/*`) | Outbound | CRUD annotations, AI detection results, detection classes |
|
||
| `flights/` service | REST (via `/api/flights/*`) | Outbound | CRUD flights + waypoints; selected-flight persistence |
|
||
| `admin/` service | REST (via `/api/admin/*`) | Outbound | Users, roles, aircraft, AI / GPS settings, auth (login + refresh) |
|
||
| `detect/` service | REST + SSE (via `/api/detect/*`) | Outbound | Sync image inference; async video inference w/ SSE progress |
|
||
| `loader/` service | REST (via `/api/loader/*`) | Outbound | (Currently nominal — UI does not call directly today) |
|
||
| `gps-denied-desktop/`, `gps-denied-onboard/` | REST (via `/api/gps-denied-*/*`) | Outbound | GPS-Denied operations + Test Mode (SITL feed) |
|
||
| `autopilot/` | REST (via `/api/autopilot/*`) | Outbound | Aircraft autopilot configuration (admin-side) |
|
||
| `resource/` | REST (via `/api/resource/*`) | Outbound | Static resource fetch (icons, configs not bundled in the SPA) |
|
||
| Suite-internal `satellite-provider` service for satellite tiles | HTTPS (Leaflet TileLayer with env-configured URL `VITE_SATELLITE_TILE_URL`); same-origin in production via nginx; cookie auth (`crossOrigin="use-credentials"`) | Outbound (intra-suite) | Satellite map raster tiles. Replaces the previously-used OpenStreetMap and Esri ArcGIS World Imagery tile servers as of cycle 2 / 2026-05-12 (AZ-498) — air-gap restriction E1 satisfied without a stub. |
|
||
| OpenWeatherMap (main SPA) | HTTPS (`api.openweathermap.org/data/2.5/onecall`) | Outbound | Wind data for flight planning. Env-resolved key + base URL via `VITE_OWM_API_KEY` / `VITE_OWM_BASE_URL` since AZ-448 / AZ-449. |
|
||
| OpenWeatherMap (mission-planner) | HTTPS (`api.openweathermap.org/data/2.5/weather`) | Outbound | Wind data for the mission-planner port. Env-resolved key + base URL via `VITE_OWM_API_KEY` / `VITE_OWM_BASE_URL` since AZ-499; `getWeatherData(lat, lon)` returns `null` and issues NO fetch when the key is unset (fail-soft contract). The previously-committed literal `335799082893fad97fa36118b131f919` is defended against re-introduction by `STC-SEC1C` and tracked for manual OWM-dashboard revocation under AC-42. |
|
||
|
||
## 2. Technology Stack
|
||
|
||
| Layer | Technology | Version | Rationale |
|
||
|-------|------------|---------|-----------|
|
||
| Language | TypeScript | 5.7 (`strict: true`) | Type-checked interfaces against the suite's REST contract |
|
||
| UI framework | React | 19 | Latest stable; React Server Components NOT used (this is a static-bundle SPA) |
|
||
| Bundler | Vite | 6 | Fast dev iteration; static-bundle output for nginx |
|
||
| Pkg manager | Bun | 1.3.11 (declared via `packageManager`) | CI image (`oven/bun:1.3.11-alpine`) matches |
|
||
| Styling | Tailwind CSS 4 + custom `az-*` design tokens (`src/index.css`) | 4 | Direct port of legacy WPF dark-navy / orange-accent / red-danger palette |
|
||
| Routing | `react-router-dom` | 7 | `/login` public; everything else under `AuthProvider → ProtectedRoute → FlightProvider → Header + nested Routes` |
|
||
| i18n | `i18next` + `react-i18next` | latest | English + Ukrainian. **Note**: `lng:'en'` hardcoded today — language detector / persistence is a Step 4 fix (finding) |
|
||
| Map | `leaflet` 1.9 + `react-leaflet` 5 + `leaflet-draw` + `leaflet-polylinedecorator` | leaflet 1.9.4 | Replaces WPF MapMatcher. **Note**: `mapIcons.ts` pins `leaflet@1.7.1` CDN URL (drift) — Step 4 fix |
|
||
| Charts | `chart.js` 4 + `react-chartjs-2` | 4 | Altitude charts in flights; class-distribution chart NOT YET built (gap) |
|
||
| HTTP transport | native `fetch` (custom thin wrapper at `src/api/client.ts`) | — | No axios / TanStack Query — explicitly minimal |
|
||
| Realtime | native `EventSource` (SSE) at `src/api/sse.ts` | — | Backend exposes SSE for long-running detect; no WebSocket |
|
||
| State management | React Context only (`AuthContext`, `FlightContext`) | — | Explicitly NO Redux / Zustand / TanStack Query. Caching is in component state. |
|
||
| File upload | `react-dropzone` | latest | Drag-drop in `MediaList` |
|
||
| DnD | `@hello-pangea/dnd` | 18 | Waypoint reorder |
|
||
| Tests | (none configured) | — | **Zero test coverage in `src/`.** Test plan owned by autodev Steps 5–7 |
|
||
| Build target | static bundle → nginx (multi-stage Dockerfile) | nginx:alpine | All routes proxied via `nginx.conf` |
|
||
| Runtime | nginx in container, **ARM64** image | — | Edge-device deployment (operator laptops, OrangePi, Jetson per legacy doc §1) |
|
||
| CI | Woodpecker | — | `.woodpecker/build-arm.yml` builds + pushes `${REGISTRY_HOST}/azaion/ui:${branch}-arm`. **No test step today.** |
|
||
|
||
**Key constraints driving the stack**:
|
||
|
||
- **Static bundle only**: the UI ships zero server-side runtime. nginx serves `dist/` and reverse-proxies `/api/<service>/` to the matching suite service.
|
||
- **ARM-first**: production target is ARM-class edge devices; CI builds ARM64 only today (no AMD64 image in the pipeline).
|
||
- **Air-gapped friendly**: the SPA is bundled fully. As of cycle 2 / 2026-05-12 (AZ-498), map tiles are served by the suite-internal `satellite-provider` service on the same origin via nginx — restriction E1 is satisfied for tiles without a stub. The only remaining direct-from-browser external dependency is OpenWeatherMap (env-resolved per AC-42; fail-soft when the key is unset). Field deployments that go fully air-gapped MUST set `VITE_OWM_API_KEY=""` (or omit it) so `getWeatherData` returns `null` instead of attempting an external fetch.
|
||
- **No test framework**: legacy carry-over; the WPF `Azaion.Test` project tested utilities only; full test infrastructure is being built fresh under autodev.
|
||
- **Bilingual UI required**: Ukrainian + English are mandatory per the legacy WPF UX. English-only SaaS-style copy is a regression — finding tracked.
|
||
|
||
## 3. Deployment Model
|
||
|
||
**Environments**: Development (local Vite dev server + suite docker-compose), Stage, Production (per the Woodpecker `dev`/`stage`/`main` branch triggers).
|
||
|
||
**Infrastructure**:
|
||
- **Container orchestration**: containerized — Dockerfile is multi-stage `oven/bun:1.3.11-alpine` build → `nginx:alpine` static serve. Orchestrator (Kubernetes / Docker Compose / Nomad) is the parent suite's concern, not this repo's.
|
||
- **Scaling strategy**: stateless; horizontal scaling is trivial (each pod is identical).
|
||
- **Edge target**: ARM64 image; deployed onto operator laptops / OrangePi / Jetson alongside the suite services.
|
||
|
||
**Environment-specific configuration**:
|
||
|
||
| Config | Development | Production |
|
||
|--------|-------------|------------|
|
||
| API base URL | Vite dev proxy (`/api → http://localhost:8080`, configured in `vite.config.ts`) | nginx reverse-proxy per `/api/<service>/` route → service hostname inside the docker network (configured in `nginx.conf`) |
|
||
| Secrets | `.env.example` **absent** (Step 4 testability fix) | Secrets are server-side; the SPA carries no API keys EXCEPT the hardcoded OpenWeatherMap key (security finding) |
|
||
| Logging | browser console | browser console (no centralized client telemetry today — Step 6 surface) |
|
||
| Auth cookie | local Vite dev proxy passes through; cookies are `Secure; HttpOnly; SameSite=Strict` set by Admin API | Same — cookies are set server-side by Admin API on `POST /api/admin/auth/login` |
|
||
| Refresh-token rotation | Same code path (handled inside Admin API; UI just retries on 401) | Same |
|
||
| OpenWeatherMap | Direct HTTPS from the browser (CORS-enabled OWM endpoint) | Same — **but should be proxied via suite to remove the hardcoded key from the bundle.** Step 4 / Step 6. |
|
||
|
||
**Build pipeline** (`.woodpecker/build-arm.yml`):
|
||
1. Triggered on push to `dev` / `stage` / `main`.
|
||
2. `bun install --frozen-lockfile` + `bun run build` (= `tsc -b && vite build`).
|
||
3. Multi-stage Dockerfile produces `nginx:alpine`-based image with `dist/` baked at `/usr/share/nginx/html`.
|
||
4. `ENV AZAION_REVISION=$CI_COMMIT_SHA` is stamped into the image.
|
||
5. Push to `${REGISTRY_HOST}/azaion/ui:${branch}-arm` with OCI labels (`org.opencontainers.image.{revision,created,source}`).
|
||
|
||
**Missing from the pipeline today**:
|
||
- No test step (no test infrastructure exists).
|
||
- No vulnerability scan / SBOM emission.
|
||
- No image signing.
|
||
- AMD64 build (only ARM64 today).
|
||
|
||
## 4. Data Model Overview
|
||
|
||
> Detailed per-component data models live in component specs and `data_model.md` (Step 3c). Below is the high-level entity map.
|
||
|
||
**Core entities** (defined in `src/types/index.ts`, mirroring the suite's REST contract):
|
||
|
||
| Entity | Description | Owned By Component | Backing service |
|
||
|--------|-------------|--------------------|-----------------|
|
||
| `AuthUser` | Logged-in user with role + permissions | `02_auth` | `admin/` |
|
||
| `User` | User CRUD entity (admin) | `08_admin` | `admin/` |
|
||
| `Aircraft` | Plane / Copter with default flag | `08_admin` (or `09_settings`) | `admin/` |
|
||
| `Flight` | Flight session entity | `05_flights` | `flights/` |
|
||
| `Waypoint` | Lat/lng/order point on a flight | `05_flights` | `flights/` |
|
||
| `Media` | One captured image or video file | `06_annotations` | `annotations/` |
|
||
| `MediaType` enum | None=0 / Image=1 / Video=2 | shared (`00_foundation`) | (matches suite spec) |
|
||
| `MediaStatus` enum | New=0 / AiProcessing=1 / AiProcessed=2 / ManualCreated=3 — **drift, missing None/Confirmed/Error** | shared (`00_foundation`) | (matches suite spec — mismatch flagged) |
|
||
| `AnnotationListItem` | Annotation row with detections | `06_annotations` | `annotations/` |
|
||
| `AnnotationStatus` enum | Created=0 / Edited=1 / Validated=2 — **drift, spec is 0/10/20/30** | shared (`00_foundation`) | (mismatch flagged) |
|
||
| `AnnotationSource` enum | AI=0 / Manual=1 | shared | (matches suite spec) |
|
||
| `Detection` | Bounding box (x/y/w/h normalized) + class + affiliation + combatReadiness | `06_annotations` (display) | `annotations/` (storage) + `detect/` (production) |
|
||
| `Affiliation` enum | Unknown=0 / Friendly=1 / Hostile=2 — **drift, spec also has 'None'** | shared | (mismatch flagged) |
|
||
| `CombatReadiness` enum | NotReady=0 / Ready=1 — **drift, spec also has 'Unknown'** | shared | (mismatch flagged) |
|
||
| `DetectionClass` | Admin-managed class with name / color / size / photoMode | `08_admin` (write) | `admin/` (write) + `annotations/` (read) |
|
||
| `DatasetItem` | Thumbnail + status row | `07_dataset` | `annotations/` (queries) |
|
||
| `ClassDistributionItem` | classNum / label / color / count | (currently unused in UI — backs missing chart) | `annotations/` |
|
||
| `SystemSettings` / `DirectorySettings` / `CameraSettings` | Server-side configs | `09_settings` + `08_admin` | `admin/` |
|
||
| `UserSettings` | Per-user preferences (incl. left/right panel widths — type exists, not wired) | `09_settings` | `admin/` |
|
||
| `PaginatedResponse<T>` | `{ items, totalCount, page, pageSize }` envelope | shared | (used everywhere) |
|
||
|
||
**Key relationships**:
|
||
- `Flight` 1:N `Waypoint` (waypoints belong to one flight; ordering preserved by `order` field)
|
||
- `Flight` 1:N `Media` (capture media is associated with the flight that produced it; `Media.waypointId` optionally pinpoints the capturing waypoint)
|
||
- `Media` 1:N `AnnotationListItem` 1:N `Detection`
|
||
- `User` N:1 `Aircraft`-default (default aircraft applies to a user's new flights — currently global per a finding in `08_admin`/`09_settings`)
|
||
- `DetectionClass` is referenced by `Detection.classNum` (not a typed FK — `classNum` is the raw int that includes the PhotoMode offset)
|
||
|
||
**Data flow summary**:
|
||
1. **Plan flight** → `flights/` REST → flight + waypoints persist; the UI optimistically updates and listens for nothing today (no SSE on flight updates).
|
||
2. **Capture media** → out-of-band: edge devices upload via the loader / annotations services; the UI surfaces them via `MediaList` polling.
|
||
3. **Annotate** → user-edits → POST/PUT/DELETE to `annotations/` REST; **no streaming** of other users' annotations into this UI today.
|
||
4. **AI Detect** (sync image) → `POST /api/detect/{mediaId}` returns inline detections.
|
||
5. **AI Detect** (async video) → `POST /api/detect/video/{mediaId}` returns a job ID → SSE on `/api/detect/stream/{jobId}` streams progress + finalized detections. **The UI does not subscribe today** (finding #10).
|
||
6. **Curate dataset** → `07_dataset` queries `annotations/` with status filters; bulk-validate transitions `AnnotationStatus.{Created,Edited} → Validated`.
|
||
7. **GPS-Denied Test Mode** → user uploads `.tlog` + video → `gps-denied-desktop/` service auto-syncs them via IMU analysis → SITL simulation feeds frames to `gps-denied-onboard/` service → results render back through `flights/` GPS-Denied tab.
|
||
|
||
## 5. Integration Points
|
||
|
||
### Internal Communication (UI → suite)
|
||
|
||
> **Step 4 verification** corrected this table after grepping every `api.*()` and `createSSE()` call in `src/`.
|
||
|
||
| From (component) | To (suite service) | Protocol | Pattern | Endpoints actually called | Notes |
|
||
|-----------------|-------------------|----------|---------|---------------------------|-------|
|
||
| `02_auth/AuthContext` | `admin/` | REST | Request-Response | `POST /api/admin/auth/login`, `POST /api/admin/auth/logout`, `GET /api/admin/auth/refresh` (bootstrap, no `credentials:'include'`), `POST /api/admin/auth/refresh` (401-retry inside `api/client.ts:44`, has `credentials:'include'`) | **Two refresh paths exist** — bootstrap GET vs. 401-retry POST. Only the POST path correctly sends the cookie. The GET bootstrap will fail → Step 4 fix. |
|
||
| `01_api-transport/sse.ts` | every service | SSE (HTTP) | Server-pushed events | Helper is named `createSSE` (NOT `subscribeSSE`). Bearer in **query string** (`?token=...`) — accepted trade-off. EventSource holds the token captured at create time; refresh-rotation breaks long subscriptions (Step 8 hardening). |
|
||
| `03_shared-ui/FlightContext` | `flights/` + `annotations/` | REST | Request-Response | `GET /api/flights?pageSize=1000` (hardcoded ceiling, finding B3). `GET /api/annotations/settings/user` to load `selectedFlightId`. `GET /api/flights/${id}` to hydrate selected flight. **`PUT /api/annotations/settings/user`** to save selection (NOT `/api/flights/select` as initially drafted). Fire-and-forget — finding B3. |
|
||
| `03_shared-ui/DetectionClasses` | `annotations/` | REST | Request-Response | `GET /api/annotations/classes` |
|
||
| `06_annotations/MediaList` | `annotations/` | REST | Request-Response + upload | `GET /api/annotations/media?...`, `GET /api/annotations/annotations?mediaId=...&pageSize=1000`, `DELETE /api/annotations/media/${id}`, **`api.upload('/api/annotations/media/batch', formData)`** (multipart). |
|
||
| `06_annotations/AnnotationsPage` | `annotations/` | REST + SSE | Request-Response + Event | `GET /api/annotations/media/${id}/file`, `POST /api/annotations/annotations` (NOT `/api/annotations` — the path is **doubly-prefixed**). Annotation save body shape: finding #32 (Step 4 fix). |
|
||
| `06_annotations/AnnotationsSidebar` | `annotations/`, `detect/` | REST + SSE | Request-Response + Event | `POST /api/detect/${mediaId}` (sync detect — used for BOTH images and videos today); `createSSE('/api/annotations/annotations/events', ...)` for **annotation-status SSE** (NOT detect progress). **No `/api/detect/video/${id}` and no `/api/detect/stream/${jobId}` are wired today** — finding #10 / #21 confirmed. |
|
||
| `06_annotations/CanvasEditor` | `annotations/` | static asset GET | — | `GET /api/annotations/annotations/${id}/image` (annotation thumbnail), `GET /api/annotations/media/${id}/file` (raw media). |
|
||
| `07_dataset/DatasetPage` | `annotations/` | REST | Request-Response | `GET /api/annotations/dataset?...`, `GET /api/annotations/dataset/${annotationId}`, `POST /api/annotations/dataset/bulk-status`, **`GET /api/annotations/dataset/class-distribution`** (the endpoint **already exists**; the chart UI is what's missing — see `01_legacy_coverage_gaps.md`), `<img src="/api/annotations/annotations/${id}/thumbnail">`. **Editor tab does not save** — finding #4. |
|
||
| `08_admin/AdminPage` | `annotations/` + `admin/` + `flights/` | REST | Request-Response | `GET /api/annotations/classes` (read), `POST /api/admin/classes` (create), `DELETE /api/admin/classes/${id}` (delete — no ConfirmDialog, finding B4), `POST /api/admin/users`, `PATCH /api/admin/users/${id}` (deactivate), `GET /api/flights/aircrafts`, `PATCH /api/flights/aircrafts/${id}`. **Cross-service reads** — admin page reads aircraft from `flights/` and classes from `annotations/`. |
|
||
| `09_settings/SettingsPage` | `annotations/` + `flights/` | REST | Request-Response | `GET/PUT /api/annotations/settings/system`, `GET/PUT /api/annotations/settings/directories`, `GET /api/flights/aircrafts`, `PATCH /api/flights/aircrafts/${id}`. **Settings endpoints route to `annotations/`**, NOT `admin/` as initially drafted. |
|
||
| `05_flights/FlightsPage` | `flights/` | REST + SSE | Request-Response + Event | `GET /api/flights/aircrafts`, `GET /api/flights/${id}/waypoints`, **`createSSE('/api/flights/${id}/live-gps', ...)` — live-GPS SSE for aircraft telemetry**, `POST /api/flights`, `DELETE /api/flights/${id}`, `DELETE /api/flights/${id}/waypoints/${wpId}` (loop), `POST /api/flights/${id}/waypoints` (loop, lossy shape — finding #20). |
|
||
| `05_flights/flightPlanUtils` | OpenWeatherMap (external) | REST | Request-Response | `GET https://api.openweathermap.org/data/2.5/onecall?...` with env-resolved key + base URL since AZ-448 / AZ-449 (closes the original security finding; see AC-20 + AC-42). |
|
||
|
||
### External Integrations
|
||
|
||
| External System | Protocol | Auth | Rate Limits | Failure Mode |
|
||
|----------------|----------|------|-------------|--------------|
|
||
| Suite-internal `satellite-provider` for satellite tiles | HTTPS (Leaflet TileLayer); same-origin via nginx in production; cookie auth (`crossOrigin="use-credentials"`) | HttpOnly same-origin cookie set by `admin/` | Bounded by suite ops (no external usage policy) | 401 / 503 on a tile request renders a broken-tile placeholder for the failing cell; rest of the SPA stays interactive (per NFT-RES-11). Cycle 2 / 2026-05-12 — AZ-498. |
|
||
| OpenWeatherMap | HTTPS | Env-resolved key (`VITE_OWM_API_KEY`); never hardcoded since AZ-448 / AZ-499 | Free-tier 60 calls/min | Errors silently swallowed in main SPA's `flightPlanUtils.ts` (existing finding); mission-planner `WeatherService.getWeatherData` now returns `null` and issues NO outbound fetch when the key is unset (AZ-499 fail-soft contract — AC-42). |
|
||
| Suite identity provider (admin/) | REST + HttpOnly refresh cookie | JWT bearer + refresh-token rotation | server-enforced | 401 → `ProtectedRoute` redirects to `/login`; refresh-token rotation handled inside `AuthContext` (mostly) |
|
||
|
||
## 6. Non-Functional Requirements
|
||
|
||
> Targets that are explicitly enforceable in code or config. Anything else is aspirational and noted as such.
|
||
|
||
| Requirement | Target | Measurement | Priority | Source |
|
||
|------------|--------|-------------|----------|--------|
|
||
| Bundle size (initial JS) | ≤ ~2 MB gzipped | `vite build` artifact inspection | High | Bundle bloat is a finding (`AltitudeChart` lazy-load opportunity in flights) |
|
||
| Initial paint | < 2 s on operator laptops over LAN | Manual inspection | Medium | No metric collection today |
|
||
| Long-running detect (video) | UI stays responsive; progress visible to user | Should be SSE-streamed | High | Currently NOT met — finding #21 (no progress UI), #10 (no SSE subscription) |
|
||
| Auth refresh | Transparent — no user-visible flicker | One refresh = one network round trip; no UI re-render past `<ProtectedRoute>`| High | `refresh` missing `credentials:'include'` is a real bug — Step 4 fix |
|
||
| Upload size | ≤ 500 MB single file | `nginx.conf` `client_max_body_size 500M` | Medium | Hard cap from server config |
|
||
| Browser support | Chromium-based + Firefox latest 2 versions | Manual | Medium | No browser-list config; ES module output assumes evergreen |
|
||
| Mobile responsiveness | Header has bottom-nav variant; main pages render at 768 px+ | `Header.tsx:113-129` | Low | Mobile is a P2 use-case |
|
||
| Accessibility | Confirm dialogs / dropdowns must be keyboard-navigable | `ConfirmDialog` lacks `aria-modal/role=dialog`; Header dropdown lacks `role=combobox`/Esc | Medium | Multiple findings flagged for Step 4 / Step 8 |
|
||
| i18n coverage | English + Ukrainian for all user-visible strings | Manual | High | Many strings hardcoded English today (admin, annotations help) — Step 4 |
|
||
| Test coverage | Aspirational target TBD by autodev Step 3 | (none yet) | High | Zero coverage today |
|
||
|
||
## 7. Security Architecture
|
||
|
||
**Authentication**:
|
||
- Operator logs in via `POST /api/admin/auth/login` → Admin service issues a JWT bearer (response body) + a `Secure; HttpOnly; SameSite=Strict` refresh-token cookie.
|
||
- The bearer is held in `AuthContext` memory (not localStorage — XSS-resistant).
|
||
- On 401, `AuthContext` calls `GET /api/admin/auth/refresh` (cookie-only credential) → new bearer + new refresh cookie. **Bug: this request is missing `credentials:'include'`** — likely fails on cross-origin path. Step 4 fix.
|
||
- WPF-era encrypted-creds command-line handoff is **intentionally not ported** (`_docs/legacy/wpf-era.md §11`).
|
||
|
||
**Authorization**:
|
||
- Admin service enforces RBAC server-side via `User.role` + `permissions[]`.
|
||
- UI inspects `AuthUser.role` to render or hide nav links and pages, but does NOT enforce. Two findings (`/admin` route lacks role-gate — security PRIORITY; `/settings` route is more nuanced).
|
||
- The browser is treated as untrusted; every action confirms with the server.
|
||
|
||
**Data protection**:
|
||
- **At rest**: not in scope for the UI.
|
||
- **In transit**: TLS terminated server-side at the suite ingress (nginx ingress → service mesh / docker network).
|
||
- **Secrets management**: secrets are NEVER in the SPA bundle, EXCEPT the **hardcoded OpenWeatherMap API key** in `src/features/flights/flightPlanUtils.ts:60`. **Must rotate at OpenWeatherMap, remove from source, and proxy via suite/`flights` service.** Step 4 / Step 6.
|
||
- **Bearer in SSE query string**: documented trade-off; the bearer is short-lived and the URL is HTTPS-encrypted on the wire. Risk: server-side access logs may capture the URL with the token. Cross-link to `security_approach.md` (Step 6).
|
||
- **CORS**: handled server-side; `credentials:'include'` is required on every authenticated fetch. The bug above is the primary CORS-related defect.
|
||
|
||
**Input validation**:
|
||
- Numeric inputs in `09_settings` use `parseInt(v) || 0` — clearing a field silently writes 0 (finding B4). Step 4 fix.
|
||
- File uploads in `06_annotations` go through `react-dropzone` MIME filter; no client-side virus scan; server is authoritative.
|
||
|
||
**Audit logging**: server-side concern; the SPA does not emit audit events directly.
|
||
|
||
**Cross-site / clickjack**: no `Content-Security-Policy` headers configured in `nginx.conf` today — Step 6 surface.
|
||
|
||
## 8. Key Architectural Decisions
|
||
|
||
> ADRs inferred from the code + legacy doc. The decisions are recorded retrospectively
|
||
> here — the original decision-making lives in commit history and the WPF-era doc.
|
||
|
||
### ADR-001: Pure SPA over server-rendered React
|
||
|
||
**Context**: The legacy WPF stack ran on the operator's machine. The new UI must deploy alongside the suite services as a peer container.
|
||
|
||
**Decision**: Static-bundle SPA served by nginx; no Node.js runtime in the production image; no SSR.
|
||
|
||
**Alternatives considered**:
|
||
1. Next.js (SSR + RSC) — rejected because edge devices can't host a Node runtime cheaply, and the suite already has nginx for `/api` reverse-proxy.
|
||
2. Plain HTML + jQuery — rejected because the WPF-era complexity (canvas editor, video sync, leaflet, charts) doesn't degrade well to non-React.
|
||
|
||
**Consequences**: Initial paint waits for the bundle. SEO and deep-linking are not concerns (the UI is operator-only). Bundle bloat is the main risk → flagged for Step 4.
|
||
|
||
### ADR-002: REST + SSE only; no WebSockets
|
||
|
||
**Context**: The suite already exposes REST + SSE on every service. WebSockets would require a duplex protocol the backend doesn't speak.
|
||
|
||
**Decision**: `src/api/client.ts` (fetch wrapper) + `src/api/sse.ts` (EventSource wrapper) cover all transport.
|
||
|
||
**Alternatives considered**: WebSockets, GraphQL Subscriptions, gRPC-Web — all rejected as needing backend changes that aren't on the roadmap.
|
||
|
||
**Consequences**: Long-running operations (video AI detect) require an SSE consumer; one is missing today (finding #10). Bidirectional updates (e.g., live "another user is editing") are not possible — accepted.
|
||
|
||
### ADR-003: HTML5 `<video>` instead of LibVLC
|
||
|
||
**Context**: Legacy used `LibVLCSharp` for frame-accurate playback. The browser cannot host LibVLC.
|
||
|
||
**Decision**: HTML5 `<video>` with a frame-accurate seeking shim.
|
||
|
||
**Alternatives considered**: WebAssembly LibVLC — rejected as too heavy in the bundle for marginal accuracy gains; HLS/DASH chunked playback — rejected as overkill for short capture videos.
|
||
|
||
**Consequences**: Per-frame stepping requires manual `currentTime ± (1/fps)` math; FPS is detected at metadata-load time. **Today fps is hardcoded `30`** in `VideoPlayer.tsx` — finding to fix in Step 4.
|
||
|
||
### ADR-004: React Context only; no Redux / TanStack Query
|
||
|
||
**Context**: Two pieces of cross-component state — current user (AuthContext) and selected flight (FlightContext) — and the rest is page-local.
|
||
|
||
**Decision**: Two Context providers; everything else is `useState`.
|
||
|
||
**Alternatives considered**: Redux — rejected as overkill for two stores; Zustand — rejected as adding a dependency; TanStack Query — rejected as we don't currently need cache invalidation across components.
|
||
|
||
**Consequences**: Pages re-fetch on mount; no shared cache. Add TanStack Query in a future iteration if cross-page caching becomes a real need (Step 5 solution surface).
|
||
|
||
### ADR-005: Tailwind 4 + custom `az-*` design tokens
|
||
|
||
**Context**: Legacy WPF had a fixed dark-navy/orange/red palette (`_docs/legacy/wpf-era.md §10`).
|
||
|
||
**Decision**: Tailwind 4 with `az-bg`, `az-text`, `az-orange`, `az-success`, `az-danger`, `az-primary` CSS variables defined in `src/index.css`.
|
||
|
||
**Consequences**: Design tokens are the single source of truth — but `index.html` body class hardcodes hex literals (`bg-[#1e1e1e] text-[#adb5bd]`) instead of using tokens (cosmetic finding, 00_discovery #11.10).
|
||
|
||
### ADR-006: nginx reverse-proxy strips `/api/<service>/` prefix per service
|
||
|
||
**Context**: The suite has 9 distinct backend services; the SPA must reach all of them via the same origin (cookie scope + CORS-free).
|
||
|
||
**Decision**: `nginx.conf` enumerates 9 routes and strips the prefix before proxying.
|
||
|
||
**Alternatives considered**: API gateway (e.g., Traefik / Kong) — rejected as adding ops complexity; service mesh — same.
|
||
|
||
**Consequences**: A new suite service requires a `nginx.conf` edit. The SPA hardcodes `/api/<service>/...` paths in source instead of an env-driven base URL — testability is poor (finding tracked).
|
||
|
||
### ADR-007: Bilingual (English + Ukrainian)
|
||
|
||
**Context**: Legacy WPF was Ukrainian-only with a tiny `translations.json` for English. Operators are Ukrainian-speaking; some allies are English-speaking.
|
||
|
||
**Decision**: `react-i18next` with `en.json` + `ua.json`. Operator can switch in the Header.
|
||
|
||
**Consequences**: Every user-visible string MUST be in both bundles. **Many strings are hardcoded English today** (admin page especially) — high-priority Step 4 fix.
|
||
|
||
### ADR-008: Bearer-token-in-query-string for SSE
|
||
|
||
**Context**: `EventSource` cannot send arbitrary headers; the suite SSE endpoints require a bearer.
|
||
|
||
**Decision**: Pass bearer in `?token=...`. Document the trade-off in `security_approach.md`.
|
||
|
||
**Consequences**: Bearer may appear in nginx access logs. Mitigation: short-lived bearers + log-redaction at the nginx layer (Step 6 surface). Also: refresh-rotation breaks live SSE subscriptions; reconnect logic is missing today (Step 8 hardening).
|
||
|
||
### ADR-009: `mission-planner/` is a vendored port-source, NOT a deployed component
|
||
|
||
**Context**: The mission-planner React 18 + MUI app is the upstream design source for `src/features/flights/`. Documenting it as a separate top-level component would imply parallel deployment; it isn't deployed.
|
||
|
||
**Decision**: Per the user's Step 2 BLOCKING-gate decision (2026-05-10), `mission-planner/` is documented INSIDE component `05_flights` as the port-source. The Vite build does NOT compile it. After the port reaches parity, the directory is a deletion candidate.
|
||
|
||
**Consequences**: Two physical trees under one logical component. The implement skill must include `mission-planner/**` in `05_flights`'s OWNED glob (called out in `module-layout.md` Verification Needed #4).
|
||
|
||
### ADR-010: GPS-Denied Test Mode is a planned sub-feature of `05_flights`
|
||
|
||
**Context**: Per `_docs/how_to_test.md`, the Test Mode lets an operator upload a `.tlog` + video pair, auto-sync them via IMU analysis, and feed SITL frames into the onboard GPS-Denied service.
|
||
|
||
**Decision**: Test Mode is a tab inside the `05_flights` GPS-Denied sub-feature. NOT a separate component.
|
||
|
||
**Consequences**: One component spans flight-planning + flight operations + GPS-Denied + Test Mode. Acceptable today; a future split is feasible if the component grows further.
|
||
|
||
---
|
||
|
||
## Open architecture questions
|
||
|
||
The following remain undecided and surface at Step 4.5 (Glossary & Architecture Vision):
|
||
|
||
1. **Sound Detections feature** (audio-analysis bbox button in legacy WPF) — port, drop, or new audio-pipeline service?
|
||
2. **Drone Maintenance feature** (`Аналіз стану БПЛА`) — port, drop, or maintenance service?
|
||
3. **Class Distribution chart** (Dataset's 3rd tab) — port or replace with admin-side analytics?
|
||
4. **Status-bar clock + help-text-blink** — keep WPF UX or replace with toasts?
|
||
5. **`IsSeed` thumbnail concept** — modern API still exposes it?
|
||
6. **Camera config persistence scope** — per-user, per-flight, or per-detect-job?
|
||
7. **Resizable-panel persistence scope** — Settings (server) or LocalStorage (client)?
|
||
8. **OpenWeatherMap routing** — proxy via `flights/` service (preferred for security) or move wind compute server-side entirely?
|
||
9. **`mission-planner/` end-state** — delete after parity port (preferred) or keep as a continuously-vendored reference?
|
||
|
||
These are the inputs for Step 4.5 user confirmation; this architecture doc proceeds without resolving them.
|