mirror of
https://github.com/azaion/ui.git
synced 2026-06-21 08:51:09 +00:00
[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>
This commit is contained in:
@@ -0,0 +1,227 @@
|
|||||||
|
# azaion-ui
|
||||||
|
|
||||||
|
React SPA that serves as the **single front-end for the entire Azaion suite**.
|
||||||
|
It does not own data or business logic; it is the operator's window onto
|
||||||
|
every backend service that lives as a sibling submodule in the
|
||||||
|
[`azaion/suite`](../README.md) monorepo.
|
||||||
|
|
||||||
|
```
|
||||||
|
suite/
|
||||||
|
├── annotations/ .NET 10 — Annotations / Media / Datasets / Settings
|
||||||
|
├── flights/ .NET 10 — Flights, waypoints, aircrafts, GPS SSE
|
||||||
|
├── detections/ Cython — YOLO inference (ONNX / TensorRT)
|
||||||
|
├── detections-semantic/ Cython — Semantic detection
|
||||||
|
├── loader/ Cython — Encrypted resource loader
|
||||||
|
├── gps-denied-onboard/ Python — GPS-denied positioning (UAV side)
|
||||||
|
├── gps-denied-desktop/ Python — GPS-denied positioning (operator side)
|
||||||
|
├── autopilot/ Python — UAV control via MAVLink
|
||||||
|
├── admin/ .NET 10 — Users, roles, detection classes, model versions
|
||||||
|
├── ai-training/ Python — YOLO training, ONNX export
|
||||||
|
├── satellite-provider/ .NET 10 — Google Maps tile cache
|
||||||
|
└── ui/ React 19 — ◄ this repository
|
||||||
|
```
|
||||||
|
|
||||||
|
For the suite-wide architecture, deployment topology, database design, and
|
||||||
|
the legacy WPF predecessor see:
|
||||||
|
|
||||||
|
- [`suite/_docs/`](../_docs/) — system-level architecture and per-service
|
||||||
|
feature docs (canonical source of truth).
|
||||||
|
- [`_docs/legacy/wpf-era.md`](_docs/legacy/wpf-era.md) — what the
|
||||||
|
pre-rewrite Windows desktop application looked like, why the React port
|
||||||
|
exists, and which behaviours are intentionally being preserved.
|
||||||
|
- [`_docs/ui_design/`](_docs/ui_design/) — page-level wireframes
|
||||||
|
(HTML mockups + a design README) inherited from the WPF UI; the
|
||||||
|
authoritative reference for layout, keyboard shortcuts, color scheme,
|
||||||
|
affiliation icons, annotation row gradient, etc.
|
||||||
|
|
||||||
|
> **Status.** The code currently in `src/` is a **rudimentary first cut**
|
||||||
|
> mechanically translated from the legacy WPF / XAML UI. It is not yet
|
||||||
|
> fully wired to the new suite APIs and has no test coverage. Formal
|
||||||
|
> bottom-up documentation, a test specification, a testability pass, and
|
||||||
|
> the initial test suite are being produced via the
|
||||||
|
> [`/autodev` existing-code flow](.cursor/skills/autodev/SKILL.md). When
|
||||||
|
> the documentation + safety net are in place, feature work resumes
|
||||||
|
> through the same flow.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What this repo is — and is not
|
||||||
|
|
||||||
|
| | this repo (`ui/`) | the suite services |
|
||||||
|
|--|---|---|
|
||||||
|
| **Owns** | rendering, routing, client-side state, i18n, drag-and-drop, the Leaflet map, `<video>` playback, optimistic UI, keyboard shortcuts, layout persistence | data, persistence, queues, inference, model delivery, telemetry, RBAC, tile caching, autopilot |
|
||||||
|
| **Talks to** | every backend submodule via REST + SSE (HTTP) | each other via REST / SSE / RabbitMQ; no shared source code |
|
||||||
|
| **Builds to** | a static bundle served by nginx (`Dockerfile`) | individual service Docker images |
|
||||||
|
| **Runs on** | operator station (laptop / tablet / mini-PC); browser of choice | edge device or remote server, per service tier (see [`suite/_docs/00_top_level_architecture.md`](../_docs/00_top_level_architecture.md)) |
|
||||||
|
| **Reaches the network** | only through the operator's browser, with the user's JWT | service-to-service inside the docker network and to `admin/` over the internet |
|
||||||
|
|
||||||
|
The React app must remain stateless beyond what `localStorage` /
|
||||||
|
`UserSettings` (Annotations API) can hold. **No business rules in the UI.**
|
||||||
|
|
||||||
|
## Pages → backend submodules
|
||||||
|
|
||||||
|
| Page | Route | Primary submodule(s) consumed |
|
||||||
|
|------|-------|-------------------------------|
|
||||||
|
| Login | `/login` | `admin/` (auth + user) |
|
||||||
|
| Flights | `/flights` | `flights/`, `gps-denied-desktop/`, `gps-denied-onboard/` (live GPS SSE), `satellite-provider/` (orthophoto reference tiles), `autopilot/` (mission status) |
|
||||||
|
| Annotations | `/annotations` | `annotations/` (media, annotations, settings), `detections/` + `detections-semantic/` (AI detect), `loader/` (model availability status only) |
|
||||||
|
| Dataset Explorer | `/dataset` | `annotations/` (queries, validation), `ai-training/` (class distribution, dataset health), `admin/` (detection classes seed) |
|
||||||
|
| Admin | `/admin` | `admin/` (users, detection classes, AI recognition + GPS device + model-version settings) |
|
||||||
|
| Settings | `/settings` | `annotations/` (tenant / directories / aircrafts), `admin/` (connection check) |
|
||||||
|
|
||||||
|
`Header` exposes a global flight selector — the **selected flight is the
|
||||||
|
application context** and most other pages auto-filter by it. The
|
||||||
|
selection is persisted server-side via `UserSettings` in the
|
||||||
|
`annotations/` API.
|
||||||
|
|
||||||
|
For the full UX spec (wireframes, interactions, keyboard shortcuts, color
|
||||||
|
tokens, affiliation icons, combat-readiness indicator, gradient annotation
|
||||||
|
list, time-window video annotation overlay, drawer behaviour, etc.) see
|
||||||
|
[`_docs/ui_design/README.md`](_docs/ui_design/README.md).
|
||||||
|
|
||||||
|
## Tech stack
|
||||||
|
|
||||||
|
- **React 19** + **TypeScript 5.7**
|
||||||
|
- **Vite 6** (`bun run dev` / `bun run build`)
|
||||||
|
- **Bun 1.3.11** as package manager and runtime (`packageManager` field)
|
||||||
|
- **Tailwind CSS 4** via `@tailwindcss/vite`
|
||||||
|
- **react-router-dom 7** for routing
|
||||||
|
- **react-i18next** for UA/EN localization (see `src/i18n/`)
|
||||||
|
- **leaflet** + **react-leaflet** + **leaflet-draw** + **leaflet-polylinedecorator**
|
||||||
|
for the Flights map
|
||||||
|
- **chart.js** + **react-chartjs-2** for telemetry / class distribution
|
||||||
|
- **@hello-pangea/dnd** for waypoint reordering
|
||||||
|
- **react-icons** for icon set
|
||||||
|
- **react-dropzone** for orthophoto upload
|
||||||
|
|
||||||
|
There is no client-side state library — local component state and
|
||||||
|
`Context` (`AuthContext`, `FlightContext`) are sufficient at this scale.
|
||||||
|
Server data is fetched on demand and not cached beyond component
|
||||||
|
lifetime; introduce TanStack Query when the test suite is in place if
|
||||||
|
the lack of caching becomes a real problem.
|
||||||
|
|
||||||
|
## Repository layout
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── main.tsx app entry point (creates the React root)
|
||||||
|
├── App.tsx routes, AuthProvider, FlightProvider
|
||||||
|
├── index.css tailwind base + custom CSS variables
|
||||||
|
├── api/
|
||||||
|
│ ├── client.ts fetch wrapper, JWT injection, error normalization
|
||||||
|
│ └── sse.ts EventSource helper used by flights + detections
|
||||||
|
├── auth/
|
||||||
|
│ ├── AuthContext.tsx JWT lifecycle, user identity, role
|
||||||
|
│ └── ProtectedRoute.tsx route guard
|
||||||
|
├── components/ shared UI (Header, FlightContext, ConfirmDialog,
|
||||||
|
│ DetectionClasses, HelpModal …)
|
||||||
|
├── features/
|
||||||
|
│ ├── login/ LoginPage
|
||||||
|
│ ├── flights/ FlightsPage + map, sidebar, params panel,
|
||||||
|
│ │ waypoint list, altitude chart, mini-map,
|
||||||
|
│ │ wind effect, JSON editor, draw control
|
||||||
|
│ ├── annotations/ AnnotationsPage + canvas editor, video player,
|
||||||
|
│ │ media list, annotations sidebar, class colors
|
||||||
|
│ ├── dataset/ DatasetPage
|
||||||
|
│ ├── admin/ AdminPage
|
||||||
|
│ └── settings/ SettingsPage
|
||||||
|
├── hooks/ useDebounce, useResizablePanel
|
||||||
|
├── i18n/ i18n.ts + en.json + ua.json
|
||||||
|
└── types/ shared TS types
|
||||||
|
|
||||||
|
_docs/
|
||||||
|
├── legacy/ history of the WPF predecessor
|
||||||
|
└── ui_design/ page-level wireframes inherited from the WPF UI
|
||||||
|
|
||||||
|
.cursor/ workspace-scoped agents/rules/skills
|
||||||
|
.woodpecker/ CI pipeline (Woodpecker, ARM build target)
|
||||||
|
Dockerfile multi-stage: bun build → nginx static
|
||||||
|
nginx.conf production nginx config (SPA fallback to /index.html)
|
||||||
|
vite.config.ts vite + react + tailwind plugin
|
||||||
|
tsconfig.json TS config
|
||||||
|
```
|
||||||
|
|
||||||
|
The skeleton mirrors the **page-per-feature, control-per-domain**
|
||||||
|
decomposition of the legacy WPF app:
|
||||||
|
`Azaion.Annotator` → `features/annotations/`,
|
||||||
|
`Azaion.Dataset` → `features/dataset/`,
|
||||||
|
`Azaion.Suite.MainSuite` → `components/Header.tsx`, etc. See
|
||||||
|
`_docs/legacy/wpf-era.md` §10 for the per-feature mapping and §11 for
|
||||||
|
the parts that are intentionally NOT being ported.
|
||||||
|
|
||||||
|
## Local development
|
||||||
|
|
||||||
|
Prerequisites:
|
||||||
|
|
||||||
|
- [Bun](https://bun.sh/) 1.3.11+
|
||||||
|
- A reachable suite (or at least the `admin/` and `annotations/`
|
||||||
|
services). For an all-in-one local stack, see
|
||||||
|
[`suite/_infra/dev/README.md`](../_infra/dev/README.md).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# from suite/ui/
|
||||||
|
bun install --frozen-lockfile
|
||||||
|
bun run dev # http://localhost:5173 (Vite)
|
||||||
|
bun run build # tsc -b + vite build → dist/
|
||||||
|
bun run preview # serve the production build locally
|
||||||
|
```
|
||||||
|
|
||||||
|
API base URL is configured via Vite environment variables. Copy
|
||||||
|
`.env.example` (when introduced — currently the wiring is hardcoded;
|
||||||
|
this is one of the testability fixes scheduled for `/autodev` Step 4)
|
||||||
|
to `.env.local` before `bun run dev`. Until then, see
|
||||||
|
`src/api/client.ts` for the current base-URL strategy.
|
||||||
|
|
||||||
|
## Production build
|
||||||
|
|
||||||
|
The production image is a static bundle behind nginx:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t azaion-ui:dev .
|
||||||
|
docker run --rm -p 8080:80 azaion-ui:dev
|
||||||
|
# open http://localhost:8080/
|
||||||
|
```
|
||||||
|
|
||||||
|
The image expects the suite's reverse-proxy / ingress to terminate TLS
|
||||||
|
and forward `/api/*` to the appropriate backend service. See
|
||||||
|
[`suite/_infra/deploy/`](../_infra/deploy/) for per-target compose files.
|
||||||
|
|
||||||
|
## CI
|
||||||
|
|
||||||
|
CI runs in self-hosted **Woodpecker**, building the multi-arch Docker
|
||||||
|
image and pushing it to the suite's Harbor registry. Pipeline: see
|
||||||
|
[`.woodpecker/build-arm.yml`](.woodpecker/build-arm.yml). Suite-wide CI
|
||||||
|
infra: [`suite/_infra/ci/README.md`](../_infra/ci/README.md).
|
||||||
|
|
||||||
|
The build is triggered on push to `dev`, `stage`, and `main`. Image tags
|
||||||
|
are branch-tagged; no `latest` tag is published.
|
||||||
|
|
||||||
|
## How this repo is being matured
|
||||||
|
|
||||||
|
1. **Document the existing code** — bottom-up, via the `/document`
|
||||||
|
skill: `_docs/02_document/` will hold module docs, component specs,
|
||||||
|
architecture, and a final report. Triggered automatically as Step 1
|
||||||
|
of the existing-code flow.
|
||||||
|
2. **Architecture baseline scan** of the produced architecture against
|
||||||
|
the codebase (`_docs/02_document/architecture_compliance_baseline.md`).
|
||||||
|
3. **Test specifications** — produced from the documentation, not from
|
||||||
|
the code, so the tests describe intended behaviour. Output:
|
||||||
|
`_docs/02_document/tests/`.
|
||||||
|
4. **Testability revision** — the smallest possible refactor that lets
|
||||||
|
tests run (replace hardcoded API URLs with env vars, etc.).
|
||||||
|
Strictly minimal; deeper refactoring is deferred to step 8.
|
||||||
|
5. **Decompose tests** into individual tasks and **implement** them.
|
||||||
|
6. **Run tests** — green tests become the safety net.
|
||||||
|
7. **(Optional) Refactor** the codebase against the safety net.
|
||||||
|
8. From there, the project enters the existing-code feature cycle:
|
||||||
|
*new task → implement → run tests → sync test specs → update docs
|
||||||
|
→ security audit → performance test → deploy → retrospective*, and
|
||||||
|
loops.
|
||||||
|
|
||||||
|
The state machine for this is in [`_docs/_autodev_state.md`](_docs/_autodev_state.md).
|
||||||
|
The orchestration definition is in
|
||||||
|
[`.cursor/skills/autodev/SKILL.md`](.cursor/skills/autodev/SKILL.md).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
See the parent suite repository.
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# Acceptance Criteria — Azaion UI
|
||||||
|
|
||||||
|
> Output of `/document` Step 6c. Criteria derived from **measurable values
|
||||||
|
> already evidenced in code or config**: server-side hard caps, validation
|
||||||
|
> rules, health checks, perf configs, and architectural non-negotiables.
|
||||||
|
> Aspirational targets without a concrete check are explicitly marked.
|
||||||
|
|
||||||
|
**Status**: synthesised-from-verified-docs (Step 6c — `/document`)
|
||||||
|
**Date**: 2026-05-10
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Format
|
||||||
|
|
||||||
|
Every criterion must have a measurable value. Each row carries a unique ID
|
||||||
|
(`AC-NN`), the criterion, a measurement method, and the source-of-truth.
|
||||||
|
|
||||||
|
| AC | Criterion | Measurable value | How to measure | Source |
|
||||||
|
|----|-----------|------------------|----------------|--------|
|
||||||
|
| AC-01 | Authenticated requests carry the HttpOnly refresh cookie | `credentials:'include'` on every authenticated `fetch` and on the refresh call | Static check (linter / test) on `src/api/client.ts` and `src/auth/AuthContext.tsx`; runtime test that 401 → POST refresh → retry succeeds | `src/api/client.ts:44`; `_docs/02_document/04_verification_log.md` F2 |
|
||||||
|
| AC-02 | Bearer is never written to client storage | Zero `localStorage.*` / `sessionStorage.*` calls touching the bearer | Code-search regression test (Grep on `src/`) | P3; `_docs/02_document/architecture.md` § 7 |
|
||||||
|
| AC-03 | Refresh cookie attributes | Cookie issued by `admin/` MUST carry `Secure HttpOnly SameSite=Strict` | Server-side concern; UI test asserts the cookie is non-readable from JS (`document.cookie` does not contain the refresh token) | `_docs/02_document/architecture.md` § 7 |
|
||||||
|
| AC-04 | Numeric enums match the suite spec on the wire | `AnnotationStatus`, `MediaStatus`, `Affiliation`, `CombatReadiness` numeric values match the spec verbatim | Unit test asserting each enum's values; contract test on every `api.*()` payload using these enums | P9; `src/types/index.ts`; `04_verification_log.md` enum drift |
|
||||||
|
| AC-05 | Annotation save endpoint | Save POSTs to `/api/annotations/annotations` (doubly-prefixed) | Integration test asserting the URL and body shape (must include `Source`, `WaypointId`, `videoTime`) | `src/features/annotations/AnnotationsPage.tsx:39`; `04_verification_log.md` F5 + finding #32 |
|
||||||
|
| AC-06 | Selected-flight persistence path | Selection persists via `PUT /api/annotations/settings/user` with `{selectedFlightId}` (NOT a dedicated `/api/flights/select` endpoint) | Integration test on `FlightContext.selectFlight` round trip | `src/components/FlightContext.tsx:24,31,34,44`; `04_verification_log.md` F3 |
|
||||||
|
| AC-07 | Bulk-validate works | `POST /api/annotations/dataset/bulk-status` transitions selected items to `AnnotationStatus.Validated` | E2E test: select N items → click Validate → assert status update | `src/features/dataset/DatasetPage.tsx:65-73,142-146`; `04_verification_log.md` F9 |
|
||||||
|
| AC-08 | Live-GPS SSE per selected flight | `createSSE('/api/flights/${flightId}/live-gps', ...)` is open while a flight is selected; closes on unselect | Integration test: select flight, observe EventSource open; deselect, observe close | `src/features/flights/FlightsPage.tsx:67`; F13 |
|
||||||
|
| AC-09 | Annotation-status SSE | `createSSE('/api/annotations/annotations/events', ...)` open during `06_annotations` page lifetime | Integration test on subscribe / unsubscribe | `src/features/annotations/AnnotationsSidebar.tsx:25`; F14 |
|
||||||
|
| AC-10 | Upload size cap | Server-side hard cap is `client_max_body_size 500M`; UI error path on 413 produces a user-visible message | nginx config check; integration test posts 501 MB → asserts 413 + UI surfaces | `nginx.conf` `client_max_body_size 500M`; `architecture.md` § 6 |
|
||||||
|
| AC-11 | Bundle size budget | Initial JS (gzipped) ≤ **~2 MB** target | `vite build` artifact size measured in CI; **no gate today** — adding the gate is a Phase B task | `architecture.md` § 6 NFR row "Bundle size" — **target, not currently enforced** |
|
||||||
|
| AC-12 | i18n coverage | Every user-visible string has both an `en.json` and `ua.json` entry; no string literals in components beyond proper-noun acronyms | Lint rule + assertion test that `Object.keys(en) === Object.keys(ua)` | P6; `src/i18n/i18n.ts` |
|
||||||
|
| AC-13 | i18n language detection / persistence | `i18next` `lng` resolves from a detector (cookie / `Accept-Language`) and persists across reloads. **Currently `lng:'en'` is hardcoded** — Step 4 fix | Manual + integration test that toggling language in Header survives reload | `src/i18n/i18n.ts`; finding |
|
||||||
|
| AC-14 | Destructive actions require `ConfirmDialog` | Class delete (`AdminPage.handleDeleteClass`) and other destructive flows MUST present `ConfirmDialog`; **`alert()` is forbidden** | Static check + integration test for delete flows | O10; finding B4; `MediaList` `alert()` finding |
|
||||||
|
| AC-15 | a11y — `ConfirmDialog` | `role=dialog` + `aria-modal=true` + focus-trap + Esc-to-cancel | Component test using `@testing-library/react` | finding (`ConfirmDialog` lacks `aria-modal/role=dialog`) |
|
||||||
|
| AC-16 | a11y — Header flight dropdown | `role=combobox`, `aria-expanded`, Esc-to-close, focus-trap, outside-click handler attached only when open | Component test | finding (`Header.tsx` outside-click handler always attached; missing combobox roles) |
|
||||||
|
| AC-17 | a11y — `ProtectedRoute` spinner | `role=status` + accessible label; loading state has a timeout | Component test asserting a11y attributes; integration test asserting timeout fallback | finding |
|
||||||
|
| AC-18 | Browser support | Chromium-based + Firefox latest 2 versions render the SPA correctly | Manual smoke (no `browserslist` enforcement today) | `architecture.md` § 6 — **manual / aspirational** |
|
||||||
|
| AC-19 | Mobile responsiveness | Header bottom-nav variant renders at < 768 px; main pages render at ≥ 768 px | Manual smoke at the two breakpoints | `Header.tsx:113-129`; `architecture.md` § 6 |
|
||||||
|
| AC-20 | OpenWeatherMap key NOT in source | `import.meta.env.VITE_OPENWEATHERMAP_API_KEY` (or proxied via suite); zero hardcoded keys in any `src/` or `mission-planner/` module | Static check (regex against the current literal); CI step | P10; `mission-planner/src/utils/flightPlanUtils.ts:60` (current violation, Step 4 fix) |
|
||||||
|
| AC-21 | UserSettings persistence — panel widths | Panel-width changes via `useResizablePanel` write back to `PUT /api/annotations/settings/user`; reload restores widths | Integration test: change width → reload → assert restored | P11; `src/hooks/useResizablePanel.ts` (current violation) |
|
||||||
|
| AC-22 | RBAC client-side route gates | `/admin` and `/settings` redirect non-privileged users to `/flights` (or `/login` if not authenticated). Server-side 403 is the authoritative gate; UI gate is convenience | Integration test: log in as non-admin → navigate to `/admin` → assert redirect | finding (`/admin` route lacks role-gate — security PRIORITY) |
|
||||||
|
| AC-23 | Auth refresh transparency | One refresh = one network round trip; **no UI re-render past `<ProtectedRoute>`** | Integration test asserting `<ProtectedRoute>` does not unmount during refresh | `architecture.md` § 6 NFR row "Auth refresh"; `04_verification_log.md` F2 |
|
||||||
|
| AC-24 | SSE bearer-rotation handling | When the bearer rotates (refresh), open SSE connections **must** reconnect with the new bearer | Integration test: open SSE → trigger refresh → assert reconnection. **Currently NOT implemented (Step 8 hardening)** | `ADR-008`; `architecture.md` § 7 |
|
||||||
|
| AC-25 | Detect endpoint correctness | Sync image detect uses `POST /api/detect/${mediaId}`. **Async video detect (`F7`) — when implemented in Phase B — uses `POST /api/detect/video/${mediaId}` returning a job ID + SSE on `/api/detect/stream/${jobId}`**. Long-video flows MUST send `X-Refresh-Token` (per `_docs/10_auth.md`) | Integration tests per path | `src/features/annotations/AnnotationsSidebar.tsx:39`; F6 / F7 / F14 |
|
||||||
|
| AC-26 | Numeric input hygiene | Numeric form inputs in `09_settings` and `08_admin` reject empty input rather than silently writing `0` | Component tests on `parseInt(v) || 0` patterns (currently a finding) | finding B4 |
|
||||||
|
| AC-27 | Save error surfacing | `09_settings` save handlers (`saveSystem`, `saveDirs`) use `try/finally` to reset `saving:true`; failure is surfaced via toast / inline error | Integration test that simulates a 500 on PUT and asserts state reset | finding B4 |
|
||||||
|
| AC-28 | Annotation overlay time window | The on-canvas annotation overlay window is asymmetric `[-50 ms, +150 ms]` around the current frame (matches WPF source `_thresholdBefore=50ms / _thresholdAfter=150ms`). **Currently symmetric ±200 ms** — Step 4 fix | Component test asserting overlay membership at `currentTime ± 50/150 ms` | finding #6; `04_verification_log.md` §2d |
|
||||||
|
| AC-29 | `mediaType` is typed | All `mediaType` references use the `MediaType` enum (`None=0`, `Image=1`, `Video=2`); zero magic literals | Static check (Grep `mediaType\s*===\s*[0-9]`) | finding #5 / #10; P9 |
|
||||||
|
| AC-30 | Class delete confirmation | `AdminPage.handleDeleteClass` shows `ConfirmDialog` before issuing `DELETE /api/admin/classes/${id}` | Integration test | finding B4 |
|
||||||
|
| AC-31 | `mission-planner/` is not in the production bundle | `vite build` output does not include any `mission-planner/**` chunk | Bundle inspection; static-import check | `vite.config.ts`; `ADR-009`; P2 |
|
||||||
|
| AC-32 | CI tags + labels | Image is pushed with `${branch}-arm` tag and OCI labels (`org.opencontainers.image.{revision,created,source}`) | Pipeline assertion on the push step | `.woodpecker/build-arm.yml` |
|
||||||
|
| AC-33 | Production runtime is `nginx:alpine` only | Final image stage is `nginx:alpine`; no Node.js binary in the production image | Container inspection (`docker inspect`) | `Dockerfile` |
|
||||||
|
| AC-34 | nginx routes 9 services | `nginx.conf` declares `/api/admin/`, `/api/flights/`, `/api/annotations/`, `/api/detect/`, `/api/loader/`, `/api/gps-denied-desktop/`, `/api/gps-denied-onboard/`, `/api/autopilot/`, `/api/resource/` — each strips its `/api/<service>/` prefix | Config assertion test | `nginx.conf`; `ADR-006` |
|
||||||
|
| AC-35 | Manual bbox draw on `CanvasEditor` | A mousedown → mousemove → mouseup gesture on the canvas creates one new local detection with `classNum = selectedClassNum + photoModeOffset` (per AC-38) and `x,y,w,h` (normalised) matching the dragged rectangle within ±1 normalised px-equivalent; the new detection is appended to local state and is rendered immediately | Component test on `CanvasEditor` with synthetic pointer events; verify local-state shape | `components/06_annotations/description.md`; `system-flows.md` Flow F5; `solution.md:165,224` |
|
||||||
|
| AC-36 | 8-handle bbox resize + canvas modifier interactions | (a) Dragging any of the 8 resize handles (4 corners + 4 edge midpoints) of a selected bbox updates only the corresponding edges; (b) `Ctrl+click` on a bbox **adds it to the selection set** (multi-select); (c) `Ctrl+wheel` over the canvas zooms in/out around the cursor; (d) `Ctrl+drag` on empty canvas pans the view. Bboxes have a minimum normalised size > 0 so handle-drag past zero clamps instead of inverting. | Component tests on `CanvasEditor` with synthetic events (one per modifier path); assert resulting bbox / selection set / viewport state | `components/06_annotations/description.md`; `glossary.md:45` (CanvasEditor); `01_legacy_coverage_gaps.md:29-30`; `solution.md:224` |
|
||||||
|
| AC-37 | Class picker (`DetectionClasses` widget) | Widget loads class list from `GET /api/annotations/classes`; **number-key 1–9** (window `keydown`) selects `classes[(num-1) + photoMode]` and emits `onSelect(class.id)`; clicking a class entry emits the same; the rendered visible label index `i+1` matches the hotkey number for that class **within the currently active PhotoMode** (per AC-38). Fallback list is used when the API returns empty or errors. Backend class ordering MUST be `[0..N-1] (Regular), [20..20+N-1] (Winter), [40..40+N-1] (Night)` — when it is not, this AC fails (Step 4 verification candidate). | Component test on `DetectionClasses` with mocked API + simulated keypresses + clicks; contract test asserting backend response ordering on a fixture | `components/03_shared-ui/description.md:37`; `modules/src__components__DetectionClasses.md`; `data_model.md:158`; `_docs/legacy/wpf-era.md` §10 |
|
||||||
|
| AC-38 | PhotoMode switcher (Regular / Winter / Night) | PhotoMode buttons emit values from the set `{0, 20, 40}` (Regular=0, Winter=+20, Night=+40). Switching mode: (a) re-filters the class list to entries whose `photoMode` equals the new mode; (b) if the previously-selected `classNum` is not in the new filtered set, auto-selects the first class of the new mode and emits `onSelect`. On annotation save, the wire `Detection.classNum` (a.k.a. *yoloId*) equals `classId + photoModeOffset`. | Component test on the mode-switch effect + integration test on the save payload | `modules/src__components__DetectionClasses.md` §22, §31-43; `data_model.md:84`; `components/11_class-colors/description.md:31-35`; `ui_design/README.md:127-128`; `ui_design/annotations.html:84-93` |
|
||||||
|
| AC-39 | Tile-splitting endpoint + wire shape | `POST /api/annotations/dataset/{id}/split` exists and is callable from the dataset surface; success response is JSON with HTTP 200. `AnnotationListItem.isSplit: boolean` and `AnnotationListItem.splitTile: string \| null` (YOLO label `<class> <cx> <cy> <w> <h>`) are honored on read. When `isSplit === true` and `splitTile` is non-null, the client parses the 5-token YOLO label without throwing; malformed `splitTile` surfaces a user-visible error (no silent swallow). `DatasetItem.isSplit?: boolean` is read on the dataset list path (parent-suite-doc fix applied — see `_docs/_process_leftovers/2026-05-10_parent-suite-doc-fixes.md`). | Integration test against a fixture response; unit test on the YOLO-label parser with valid + malformed inputs | `components/07_dataset/description.md:28`; `data_model.md:104-105,130,164`; `modules/src__features__annotations.md:31,75`; `modules/src__types__index.md:24-28` |
|
||||||
|
| AC-40 | Tile-zoom auto-zoom on split-image annotation open | When the user opens a `splitTile`-bearing annotation (double-click in `AnnotationsSidebar` or seek via the annotation list), `CanvasEditor` auto-zooms to the tile region encoded by `splitTile` (parsed per AC-39). The visible viewport rectangle equals the tile rectangle within ±1 px on each edge. A small visual tile-zoom indicator (icon / badge) is rendered while the tile zoom is active so the operator knows the view is constrained. **Currently MISSING** — finding #24 in `modules/src__features__annotations.md`; Step 4 / Phase B fix. | Component test on `CanvasEditor` with a `splitTile`-bearing annotation; assert viewport rect + presence of the tile-zoom indicator | `components/06_annotations/description.md:62, 103`; `modules/src__features__annotations.md:75` finding #24; `legacy/wpf-era.md` (OpenAnnotationResult seek + ZoomTo) |
|
||||||
|
|
||||||
|
## Anti-criteria — explicit non-goals
|
||||||
|
|
||||||
|
| AC# | Statement | Source |
|
||||||
|
|-----|-----------|--------|
|
||||||
|
| AC-N1 | The UI does NOT support real-time multi-user collaborative annotation. | F14 caveat: server pushes status events, the UI consumes; no concurrent edit semantics |
|
||||||
|
| AC-N2 | The UI does NOT host any in-browser ML model. All inference is server-side. | `package.json` has no ML libs |
|
||||||
|
| AC-N3 | The UI does NOT support offline mode. (Tile cache for field deployments is a separate, future concern.) | `architecture.md` § 2 |
|
||||||
|
| AC-N4 | The UI does NOT enforce a server-side response signature / checksum on REST replies. (Server is trusted within the suite network.) | absence of any signature library in `package.json` |
|
||||||
|
| AC-N5 | The UI does NOT port WPF Sound Detections or Drone Maintenance — both **dropped** per Step 4.5 decision. | `01_legacy_coverage_gaps.md` Step 4.5 update |
|
||||||
|
|
||||||
|
## Coverage status
|
||||||
|
|
||||||
|
- **Currently met & enforced**: AC-02 (no token storage), AC-05 (annotation save URL — body shape pending), AC-06, AC-07, AC-08, AC-09, AC-10 (server cap; UI surface is a finding), AC-25 (sync path; async path is target-only), AC-31, AC-33, AC-34.
|
||||||
|
- **Currently met but not enforced by CI**: AC-04 (enum values), AC-12 (i18n parity), AC-29 (typed `mediaType`), AC-35 (manual bbox draw), AC-37 (class picker — pending Step 4 backend-ordering verification), AC-38 (PhotoMode switcher).
|
||||||
|
- **Currently violated — Step 4 fix candidates**: AC-01 (bootstrap refresh), AC-13 (i18n detector), AC-14 / AC-30 (class-delete dialog; `alert()` use), AC-15–AC-17 (a11y), AC-20 (OWM key), AC-21 (panel widths), AC-22 (route role-gate), AC-23 (refresh re-render — code-path correct, but bootstrap-refresh fix needed), AC-26 (numeric input hygiene), AC-27 (save error surfacing), AC-28 (overlay window), AC-36 (Ctrl-multi-select / Ctrl-wheel zoom / Ctrl-drag pan flagged "Partially missing"), AC-40 (tile-zoom auto-zoom — finding #24, no consumer of `splitTile` today).
|
||||||
|
- **Phase B targets (not currently in scope of `/document` Step 6)**: AC-11 (bundle gate), AC-18 (browser-list), AC-19 (mobile floor), AC-24 (SSE refresh re-subscribe), AC-25 async path, AC-32 (CI label assertions), AC-39 (tile-split endpoint — parent-suite-doc fix applied for `isSplit`; the YOLO-label parser hardening lands when the splitTile consumer is wired in Phase B).
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
# Data Parameters — Azaion UI
|
||||||
|
|
||||||
|
> Output of `/document` Step 6d. The Azaion UI is a **thin client over a typed
|
||||||
|
> REST + SSE contract**; it carries no database. "Input data" therefore means
|
||||||
|
> the data shapes the SPA consumes (REST response payloads, SSE event
|
||||||
|
> payloads, env config). All claims trace to `_docs/02_document/data_model.md`,
|
||||||
|
> `architecture.md` § 4–5, and per-component descriptions.
|
||||||
|
|
||||||
|
**Status**: synthesised-from-verified-docs (Step 6d — `/document`)
|
||||||
|
**Date**: 2026-05-10
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Categories of input data
|
||||||
|
|
||||||
|
The SPA consumes four categories:
|
||||||
|
|
||||||
|
1. **Typed REST entities** — see `_docs/02_document/data_model.md` for the
|
||||||
|
full ER map; key shapes summarised below.
|
||||||
|
2. **SSE event payloads** — `live-gps`, `annotation-status`, planned
|
||||||
|
`detect-stream`.
|
||||||
|
3. **Configuration / environment variables** — runtime config injected at
|
||||||
|
build time or via env.
|
||||||
|
4. **Static assets** — translation bundles, icons, design tokens (compiled
|
||||||
|
into the bundle).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Typed REST entities (defined in `src/types/index.ts`)
|
||||||
|
|
||||||
|
> Every entity below mirrors the suite's REST contract. Values listed here
|
||||||
|
> match the **suite spec**, which is the source of truth per principle P9.
|
||||||
|
> Where the UI's current TypeScript enum drifts from the spec, the row notes
|
||||||
|
> the drift and the Step 4 fix.
|
||||||
|
|
||||||
|
### Auth
|
||||||
|
|
||||||
|
| Entity | Fields | Source |
|
||||||
|
|--------|--------|--------|
|
||||||
|
| `AuthUser` | `id`, `email`, `name`, `role`, `permissions: string[]`, `aircraftId?` | `02_auth`; `admin/` service |
|
||||||
|
| `LoginRequest` | `{ email, password }` | `POST /api/admin/auth/login` body |
|
||||||
|
| `LoginResponse` | `{ bearer, user: AuthUser }` (refresh cookie set server-side) | `POST /api/admin/auth/login` 200 |
|
||||||
|
|
||||||
|
### Flights
|
||||||
|
|
||||||
|
| Entity | Fields | Source |
|
||||||
|
|--------|--------|--------|
|
||||||
|
| `Flight` | `id`, `name`, `aircraftId`, `startDate?`, `endDate?`, `description?` | `flights/` service |
|
||||||
|
| `Waypoint` | **Spec**: `{ Geopoint: { Lat, Lon, MGRS }, Source, Objective, OrderNum, Height }`. **UI today**: `{ name, latitude, longitude, order }` — drift, finding #20 / Step 4 fix | `05_flights`; `flights/` service |
|
||||||
|
| `Aircraft` | `id`, `name`, `model`, `isDefault`, `serialNumber?` | `flights/` (read+write); `08_admin` mutation |
|
||||||
|
| `LiveGpsEvent` (SSE) | `{ flightId, lat, lon, alt, heading, speed, ts }` | `createSSE('/api/flights/${id}/live-gps')`; F13 |
|
||||||
|
|
||||||
|
### Annotations + Media
|
||||||
|
|
||||||
|
| Entity | Fields | Source |
|
||||||
|
|--------|--------|--------|
|
||||||
|
| `Media` | `id`, `flightId`, `mediaType: MediaType`, `mediaStatus: MediaStatus`, `filename`, `waypointId?`, `videoTime?`, `thumbnail?` | `annotations/` service |
|
||||||
|
| `MediaType` enum | **Spec**: `None=0`, `Image=1`, `Video=2`. **UI**: same. | `00_foundation`; P9 |
|
||||||
|
| `MediaStatus` enum | **Spec**: must include `None`, `Confirmed`, `Error` plus the existing `New`, `AiProcessing`, `AiProcessed`, `ManualCreated`. **UI today**: only `New=0` / `AiProcessing=1` / `AiProcessed=2` / `ManualCreated=3` — drift, Step 4 fix | `00_foundation`; finding |
|
||||||
|
| `AnnotationListItem` | `id`, `mediaId`, `videoTime`, `status: AnnotationStatus`, `source: AnnotationSource`, `detections: Detection[]`, `isSeed?: boolean` | `annotations/` |
|
||||||
|
| `AnnotationStatus` enum | **Spec**: `None=0`, `Created=10`, `Edited=20`, `Validated=30`, `Deleted=40`. **UI today**: `Created=0`, `Edited=1`, `Validated=2` — drift, Step 4 fix per P9 | `00_foundation`; `04_verification_log.md` |
|
||||||
|
| `AnnotationSource` enum | `AI=0`, `Manual=1` (matches spec) | `00_foundation` |
|
||||||
|
| `Detection` | `{ classNum: number, x, y, w, h: number, affiliation: Affiliation, combatReadiness: CombatReadiness, confidence?: number }` (normalised pixel coords) | `06_annotations` |
|
||||||
|
| `Affiliation` enum | **Spec**: must include `None` plus `Unknown`, `Friendly`, `Hostile`. **UI today**: `Unknown=0`, `Friendly=1`, `Hostile=2` — drift, Step 4 fix | finding |
|
||||||
|
| `CombatReadiness` enum | **Spec**: must include `Unknown` plus `NotReady`, `Ready`. **UI today**: `NotReady=0`, `Ready=1` — drift, Step 4 fix | finding |
|
||||||
|
| `DetectionClass` | `{ id, name, color, photoMode, maxSizeM }` | `08_admin` (write) + `annotations/` (read) |
|
||||||
|
| Annotation save body | **Required** (per finding #32): `Source`, `WaypointId`, `videoTime`, plus `mediaId`, `detections`, `status`. **UI today**: missing `Source` and `WaypointId`; uses `time` instead of `videoTime` — Step 4 fix | `06_annotations/AnnotationsPage.tsx` |
|
||||||
|
| `AnnotationStatusEvent` (SSE) | `{ annotationId, mediaId, oldStatus, newStatus, ts }` | `createSSE('/api/annotations/annotations/events')`; F14 |
|
||||||
|
|
||||||
|
### Dataset
|
||||||
|
|
||||||
|
| Entity | Fields | Source |
|
||||||
|
|--------|--------|--------|
|
||||||
|
| `DatasetItem` | `id`, `mediaId`, `classNum`, `status: AnnotationStatus`, `thumbnail`, `isSeed?: boolean`, `isSplit?: boolean` (parent-suite-doc fix applied for `isSplit`) | `07_dataset`; `annotations/` |
|
||||||
|
| `ClassDistributionItem` | `{ classNum, label, color, count }` | `annotations/` |
|
||||||
|
| Bulk-validate body | `{ ids: number[], targetStatus: AnnotationStatus.Validated }` | `POST /api/annotations/dataset/bulk-status` |
|
||||||
|
|
||||||
|
### Settings + Admin
|
||||||
|
|
||||||
|
| Entity | Fields | Source |
|
||||||
|
|--------|--------|--------|
|
||||||
|
| `SystemSettings` | as defined per `09_settings/SettingsPage.tsx` (settings keys per the suite spec) | `annotations/` (`/api/annotations/settings/system`) |
|
||||||
|
| `DirectorySettings` | per `SettingsPage` directory tab | `annotations/` (`/api/annotations/settings/directories`) |
|
||||||
|
| `CameraSettings` | per `SettingsPage` camera tab | `annotations/` |
|
||||||
|
| `UserSettings` | `selectedFlightId?: number`, `panelWidths?: { ... }`, plus other per-user UI state | `annotations/` (`/api/annotations/settings/user`) |
|
||||||
|
| `User` | `id`, `email`, `role`, `isActive`, `createdAt?` | `admin/` |
|
||||||
|
|
||||||
|
### Pagination
|
||||||
|
|
||||||
|
| Entity | Shape | Source |
|
||||||
|
|--------|-------|--------|
|
||||||
|
| `PaginatedResponse<T>` | `{ items: T[], totalCount: number, page: number, pageSize: number }` | shared envelope used by every list endpoint |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. SSE event payloads
|
||||||
|
|
||||||
|
| Stream | URL | Payload shape | Where consumed |
|
||||||
|
|--------|-----|---------------|----------------|
|
||||||
|
| Live-GPS per flight (F13) | `GET /api/flights/${flightId}/live-gps?token=${bearer}` | `LiveGpsEvent` (see above) | `src/features/flights/FlightsPage.tsx:67` |
|
||||||
|
| Annotation-status events (F14) | `GET /api/annotations/annotations/events?token=${bearer}` | `AnnotationStatusEvent` | `src/features/annotations/AnnotationsSidebar.tsx:25` |
|
||||||
|
| Async detect progress (F7) | `GET /api/detect/stream/${jobId}?token=${bearer}` — **target-only, NOT wired today** | `{ jobId, progress: 0..1, detections?: Detection[], status, ts }` (anticipated) | not consumed today; planned per `04_verification_log.md` F7 |
|
||||||
|
|
||||||
|
Bearer goes in the **query string** (`?token=...`) per `ADR-008` — `EventSource`
|
||||||
|
cannot send headers. Refresh-rotation breaks live SSE; reconnect is missing
|
||||||
|
today (Step 8 hardening per `architecture.md` § Architecture Vision).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Configuration / environment variables
|
||||||
|
|
||||||
|
| Variable | Where read | Type | Default | Source |
|
||||||
|
|----------|-----------|------|---------|--------|
|
||||||
|
| `VITE_OPENWEATHERMAP_API_KEY` | (target — Step 4 fix) `mission-planner/src/utils/flightPlanUtils.ts` | string (secret) | currently hardcoded `'335799082893fad97fa36118b131f919'` (must rotate) | P10 violation, Step 4 fix |
|
||||||
|
| `VITE_SATELLITE_TILE_URL` | mission-planner Leaflet `TileLayer` | URL | none (unset breaks satellite imagery) | mission-planner only today |
|
||||||
|
| `AZAION_REVISION` | stamped into the production image at build time | string (commit SHA) | `$CI_COMMIT_SHA` from CI | `Dockerfile`; `.woodpecker/build-arm.yml` |
|
||||||
|
| `REGISTRY_HOST` | CI registry push | string | per pipeline secret | `.woodpecker/build-arm.yml` |
|
||||||
|
| `i18next.lng` | `src/i18n/i18n.ts` | language code | hardcoded `'en'` (Step 4 fix — should resolve from detector) | `i18n.ts`; AC-13 |
|
||||||
|
| nginx upstream hosts | `nginx.conf` | hostnames per service | docker-compose service names | `nginx.conf` |
|
||||||
|
|
||||||
|
The SPA bundle MUST NOT carry secrets at build time — except OpenWeatherMap
|
||||||
|
once it is moved to `.env` (per P10 the proper long-term answer is to proxy
|
||||||
|
the OWM call through `flights/` so no key reaches the browser; the `.env`
|
||||||
|
move is the interim Step 4 testability fix).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Static assets
|
||||||
|
|
||||||
|
| Asset | Location | Notes |
|
||||||
|
|-------|----------|-------|
|
||||||
|
| Translation bundles | `src/i18n/en.json`, `src/i18n/ua.json` | English + Ukrainian; key parity is mandatory (AC-12) |
|
||||||
|
| Design tokens | `src/index.css` (`az-bg`, `az-text`, `az-orange`, `az-success`, `az-danger`, `az-primary`, ...) | Tailwind 4; `ADR-005` |
|
||||||
|
| Map icons | `src/features/flights/mapIcons.ts` | defaultIcon CDN URL pinned to `leaflet@1.7.1` (drift — finding) |
|
||||||
|
| Aircraft / waypoint icons | bundled SVG / PNG under `src/features/flights/icons/*` (mission-planner port-source still has the larger set) | `05_flights` |
|
||||||
|
| Detection-class colors | `src/features/annotations/classColors.ts` (logically owned by `11_class-colors`) | file-move pending (P11 / module-layout Verification Needed) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Data flow summary
|
||||||
|
|
||||||
|
1. **Plan flight** — UI fetches `aircrafts` from `flights/`; submits flight +
|
||||||
|
waypoints; receives the persisted flight (today: delete-then-recreate
|
||||||
|
waypoint cycle, finding #19; lossy POST shape, finding #20).
|
||||||
|
2. **Capture media** — out-of-band via the loader / annotations services;
|
||||||
|
the UI surfaces uploaded items via `MediaList` polling.
|
||||||
|
3. **Annotate** — operator edits → `POST /api/annotations/annotations`;
|
||||||
|
`F14` SSE pushes other-user status changes (admin-wide stream,
|
||||||
|
client-side filtered).
|
||||||
|
4. **AI Detect (sync image)** — `POST /api/detect/${id}` returns inline
|
||||||
|
detections. **Used for both image and video today** (silent UX hazard
|
||||||
|
for long videos — `F7` to ship in Phase B).
|
||||||
|
5. **AI Detect (async video — target)** — `POST /api/detect/video/${id}`
|
||||||
|
returns a job ID → SSE on `/api/detect/stream/${jobId}` streams progress.
|
||||||
|
Long videos require `X-Refresh-Token` header per `_docs/10_auth.md`.
|
||||||
|
6. **Curate dataset** — UI queries `annotations/` with status filters;
|
||||||
|
bulk-validate transitions to `AnnotationStatus.Validated`; class-distribution
|
||||||
|
chart loads from `/api/annotations/dataset/class-distribution`.
|
||||||
|
7. **Settings** — system / directory / camera saves go to `annotations/`;
|
||||||
|
aircraft default-toggle goes to `flights/` (cross-service mutation,
|
||||||
|
accepted).
|
||||||
|
8. **GPS-Denied Test Mode (target — F12)** — `.tlog` + video upload to
|
||||||
|
`gps-denied-desktop/`; SITL drives `gps-denied-onboard/`; results render
|
||||||
|
back through `flights/` GPS-Denied tab.
|
||||||
|
|
||||||
|
Full sequence diagrams: `_docs/02_document/system-flows.md`.
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
{
|
||||||
|
"$schema_note": "Pinned numeric values for the suite's wire-format enums per the suite spec. Tests FT-P-04, FT-P-05, FT-P-06 assert that src/types/index.ts matches these values exactly. Drift between the UI and this snapshot is a Step 4 fix candidate (see acceptance_criteria.md AC-04, restrictions.md O7).",
|
||||||
|
"source_of_truth": [
|
||||||
|
{"file": "../_docs/00_database_schema.md", "extracted_at": "2026-05-10T22:00:00+03:00", "note": "Authoritative — the DB schema pins the numeric values directly."},
|
||||||
|
{"file": "../_docs/01_annotations.md", "note": "Wire-format declaration (line 26): all enum fields serialize as numeric integers."},
|
||||||
|
{"file": "../_docs/09_dataset_explorer.md", "note": "JSON examples for Affiliation and CombatReadiness use stale sequential values (affiliation:2 // Hostile, combatReadiness:1 // Ready) and predate the schema's 0/10/20/30 scheme. Parent-suite doc fix pending — record in _docs/_process_leftovers/ when populated."}
|
||||||
|
],
|
||||||
|
"ui_drift_summary": {
|
||||||
|
"AnnotationStatus": {"ui_values": {"Created": 0, "Edited": 1, "Validated": 2}, "spec_values": "see enums.AnnotationStatus", "fix_target": "src/types/index.ts (Step 4)"},
|
||||||
|
"MediaStatus": {"ui_values": {"New": 0, "AiProcessing": 1, "AiProcessed": 2, "ManualCreated": 3}, "spec_values": "see enums.MediaStatus", "fix_target": "src/types/index.ts (Step 4) — UI must add None=0, Confirmed=5, Error=6 and renumber existing members"},
|
||||||
|
"Affiliation": {"ui_values": {"Unknown": 0, "Friendly": 1, "Hostile": 2}, "spec_values": "see enums.Affiliation", "fix_target": "src/types/index.ts (Step 4) — UI must add None=0 and renumber existing members to spec values"},
|
||||||
|
"CombatReadiness": {"ui_values": {"NotReady": 0, "Ready": 1}, "spec_values": "see enums.CombatReadiness", "fix_target": "src/types/index.ts (Step 4) — UI must add Unknown and confirm numeric values via .NET service inspection"},
|
||||||
|
"MediaType": {"ui_values": {"None": 0, "Image": 1, "Video": 2}, "spec_values": "see enums.MediaType", "fix_target": "src/types/index.ts (Step 4) — spec schema order is None|Video|Image which implies Video=1, Image=2; UI has Image=1, Video=2. NEW DRIFT not previously called out in data_parameters.md."}
|
||||||
|
},
|
||||||
|
"enums": {
|
||||||
|
"AnnotationStatus": {
|
||||||
|
"source": "../_docs/00_database_schema.md line 79: enum AnnotationStatus \"None(0)|Created(10)|Edited(20)|Validated(30)|Deleted(40)\"",
|
||||||
|
"values": {"None": 0, "Created": 10, "Edited": 20, "Validated": 30, "Deleted": 40},
|
||||||
|
"verification_pending": false,
|
||||||
|
"notes": "Authoritative. Wire format used by POST /annotations, PATCH /annotations/{id}/status, POST /dataset/bulk-status, and the F14 AnnotationStatusEvent SSE payload."
|
||||||
|
},
|
||||||
|
"MediaStatus": {
|
||||||
|
"source": "../_docs/00_database_schema.md line 66: enum MediaStatus \"None(0)|New(1)|AIProcessing(2)|AIProcessed(3)|ManualCreated(4)|Confirmed(5)|Error(6)\"",
|
||||||
|
"values": {"None": 0, "New": 1, "AIProcessing": 2, "AIProcessed": 3, "ManualCreated": 4, "Confirmed": 5, "Error": 6},
|
||||||
|
"verification_pending": false,
|
||||||
|
"case_note": "Schema uses 'AIProcessing' (uppercase AI); UI uses 'AiProcessing' (camelCase). The wire payload is numeric only, so the TypeScript identifier casing is internal. Recommend matching the spec casing on rename for consistency."
|
||||||
|
},
|
||||||
|
"Affiliation": {
|
||||||
|
"source": "../_docs/00_database_schema.md line 94: enum Affiliation \"None(0)|Friendly(10)|Hostile(20)|Unknown(30)\"",
|
||||||
|
"values": {"None": 0, "Friendly": 10, "Hostile": 20, "Unknown": 30},
|
||||||
|
"verification_pending": false,
|
||||||
|
"stale_example_note": "../_docs/01_annotations.md line 208 and ../_docs/09_dataset_explorer.md line 165 still show 'affiliation: 2 // Affiliation.Hostile' — STALE per the schema. Flag as parent-suite-doc fix leftover."
|
||||||
|
},
|
||||||
|
"CombatReadiness": {
|
||||||
|
"source": "../_docs/00_database_schema.md line 95: enum CombatReadiness \"Ready|NotReady|Unknown\" — numeric values NOT pinned in the schema",
|
||||||
|
"values": {"NotReady": 0, "Ready": 1, "Unknown": 2},
|
||||||
|
"verification_pending": true,
|
||||||
|
"verification_note": "Numeric values inferred as sequential per the spec's member-listing order. The 01_annotations.md JSON example shows 'combatReadiness: 1 // CombatReadiness.Ready' which is consistent with Ready=1. Step 4 .NET-service inspection must confirm or override. Alternative possibility: the spec lists Ready first by intent (Ready=0, NotReady=1, Unknown=2) — schema text 'Ready|NotReady|Unknown' is ambiguous on intent."
|
||||||
|
},
|
||||||
|
"MediaType": {
|
||||||
|
"source": "../_docs/00_database_schema.md line 65: enum MediaType \"None|Video|Image\"",
|
||||||
|
"values": {"None": 0, "Video": 1, "Image": 2},
|
||||||
|
"verification_pending": true,
|
||||||
|
"verification_note": "Numeric values inferred sequentially per schema member-order (None|Video|Image). This contradicts the UI's current src/types/index.ts which has Image=1, Video=2. Step 4 .NET-service inspection must confirm. If the .NET service in fact uses None=0, Image=1, Video=2 (the UI's current shape), then the schema text is misleading and the UI is correct; otherwise the UI is drifted and needs the fix."
|
||||||
|
},
|
||||||
|
"AnnotationSource": {
|
||||||
|
"source": "../_docs/01_annotations.md lines 19-24 (table) + ../_docs/00_database_schema.md line 78 (enum Source \"AI|Manual\")",
|
||||||
|
"values": {"AI": 0, "Manual": 1},
|
||||||
|
"verification_pending": false,
|
||||||
|
"notes": "Both files agree: AI=0, Manual=1. UI matches."
|
||||||
|
},
|
||||||
|
"WaypointSource": {
|
||||||
|
"source": "../_docs/00_database_schema.md line 55: enum WaypointSource \"Auto|Manual\"",
|
||||||
|
"values": {"Auto": 0, "Manual": 1},
|
||||||
|
"verification_pending": true,
|
||||||
|
"verification_note": "Inferred sequentially. Not asserted by any test in this round; recorded for completeness because data_parameters.md / acceptance_criteria.md flag Waypoint POST shape drift for Step 4."
|
||||||
|
},
|
||||||
|
"WaypointObjective": {
|
||||||
|
"source": "../_docs/00_database_schema.md line 56: enum WaypointObjective \"Surveillance|Strike|Recon\"",
|
||||||
|
"values": {"Surveillance": 0, "Strike": 1, "Recon": 2},
|
||||||
|
"verification_pending": true,
|
||||||
|
"verification_note": "Inferred sequentially. Same caveat as WaypointSource — not currently asserted by a UI test."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"downstream_actions": {
|
||||||
|
"step_4_fixes": [
|
||||||
|
"src/types/index.ts AnnotationStatus → align to {None:0, Created:10, Edited:20, Validated:30, Deleted:40}",
|
||||||
|
"src/types/index.ts MediaStatus → add None, Confirmed, Error; renumber per spec",
|
||||||
|
"src/types/index.ts Affiliation → add None; renumber per spec",
|
||||||
|
"src/types/index.ts CombatReadiness → add Unknown; confirm numerics via .NET inspection; renumber if needed",
|
||||||
|
"src/types/index.ts MediaType → confirm numerics via .NET inspection; renumber if spec schema (Video=1, Image=2) wins"
|
||||||
|
],
|
||||||
|
"parent_suite_doc_fixes": [
|
||||||
|
"../_docs/01_annotations.md line 208 — Affiliation example value (currently affiliation:2 // Hostile) must update to 20 per the schema",
|
||||||
|
"../_docs/09_dataset_explorer.md line 165 — same fix",
|
||||||
|
"Both files — surface CombatReadiness numeric pinning in the table-of-truth (currently only in member listing)"
|
||||||
|
],
|
||||||
|
"phase_3_disposition": "FT-P-04 (AnnotationStatus) gates today. FT-P-05 (MediaStatus + Affiliation) gates today. FT-P-06 partial (detection enum payload check) gates for Affiliation today; CombatReadiness assertion runs with the verification_pending: true caveat (Phase 4 runner script can downgrade it to documentary until Step 4 inspection lands)."
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,332 @@
|
|||||||
|
# Expected Results — Azaion UI
|
||||||
|
|
||||||
|
> Maps every behavioral test trigger (REST request, SSE event, user action,
|
||||||
|
> build/config artifact, static check) to a **quantifiable** expected result.
|
||||||
|
> Sourced from `_docs/00_problem/acceptance_criteria.md` (34 ACs + 5
|
||||||
|
> anti-criteria) and cross-checked against `restrictions.md`,
|
||||||
|
> `data_parameters.md`, and `_docs/02_document/architecture.md`.
|
||||||
|
>
|
||||||
|
> **Project shape note.** The Azaion UI is a thin SPA over a typed REST + SSE
|
||||||
|
> contract; it carries no database and consumes no sample-image / sample-video
|
||||||
|
> input files of its own. "Input" here therefore means a trigger condition
|
||||||
|
> (HTTP request, SSE message, click, build invocation, code search) and the
|
||||||
|
> "expected result" is the observable behavior the test asserts. Almost every
|
||||||
|
> row uses the **behavioral shape** defined in `test-spec/SKILL.md` ("trigger
|
||||||
|
> + observable + quantifiable pass/fail"); a few rows that exchange concrete
|
||||||
|
> JSON bodies use the **input/output shape**.
|
||||||
|
>
|
||||||
|
> No reference files are required at this stage — every observable in the
|
||||||
|
> table below is small enough to inline. If `Phase 2` of `/test-spec` finds a
|
||||||
|
> case that needs a multi-row expected payload (e.g. full
|
||||||
|
> `traceability-matrix` output), a JSON / CSV file will be added in this
|
||||||
|
> directory and referenced from the corresponding row.
|
||||||
|
|
||||||
|
**Status**: agent-drafted (autodev Step 3 — Test Spec, Phase 1 prereq)
|
||||||
|
**Date**: 2026-05-10
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Result Format Legend
|
||||||
|
|
||||||
|
| Result Type | When to Use | Example |
|
||||||
|
|-------------|-------------|---------|
|
||||||
|
| Exact value | Output must match precisely | `status_code: 200`, `count: 0` |
|
||||||
|
| Tolerance range | Numeric output with acceptable variance | `position ± 50ms`, `width ± 1px` |
|
||||||
|
| Threshold | Output must exceed or stay below a limit | `latency ≤ 500ms`, `count == 0` |
|
||||||
|
| Pattern match | Output must match a string/regex pattern | URL regex, header presence |
|
||||||
|
| Set/count | Output must contain specific items or counts | `keys(en) == keys(ua)` |
|
||||||
|
|
||||||
|
## Comparison Methods
|
||||||
|
|
||||||
|
| Method | Description | Tolerance Syntax |
|
||||||
|
|--------|-------------|------------------|
|
||||||
|
| `exact` | Actual == Expected | N/A |
|
||||||
|
| `numeric_tolerance` | abs(actual - expected) ≤ tolerance | `± <value>` |
|
||||||
|
| `range` | min ≤ actual ≤ max | `[min, max]` |
|
||||||
|
| `threshold_min` | actual ≥ threshold | `≥ <value>` |
|
||||||
|
| `threshold_max` | actual ≤ threshold | `≤ <value>` |
|
||||||
|
| `regex` | actual matches regex pattern | regex string |
|
||||||
|
| `substring` | actual contains substring | substring |
|
||||||
|
| `set_equals` | sets of items match exactly | set notation |
|
||||||
|
| `set_contains` | actual set ⊇ expected | subset notation |
|
||||||
|
| `present` / `absent` | observable exists / does not exist | N/A |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Input → Expected Result Mapping
|
||||||
|
|
||||||
|
### Group 1 — Authentication & Token Handling (AC-01 / AC-02 / AC-03 / AC-22 / AC-23 / AC-24)
|
||||||
|
|
||||||
|
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||||
|
|----|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||||
|
| 01 | Any authenticated `fetch` issued via `apiClient` | Outbound API call that requires the bearer | RequestInit MUST include `credentials: 'include'` | exact | N/A | N/A |
|
||||||
|
| 02 | Bootstrap refresh call on app mount (`AuthContext` init) | The refresh-on-load flow before any user interaction | RequestInit MUST include `credentials: 'include'`; cookie sent | exact | N/A | N/A |
|
||||||
|
| 03 | First 401 from `/api/admin/...` while a session is active | Bearer expired mid-session | Sequence: `POST /api/admin/auth/refresh` (cookie-bound) → original request retried with new bearer → final response 200 | exact (sequence) | N/A | N/A |
|
||||||
|
| 04 | Code-search across `src/` for `localStorage.|sessionStorage.` references that touch the bearer | Static check on token-storage policy | `match_count == 0` | exact | N/A | N/A |
|
||||||
|
| 05 | Code-search across `src/` for `document.cookie` reads that target the refresh token | Static check on cookie-readability policy | `match_count == 0` (cookie is HttpOnly server-side; UI must not even attempt) | exact | N/A | N/A |
|
||||||
|
| 06 | Programmatic read of `document.cookie` after login | Browser-level visibility test of the refresh cookie | Returned string MUST NOT contain the refresh-token value | absent | N/A | N/A |
|
||||||
|
| 07 | Refresh-cookie response header from `/api/admin/auth/login` (test fixture) | Set-Cookie attribute audit | Header value matches regex `Secure;.*HttpOnly;.*SameSite=Strict` (order tolerant, case insensitive) | regex | case-insensitive, attribute-order-tolerant | N/A |
|
||||||
|
| 08 | Authenticated user with `role != admin` navigating to `/admin` | RBAC route gate (admin) | Final URL is `/flights`; `<AdminPage>` MUST NOT mount | exact (URL), absent (component) | N/A | N/A |
|
||||||
|
| 09 | Unauthenticated user navigating to `/admin` | RBAC route gate (login) | Final URL is `/login` | exact | N/A | N/A |
|
||||||
|
| 10 | Authenticated user without settings permission navigating to `/settings` | RBAC route gate (settings) | Final URL is `/flights`; `<SettingsPage>` MUST NOT mount | exact, absent | N/A | N/A |
|
||||||
|
| 11 | Auth refresh occurring while the user is on `/flights` (mid-session) | Refresh transparency | `<ProtectedRoute>` MUST NOT unmount its children during refresh; render-counter delta ≤ 1 across refresh | numeric_tolerance | `≤ 1` re-render | N/A |
|
||||||
|
| 12 | Single `/api/admin/auth/refresh` invocation | Refresh round-trip count | Exactly 1 outbound network call observed during one refresh cycle | exact | N/A | N/A |
|
||||||
|
| 13 | Bearer rotation while two SSE streams are open | SSE refresh-rotation handling | Both EventSource instances MUST close, reconnect with new token in query string, and resume within 5 s | exact (close+open count), threshold_max | `≤ 5000ms` | N/A |
|
||||||
|
|
||||||
|
### Group 2 — Wire Contract & Enum Compliance (AC-04 / AC-29)
|
||||||
|
|
||||||
|
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||||
|
|----|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||||
|
| 14 | Read `AnnotationStatus` enum from `src/types/index.ts` | Numeric values must match suite spec | `{None:0, Created:10, Edited:20, Validated:30, Deleted:40}` | exact (key+value map) | N/A | N/A |
|
||||||
|
| 15 | Read `MediaStatus` enum | Numeric values must match suite spec | Members `{None, New, AiProcessing, AiProcessed, ManualCreated, Confirmed, Error}` present; numeric values match the spec map | set_contains, exact (per member) | N/A | N/A |
|
||||||
|
| 16 | Read `Affiliation` enum | Numeric values must match suite spec | Members `{None, Unknown, Friendly, Hostile}` present; numeric values match the spec map | set_contains, exact | N/A | N/A |
|
||||||
|
| 17 | Read `CombatReadiness` enum | Numeric values must match suite spec | Members `{Unknown, NotReady, Ready}` present; numeric values match the spec map | set_contains, exact | N/A | N/A |
|
||||||
|
| 18 | Outbound payload of `POST /api/annotations/annotations` containing `status` field | Wire-format check | `body.status` is a number from the `AnnotationStatus` value set | set_contains | N/A | N/A |
|
||||||
|
| 19 | Outbound payload containing a `Detection` array | Per-detection wire check | Every `detection.affiliation ∈ Affiliation values` and `detection.combatReadiness ∈ CombatReadiness values` | set_contains (per element) | N/A | N/A |
|
||||||
|
| 20 | Code-search `src/` for `mediaType\s*[!=]==?\s*[0-9]` | Magic-literal hygiene for `MediaType` | `match_count == 0` | exact | N/A | N/A |
|
||||||
|
| 21 | Code-search `src/` for `mediaType\s*[!=]==?\s*['"]` | Magic-string hygiene for `MediaType` | `match_count == 0` | exact | N/A | N/A |
|
||||||
|
|
||||||
|
### Group 3 — Annotations endpoints, payload, SSE, overlay window (AC-05 / AC-09 / AC-25 / AC-28)
|
||||||
|
|
||||||
|
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||||
|
|----|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||||
|
| 22 | Save action in `<AnnotationsPage>` | Annotation save endpoint | Exactly one `POST` to URL matching `^/api/annotations/annotations$` | exact (count + URL) | N/A | N/A |
|
||||||
|
| 23 | Annotation save body | Required fields | Body is JSON containing keys `{Source, WaypointId, videoTime, mediaId, detections, status}` (no `time` key) | set_contains, absent (`time`) | N/A | N/A |
|
||||||
|
| 24 | Mount of `06_annotations` page | Status-events SSE subscribe | Exactly one EventSource opened to URL matching `^/api/annotations/annotations/events(\?|$)` | exact (count + URL regex) | N/A | N/A |
|
||||||
|
| 25 | Unmount of `06_annotations` page | Status-events SSE unsubscribe | EventSource readyState transitions to `CLOSED` (2) within 1 s | exact (state), threshold_max | `≤ 1000ms` | N/A |
|
||||||
|
| 26 | Sync image detect trigger (`<AnnotationsSidebar>` Detect on a `MediaType.Image`) | Detect endpoint correctness | Exactly one `POST` to URL matching `^/api/detect/[0-9]+$` | exact, regex | N/A | N/A |
|
||||||
|
| 27 | Async video detect trigger (Phase B; behind feature flag if pre-shipped) | Async detect endpoint | Exactly one `POST` to URL matching `^/api/detect/video/[0-9]+$`; response `{jobId: <int>}`; subsequent EventSource opened to `^/api/detect/stream/[0-9]+(\?|$)` | exact, regex (3 assertions) | N/A | N/A |
|
||||||
|
| 28 | Long-video async detect | Header policy per `_docs/10_auth.md` | Outgoing request includes `X-Refresh-Token` header (non-empty) | present (header) | N/A | N/A |
|
||||||
|
| 29 | Annotation overlay membership at `currentTime = T` | Asymmetric time window | Annotation with `videoTime` in `[T-50ms, T+150ms]` is rendered; outside this window NOT rendered | range, absent | exact bounds | N/A |
|
||||||
|
| 30 | Overlay membership at `currentTime = T` with annotation at `T - 60ms` | Lower-bound exclusion | Annotation NOT rendered | absent | N/A | N/A |
|
||||||
|
| 31 | Overlay membership at `currentTime = T` with annotation at `T + 160ms` | Upper-bound exclusion | Annotation NOT rendered | absent | N/A | N/A |
|
||||||
|
|
||||||
|
### Group 4 — Flight selection persistence + Live-GPS SSE (AC-06 / AC-08)
|
||||||
|
|
||||||
|
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||||
|
|----|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||||
|
| 32 | `FlightContext.selectFlight(flightId)` call | Selected-flight persistence path | Exactly one `PUT /api/annotations/settings/user` with body containing `{selectedFlightId: flightId}`; NO call to `/api/flights/select` (deprecated path must not exist) | exact (count + URL + body subset), absent | N/A | N/A |
|
||||||
|
| 33 | Reload after a flight was selected | Selected-flight rehydration | On boot, `userSettings.selectedFlightId` is read and the flight is reselected without a user click | exact (selection state matches stored id) | N/A | N/A |
|
||||||
|
| 34 | A flight is selected | Live-GPS SSE open | Exactly one EventSource to URL matching `^/api/flights/[0-9]+/live-gps(\?|$)`; readyState reaches `OPEN` (1) within 5 s | exact (count + URL regex), threshold_max | `≤ 5000ms` | N/A |
|
||||||
|
| 35 | Flight is deselected | Live-GPS SSE close | All EventSources matching `^/api/flights/[0-9]+/live-gps` reach readyState `CLOSED` (2) within 1 s | exact (count → 0), threshold_max | `≤ 1000ms` | N/A |
|
||||||
|
|
||||||
|
### Group 5 — Dataset bulk-validate (AC-07)
|
||||||
|
|
||||||
|
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||||
|
|----|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||||
|
| 36 | Select N dataset items → click `Validate` | Bulk-validate endpoint + body | Exactly one `POST /api/annotations/dataset/bulk-status` with body `{ids: <length N int array>, targetStatus: 30}` (`AnnotationStatus.Validated`) | exact (URL + body) | N/A | N/A |
|
||||||
|
| 37 | Successful 200 response from bulk-validate | UI status reflection | Each selected item's row status changes to `Validated` within 2 s of response | exact (per-row state), threshold_max | `≤ 2000ms` | N/A |
|
||||||
|
|
||||||
|
### Group 6 — Upload size cap (AC-10)
|
||||||
|
|
||||||
|
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||||
|
|----|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||||
|
| 38 | Read `client_max_body_size` from `nginx.conf` | nginx upload cap | Value equals `500M` | exact | N/A | N/A |
|
||||||
|
| 39 | UI upload of a synthetic 501 MB file | 413 surfacing | API call resolves with HTTP 413; UI presents a user-visible error containing the i18n key for "file too large" (or its rendered string) — NO silent failure, NO `alert()` | exact (status), substring (rendered error), absent (`alert()`) | N/A | N/A |
|
||||||
|
|
||||||
|
### Group 7 — Build, bundle, and routing (AC-11 / AC-31 / AC-33 / AC-34)
|
||||||
|
|
||||||
|
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||||
|
|----|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||||
|
| 40 | `vite build` output `dist/` | Initial JS bundle budget (target — not yet enforced in CI) | Sum of gzipped initial-route JS chunks ≤ 2 MB | threshold_max | `≤ 2 MB gzipped` | N/A |
|
||||||
|
| 41 | `vite build` output `dist/` | mission-planner exclusion | No file under `dist/` originates from `mission-planner/**`; static-import scan from `src/main.tsx` does not reach into `mission-planner/` | absent (file origin), absent (graph edge) | N/A | N/A |
|
||||||
|
| 42 | `docker inspect azaion/ui:<tag>` | Production runtime image | Final image is based on `nginx:alpine`; no `node` binary present in the image filesystem | exact (base image), absent (binary) | N/A | N/A |
|
||||||
|
| 43 | Read `nginx.conf` route blocks | Service routing | Exactly the 9 `location` blocks present: `/api/admin/`, `/api/flights/`, `/api/annotations/`, `/api/detect/`, `/api/loader/`, `/api/gps-denied-desktop/`, `/api/gps-denied-onboard/`, `/api/autopilot/`, `/api/resource/` | set_equals | N/A | N/A |
|
||||||
|
| 44 | Each of the 9 `location` blocks | Prefix stripping | Each block rewrites/strips its `/api/<service>/` prefix before forwarding upstream (verified via `proxy_pass`/`rewrite` directive shape) | regex per block | N/A | N/A |
|
||||||
|
|
||||||
|
### Group 8 — Internationalization (AC-12 / AC-13)
|
||||||
|
|
||||||
|
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||||
|
|----|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||||
|
| 45 | Compare `src/i18n/en.json` vs `src/i18n/ua.json` | Key parity | `keys(en) == keys(ua)` (deep, sorted) | set_equals | N/A | N/A |
|
||||||
|
| 46 | Lint sweep over `src/**/*.tsx` | No raw user-visible strings | Every JSX text node and every `aria-label` / `placeholder` / `title` string resolves through `t(...)` (or is a proper-noun acronym in the allow-list) | exact (lint findings == 0) | acronym allow-list | N/A |
|
||||||
|
| 47 | First boot in a clean profile | i18n detector | `i18next.language` resolves from cookie or `Accept-Language`; not the literal `'en'` from a hardcoded init | exact (detector path used), absent (hardcoded `lng:'en'`) | N/A | N/A |
|
||||||
|
| 48 | Toggle language in `<Header>` then reload | i18n persistence | After reload, `i18next.language` equals the previously selected language | exact | N/A | N/A |
|
||||||
|
|
||||||
|
### Group 9 — Destructive-action UX (AC-14 / AC-30)
|
||||||
|
|
||||||
|
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||||
|
|----|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||||
|
| 49 | Click `Delete class` in `<AdminPage>` | Class-delete confirmation | `<ConfirmDialog>` is rendered before any HTTP request fires; on Cancel NO `DELETE` request is made; on Confirm exactly one `DELETE` to URL matching `^/api/admin/classes/[0-9]+$` | exact (sequence), exact (count + URL regex) | N/A | N/A |
|
||||||
|
| 50 | Code-search `src/` and `mission-planner/src/` for `\balert\(` | `alert()` ban | `match_count == 0` | exact | N/A | N/A |
|
||||||
|
| 51 | Each destructive action surfaced in `_docs/ui_design/` (delete, validate-with-overwrite, irreversible bulk) | Confirm-before-fire policy | A `<ConfirmDialog>` opens before the destructive request fires (no direct submit path) | present (dialog), exact (sequence) | N/A | N/A |
|
||||||
|
|
||||||
|
### Group 10 — Accessibility (AC-15 / AC-16 / AC-17)
|
||||||
|
|
||||||
|
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||||
|
|----|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||||
|
| 52 | Render `<ConfirmDialog>` open | a11y attributes | Root element has `role="dialog"` AND `aria-modal="true"` | exact | N/A | N/A |
|
||||||
|
| 53 | Open `<ConfirmDialog>` and Tab through | Focus trap | Focus stays inside the dialog (first ↔ last loop); never reaches an element outside | exact (focus stays in tree) | N/A | N/A |
|
||||||
|
| 54 | Press `Escape` while `<ConfirmDialog>` is open | Cancel-on-Escape | Dialog unmounts; cancel callback invoked exactly once; no destructive HTTP request fires | exact (count) | N/A | N/A |
|
||||||
|
| 55 | Render `<Header>` flight dropdown closed | Combobox a11y attrs | Trigger has `role="combobox"`, `aria-expanded="false"`, `aria-haspopup="listbox"` | exact | N/A | N/A |
|
||||||
|
| 56 | Open the flight dropdown | Combobox a11y on open | `aria-expanded` switches to `"true"`; outside-click handler is now attached (was NOT attached while closed) | exact | N/A | N/A |
|
||||||
|
| 57 | Press `Escape` while flight dropdown is open | Close-on-Escape | Dropdown closes; `aria-expanded` returns to `"false"`; outside-click handler is detached | exact | N/A | N/A |
|
||||||
|
| 58 | `<ProtectedRoute>` shows the loading state | a11y on spinner | Spinner element has `role="status"` and an accessible label (non-empty `aria-label` or visually-hidden text) | exact, present (label) | N/A | N/A |
|
||||||
|
| 59 | `<ProtectedRoute>` loading exceeds the timeout (10 s simulated) | Timeout fallback | A user-visible fallback (retry CTA or error message) is rendered; the indeterminate spinner is unmounted | present (fallback), absent (spinner) | timeout configurable | N/A |
|
||||||
|
|
||||||
|
### Group 11 — Browser support & responsive layout (AC-18 / AC-19)
|
||||||
|
|
||||||
|
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||||
|
|----|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||||
|
| 60 | Smoke render of `/flights`, `/annotations`, `/dataset` in headless Chromium and Firefox (latest 2 versions) | Browser support floor | No console error logged; main page region has rendered with expected landmark roles | exact (errors == 0), present (landmarks) | N/A | N/A |
|
||||||
|
| 61 | Headless render at viewport width 480 px | Mobile bottom-nav variant | `<Header>` bottom-nav variant renders; top-bar variant is NOT in DOM | present, absent | N/A | N/A |
|
||||||
|
| 62 | Headless render at viewport width 1024 px | Desktop variant | Top-bar `<Header>` renders; bottom-nav variant NOT in DOM | present, absent | N/A | N/A |
|
||||||
|
|
||||||
|
### Group 12 — Secrets (AC-20)
|
||||||
|
|
||||||
|
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||||
|
|----|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||||
|
| 63 | Code-search `src/` and `mission-planner/src/` for the literal OWM key value (and for any `appid=` / `api_key=` in source URLs) | Secrets-in-source check | `match_count == 0` for the literal key; the key is read only via `import.meta.env.VITE_OPENWEATHERMAP_API_KEY` (or proxied through the suite) | exact, exact (single read site) | N/A | N/A |
|
||||||
|
|
||||||
|
### Group 13 — User-settings persistence (AC-21)
|
||||||
|
|
||||||
|
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||||
|
|----|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||||
|
| 64 | Drag a resizable panel divider via `useResizablePanel` and release | Persist on resize-end | Within 1 s of release, exactly one `PUT /api/annotations/settings/user` fires with body containing `panelWidths: { ... }` reflecting the new sizes | exact (count + URL), substring (key), threshold_max (1 s) | debounce-aware | N/A |
|
||||||
|
| 65 | Reload after a panel resize | Width rehydration | Restored panel widths equal the pre-reload widths within 1 px | numeric_tolerance | `± 1px` | N/A |
|
||||||
|
|
||||||
|
### Group 14 — Form hygiene (AC-26 / AC-27)
|
||||||
|
|
||||||
|
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||||
|
|----|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||||
|
| 66 | Submit a `09_settings` numeric field with empty value | No silent zero | Form does NOT save `0`; submit button disabled OR explicit validation error rendered; NO `PUT` request fires | absent (request), present (validation surface) | N/A | N/A |
|
||||||
|
| 67 | Submit `09_settings` numeric field with non-numeric input | Reject non-numeric | Validation error rendered; NO `PUT` fires | absent (request), present (error) | N/A | N/A |
|
||||||
|
| 68 | `09_settings` save action where the upstream PUT returns HTTP 500 | Error surfacing | A toast / inline error renders within 2 s; `saving` flag returns to `false` (i.e. button is re-enabled); NO route navigation occurs | present (error), exact (state), absent (navigation), threshold_max (2 s) | N/A | N/A |
|
||||||
|
| 69 | `09_settings` save action where the PUT throws (network failure) | Error path via `try/finally` | `saving` flag returns to `false`; user-visible error rendered | exact (state), present (error) | N/A | N/A |
|
||||||
|
|
||||||
|
### Group 15 — CI / image / labels (AC-32)
|
||||||
|
|
||||||
|
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||||
|
|----|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||||
|
| 70 | Image push step in `.woodpecker/build-arm.yml` for branch `main` | Tag scheme | Pushed tag matches `^main-arm$` | regex | N/A | N/A |
|
||||||
|
| 71 | Image push step | OCI labels | Pushed image carries non-empty labels `org.opencontainers.image.revision`, `org.opencontainers.image.created`, `org.opencontainers.image.source` | present (each), exact (count == 3) | N/A | N/A |
|
||||||
|
| 72 | `org.opencontainers.image.revision` label value | Revision plumbing | Equals `$CI_COMMIT_SHA` from the pipeline run | exact | N/A | N/A |
|
||||||
|
|
||||||
|
### Group 16 — Manual annotation interactions on `CanvasEditor` + `DetectionClasses` (AC-35 / AC-36 / AC-37 / AC-38)
|
||||||
|
|
||||||
|
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||||
|
|----|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||||
|
| 73 | Synthetic `mousedown(x1,y1) → mousemove(x2,y2) → mouseup` over `CanvasEditor` with `selectedClassNum = C` and `photoMode = P` | Manual bbox draw (AC-35) | Exactly one new local detection appended with `classNum == C + P`, `x == min(x1,x2)/W`, `y == min(y1,y2)/H`, `w == |x2-x1|/W`, `h == |y2-y1|/H` (W,H = canvas pixel size) | numeric_tolerance (per coord), exact (count + classNum) | `± 1px / canvas px` | N/A |
|
||||||
|
| 74 | `mousedown` on resize handle `h ∈ {NW, N, NE, W, E, SW, S, SE}` over a selected bbox, drag by `(dx, dy)`, mouseup | 8-handle resize (AC-36a) | Only the edges adjacent to `h` move; opposite edges unchanged; resulting bbox dimensions clamped to a minimum normalised size > 0 (no negative or zero w/h) | exact (per-edge invariance), threshold_min (w,h > 0) | N/A | N/A |
|
||||||
|
| 75 | `Ctrl+click` on a bbox that is not currently selected | Canvas multi-select (AC-36b) | The bbox is added to the selection set; previous selection is preserved; clicking the same bbox a second time with Ctrl removes it from the set | exact (selection set delta), idempotent (toggle) | N/A | N/A |
|
||||||
|
| 76 | `Ctrl+wheel` over the canvas at cursor position `(cx, cy)` | Canvas zoom (AC-36c) | Viewport zoom level changes; the world point at `(cx, cy)` before zoom maps back to `(cx, cy)` after zoom (zoom-around-cursor invariant) | numeric_tolerance | `± 1 viewport px` | N/A |
|
||||||
|
| 77 | `Ctrl+drag` starting on empty canvas (no bbox under the pointer) | Canvas pan (AC-36d) | Viewport origin translates by exactly `(-dx, -dy)`; no bbox is created or modified | numeric_tolerance, absent (state delta) | `± 1 viewport px` | N/A |
|
||||||
|
| 78 | Mount of `<DetectionClasses>` with a successful `GET /api/annotations/classes` response of N classes (mode-ordered) | Class list load (AC-37 / load path) | All N entries are rendered; the active-mode filter is applied; no fallback list is shown | exact (count rendered), absent (fallback) | N/A | N/A |
|
||||||
|
| 79 | `keydown` on `window` with key `'1'..'9'` while `photoMode = P` and `classes` are mode-ordered per the contract | Class hotkey 1–9 (AC-37 / hotkey path) | `onSelect` fires exactly once with `class.id == classes[(key-1) + P].id`; the visible label index `i+1` on the rendered list element matches `key` | exact | N/A | N/A |
|
||||||
|
| 80 | Click on a class entry in the strip | Class click path (AC-37 / click path) | `onSelect` fires once with that entry's `class.id` | exact (count + value) | N/A | N/A |
|
||||||
|
| 81 | `GET /api/annotations/classes` returning empty or 5xx | Fallback list (AC-37 / fallback) | `FALLBACK_CLASS_NAMES` × 3 PhotoMode offsets is rendered (IDs in the contiguous `[0..N-1, 20..20+N-1, 40..40+N-1]` shape) | exact (set of IDs) | N/A | N/A |
|
||||||
|
| 82 | Click the `Winter` PhotoMode button while `photoMode = 0` | PhotoMode switch — mode + filter (AC-38 / mode set + filter) | Outgoing `onPhotoModeChange(20)` fires once; rendered class list is filtered to entries whose `photoMode == 20` | exact (call + filter) | N/A | N/A |
|
||||||
|
| 83 | PhotoMode switch where the previously-selected `classNum` is NOT in the new filtered set | PhotoMode auto-select (AC-38 / auto-select) | `onSelect` fires once with `modeClasses[0].id` (first class of the new mode) | exact (count + value) | N/A | N/A |
|
||||||
|
| 84 | Save annotation after drawing bbox with `selectedClassNum = C` and `photoMode = P` | yoloId on wire (AC-38 / wire) | Outgoing `POST /api/annotations/annotations` body has `detections[i].classNum == C + P` for the newly drawn detection | exact | N/A | N/A |
|
||||||
|
|
||||||
|
### Group 17 — Tile splitting (AC-39 / AC-40)
|
||||||
|
|
||||||
|
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||||
|
|----|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||||
|
| 85 | Click `Split tile` action on a dataset item | Split endpoint contract (AC-39 / endpoint) | Exactly one `POST` to URL matching `^/api/annotations/dataset/[0-9]+/split$`; success response is HTTP 200 JSON | exact (count + URL regex + status) | N/A | N/A |
|
||||||
|
| 86 | `AnnotationListItem` with `isSplit: true, splitTile: "3 0.5 0.5 0.2 0.2"` | YOLO label parse — valid (AC-39 / parser happy) | Parser yields `{ classNum: 3, cx: 0.5, cy: 0.5, w: 0.2, h: 0.2 }` without throwing | exact | N/A | N/A |
|
||||||
|
| 87 | `AnnotationListItem` with `isSplit: true, splitTile: "garbage"` | YOLO label parse — malformed (AC-39 / parser sad) | User-visible error surfaced (toast or inline); NO silent swallow; NO render with NaN values | present (error), absent (NaN render) | N/A | N/A |
|
||||||
|
| 88 | `DatasetItem` response containing `isSplit: true` from `GET /api/annotations/dataset` | DatasetItem.isSplit honored (AC-39 / dataset list) | UI reads `item.isSplit` without crash; downstream rendering uses the boolean (rendering policy is per item type, but presence is required) | present (field read) | N/A | N/A |
|
||||||
|
| 89 | Double-click a `splitTile`-bearing annotation in `<AnnotationsSidebar>` | Tile auto-zoom viewport (AC-40 / viewport) | `CanvasEditor` viewport rect equals the tile rect encoded by `splitTile` (per AC-39 parse) within ±1 px per edge | numeric_tolerance | `± 1px per edge` | N/A |
|
||||||
|
| 90 | While tile zoom is active | Tile-zoom indicator (AC-40 / indicator) | A visible tile-zoom indicator (icon or badge) is present in the canvas chrome; clearing the tile zoom removes it | present, absent (after clear) | N/A | N/A |
|
||||||
|
|
||||||
|
### Group 18 — Anti-criteria (AC-N1..AC-N5) — negative behavioral assertions
|
||||||
|
|
||||||
|
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||||
|
|----|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||||
|
| 91 | Two browsers editing the same annotation simultaneously | No collaborative-edit semantics (AC-N1) | UI MUST NOT reconcile concurrent edits; last-write-wins on the server is the expected behavior (no merge UI, no presence indicators) | absent (merge UI) | N/A | N/A |
|
||||||
|
| 92 | Static dependency scan of `package.json` and `mission-planner/package.json` | No in-browser ML (AC-N2) | No package matching the pattern `(onnxruntime|tensorflow|tflite|coreml|tfjs|@tensorflow/.*|@huggingface/.*|transformers\.js)` is declared | absent | N/A | N/A |
|
||||||
|
| 93 | App boot in a network-disabled environment (offline) | No offline mode (AC-N3) | App enters an error / login-failed state; does NOT serve cached app data; no service worker registered | present (error), absent (sw) | N/A | N/A |
|
||||||
|
| 94 | Static dependency scan | No response-signature library (AC-N4) | No package matching `(jsrsasign|tweetnacl|@noble/.*|jose)` is imported on the request-validation path | absent | N/A | N/A |
|
||||||
|
| 95 | Code-search across `src/` and `mission-planner/` for symbols/components named `SoundDetections|DroneMaintenance` | Dropped legacy features (AC-N5) | `match_count == 0` | exact | N/A | N/A |
|
||||||
|
|
||||||
|
### Group 19 — Phase-3-added (Data Validation Gate)
|
||||||
|
|
||||||
|
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||||
|
|----|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||||
|
| 96 | Click `Download` in `<AnnotationsPage>` on a canvas tainted by a cross-origin video frame (CORS-less video source) | Tainted-canvas fallback (NFT-RES-09; derived from `modules/src__features__annotations.md` finding on `handleDownload`) | A user-visible error (toast or inline message) is rendered; NO silent swallow; NO `alert()` invoked; no fabricated blob is offered for download | present (error), absent (silent swallow), absent (`alert()` invocation) | N/A | N/A |
|
||||||
|
| 97 | Server force-closes an open `EventSource` (live-GPS or annotation-status) without rotation | SSE server disconnect indicator (NFT-RES-10; derived from AC-08 + AC-09 + AC-24) | UI either renders a connection-lost indicator (badge/icon) OR invokes a reconnect attempt within 10 s. Stale event data is NOT re-rendered as live; the most recent live timestamp is frozen + flagged stale | present (indicator OR reconnect), absent (stale data rendered as live), exact (reconnect_attempts ≤ 1 in the 10 s window) | dt ≤ 10 000 ms | N/A |
|
||||||
|
| 98 | Warm-cache navigation to `/flights` on the post-login route in headless Chromium on the edge profile (2 vCPU / 4 GB RAM) | First Contentful Paint baseline (NFT-PERF-10; derived from AC-11 target + H2 edge deploy) | `performance.getEntriesByName('first-contentful-paint')[0].startTime` reported by the browser is below the threshold | threshold_max | `≤ 3 000 ms` | N/A |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Coverage Summary
|
||||||
|
|
||||||
|
| AC ID | Mapped row(s) | Coverage |
|
||||||
|
|--------|---------------|----------|
|
||||||
|
| AC-01 | 01, 02, 03 | full (default + bootstrap + 401 retry) |
|
||||||
|
| AC-02 | 04 | full |
|
||||||
|
| AC-03 | 05, 06, 07 | full (JS-readable check, fixture cookie regex) |
|
||||||
|
| AC-04 | 14, 15, 16, 17, 18, 19 | full (per enum + payload contract) |
|
||||||
|
| AC-05 | 22, 23 | full (URL + required body fields) |
|
||||||
|
| AC-06 | 32, 33 | full (write path + boot rehydration) |
|
||||||
|
| AC-07 | 36, 37 | full (request + UI reflection) |
|
||||||
|
| AC-08 | 34, 35 | full (open + close) |
|
||||||
|
| AC-09 | 24, 25 | full (subscribe + unsubscribe) |
|
||||||
|
| AC-10 | 38, 39 | full (server cap + UI surfacing) |
|
||||||
|
| AC-11 | 40 | target (no CI gate today; row documents the threshold) |
|
||||||
|
| AC-12 | 45, 46 | full |
|
||||||
|
| AC-13 | 47, 48 | full |
|
||||||
|
| AC-14 | 49, 50, 51 | full (dialog presence + alert ban + general policy) |
|
||||||
|
| AC-15 | 52, 53, 54 | full |
|
||||||
|
| AC-16 | 55, 56, 57 | full |
|
||||||
|
| AC-17 | 58, 59 | full |
|
||||||
|
| AC-18 | 60 | manual smoke today (test row exists; gate is target) |
|
||||||
|
| AC-19 | 61, 62 | full (two breakpoint smokes) |
|
||||||
|
| AC-20 | 63 | full |
|
||||||
|
| AC-21 | 64, 65 | full |
|
||||||
|
| AC-22 | 08, 09, 10 | full (admin + login + settings) |
|
||||||
|
| AC-23 | 11, 12 | full |
|
||||||
|
| AC-24 | 13 | target (Phase B / Step 8 hardening) |
|
||||||
|
| AC-25 | 26, 27, 28 | full (sync image + async video + token header) |
|
||||||
|
| AC-26 | 66, 67 | full |
|
||||||
|
| AC-27 | 68, 69 | full |
|
||||||
|
| AC-28 | 29, 30, 31 | full (in-window + below + above) |
|
||||||
|
| AC-29 | 20, 21 | full |
|
||||||
|
| AC-30 | 49 | overlapped with AC-14 row 49 |
|
||||||
|
| AC-31 | 41 | full |
|
||||||
|
| AC-32 | 70, 71, 72 | full |
|
||||||
|
| AC-33 | 42 | full |
|
||||||
|
| AC-34 | 43, 44 | full (route set + prefix strip) |
|
||||||
|
| AC-35 | 73 | full (one-shot draw assertion) |
|
||||||
|
| AC-36 | 74, 75, 76, 77 | full (resize + multi-select + zoom + pan) |
|
||||||
|
| AC-37 | 78, 79, 80, 81 | full (load + hotkey + click + fallback) |
|
||||||
|
| AC-38 | 82, 83, 84 | full (mode set + filter + auto-select + wire yoloId) |
|
||||||
|
| AC-39 | 85, 86, 87, 88 | full (endpoint + parser happy + parser sad + DatasetItem flag) |
|
||||||
|
| AC-40 | 89, 90 | target (UX missing today — finding #24; rows assert when implementation lands) |
|
||||||
|
| AC-N1 | 91 | full |
|
||||||
|
| AC-N2 | 92 | full |
|
||||||
|
| AC-N3 | 93 | full |
|
||||||
|
| AC-N4 | 94 | full |
|
||||||
|
| AC-N5 | 95 | full |
|
||||||
|
| (NFT-RES-09 anchor) | 96 | added Phase 3 — tainted-canvas fallback observable |
|
||||||
|
| (NFT-RES-10 anchor) | 97 | added Phase 3 — SSE server-disconnect observable |
|
||||||
|
| (NFT-PERF-10 anchor) | 98 | added Phase 3 — FCP baseline threshold |
|
||||||
|
|
||||||
|
Every AC + anti-criterion has at least one row. Every row is quantifiable.
|
||||||
|
|
||||||
|
## Open Items For Phase 1 Validation
|
||||||
|
|
||||||
|
- **AC-04 enum maps**: rows 14–17 reference "the spec map" for `MediaStatus`,
|
||||||
|
`Affiliation`, `CombatReadiness`. The exact numeric values must be harvested
|
||||||
|
from the suite spec at Phase 1 time and inlined; they are deliberately left
|
||||||
|
symbolic here because the UI today drifts (per `restrictions.md` O7 + the
|
||||||
|
data-parameters drift notes) and we want the test to assert against the
|
||||||
|
spec, not against the current `src/types/index.ts`.
|
||||||
|
- **AC-11 / AC-18 / AC-24 / AC-25 (async) / AC-40** are flagged "Phase B /
|
||||||
|
target" or "Step 4 fix" in `acceptance_criteria.md`. The rows above produce
|
||||||
|
verifiable assertions when those features ship; until then `/test-spec`
|
||||||
|
Phase 3 may downgrade them from blocking to documentary. AC-40 in particular
|
||||||
|
has zero consumer of `splitTile` in the UI today (finding #24 in
|
||||||
|
`modules/src__features__annotations.md`); its rows are written so the test
|
||||||
|
exists the day the tile-zoom UX ships.
|
||||||
|
- **AC-37 backend ordering**: the class-hotkey contract depends on the
|
||||||
|
`annotations/` service returning classes in the contiguous
|
||||||
|
`[0..N-1, 20..20+N-1, 40..40+N-1]` shape. This was flagged Step 4
|
||||||
|
verification in `data_model.md:158`. If the backend ordering does not match,
|
||||||
|
AC-37 row 79 will fail at integration time and we may need a server-side
|
||||||
|
fix or a client-side resort.
|
||||||
|
- **Reference files**: none required at this stage. If `/test-spec` Phase 2
|
||||||
|
produces a per-route JSON expectation that doesn't fit a single row,
|
||||||
|
reference files will be added in this folder following the
|
||||||
|
`<input_name>_expected.<ext>` convention.
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
# Problem Statement — Azaion UI
|
||||||
|
|
||||||
|
> Output of `/document` Step 6a. Retrospective problem definition synthesised
|
||||||
|
> from `_docs/02_document/architecture.md` (System Context, Components),
|
||||||
|
> component descriptions, system flows, and the Step 4.5 Architecture Vision.
|
||||||
|
> No README exists in the repo today (the workspace's tracked README is the
|
||||||
|
> parent suite's; the UI repo has only the autodev-generated `README.md`
|
||||||
|
> stub). All claims here trace to verified `_docs/02_document/` artifacts.
|
||||||
|
|
||||||
|
**Status**: synthesised-from-verified-docs (Step 6a — `/document`)
|
||||||
|
**Date**: 2026-05-10
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What is this system?
|
||||||
|
|
||||||
|
Azaion UI is the **operator-facing browser** of the Azaion UAV operations
|
||||||
|
suite — a single-page React 19 application served by nginx as a static bundle
|
||||||
|
inside an ARM64 container. It is the **React rewrite of the front-end half**
|
||||||
|
of the legacy WPF stack (`Azaion.Annotator` + `Azaion.Dataset` +
|
||||||
|
`MapMatcher`); the heavyweight machinery (LibVLC playback, Cython sidecars,
|
||||||
|
SQLite outbox, per-app DI host, binary-split key-fragment loader handoff)
|
||||||
|
moved server-side into the parent suite (`suite/`) as separate services.
|
||||||
|
|
||||||
|
The UI's narrowed responsibility is to **render the suite's REST + SSE
|
||||||
|
contract beautifully and accessibly** — it carries no domain logic that
|
||||||
|
belongs on the server, no in-browser persistence beyond a single bearer
|
||||||
|
in memory and a `Secure HttpOnly` refresh cookie, and no Node.js runtime in
|
||||||
|
the production image.
|
||||||
|
|
||||||
|
## What problem does it solve?
|
||||||
|
|
||||||
|
Operators of UAV / aerial-imagery missions (military and defense use cases)
|
||||||
|
need a single browser surface to:
|
||||||
|
|
||||||
|
1. **Plan flights** — define waypoints, altitudes, aircraft, GPS-Denied
|
||||||
|
parameters; consult wind data; visualise routes on a map.
|
||||||
|
2. **Capture and review media** — browse uploaded images and videos scoped
|
||||||
|
to a flight; play video frame-accurately; tag bounding boxes manually or
|
||||||
|
via AI inference.
|
||||||
|
3. **Run AI object detection** — synchronously on images today; asynchronously
|
||||||
|
on video (target — not wired today, see `04_verification_log.md` F7).
|
||||||
|
4. **Curate datasets** — filter by class / status, validate annotations in
|
||||||
|
bulk, view class-distribution analytics.
|
||||||
|
5. **Administer** the system — manage detection classes, users, aircraft,
|
||||||
|
AI / GPS settings.
|
||||||
|
6. **Operate GPS-Denied positioning** — including a planned **Test Mode**
|
||||||
|
that drives SITL simulation from a pre-recorded `.tlog` + video pair
|
||||||
|
(`_docs/how_to_test.md`).
|
||||||
|
|
||||||
|
This UI replaces the WPF desktop applications; it is the **single browser
|
||||||
|
client** for all of the above use cases. Every action goes through the
|
||||||
|
suite's typed REST contract or SSE stream — the browser is treated as
|
||||||
|
untrusted, so the server is authoritative for every state transition.
|
||||||
|
|
||||||
|
## Who are the users?
|
||||||
|
|
||||||
|
- **Operator** (primary persona) — flies missions, reviews captured media,
|
||||||
|
runs AI detect, curates datasets. The UI's default authenticated route
|
||||||
|
(`/flights`) targets this persona. Bilingual UI is mandatory (Ukrainian
|
||||||
|
+ English).
|
||||||
|
- **Admin** (privileged operator) — adds detection classes, manages users
|
||||||
|
and aircraft, configures AI / GPS settings. Lives at `/admin`. Today this
|
||||||
|
route lacks a client-side role-gate (server-side RBAC is authoritative;
|
||||||
|
the missing UI gate is a finding).
|
||||||
|
- **System integrator** — uses the GPS-Denied Test Mode and the Settings
|
||||||
|
pages to validate end-to-end pipelines.
|
||||||
|
|
||||||
|
The UI does NOT have an end-customer / public-facing surface. It is internal
|
||||||
|
to authenticated operators.
|
||||||
|
|
||||||
|
## How does it work at a high level?
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
Operator[Operator browser] -->|HTTPS| Nginx[nginx<br/>static SPA + reverse-proxy]
|
||||||
|
Nginx -->|/api/admin/*| Admin[admin/]
|
||||||
|
Nginx -->|/api/flights/*| Flights[flights/]
|
||||||
|
Nginx -->|/api/annotations/*| Ann[annotations/]
|
||||||
|
Nginx -->|/api/detect/*| Detect[detect/]
|
||||||
|
Nginx -->|/api/gps-denied-*/*| GPS[gps-denied-*/]
|
||||||
|
Nginx -->|/api/resource/*| Resource[resource/]
|
||||||
|
Nginx -->|/api/autopilot/*| Autopilot[autopilot/]
|
||||||
|
Operator -->|HTTPS direct| OWM[OpenWeatherMap]
|
||||||
|
Operator -->|HTTPS direct| OSM[OSM tile servers]
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Operator hits the nginx host. nginx serves `dist/index.html` + chunks.
|
||||||
|
2. The SPA boots; `AuthContext` attempts a bootstrap refresh (currently
|
||||||
|
broken — Step 4 fix candidate); on 401 the `ProtectedRoute` redirects
|
||||||
|
to `/login`.
|
||||||
|
3. Login (`POST /api/admin/auth/login`) returns a bearer in the response
|
||||||
|
body and sets a `Secure HttpOnly` refresh cookie.
|
||||||
|
4. Subsequent authenticated requests carry the bearer in the `Authorization`
|
||||||
|
header. On 401, the `01_api-transport` layer issues `POST
|
||||||
|
/api/admin/auth/refresh` with `credentials:'include'` and retries.
|
||||||
|
5. Page-level fetches go to the matching suite service; nginx strips the
|
||||||
|
`/api/<service>/` prefix and reverse-proxies. Long-lived streams (live-GPS
|
||||||
|
per flight, annotation-status events) come over SSE.
|
||||||
|
6. State is two React Contexts (`AuthContext`, `FlightContext`) plus
|
||||||
|
page-local `useState`. No Redux, no Zustand, no TanStack Query
|
||||||
|
(`ADR-004`, P4).
|
||||||
|
|
||||||
|
The dominant runtime pattern is **thin client over a typed REST contract** —
|
||||||
|
no business logic in the browser; the server is the authority for every
|
||||||
|
mutation.
|
||||||
|
|
||||||
|
## Cross-reference with README
|
||||||
|
|
||||||
|
The repo's tracked `README.md` is a placeholder (untracked at the time of
|
||||||
|
this analysis — see `git status`). The parent suite's docs (`suite/_docs/*`)
|
||||||
|
are the canonical product reference; the UI's own derived docs in
|
||||||
|
`_docs/02_document/` complement those.
|
||||||
|
|
||||||
|
If a user-facing README is created in a future cycle, it should mirror the
|
||||||
|
"What this system is" paragraph above and link to `_docs/02_document/architecture.md`
|
||||||
|
for the full technical view.
|
||||||
|
|
||||||
|
## What this system explicitly does NOT do
|
||||||
|
|
||||||
|
- **No in-browser persistence beyond bearer + i18n cache** — every reload
|
||||||
|
re-fetches.
|
||||||
|
- **No SSR / no React Server Components** — `Dockerfile` + `nginx.conf` ship
|
||||||
|
a static bundle (`ADR-001`, P2).
|
||||||
|
- **No WebSocket** — REST + SSE only (`ADR-002`, P1).
|
||||||
|
- **No localStorage / sessionStorage for tokens** — bearer is in memory;
|
||||||
|
refresh is in HttpOnly cookie (`ADR-001` consequence, P3).
|
||||||
|
- **No SEO** — operator-only application.
|
||||||
|
- **No mobile-first design** — Header has a bottom-nav variant for ≥ 768 px;
|
||||||
|
mobile is a P2 use-case (see `_docs/02_document/architecture.md` § 6 NFRs).
|
||||||
|
- **No port of three legacy WPF features**: WPF-era encrypted-creds
|
||||||
|
command-line handoff (P8 — security infra moved server-side), Sound
|
||||||
|
Detections (Step 4.5 decision — dropped), Drone Maintenance / "Аналіз
|
||||||
|
стану БПЛА" (Step 4.5 decision — dropped).
|
||||||
|
|
||||||
|
## Open product questions (carried forward)
|
||||||
|
|
||||||
|
These are **not blocking** Step 6 retrospective extraction; they are recorded
|
||||||
|
in `_docs/02_document/architecture.md` § Architecture Vision "Open questions
|
||||||
|
/ drift signals". Phase B feature cycles will resolve them per task.
|
||||||
|
|
||||||
|
1. Async video detect (`F7`) wiring — when in Phase B does the SSE consumer
|
||||||
|
ship?
|
||||||
|
2. `IsSeed` annotation visual — does the modern API still expose `isSeed`?
|
||||||
|
3. Camera-config side panel (GSD) — per-user, per-flight, or per-detect-job?
|
||||||
|
4. Status-bar clock + help-text-blink — port WPF UX or replace with toasts?
|
||||||
|
5. `mission-planner/` end-state — delete after parity port (preferred per
|
||||||
|
Step 4.5 decision) or keep as continuously-vendored reference?
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# Restrictions — Azaion UI
|
||||||
|
|
||||||
|
> Output of `/document` Step 6b. Constraints **actually evidenced** in code,
|
||||||
|
> configs, Dockerfiles, CI configs, and dependency manifests. Inferred
|
||||||
|
> aspirations are NOT included unless the source is cited. Categorised as
|
||||||
|
> Hardware / Software / Environment / Operational per the document skill
|
||||||
|
> template.
|
||||||
|
|
||||||
|
**Status**: synthesised-from-verified-docs (Step 6b — `/document`)
|
||||||
|
**Date**: 2026-05-10
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hardware
|
||||||
|
|
||||||
|
| # | Restriction | Source / Evidence |
|
||||||
|
|---|-------------|--------------------|
|
||||||
|
| H1 | **ARM64-only production image** today (no AMD64 build in CI). | `.woodpecker/build-arm.yml` (the only pipeline file); `_docs/02_document/architecture.md` § 3 Deployment Model "Missing from the pipeline today" |
|
||||||
|
| H2 | **Edge-device deployment target** — operator laptops, OrangePi, Jetson — alongside suite services. | `_docs/legacy/wpf-era.md` §1; `_docs/02_document/architecture.md` § 2 |
|
||||||
|
| H3 | **No GPU expectation in the UI image** — all AI inference happens server-side; the UI only renders detections. | `nginx:alpine` runtime; no client-side ML libs in `package.json` |
|
||||||
|
| H4 | **Browser-rendering capability minimum**: HTML5 `<video>` + `<canvas>` + `EventSource`. Operates on Chromium-based + Firefox latest 2 versions. | `ADR-003` (HTML5 video over LibVLC); `_docs/02_document/architecture.md` § 6 NFR row "Browser support" |
|
||||||
|
|
||||||
|
## Software
|
||||||
|
|
||||||
|
| # | Restriction | Source / Evidence |
|
||||||
|
|---|-------------|--------------------|
|
||||||
|
| S1 | **TypeScript strict mode**. | `tsconfig.json` (`strict: true`) per `_docs/02_document/architecture.md` § 2 Tech Stack |
|
||||||
|
| S2 | **React 19** — latest stable; React Server Components NOT used. | `package.json` `react@19`; `ADR-001` |
|
||||||
|
| S3 | **Vite 6** as the bundler. | `package.json` `vite@6`; `vite.config.ts` |
|
||||||
|
| S4 | **Bun 1.3.11** as the package manager (declared via `packageManager`). CI image is `oven/bun:1.3.11-alpine`. | `package.json` `packageManager` field; `Dockerfile`; `.woodpecker/build-arm.yml` |
|
||||||
|
| S5 | **Static-bundle output only** — production runtime is `nginx:alpine`; **no Node.js in production**. | `Dockerfile` multi-stage build; `_docs/02_document/architecture.md` § 3 |
|
||||||
|
| S6 | **REST + SSE only** — no WebSocket, no GraphQL, no gRPC-Web. | `src/api/client.ts` + `src/api/sse.ts` are the only transports; `ADR-002`, P1 |
|
||||||
|
| S7 | **Two React Contexts only** for cross-cutting state (`AuthContext`, `FlightContext`). No Redux / Zustand / TanStack Query. | `src/auth/AuthContext.tsx`, `src/components/FlightContext.tsx`; `ADR-004`, P4 |
|
||||||
|
| S8 | **Tailwind 4** + `az-*` design tokens are the styling source of truth. | `src/index.css`; `ADR-005` |
|
||||||
|
| S9 | **Map**: `leaflet@1.9.4` + `react-leaflet@5` (+ `leaflet-draw`, `leaflet-polylinedecorator`). Not Mapbox / Cesium / OpenLayers. | `package.json` |
|
||||||
|
| S10 | **Charts**: `chart.js@4` + `react-chartjs-2@4`. | `package.json` |
|
||||||
|
| S11 | **DnD**: `@hello-pangea/dnd@18` for waypoint reorder. | `package.json` |
|
||||||
|
| S12 | **i18n**: `i18next` + `react-i18next` with English + Ukrainian bundles only. | `src/i18n/i18n.ts`; `_docs/02_document/architecture.md` § ADR-007 |
|
||||||
|
| S13 | **No client-side persistence library** (no IndexedDB wrapper, no localForage). Bearer is in memory; refresh is in HttpOnly cookie. | `src/auth/AuthContext.tsx`; P3 |
|
||||||
|
| S14 | **No test framework configured today** — `package.json` has zero test deps; `src/**/*.test.*` is empty. Test runner choice deferred to autodev Step 5 (Decompose Tests) per Step 4.5 decision. | `04_verification_log.md` §1; `architecture.md` § Architecture Vision Open Questions item 7 |
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
|
||||||
|
| # | Restriction | Source / Evidence |
|
||||||
|
|---|-------------|--------------------|
|
||||||
|
| E1 | **Air-gap-friendly bundle** — the SPA ships fully; only OpenWeatherMap and map tiles need internet. (Field deployments need an offline tile cache; not implemented today.) | `_docs/02_document/architecture.md` § 2 "Key constraints driving the stack" |
|
||||||
|
| E2 | **nginx reverse-proxy strips `/api/<service>/` per service** before forwarding. The SPA's `/api/...` URLs are coupled to this routing. | `nginx.conf` (9 routes); `ADR-006` |
|
||||||
|
| E3 | **`Secure HttpOnly SameSite=Strict` refresh cookie** issued by `admin/`. Browser MUST use the same origin (or proxied origin) so the cookie scopes correctly. | `_docs/02_document/architecture.md` § 7 Security Architecture |
|
||||||
|
| E4 | **Vite dev proxy** at `/api → http://localhost:8080` (developers run the suite docker-compose locally). | `vite.config.ts` |
|
||||||
|
| E5 | **`AZAION_REVISION` env var** is stamped into the production image at build time (`$CI_COMMIT_SHA`). | `Dockerfile`; `.woodpecker/build-arm.yml` |
|
||||||
|
| E6 | **OCI image labels** — `org.opencontainers.image.{revision,created,source}` are mandatory at push time. | `.woodpecker/build-arm.yml` |
|
||||||
|
| E7 | **Image registry** is `${REGISTRY_HOST}/azaion/ui:${branch}-arm`; tag scheme is `branch-arm`. | `.woodpecker/build-arm.yml` |
|
||||||
|
| E8 | **Branch triggers**: CI runs on push to `dev` / `stage` / `main` (mapping to environment names). | `.woodpecker/build-arm.yml` |
|
||||||
|
| E9 | **`client_max_body_size 500M`** — the server-side hard cap on file uploads (annotation-media batch). | `nginx.conf` |
|
||||||
|
| E10 | **OpenWeatherMap is consumed directly from the browser** today (CORS-enabled OWM endpoint). The hardcoded API key (P10 violation) is the security concern; the routing pattern itself is the structural concern (Step 6 surface — proxy via suite). | `mission-planner/src/utils/flightPlanUtils.ts:60`; `architecture.md` § Architecture Vision Open Questions item 8 |
|
||||||
|
|
||||||
|
## Operational
|
||||||
|
|
||||||
|
| # | Restriction | Source / Evidence |
|
||||||
|
|---|-------------|--------------------|
|
||||||
|
| O1 | **Bilingual UI is mandatory** (English + Ukrainian). English-only UX is a regression. | P6; `ADR-007`; `_docs/legacy/wpf-era.md` |
|
||||||
|
| O2 | **Bearer never written to localStorage / sessionStorage**. | P3; `src/auth/AuthContext.tsx` (zero `storage.*` calls) |
|
||||||
|
| O3 | **All authenticated `fetch` requests must include `credentials:'include'`** for the HttpOnly refresh cookie to flow. The bootstrap refresh in `AuthContext.tsx:24` violates this and is a Step 4 fix. | `src/api/client.ts:44` (correct path); `src/auth/AuthContext.tsx:24` (broken path); `04_verification_log.md` F2 |
|
||||||
|
| O4 | **RBAC is server-enforced**. The UI MUST NOT trust `AuthUser.role` for security; it is used only for nav rendering. | P3 / `architecture.md` § 7 Authorization |
|
||||||
|
| O5 | **`Secure HttpOnly SameSite=Strict` refresh cookie** is the single source of refresh-token authority. | `architecture.md` § 7 |
|
||||||
|
| O6 | **No hardcoded credentials in source** (P10). Current violation: OpenWeatherMap key in `mission-planner/src/utils/flightPlanUtils.ts:60` — Step 4 fix candidate. | P10; `architecture.md` § Architecture Vision |
|
||||||
|
| O7 | **Spec is the source of truth for numeric enums** (`AnnotationStatus`, `MediaStatus`, `Affiliation`, `CombatReadiness`). UI types file matches the spec verbatim with inline numeric-meaning comments. | P9; `src/types/index.ts`; `04_verification_log.md` enum drift |
|
||||||
|
| O8 | **Persist what you type** (P11) — fields declared in `UserSettings` (incl. resizable-panel widths) MUST be persisted by the writers; reading without writing back is a violation. Current violation: `useResizablePanel` (Step 4 fix). | P11; `src/hooks/useResizablePanel.ts` |
|
||||||
|
| O9 | **Admin can edit existing detection classes** (P12) — full CRUD surface. Current code is add + delete only; edit (`PATCH /api/admin/classes/{id}`) is to be re-introduced. | P12; `04_verification_log.md` F10 |
|
||||||
|
| O10 | **Destructive actions require `ConfirmDialog`** confirmation. Current violations: `AdminPage.handleDeleteClass` (no dialog); `MediaList` uses `alert()` instead. | `_docs/ui_design/README.md` confirmation-dialogs spec; finding B4 |
|
||||||
|
| O11 | **No SSR / React Server Components** (P2). | `Dockerfile`; `ADR-001` |
|
||||||
|
| O12 | **The `mission-planner/` tree is NOT compiled by the production Vite build**. It is the port-source for `05_flights` and is on a multi-cycle path to deletion. | `vite.config.ts`; `ADR-009`; `architecture.md` § Mission-planner convergence plan |
|
||||||
|
| O13 | **Bundle size budget**: ≤ ~2 MB gzipped initial JS (target). Currently no CI gate. | `architecture.md` § 6 NFR row "Bundle size (initial JS)" |
|
||||||
|
| O14 | **CI test step does not exist today**. To be added once a test framework is selected (autodev Step 5 — Decompose Tests). | `.woodpecker/build-arm.yml`; `architecture.md` § 3 "Missing from the pipeline today" |
|
||||||
|
| O15 | **No vulnerability scan / SBOM emission / image signing** in the pipeline today. Step 6 surface (security_approach.md). | `.woodpecker/build-arm.yml` |
|
||||||
|
|
||||||
|
## Notes on items NOT in this list
|
||||||
|
|
||||||
|
- **Browser support matrix** is **not enforced** (no `browserslist` config). The "Chromium + Firefox latest 2" target is aspirational per `architecture.md` § 6.
|
||||||
|
- **Performance budgets** beyond bundle size and the 500 MB upload cap are **not enforced** in code or CI today.
|
||||||
|
- **Accessibility floor**: WCAG-level conformance is **not declared**. Multiple a11y findings are recorded for Step 4 / Step 8 (see `architecture.md` § 6 NFR row "Accessibility").
|
||||||
|
- **Telemetry / observability**: no centralized client telemetry today. Logging is browser-console only. Step 6 surface (`_docs/02_document/deployment/observability.md`).
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
# Security Approach — Azaion UI
|
||||||
|
|
||||||
|
> Output of `/document` Step 6e. Retrospective security view of the SPA
|
||||||
|
> grounded in code (`src/auth/AuthContext.tsx`, `src/api/client.ts`,
|
||||||
|
> `src/api/sse.ts`), config (`nginx.conf`, `Dockerfile`,
|
||||||
|
> `.woodpecker/build-arm.yml`), and the verified architecture
|
||||||
|
> (`_docs/02_document/architecture.md` § 7). Every claim cites its evidence.
|
||||||
|
|
||||||
|
**Status**: synthesised-from-verified-docs (Step 6e — `/document`)
|
||||||
|
**Date**: 2026-05-10
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Threat model summary
|
||||||
|
|
||||||
|
The UI is **operator-internal**, not public. The trust model is:
|
||||||
|
|
||||||
|
- **Trusted**: the suite services (reached via nginx reverse-proxy on the
|
||||||
|
same origin); the suite's identity provider (`admin/`); the operator's
|
||||||
|
authenticated browser session.
|
||||||
|
- **Untrusted**: the browser itself (XSS-resistant design — bearer in
|
||||||
|
memory only); operator network if not on the suite VPN; OpenWeatherMap
|
||||||
|
(currently exfiltrated to via a hardcoded key — finding); OSM tile
|
||||||
|
servers (read-only third-party).
|
||||||
|
|
||||||
|
Primary threats considered: **token theft via XSS**; **CSRF via cookie
|
||||||
|
auto-attach**; **bearer leakage via SSE query string**; **secret leakage in
|
||||||
|
bundle**; **privilege escalation via missing client-side route gates**;
|
||||||
|
**clickjacking / framing**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Authentication
|
||||||
|
|
||||||
|
### Login
|
||||||
|
|
||||||
|
- `POST /api/admin/auth/login` with `{ email, password }`.
|
||||||
|
- `admin/` service responds with:
|
||||||
|
- **Bearer JWT** in the response body — held in `AuthContext` memory
|
||||||
|
only (never written to `localStorage` / `sessionStorage`, P3).
|
||||||
|
- **`Secure HttpOnly SameSite=Strict` refresh cookie** — issued by
|
||||||
|
server, scoped to the suite origin.
|
||||||
|
|
||||||
|
Source: `src/auth/AuthContext.tsx`; `architecture.md` § 7.
|
||||||
|
|
||||||
|
### Session bootstrap (cold load)
|
||||||
|
|
||||||
|
- On mount, `AuthContext` attempts `GET /api/admin/auth/refresh` to obtain
|
||||||
|
a new bearer.
|
||||||
|
- **Bug**: this call is missing `credentials:'include'` — the HttpOnly
|
||||||
|
refresh cookie is NOT sent → cold-load refresh fails → user is
|
||||||
|
redirected to `/login` even with a valid cookie. **Step 4 fix
|
||||||
|
candidate**.
|
||||||
|
|
||||||
|
Source: `src/auth/AuthContext.tsx:24`; `04_verification_log.md` F2.
|
||||||
|
|
||||||
|
### 401-retry path
|
||||||
|
|
||||||
|
- The `01_api-transport` `client.ts` wraps every authenticated `fetch`.
|
||||||
|
On 401 it issues `POST /api/admin/auth/refresh` **with**
|
||||||
|
`credentials:'include'`, replaces the bearer in `AuthContext`, and
|
||||||
|
retries the original request.
|
||||||
|
- This path is correct and is the working refresh mechanism today.
|
||||||
|
|
||||||
|
Source: `src/api/client.ts:44`; `04_verification_log.md` F2.
|
||||||
|
|
||||||
|
### Logout
|
||||||
|
|
||||||
|
- `POST /api/admin/auth/logout` — clears the bearer in memory; server
|
||||||
|
invalidates the refresh cookie.
|
||||||
|
|
||||||
|
Source: `src/auth/AuthContext.tsx`.
|
||||||
|
|
||||||
|
### Pre-port (legacy WPF)
|
||||||
|
|
||||||
|
- The WPF-era encrypted-creds command-line handoff (binary-split key
|
||||||
|
fragments + DPAPI) is **intentionally not ported** — the browser cannot
|
||||||
|
participate in that handoff and the suite identity infrastructure now
|
||||||
|
lives server-side. P8.
|
||||||
|
|
||||||
|
Source: `_docs/legacy/wpf-era.md` §11.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Authorization
|
||||||
|
|
||||||
|
- RBAC is **server-enforced** — every authenticated endpoint validates
|
||||||
|
`User.role` + `permissions[]` server-side.
|
||||||
|
- The UI inspects `AuthUser.role` to render or hide nav links and pages,
|
||||||
|
but does **NOT** treat the result as a security gate.
|
||||||
|
- Browser is treated as untrusted; every action confirms with the server.
|
||||||
|
|
||||||
|
### Findings
|
||||||
|
|
||||||
|
- **`/admin` route lacks a client-side role-gate** (PRIORITY — security
|
||||||
|
finding, AC-22). Server-side 403 IS the authoritative gate, but a
|
||||||
|
non-admin user navigating to `/admin` today sees the broken admin UI
|
||||||
|
flicker before the server rejects requests. **Step 4 / Step 8 fix.**
|
||||||
|
- **`/settings` route gate is more nuanced** — there is no explicit
|
||||||
|
`SETTINGS` permission code in the suite spec; gating relies on
|
||||||
|
server-side 403. Treat as a soft gate (don't expose the link in the
|
||||||
|
Header for non-admins) rather than a hard redirect.
|
||||||
|
|
||||||
|
Source: `architecture.md` § 7 Authorization; `App.tsx`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Token handling
|
||||||
|
|
||||||
|
| Token | Lifetime | Where it lives | Where it appears on the wire |
|
||||||
|
|-------|----------|----------------|------------------------------|
|
||||||
|
| Bearer JWT | Short (server-issued; refreshed on 401) | `AuthContext` React state — **memory only** | `Authorization: Bearer ${token}` header on every authenticated `fetch`; `?token=${bearer}` query string on SSE (`ADR-008`) |
|
||||||
|
| Refresh token | Long (server-issued) | **`Secure HttpOnly SameSite=Strict` cookie** — never accessible to JS | Cookie header on `POST /api/admin/auth/refresh` (and the broken bootstrap GET — Step 4 fix) |
|
||||||
|
| `X-Refresh-Token` header | Per-request (long-running video detect) | passed in by `01_api-transport` for endpoints that need it | `X-Refresh-Token: ${value}` per `_docs/10_auth.md`. **Currently NOT sent on `POST /api/detect/${mediaId}` for video** — long videos can blow the access-token TTL → silent failure. Step 4 fix candidate (finding #29). |
|
||||||
|
|
||||||
|
### Key invariants (P3)
|
||||||
|
|
||||||
|
- Bearer is **never** written to `localStorage` / `sessionStorage` / IndexedDB.
|
||||||
|
- Refresh token is **never** read from JS — `HttpOnly` enforces this.
|
||||||
|
- Code-search regression test: zero matches for `localStorage|sessionStorage`
|
||||||
|
touching the bearer token in `src/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. SSE bearer-in-query-string
|
||||||
|
|
||||||
|
`EventSource` cannot send arbitrary headers, so `src/api/sse.ts` passes the
|
||||||
|
bearer in the URL: `?token=${bearer}`.
|
||||||
|
|
||||||
|
### Trade-offs
|
||||||
|
|
||||||
|
- **Bearer is short-lived** — minimises window of compromise.
|
||||||
|
- **HTTPS encrypts the URL on the wire** — but the URL still appears in:
|
||||||
|
- **nginx access logs** (mitigation: log redaction at the nginx layer —
|
||||||
|
Step 6 surface; not configured today).
|
||||||
|
- **Browser history** (low risk for SSE URLs, but document).
|
||||||
|
- **Refresh-rotation breaks open SSE connections** — the URL was created
|
||||||
|
with the **old** bearer; no reconnect logic exists today (Step 8
|
||||||
|
hardening — AC-24).
|
||||||
|
|
||||||
|
Source: `src/api/sse.ts`; `ADR-008`; `architecture.md` § 7.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Secrets management
|
||||||
|
|
||||||
|
### Hardcoded OpenWeatherMap API key — P10 violation
|
||||||
|
|
||||||
|
- **File**: `mission-planner/src/utils/flightPlanUtils.ts:60`
|
||||||
|
- **Value**: a 32-char hex key shipped in the production bundle.
|
||||||
|
- **Risk**: anyone with access to the bundle can extract and reuse the
|
||||||
|
key (rate-limit theft; provider account abuse). The key is committed to
|
||||||
|
git history.
|
||||||
|
- **Fix sequence (Step 4 / Phase B)**:
|
||||||
|
1. **Rotate** the key at OpenWeatherMap (out-of-band, user action).
|
||||||
|
2. **Move to env** — `import.meta.env.VITE_OPENWEATHERMAP_API_KEY`
|
||||||
|
read at build time (interim).
|
||||||
|
3. **Proxy via suite** — long-term, route the wind compute through
|
||||||
|
`flights/` so no key ever reaches the browser (preferred; per
|
||||||
|
`architecture.md` § Architecture Vision Open Questions item 8).
|
||||||
|
|
||||||
|
### Other secrets
|
||||||
|
|
||||||
|
- **No other hardcoded keys** in `src/` per Grep audit at Step 4.
|
||||||
|
- Suite service URLs are not secrets (they are docker-network hostnames).
|
||||||
|
- The bearer is the only sensitive value in browser memory, and it is
|
||||||
|
short-lived.
|
||||||
|
|
||||||
|
Source: P10; `architecture.md` § Architecture Vision; finding (security).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. CORS, cookie scope, CSRF
|
||||||
|
|
||||||
|
- **Same-origin via nginx**: the SPA is served by the same nginx that
|
||||||
|
reverse-proxies `/api/<service>/`. The browser sees a single origin →
|
||||||
|
cookies scope cleanly; CORS preflight is unnecessary for the suite
|
||||||
|
endpoints.
|
||||||
|
- **`credentials:'include'`** is required on every authenticated `fetch`
|
||||||
|
for the HttpOnly refresh cookie to flow. The 401-retry path
|
||||||
|
(`api/client.ts:44`) is correct; the bootstrap refresh
|
||||||
|
(`AuthContext.tsx:24`) is **broken**.
|
||||||
|
- **CSRF**: `SameSite=Strict` on the refresh cookie + bearer-in-header on
|
||||||
|
authenticated requests. The bearer header cannot be auto-attached by a
|
||||||
|
cross-origin form submit. **No additional CSRF token** is used today —
|
||||||
|
the architecture pattern (header-based bearer + SameSite=Strict cookie)
|
||||||
|
obviates it.
|
||||||
|
|
||||||
|
Source: `src/api/client.ts`; `nginx.conf`; `architecture.md` § 7.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Input validation
|
||||||
|
|
||||||
|
- **Server is authoritative.** The UI does not duplicate validation logic
|
||||||
|
it cannot guarantee.
|
||||||
|
- **Numeric inputs in `09_settings`** use `parseInt(v) || 0` — clearing a
|
||||||
|
field silently writes `0` (finding B4, AC-26). Step 4 fix.
|
||||||
|
- **File upload**: `react-dropzone` filters by MIME / extension client-side;
|
||||||
|
the server is authoritative on virus scanning and size enforcement
|
||||||
|
(`client_max_body_size 500M`).
|
||||||
|
- **Annotation save** body must include `Source`, `WaypointId`, `videoTime`
|
||||||
|
(currently incomplete — finding #32). The wire format is validated by
|
||||||
|
the `annotations/` service.
|
||||||
|
|
||||||
|
Source: `09_settings/SettingsPage.tsx`; `06_annotations/MediaList.tsx`;
|
||||||
|
`nginx.conf`; finding B4 / #32.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Output encoding / XSS surface
|
||||||
|
|
||||||
|
- React 19 escapes JSX text by default — string content is safe.
|
||||||
|
- **`dangerouslySetInnerHTML`** is **not used** in `src/` (Grep audit).
|
||||||
|
- **`HelpModal`** ships hardcoded English strings inline — XSS-safe but
|
||||||
|
P6 violation (i18n).
|
||||||
|
- **Tainted-canvas** risk on annotation download (`AnnotationsPage.handleDownload`
|
||||||
|
finding) — cross-origin image data may taint the canvas; the download
|
||||||
|
silently fails. Pure UX bug, not a security defect, but flagged.
|
||||||
|
|
||||||
|
Source: `06_annotations/AnnotationsPage.tsx`; `HelpModal.tsx`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Headers / hardening at the nginx layer
|
||||||
|
|
||||||
|
### Currently configured
|
||||||
|
|
||||||
|
- nginx serves `dist/` and reverse-proxies `/api/<service>/` to suite
|
||||||
|
services.
|
||||||
|
- `client_max_body_size 500M`.
|
||||||
|
|
||||||
|
### Currently MISSING (Step 6 surface)
|
||||||
|
|
||||||
|
- **`Content-Security-Policy`** — no CSP header. Recommended starting
|
||||||
|
point: `default-src 'self'; img-src 'self' https: data:; connect-src
|
||||||
|
'self' https://api.openweathermap.org/ https://*.tile.openstreetmap.org/;
|
||||||
|
frame-ancestors 'none'; object-src 'none'`.
|
||||||
|
- **`X-Frame-Options: DENY`** (or covered by CSP `frame-ancestors`) —
|
||||||
|
clickjacking protection.
|
||||||
|
- **`Referrer-Policy: strict-origin-when-cross-origin`**.
|
||||||
|
- **`Strict-Transport-Security`** — depends on suite ingress; document the
|
||||||
|
expected value.
|
||||||
|
- **`X-Content-Type-Options: nosniff`**.
|
||||||
|
- **Bearer-redaction** in nginx access logs for SSE URLs.
|
||||||
|
|
||||||
|
These are nginx config additions (server-side), not SPA changes — but the
|
||||||
|
SPA depends on them for hardening. Track at suite level.
|
||||||
|
|
||||||
|
Source: `nginx.conf`; `architecture.md` § 7 row "Cross-site / clickjack".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Audit logging
|
||||||
|
|
||||||
|
- **Server-side concern** — the `admin/`, `flights/`, `annotations/`, etc.
|
||||||
|
services are responsible for audit-event emission.
|
||||||
|
- The SPA does **not** emit audit events directly. It does not maintain
|
||||||
|
any client-side audit log.
|
||||||
|
- The browser console is the only client-side log surface today; no
|
||||||
|
centralized client telemetry (Step 6 surface — `_docs/02_document/deployment/observability.md`).
|
||||||
|
|
||||||
|
Source: `architecture.md` § 7 Audit logging.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Image / supply-chain
|
||||||
|
|
||||||
|
### Currently in pipeline
|
||||||
|
|
||||||
|
- Multi-stage Dockerfile: `oven/bun:1.3.11-alpine` (build) →
|
||||||
|
`nginx:alpine` (runtime).
|
||||||
|
- `bun install --frozen-lockfile` enforces lockfile fidelity.
|
||||||
|
- `AZAION_REVISION=$CI_COMMIT_SHA` and OCI labels stamped at push time.
|
||||||
|
|
||||||
|
### Currently MISSING (Step 6 surface)
|
||||||
|
|
||||||
|
- **No vulnerability scan** (Trivy / Grype) on the produced image.
|
||||||
|
- **No SBOM emission** (Syft / cyclonedx).
|
||||||
|
- **No image signing** (cosign).
|
||||||
|
- **No dependency audit step** in CI (`bun audit` equivalent — Bun does
|
||||||
|
not yet have a first-party audit; `npm audit --omit=dev` against the
|
||||||
|
lockfile is a reasonable substitute).
|
||||||
|
|
||||||
|
Source: `.woodpecker/build-arm.yml`; `architecture.md` § 3 "Missing from the
|
||||||
|
pipeline today".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Findings → Fix Map
|
||||||
|
|
||||||
|
| Finding | AC | Fix step |
|
||||||
|
|---------|----|----------|
|
||||||
|
| Bootstrap refresh missing `credentials:'include'` (F2) | AC-01 | Step 4 (Code Testability Revision) |
|
||||||
|
| Bearer-in-query SSE — refresh-rotation breaks subscription | AC-24 | Step 8 (Refactor — optional) or Phase B |
|
||||||
|
| Hardcoded OpenWeatherMap key (P10) | AC-20 | Step 4 (env move); Phase B (suite proxy) |
|
||||||
|
| `/admin` route lacks role-gate | AC-22 | Step 4 |
|
||||||
|
| `09_settings` numeric input writes `0` on empty | AC-26 | Step 4 |
|
||||||
|
| `09_settings` save handlers leak `saving:true` on PUT failure | AC-27 | Step 4 |
|
||||||
|
| `AdminPage.handleDeleteClass` lacks ConfirmDialog | AC-30 | Step 4 |
|
||||||
|
| `MediaList` uses `alert()` | AC-14 | Step 4 |
|
||||||
|
| `ConfirmDialog` lacks `aria-modal/role=dialog` | AC-15 | Step 4 / Step 8 |
|
||||||
|
| Header dropdown lacks combobox/expanded/Esc/focus-trap | AC-16 | Step 4 / Step 8 |
|
||||||
|
| Annotation save body missing `Source`, `WaypointId`, wrong `time` field | AC-05 | Step 4 |
|
||||||
|
| `X-Refresh-Token` not sent on long-video detect (#29) | — | Step 4 |
|
||||||
|
| Numeric enum drift (`AnnotationStatus`, `MediaStatus`, `Affiliation`, `CombatReadiness`) | AC-04 | Step 4 (P9 alignment) |
|
||||||
|
| No CSP / hardening headers in `nginx.conf` | — | Step 6 — track at suite level |
|
||||||
|
| No vulnerability scan / SBOM / image signing in CI | — | Phase B |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Compliance / standards
|
||||||
|
|
||||||
|
The UI does NOT claim conformance to any specific standard today:
|
||||||
|
|
||||||
|
- **No WCAG-level declaration** (multiple a11y findings recorded).
|
||||||
|
- **No SOC2 / ISO27001 controls** are implemented at the SPA layer
|
||||||
|
(server-side concern of the suite).
|
||||||
|
- **No FIPS / specific crypto-mode requirements** at the SPA layer (TLS
|
||||||
|
is terminated server-side; bearer JWT signing is server-side).
|
||||||
|
|
||||||
|
These are recorded as anti-criteria (AC-N4) — the UI is **internal**,
|
||||||
|
**operator-only**, and **trusts the suite** for compliance enforcement.
|
||||||
|
Phase B may revisit if a regulated deployment surface emerges.
|
||||||
@@ -0,0 +1,294 @@
|
|||||||
|
# Azaion UI — Retrospective Solution
|
||||||
|
|
||||||
|
> Output of `/document` Step 5. Synthesis of the **implemented** architecture
|
||||||
|
> and per-component choices, derived from the verified technical docs:
|
||||||
|
> `_docs/02_document/architecture.md` (Step 3a), `system-flows.md` (Step 3b),
|
||||||
|
> `data_model.md` (Step 3c), `deployment/*.md` (Step 3d),
|
||||||
|
> `components/*/description.md` (Step 2), `04_verification_log.md` (Step 4),
|
||||||
|
> `glossary.md` and `architecture.md` § Architecture Vision (Step 4.5).
|
||||||
|
>
|
||||||
|
> This is retrospective — it describes the solution **as it is**, with
|
||||||
|
> observed limitations called out per component. Future work (testability
|
||||||
|
> fixes, async-detect wiring, mission-planner convergence) is referenced
|
||||||
|
> by source and not re-stated as a plan here.
|
||||||
|
|
||||||
|
**Status**: synthesised-from-verified-docs (Step 5 — `/document`)
|
||||||
|
**Date**: 2026-05-10
|
||||||
|
**Project**: Azaion UI (operator-facing browser SPA)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Product Solution Description
|
||||||
|
|
||||||
|
Azaion UI is a single-page React 19 application, statically built and served
|
||||||
|
by nginx inside an ARM64 container, that operates the browser-facing half of
|
||||||
|
the Azaion UAV operations suite. It lets an operator plan flights, browse and
|
||||||
|
annotate captured media, run AI object detection (synchronous on images;
|
||||||
|
asynchronous video detect is **target-only — not wired today**, see
|
||||||
|
`04_verification_log.md` F7), curate datasets, manage detection classes /
|
||||||
|
users / aircraft, and operate the GPS-Denied positioning workflow including a
|
||||||
|
planned Test Mode driven by `.tlog` + video pairs through SITL.
|
||||||
|
|
||||||
|
The solution communicates with the parent suite's microservices over **REST
|
||||||
|
and Server-Sent Events only** — no WebSocket, no GraphQL, no in-browser
|
||||||
|
persistence beyond a single bearer token in memory and a `Secure HttpOnly`
|
||||||
|
refresh cookie. State management is two React Contexts (`AuthContext` and
|
||||||
|
`FlightContext`); everything else is page-local.
|
||||||
|
|
||||||
|
A second React 18 + MUI 5 tree (`mission-planner/`) lives at the repo root as
|
||||||
|
the **port-source** for `05_flights` — it is **NOT deployed**, **NOT
|
||||||
|
compiled** by the production Vite build, and is on a multi-cycle path to
|
||||||
|
deletion as features migrate into `src/features/flights/` (Phase B feature
|
||||||
|
cycles per the convergence plan in `architecture.md` § Architecture Vision).
|
||||||
|
|
||||||
|
### Component interaction diagram
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
subgraph Browser SPA
|
||||||
|
AppShell[10_app-shell]
|
||||||
|
AppShell --> Auth[02_auth]
|
||||||
|
AppShell --> Login[04_login]
|
||||||
|
Auth --> Shared[03_shared-ui<br/>Header, FlightContext,<br/>ConfirmDialog, HelpModal]
|
||||||
|
Shared --> Flights[05_flights]
|
||||||
|
Shared --> Annotations[06_annotations]
|
||||||
|
Shared --> Dataset[07_dataset]
|
||||||
|
Shared --> Admin[08_admin]
|
||||||
|
Shared --> Settings[09_settings]
|
||||||
|
Foundation[00_foundation<br/>types, hooks, i18n] -.shared.-> Auth
|
||||||
|
Foundation -.shared.-> Shared
|
||||||
|
Foundation -.shared.-> Flights
|
||||||
|
Foundation -.shared.-> Annotations
|
||||||
|
Foundation -.shared.-> Dataset
|
||||||
|
Foundation -.shared.-> Admin
|
||||||
|
Foundation -.shared.-> Settings
|
||||||
|
ClassColors[11_class-colors] -.shared.-> Shared
|
||||||
|
ClassColors -.shared.-> Annotations
|
||||||
|
ClassColors -.shared.-> Dataset
|
||||||
|
Transport[01_api-transport<br/>fetch + EventSource]
|
||||||
|
Auth --> Transport
|
||||||
|
Flights --> Transport
|
||||||
|
Annotations --> Transport
|
||||||
|
Dataset --> Transport
|
||||||
|
Admin --> Transport
|
||||||
|
Settings --> Transport
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph nginx reverse-proxy
|
||||||
|
Transport --> Nginx[nginx<br/>strip /api/svc/]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Suite services
|
||||||
|
Nginx --> AdminSvc[admin/]
|
||||||
|
Nginx --> FlightsSvc[flights/]
|
||||||
|
Nginx --> AnnotSvc[annotations/]
|
||||||
|
Nginx --> DetectSvc[detect/]
|
||||||
|
Nginx --> GpsDenied[gps-denied-*/]
|
||||||
|
Nginx --> Resource[resource/]
|
||||||
|
Nginx --> Autopilot[autopilot/]
|
||||||
|
Nginx --> Loader[loader/]
|
||||||
|
end
|
||||||
|
|
||||||
|
Flights --> OWM[OpenWeatherMap<br/>direct HTTPS<br/>hardcoded key — finding]
|
||||||
|
Flights --> OSM[OSM tile servers<br/>direct HTTPS]
|
||||||
|
```
|
||||||
|
|
||||||
|
Detailed per-flow sequences (F1–F14): `_docs/02_document/system-flows.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
The solution is organised as **11 components** under a strict layering
|
||||||
|
(`_docs/02_document/module-layout.md`):
|
||||||
|
|
||||||
|
- **L0 (Foundation)**: `00_foundation`, `11_class-colors`
|
||||||
|
- **L1 (Transport)**: `01_api-transport`
|
||||||
|
- **L2 (Auth + Shared UI)**: `02_auth`, `03_shared-ui`
|
||||||
|
- **L3 (Feature pages)**: `04_login`, `05_flights`, `06_annotations`,
|
||||||
|
`07_dataset`, `08_admin`, `09_settings`
|
||||||
|
- **L4 (App shell)**: `10_app-shell`
|
||||||
|
|
||||||
|
Component dependency graph: `_docs/02_document/diagrams/components.md`.
|
||||||
|
|
||||||
|
### Cross-cutting principles (binding constraints — `architecture.md` § Architecture Vision)
|
||||||
|
|
||||||
|
P1 REST + SSE only · P2 Static bundle + nginx · P3 Bearer in memory + refresh
|
||||||
|
in HttpOnly cookie · P4 Two-context state (Auth + Flight) · P5 ARM-first edge
|
||||||
|
deployment · P6 Bilingual (en + ua) · P7 Lift cross-cutting at 2+ touches ·
|
||||||
|
P8 WPF parity is a goal not a constraint · P9 Spec is source of truth for
|
||||||
|
numeric enums · P10 No hardcoded credentials in source · P11 Persist what you
|
||||||
|
type · P12 Admin can edit existing detection classes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Component: `00_foundation`
|
||||||
|
|
||||||
|
| Solution | Tools | Advantages | Limitations | Requirements | Security | Cost | Fit |
|
||||||
|
|----------|-------|-----------|-------------|--------------|----------|------|-----|
|
||||||
|
| Shared types + hooks + i18n bundles, no domain logic | `typescript@5.7 strict`, `i18next` + `react-i18next`, custom hooks (`useDebounce`, `useResizablePanel`) | Single source of truth for the suite's typed REST contract; zero runtime cost (types erased); all bilingual strings live here | `useResizablePanel` reads `UserSettings.panelWidths` but **never writes back** — violates principle P11 (`04_verification_log.md` finding #11). `i18next` `lng:'en'` is hardcoded — no detector / no persistence. Inline numeric-enum comments are required by P9 (already added 2026-05-10) | TypeScript strict mode; bilingual coverage; numeric-enum drift between `src/types/index.ts` and the suite spec must be resolved | None — shared types only | Negligible (transitive only) | **Selected — current solution.** Layer-0 placement keeps every other component dependency-free w.r.t. types/hooks. |
|
||||||
|
|
||||||
|
### Component: `01_api-transport`
|
||||||
|
|
||||||
|
| Solution | Tools | Advantages | Limitations | Requirements | Security | Cost | Fit |
|
||||||
|
|----------|-------|-----------|-------------|--------------|----------|------|-----|
|
||||||
|
| Native `fetch` wrapper + native `EventSource` wrapper | `src/api/client.ts` (fetch + 401-retry refresh), `src/api/sse.ts` (`createSSE` helper) | Zero added dependencies (no axios / TanStack / SWR); 401-retry is centralised — every authenticated request gets refresh-token rotation for free | Bearer for SSE goes in the **query string** (`?token=...`); EventSource cannot send headers (`ADR-008`). EventSource holds the bearer captured at create time — refresh-rotation breaks long-running subscriptions; reconnect logic is missing today (Step 8 hardening) | All authenticated `fetch` requests must include `credentials:'include'` for the HttpOnly refresh cookie to flow; SSE endpoints must accept the bearer in the URL | 401-retry path is **secure** (POST + cookie); bootstrap GET refresh in `AuthContext.tsx:24` is **broken** (no `credentials:'include'`) — Step 4 fix | Negligible | **Selected — current solution.** Fits P1 (REST + SSE only) and P3 (no localStorage). |
|
||||||
|
|
||||||
|
### Component: `02_auth`
|
||||||
|
|
||||||
|
| Solution | Tools | Advantages | Limitations | Requirements | Security | Cost | Fit |
|
||||||
|
|----------|-------|-----------|-------------|--------------|----------|------|-----|
|
||||||
|
| `AuthContext` + `ProtectedRoute` + login/logout/bootstrap-refresh | `react-router-dom@7`, React Context, `01_api-transport` | XSS-resistant (bearer in memory, never in storage); login UX matches WPF era; refresh-token rotation is server-driven | **Two refresh paths in code** (`F2`): bootstrap GET (`AuthContext.tsx:24` — broken, missing `credentials:'include'`) vs. 401-retry POST (`api/client.ts:44` — correct). Bootstrap path will fail on cross-origin and force a re-login on cold load — Step 4 fix priority. `ProtectedRoute` spinner has no `role='status'` / no timeout | RBAC is server-enforced; UI must NOT trust `AuthUser.role` for security — only for showing/hiding nav | Bearer never written to storage (P3); refresh cookie is `Secure HttpOnly SameSite=Strict` (issued server-side); WPF-era encrypted-creds command-line handoff intentionally NOT ported (P8) | Negligible | **Selected — current solution.** Refresh-path consolidation (single POST with `credentials:'include'`) is the planned fix; structurally sound. |
|
||||||
|
|
||||||
|
### Component: `03_shared-ui`
|
||||||
|
|
||||||
|
| Solution | Tools | Advantages | Limitations | Requirements | Security | Cost | Fit |
|
||||||
|
|----------|-------|-----------|-------------|--------------|----------|------|-----|
|
||||||
|
| Header + flight dropdown + `FlightContext` + `ConfirmDialog` + `HelpModal` + `DetectionClasses` strip | React Context (`FlightContext`), Tailwind, `01_api-transport` | One place for cross-page chrome; `FlightContext` is the only flight-selection store (P4); `ConfirmDialog` reused by 4 components | `FlightContext` ceiling: `GET /api/flights?pageSize=1000` is a hardcoded magic number (finding B3). `selectFlight` is **fire-and-forget** PUT — no error path. Header dropdown lacks `role=combobox` / `aria-expanded` / Esc-to-close / focus-trap. `ConfirmDialog` lacks `aria-modal` / `role=dialog`. `HelpModal` does NOT close on Esc (inconsistent with `ConfirmDialog`); `GUIDELINES` are hardcoded English instead of i18n | Selected flight persists as a `UserSettings` field via `PUT /api/annotations/settings/user` (NOT `/api/flights/select` — `04_verification_log.md` F3) | None additional | Negligible | **Selected — current solution.** Flight-dropdown a11y + 1000-row pagination ceiling are Step 4 / Step 8 candidates. |
|
||||||
|
|
||||||
|
### Component: `04_login`
|
||||||
|
|
||||||
|
| Solution | Tools | Advantages | Limitations | Requirements | Security | Cost | Fit |
|
||||||
|
|----------|-------|-----------|-------------|--------------|----------|------|-----|
|
||||||
|
| Public `/login` route with username + password, calls `02_auth` login | React, Tailwind | Single dedicated public surface; clean separation from `ProtectedRoute` | `runUnlockSequence` 4×600 ms theatrical animation is decorative; document only (`finding B4`) | Receives bearer from server; cookie set server-side | Login form does NOT autocomplete sensitive values; only public route in the SPA | Negligible | **Selected — current solution.** No structural concerns. |
|
||||||
|
|
||||||
|
### Component: `05_flights`
|
||||||
|
|
||||||
|
| Solution | Tools | Advantages | Limitations | Requirements | Security | Cost | Fit |
|
||||||
|
|----------|-------|-----------|-------------|--------------|----------|------|-----|
|
||||||
|
| Flight CRUD + waypoints + altitude profile + GPS-Denied (Operations + planned Test Mode) — currently being ported from `mission-planner/` (React 18 + MUI 5) into `src/features/flights/` (React 19 + Tailwind 4) | `leaflet@1.9.4` + `react-leaflet@5` + `leaflet-draw` + `leaflet-polylinedecorator`; `chart.js@4` + `react-chartjs-2`; `@hello-pangea/dnd@18`; native `fetch`; `EventSource` for `F13 live-GPS SSE`; OpenWeatherMap (direct HTTPS) | Replaces WPF `MapMatcher` with browser-native cartography; live-GPS telemetry is real (F13); altitude charts work; mission-planner port gives a high-fidelity reference UX | **Component spans two physical trees** (one component, two trees — `ADR-009`). `mission-planner/src/utils/flightPlanUtils.ts:60` carries a **hardcoded OpenWeatherMap API key** (P10 violation — Step 4 fix). Wind errors are silently swallowed; sequential per-segment `await` is a perf trap; battery-capacity unit ambiguous (Wh vs Ws); km vs m altitude mixing. `mapIcons.ts` defaultIcon CDN URL pinned to `leaflet@1.7.1` (drift). Waypoint POST shape **mismatches** the suite spec — UI sends `{name, latitude, longitude, order}`; spec wants `{Geopoint:{Lat,Lon,MGRS}, Source, Objective, OrderNum, Height}` (finding #20 — likely 400s on a strict server). Edit-cycle is **delete-then-recreate** today (finding #19). FlightsPage save is N+M round-trips (delete + recreate per waypoint). `MiniMap` licence/responsive concerns; `AltitudeDialog` / `JsonEditorDialog` modal a11y; `WaypointList` drag/touch a11y; `AltitudeChart` bundle bloat. **Test Mode (F12) is target-only** — `.tlog` + video upload, IMU sync, SITL feed — none wired today | Wind data fetch + map tile fetch require browser internet; field deployments need an offline tile cache (not implemented); `.tlog` parser must be available client-side or server-side once Test Mode lands | OpenWeatherMap key must move to `.env` per P10 (Step 4 testability fix); satellite tile URL is env-driven via `VITE_SATELLITE_TILE_URL` in mission-planner only (target: `src/` once port lands) | Direct browser → external HTTP costs for OWM + tiles; otherwise compute is client-side | **Selected — current solution; under active convergence.** Per `architecture.md` § Architecture Vision, the mission-planner tree is on a multi-cycle path to deletion (`mission-planner/` → `src/features/flights/`); convergence happens in Phase B feature cycles, not in Step 8. |
|
||||||
|
|
||||||
|
### Component: `06_annotations`
|
||||||
|
|
||||||
|
| Solution | Tools | Advantages | Limitations | Requirements | Security | Cost | Fit |
|
||||||
|
|----------|-------|-----------|-------------|--------------|----------|------|-----|
|
||||||
|
| Bounding-box editor (`CanvasEditor`), `VideoPlayer`, AI Detect (sync only today), `AnnotationsSidebar`, `MediaList` browser scoped to selected flight, `AnnotationsPage` orchestrator | HTML5 `<canvas>` + HTML5 `<video>` (replaces WPF LibVLC — `ADR-003`); native `fetch` + SSE; `react-dropzone` for upload; Tailwind | Frame-accurate-ish video review without LibVLC; doubly-prefixed `POST /api/annotations/annotations` save path verified at Step 4 (F5); `F14 annotation-status SSE` (admin-wide, client-side filtered) is real | **Async video detect (F7) is NOT wired** — no `/api/detect/video/{id}`, no `/api/detect/stream/{jobId}` calls anywhere; sync `POST /api/detect/${id}` is used for **both** images and videos today (silent UX hazard for long videos). `VideoPlayer` hardcoded `fps=30` (`ADR-003` consequence). `CanvasEditor` missing pan; wrong time-window (symmetric ±200 ms instead of asymmetric `[-50, +150]` ms — finding #6); missing affiliation icons; missing CombatReadiness indicator; dead `AFFILIATION_COLORS`. `AnnotationsSidebar` AI-detect doesn't stream progress; silent catches. `AnnotationsPage` no panel-width persistence (P11 violation); `handleDownload` tainted-canvas risk; `handleSave` fallback hides save loss; **annotation save body shape mismatches spec** — must add `Source`, `WaypointId`, rename `time→videoTime` (finding #32). `MediaList` uses `alert()`; blob: locals ignore filter. **Missing keyboard shortcuts** (R, V, PageUp/Down). Missing Camera config side panel (GSD computation — finding #17). Missing Tile zoom for `splitTile`. **Hardcoded English strings** in help / sidebar | Detect must be wired to `detect/` async pipeline once F7 ships (Phase B); `X-Refresh-Token` header required for long-running video detect (per `_docs/10_auth.md`); annotation overlay window must align to spec asymmetric `[-50, +150]` ms | XSS-safe canvas rendering; uploads filtered by `react-dropzone` MIME; server is authoritative for virus scan; tainted-canvas risk on download (finding) | Detect-pipeline cost is server-side; UI compute is client-side | **Selected — current solution.** Async-detect wiring + canvas-editor parity + a11y are Phase B targets; structurally sound. |
|
||||||
|
|
||||||
|
### Component: `07_dataset`
|
||||||
|
|
||||||
|
| Solution | Tools | Advantages | Limitations | Requirements | Security | Cost | Fit |
|
||||||
|
|----------|-------|-----------|-------------|--------------|----------|------|-----|
|
||||||
|
| Dataset Explorer with three tabs (annotations / editor / class-distribution), bulk-validate, status filters | React, Tailwind, `chart.js@4` for class-distribution chart (`DatasetPage.tsx:151`), reuses `CanvasEditor` from `06_annotations` (cross-feature edge — finding) | **Validate button is wired** (Step 4 correction — `04_verification_log.md` F9); class-distribution chart **is implemented** (Step 4 correction); "objectsOnly" checkbox **is implemented**; `objectsOnly` filter works | **`[V]` keyboard shortcut missing** (only the button works); `Refresh thumbnails` button missing; status-bar `StatusText` slots missing; `IsSeed` highlight missing (legacy 8 px IndianRed border — open question); editor tab **does not save** (finding #4); magic `mediaType=1` literal (finding #5); dead `ConfirmDialog` import; silent catches; status filter conflates `None` with `All`; `classNum=0` sentinel collides with real class 0 (finding #9); no virtualisation; no keyboard shortcuts at all | `AnnotationStatus` numeric drift (UI 0/1/2 vs spec 0/10/20/30) surfaces as wrong status filter values on the wire — Step 4 fix per P9; `DatasetItem.isSplit` not in spec response — parent-suite doc fix recorded in leftovers and applied | None additional | Class-distribution endpoint server-side cost; otherwise client-side | **Selected — current solution.** Bulk-validate + chart parity confirm the React port is closer to WPF parity than the original draft suggested; remaining gaps are surface-level + a11y. |
|
||||||
|
|
||||||
|
### Component: `08_admin`
|
||||||
|
|
||||||
|
| Solution | Tools | Advantages | Limitations | Requirements | Security | Cost | Fit |
|
||||||
|
|----------|-------|-----------|-------------|--------------|----------|------|-----|
|
||||||
|
| Class CRUD (add + delete; **edit to be re-introduced** per P12), user management, AI/GPS settings forms, aircraft default-toggle | React forms with `useState`; native `fetch`; reuses `ConfirmDialog` (only on user-deactivation) | Single page consolidates 4 admin surfaces; aircraft cross-service mutation works (`PATCH /api/flights/aircrafts/${id}`) | **AI Settings & GPS Settings forms render with `defaultValue` only** — no state, no submit handler, the Save button **does nothing** (PRIORITY — `Step 6` problem-extraction surface). Hardcoded GPS device default `'192.168.1.100'` / port `'5535'` shipped in production bundle. `handleDeleteClass` has **no `ConfirmDialog`** despite being destructive (finding B4). Detection-class read uses `/api/annotations/classes` but write uses `/api/admin/classes` (cross-service split — accepted but documented). `handleToggleDefault` duplicated in `09_settings` (aircraft default lives in two pages — surface intent at Step 6). Many hardcoded English strings (P6 violation — Step 4 fix). Admin **cannot edit existing classes** today (P12 violation; `PATCH /api/admin/classes/{id}` to introduce — Phase B task) | RBAC is server-enforced (UI MUST NOT trust `AuthUser.role` for security); `/admin` route lacks role-gate — security PRIORITY | RBAC server-enforced; class-delete bypasses ConfirmDialog (Step 4 fix) | Compute is server-side | **Selected — current solution; multiple Step 4 / Step 6 / Phase B fixes queued.** Structurally sound; the broken Save buttons are a P0 product-correctness defect. |
|
||||||
|
|
||||||
|
### Component: `09_settings`
|
||||||
|
|
||||||
|
| Solution | Tools | Advantages | Limitations | Requirements | Security | Cost | Fit |
|
||||||
|
|----------|-------|-----------|-------------|--------------|----------|------|-----|
|
||||||
|
| System / Directory / Camera / User settings forms; aircraft default-toggle | React forms, `useState`, native `fetch` | Settings persist via `annotations/` service (`/api/annotations/settings/system` + `/api/annotations/settings/directories`) — verified Step 4 (F11); aircraft default-toggle goes to `flights/` (cross-service — accepted) | `saveSystem` / `saveDirs` lack `try/finally` — PUT failure leaves `saving:true` permanently (finding B4). Numeric inputs use `parseInt(v) || 0` — clearing a field silently writes 0 (finding B4). No optimistic concurrency (Step 6 surface). `UserSettings.panelWidths` is typed but `useResizablePanel` doesn't write back (P11 violation — Step 4 fix). `/settings` route role-gate is more nuanced than `/admin` (server-enforced via 403; no SETTINGS permission code in spec) | `/settings` role-gate per `_docs/02_document/architecture.md` § Security; aircraft default is a global config today (finding) | RBAC server-enforced | Compute is server-side | **Selected — current solution.** Settings persistence path is correct; form-state hygiene is the main fix. |
|
||||||
|
|
||||||
|
### Component: `10_app-shell`
|
||||||
|
|
||||||
|
| Solution | Tools | Advantages | Limitations | Requirements | Security | Cost | Fit |
|
||||||
|
|----------|-------|-----------|-------------|--------------|----------|------|-----|
|
||||||
|
| `App.tsx` + `main.tsx` + routing tree + global CSS | `react-router-dom@7`, Tailwind 4 + `az-*` design tokens (`src/index.css`), React 19 root | Single composition root; clear `AuthProvider → ProtectedRoute → FlightProvider → Header + Routes` chain; `/flights` is the default authenticated route | `/admin` and `/settings` lack **role-based route guards** (PRIORITY — security finding; complements server-side RBAC). No `ErrorBoundary`. No lazy code-splitting / no chunked routes (bundle bloat finding). `index.html` body class hardcodes hex literals (`bg-[#1e1e1e] text-[#adb5bd]`) instead of `az-*` tokens (cosmetic — `ADR-005`) | Routing tree is the security surface for client-side navigation; server-side RBAC is authoritative | UI role-gate is **convenience, not security**; server-enforced 403 is the actual gate | Negligible | **Selected — current solution.** Adding `ErrorBoundary` + lazy routes + role-gate are Step 4 / Step 8 candidates. |
|
||||||
|
|
||||||
|
### Component: `11_class-colors`
|
||||||
|
|
||||||
|
| Solution | Tools | Advantages | Limitations | Requirements | Security | Cost | Fit |
|
||||||
|
|----------|-------|-----------|-------------|--------------|----------|------|-----|
|
||||||
|
| Class → color + text mapping; `getPhotoModeSuffix` helper; consumed by `03_shared-ui/DetectionClasses`, `06_annotations`, `07_dataset` | Pure TypeScript module; no external deps | Lifted out of `06_annotations` at Step 2 (P7); single source of truth for class color tokens; `yoloId = classId + photoModeOffset` mapping is centralised | Physical file still lives at `src/features/annotations/classColors.ts` — the layout-doc-only mapping pending a Step 4 file move (`module-layout.md` Verification Needed #1, #8). `getPhotoModeSuffix` may duplicate the typed `DetectionClass.photoMode` field — likely redundant; possible deletion after typed propagation | None | None | Negligible | **Selected — current solution.** Cleanest L0 component; the file-move is purely cosmetic / layering hygiene. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
### Current state — verified at Step 4
|
||||||
|
|
||||||
|
- **Zero test coverage in `src/`** — `src/**/*.test.*` returns **zero matches** (Grep verified at Step 4, `04_verification_log.md` §1).
|
||||||
|
- **No test framework configured** in `package.json` (Vitest / Jest / Playwright are all absent).
|
||||||
|
- **No test step in CI** — `.woodpecker/build-arm.yml` runs `bun install` + `bun run build` only.
|
||||||
|
- The legacy WPF stack had an `Azaion.Test` xUnit project that tested utilities only; that test surface did NOT migrate.
|
||||||
|
|
||||||
|
### Target test pyramid (to be defined at autodev Step 5 — Decompose Tests)
|
||||||
|
|
||||||
|
The test-runner choice is **deferred to autodev Step 3 (Test Spec) → Step 5 (Decompose Tests)** per the Step 4.5 decision (`architecture.md` § Architecture Vision, item 7 of Open Questions). This document does not pre-empt that decision; it lists the **categories** the existing code already implies.
|
||||||
|
|
||||||
|
#### Unit / module-level
|
||||||
|
|
||||||
|
- **Foundation hooks**: `useDebounce`, `useResizablePanel` — pure timing + state behavior; testable with React Testing Library + fake timers.
|
||||||
|
- **Foundation utilities**: `flightPlanUtils.ts` (battery / distance / wind compute) — pure functions; testable in isolation once the OpenWeatherMap key is moved to `.env` (P10 / Step 4).
|
||||||
|
- **Class-colors module**: `getPhotoModeSuffix`, `yoloId` mapping — pure functions.
|
||||||
|
- **i18n bundles**: assert that every English key has a Ukrainian counterpart and vice versa (P6 mandatory check).
|
||||||
|
- **Numeric-enum coverage**: assert that `AnnotationStatus`, `MediaStatus`, `Affiliation`, `CombatReadiness` numeric values match the suite spec (`P9` enforcement — directly catches the enum drift that motivated Step 4 corrections).
|
||||||
|
|
||||||
|
#### Component / integration
|
||||||
|
|
||||||
|
- **`AuthContext`** — login, bootstrap-refresh (currently broken — fix first), 401-retry refresh, logout. Mock `01_api-transport` to assert request shape (`credentials:'include'` is the regression to lock down).
|
||||||
|
- **`FlightContext`** — list pagination ceiling, `selectFlight` round-trip, hydration of selected flight from `UserSettings`.
|
||||||
|
- **`CanvasEditor`** — bbox draw / 8-handle resize / Ctrl-multi-select; affiliation overlay; CombatReadiness indicator (once dead `AFFILIATION_COLORS` is wired).
|
||||||
|
- **`MediaList`** — pagination, filter, drag-drop upload, delete with `ConfirmDialog`.
|
||||||
|
- **`DatasetPage`** — bulk-validate flow (POST + UI state); class-distribution chart load; status filter (specifically that `None` and `All` are NOT conflated — finding fix).
|
||||||
|
- **`AdminPage`** — class add / delete (currently no edit; will gain edit per P12). User add / deactivate. **AI Settings / GPS Settings forms** (currently broken — the Save button must POST something — Step 6 product-correctness defect).
|
||||||
|
- **`SettingsPage`** — system / directory / camera saves; aircraft default toggle (cross-service `flights/` mutation).
|
||||||
|
|
||||||
|
#### End-to-end (browser)
|
||||||
|
|
||||||
|
- **Login → /flights default route → flight selection → MediaList → annotation save → bulk-validate** — the operator's primary loop. Already exercised manually at every release.
|
||||||
|
- **GPS-Denied Test Mode** (`F12`) — once implemented (Phase B target). Inputs: `.tlog` + video; assertion: SITL feed reaches the onboard service and produces a positioning trace.
|
||||||
|
- **Async video detect** (`F7`) — once implemented (Phase B target). Inputs: long video; assertion: SSE progress visible; final detections persisted.
|
||||||
|
|
||||||
|
#### Non-functional
|
||||||
|
|
||||||
|
- **Bundle size budget**: `vite build` artifact ≤ ~2 MB gzipped (currently no enforcement — CI gate to add).
|
||||||
|
- **Auth refresh transparency**: 401 → POST refresh → retry, **no UI re-render past `<ProtectedRoute>`** (regression test — locks the bootstrap-refresh fix).
|
||||||
|
- **Route guards**: unauth user → `/admin` → redirected to `/login` (locks the missing role-gate fix).
|
||||||
|
- **i18n coverage**: every visible string in both `en.json` and `ua.json` (P6 mandatory).
|
||||||
|
- **a11y smoke**: `ConfirmDialog` has `role=dialog` + `aria-modal`; Header dropdown has `role=combobox` + `aria-expanded` + Esc-to-close; spinner has `role=status`.
|
||||||
|
- **Endpoint contract**: every `api.*()` call URL matches the suite OpenAPI; specifically that the doubly-prefixed `/api/annotations/annotations` save path stays correct after any refactor.
|
||||||
|
- **Performance**: long video detect — UI stays responsive; progress visible (currently NOT met — `04_verification_log.md` F7 / finding #21).
|
||||||
|
|
||||||
|
### Test environment expectations
|
||||||
|
|
||||||
|
- **Test DB / services**: tests run against the suite docker-compose stack or a service-mock layer. The UI is HTTP-only — there is no UI-side database to bootstrap.
|
||||||
|
- **CI integration**: a test step must be added to `.woodpecker/build-arm.yml` (currently missing — `architecture.md` § Deployment Model "Missing from the pipeline today").
|
||||||
|
- **Coverage target**: TBD at Step 3 (Test Spec) — this document does not commit to a percentage.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
### Source docs (verified inputs)
|
||||||
|
|
||||||
|
- `_docs/02_document/00_discovery.md` — Step 0 codebase discovery, dep graph, topo order
|
||||||
|
- `_docs/02_document/architecture.md` — Step 3a system architecture (with Step 4.5 § Architecture Vision)
|
||||||
|
- `_docs/02_document/system-flows.md` — Step 3b 14 sequence flows F1–F14 (with Step 4 corrections)
|
||||||
|
- `_docs/02_document/data_model.md` — Step 3c entity-relationship + enum-drift map
|
||||||
|
- `_docs/02_document/deployment/containerization.md` — multi-stage Dockerfile, ARM64, nginx static serve
|
||||||
|
- `_docs/02_document/deployment/ci_cd_pipeline.md` — Woodpecker `.woodpecker/build-arm.yml`
|
||||||
|
- `_docs/02_document/deployment/environment_strategy.md` — dev / stage / production
|
||||||
|
- `_docs/02_document/deployment/observability.md` — current state (no centralized client telemetry — Step 6 surface)
|
||||||
|
- `_docs/02_document/04_verification_log.md` — Step 4 Verification Pass corrections
|
||||||
|
- `_docs/02_document/01_legacy_coverage_gaps.md` — WPF parity rollup
|
||||||
|
- `_docs/02_document/glossary.md` — Step 4.5 confirmed terminology
|
||||||
|
- `_docs/02_document/module-layout.md` — Step 2.5 file-ownership map
|
||||||
|
- `_docs/02_document/components/00_foundation/description.md` — through `11_class-colors/description.md`
|
||||||
|
- `_docs/02_document/modules/*.md` — 22 module docs covering all 77 modules
|
||||||
|
- `_docs/legacy/wpf-era.md` — legacy reference
|
||||||
|
|
||||||
|
### Configuration evidence
|
||||||
|
|
||||||
|
- `package.json` — React 19, Vite 6, TypeScript 5.7 strict, Bun 1.3.11
|
||||||
|
- `vite.config.ts` — dev `/api → http://localhost:8080` proxy
|
||||||
|
- `Dockerfile` — multi-stage `oven/bun:1.3.11-alpine` → `nginx:alpine`
|
||||||
|
- `nginx.conf` — 9 `/api/<service>/` reverse-proxy routes, `client_max_body_size 500M`
|
||||||
|
- `.woodpecker/build-arm.yml` — ARM64-only build pipeline, no test step today
|
||||||
|
- `src/index.css` — `az-*` Tailwind 4 design tokens
|
||||||
|
- `src/i18n/en.json`, `src/i18n/ua.json` — bilingual bundles
|
||||||
|
- `src/types/index.ts` — typed REST contract (with Step 4.5 inline numeric-enum comments per P9)
|
||||||
|
|
||||||
|
### External integrations
|
||||||
|
|
||||||
|
- OpenWeatherMap: `api.openweathermap.org/data/2.5/onecall` (hardcoded key in source — P10 violation, Step 4 fix)
|
||||||
|
- OpenStreetMap: tile servers via `react-leaflet` `TileLayer` defaults
|
||||||
|
- Suite services (via nginx): `admin/`, `flights/`, `annotations/`, `detect/`, `loader/`, `gps-denied-{desktop,onboard}/`, `autopilot/`, `resource/`
|
||||||
|
|
||||||
|
### Related artifacts (downstream)
|
||||||
|
|
||||||
|
- `_docs/00_problem/` — Step 6 retrospective problem extraction (problem.md, restrictions.md, acceptance_criteria.md, input_data/, security_approach.md) — to be produced next
|
||||||
|
- `_docs/02_document/FINAL_report.md` — Step 7 final integrated report — to be produced after Step 6
|
||||||
@@ -0,0 +1,463 @@
|
|||||||
|
# 00 — Codebase Discovery
|
||||||
|
|
||||||
|
> **Step 0 output for `/document`.** Read by Step 1 (per-module docs) to drive
|
||||||
|
> processing order, by Step 2 (component assembly) to seed groupings, and by
|
||||||
|
> Step 3 (system synthesis) for the tech-stack table.
|
||||||
|
>
|
||||||
|
> **Scope** (chosen at the autodev gate, 2026-05-10):
|
||||||
|
> - `src/` — Azaion UI (React 19 SPA, the live front-end of the suite).
|
||||||
|
> - `mission-planner/` — embedded React 18 + MUI sub-project (port-source for
|
||||||
|
> `src/features/flights/`). Documented as a separate component group.
|
||||||
|
> - `_docs/` already contained user-curated reference content
|
||||||
|
> (`legacy/wpf-era.md`, `ui_design/`, `_autodev_state.md`); the document
|
||||||
|
> skill writes alongside, not over.
|
||||||
|
>
|
||||||
|
> **Out of scope**: `node_modules/`, `dist/`, `bun.lock`, `package-lock.json`,
|
||||||
|
> `.git/`, `.cursor/`, `_docs/` (read-only inputs), `.idea/`, `.claude/`,
|
||||||
|
> `.superpowers/`, `mission-planner/public/` static assets.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Workspace at a glance
|
||||||
|
|
||||||
|
```
|
||||||
|
suite/ui/ ← Cursor workspace root
|
||||||
|
├── src/ ← Azaion UI (React 19, the live SPA)
|
||||||
|
├── mission-planner/ ← embedded port-source (React 18 + MUI)
|
||||||
|
├── _docs/ ← user-curated + autodev artifacts
|
||||||
|
│ ├── legacy/wpf-era.md read-only reference (WPF predecessor)
|
||||||
|
│ ├── ui_design/ read-only reference (HTML wireframes)
|
||||||
|
│ ├── _autodev_state.md autodev state pointer
|
||||||
|
│ └── 02_document/ ← this folder (autodev outputs)
|
||||||
|
├── .cursor/ skills/rules/agents
|
||||||
|
├── .woodpecker/build-arm.yml CI: arm64 Docker build → Harbor
|
||||||
|
├── Dockerfile multi-stage: bun build → nginx static
|
||||||
|
├── nginx.conf /api/* reverse proxy → suite services
|
||||||
|
├── index.html SPA shell (mounts /src/main.tsx)
|
||||||
|
├── package.json react 19, bun 1.3.11, vite 6, tw 4
|
||||||
|
├── tsconfig.json strict ESM, `@/*` → `src/*`
|
||||||
|
├── vite.config.ts react + tailwind plugin, /api proxy
|
||||||
|
├── README.md repo overview + maturation plan
|
||||||
|
└── .gitignore node_modules, .env.*, playwright-report/
|
||||||
|
```
|
||||||
|
|
||||||
|
`src/` and `mission-planner/` are **disjoint** — no file in one imports from
|
||||||
|
the other. The Vite alias `@ → src` is defined only in the workspace
|
||||||
|
`vite.config.ts`; `mission-planner/vite.config.ts` has no aliases. Each has
|
||||||
|
its own `package.json`, `tsconfig`, `index.html`, and entry point. The
|
||||||
|
production bundle (`Dockerfile`) builds **only the workspace**, not
|
||||||
|
`mission-planner/`.
|
||||||
|
|
||||||
|
`mission-planner/` ships a (mostly) functional flight-mission UI that the
|
||||||
|
`src/features/flights/*` files are mechanically translating into the new
|
||||||
|
SPA. Per the workspace `README.md`, `mission-planner/` is **not** part of
|
||||||
|
the deployed product.
|
||||||
|
|
||||||
|
## 2. Tech stack
|
||||||
|
|
||||||
|
### 2a. Workspace `src/` (Azaion UI)
|
||||||
|
|
||||||
|
| Concern | Choice | Source |
|
||||||
|
|----------------|-------------------------------------|--------------------------|
|
||||||
|
| Language | TypeScript 5.7 (`strict: true`) | `tsconfig.json` |
|
||||||
|
| UI framework | React 19 (`react-dom/client`) | `package.json` |
|
||||||
|
| Bundler | Vite 6 | `package.json`, `vite.config.ts` |
|
||||||
|
| Pkg manager | Bun 1.3.11 (declared via `packageManager`) | `package.json` |
|
||||||
|
| Styling | Tailwind CSS 4 (`@tailwindcss/vite`) + custom `az-*` tokens in `src/index.css` | `package.json`, `vite.config.ts` |
|
||||||
|
| Routing | `react-router-dom` 7 | `src/App.tsx` |
|
||||||
|
| i18n | `i18next` + `react-i18next` (UA / EN) | `src/i18n/i18n.ts` |
|
||||||
|
| Map | `leaflet` 1.9 + `react-leaflet` 5 + `leaflet-draw` + `leaflet-polylinedecorator` | `package.json`, `src/features/flights/*` |
|
||||||
|
| Charts | `chart.js` 4 + `react-chartjs-2` | `package.json` |
|
||||||
|
| DnD | `@hello-pangea/dnd` 18 | `package.json` |
|
||||||
|
| File upload | `react-dropzone` | `package.json` |
|
||||||
|
| Icon set | `react-icons` | `package.json` |
|
||||||
|
| HTTP transport | native `fetch` (custom thin wrapper) | `src/api/client.ts` |
|
||||||
|
| Realtime | native `EventSource` (SSE) | `src/api/sse.ts` |
|
||||||
|
| State mgmt | React Context only — `AuthContext`, `FlightContext`. **No** Redux / Zustand / TanStack Query. | `src/auth/`, `src/components/FlightContext.tsx` |
|
||||||
|
| Tests | **none** (no test framework configured) | (verified via Glob) |
|
||||||
|
| Build target | static bundle → nginx (multi-stage Dockerfile) | `Dockerfile`, `nginx.conf` |
|
||||||
|
| Runtime | nginx in container, ARM64 image | `.woodpecker/build-arm.yml` |
|
||||||
|
|
||||||
|
### 2b. `mission-planner/` (port-source)
|
||||||
|
|
||||||
|
| Concern | Choice | Source |
|
||||||
|
|----------------|------------------------------------------|------------------------------|
|
||||||
|
| Language | TypeScript 5.7 (`strict: true`) | `mission-planner/tsconfig.app.json` |
|
||||||
|
| UI framework | React 18 | `mission-planner/package.json` |
|
||||||
|
| Bundler | Vite 6 | `mission-planner/vite.config.ts` |
|
||||||
|
| Pkg manager | npm (lockfile not committed) | (no `bun.lock` in `mission-planner/`) |
|
||||||
|
| UI library | MUI 5 (`@mui/material` + `@mui/icons-material` + `@emotion/*`) | `mission-planner/package.json` |
|
||||||
|
| Map | `leaflet` 1.9 + `react-leaflet` 4.2 + `leaflet-draw` + `leaflet-polylinedecorator` | `mission-planner/package.json` |
|
||||||
|
| Charts | `chart.js` 4 + `react-chartjs-2` | `mission-planner/package.json` |
|
||||||
|
| DnD | `@hello-pangea/dnd` 16 | `mission-planner/package.json` |
|
||||||
|
| Flags | `react-world-flags` | `mission-planner/package.json` |
|
||||||
|
| Tests | Jest implied (`@testing-library/jest-dom` import in `setupTests.ts`, `describe/it/expect` in `src/test/jsonImport.test.ts`) but **no** Jest dep nor config in `package.json` — test currently cannot run as-is. | `mission-planner/package.json`, `src/setupTests.ts`, `src/test/jsonImport.test.ts` |
|
||||||
|
| Env config | `.env.example` declares `VITE_SATELLITE_TILE_URL` | `mission-planner/.env.example` |
|
||||||
|
| Build target | not built or shipped by the suite | (Dockerfile copies only workspace `src/`) |
|
||||||
|
|
||||||
|
## 3. Configuration & infrastructure files
|
||||||
|
|
||||||
|
| Path | Role |
|
||||||
|
|-------------------------------------|----------------------------------------------------------------------|
|
||||||
|
| `package.json` (workspace) | scripts: `dev`, `build` (`tsc -b && vite build`), `preview`. No `test`. |
|
||||||
|
| `tsconfig.json` | `strict: true`, `noUnusedLocals/Parameters: false` (lax), `paths: {"@/*": ["./src/*"]}`. |
|
||||||
|
| `vite.config.ts` | `@vitejs/plugin-react` + `@tailwindcss/vite`. Dev proxy `/api → http://localhost:8080`. |
|
||||||
|
| `index.html` | `<div id="root"></div>` + `<script type="module" src="/src/main.tsx">`. Body class hardcodes `bg-[#1e1e1e] text-[#adb5bd]`. |
|
||||||
|
| `Dockerfile` | Stage 1: `oven/bun:1.3.11-alpine`, `bun install --frozen-lockfile`, `bun run build`. Stage 2: `nginx:alpine`, copies `dist/` to `/usr/share/nginx/html`, exposes 80. `ENV AZAION_REVISION=$CI_COMMIT_SHA`. |
|
||||||
|
| `nginx.conf` | Reverse-proxies 9 `/api/<service>/` paths → `http://<service>:8080/`. Enumerates: annotations, flights, admin, resource, detect, loader, gps-denied-desktop, gps-denied-onboard, autopilot. SPA fallback `/index.html`. `client_max_body_size 500M`. |
|
||||||
|
| `.woodpecker/build-arm.yml` | Triggers on push to `dev`/`stage`/`main`. Builds + pushes `${REGISTRY_HOST}/azaion/ui:${branch}-arm` with OCI labels (revision, created, source). |
|
||||||
|
| `.gitignore` | `node_modules/`, `.env.local`, `.env.development.local`, `.env.test.local`, `.env.production.local`, `package-lock.json`, `yarn.lock`, `/test-results/`, `/playwright-report/`, `/blob-report/`, `/playwright/.cache/`, `/playwright/.auth/`. (Playwright entries are aspirational — no Playwright installed.) |
|
||||||
|
| `.env.example` (workspace) | **absent** — README §"Local development" notes this as a testability fix scheduled for Step 4. API base URL is currently hardcoded via the dev proxy + nginx routing. |
|
||||||
|
| `mission-planner/package.json` | scripts: `dev`, `build`, `preview`. **No** `test` script despite the test file. |
|
||||||
|
| `mission-planner/tsconfig.app.json` | `exclude: ["src/**/*.test.ts", "src/**/*.test.tsx", "src/setupTests.ts"]`. |
|
||||||
|
| `mission-planner/.env.example` | `VITE_SATELLITE_TILE_URL` only. |
|
||||||
|
| `mission-planner/public/manifest.json` | PWA manifest (vestigial CRA scaffolding). |
|
||||||
|
|
||||||
|
## 4. Entry points
|
||||||
|
|
||||||
|
| Project | File | Mounts |
|
||||||
|
|------------------|-------------------------------------------------|---------------------------------------------------------------------|
|
||||||
|
| `src/` | `src/main.tsx` | `<StrictMode><BrowserRouter><App /></BrowserRouter></StrictMode>` into `#root`. Imports `./i18n/i18n` for side effects (i18next init) and `./index.css`. |
|
||||||
|
| `src/` | `src/App.tsx` | Top-level `Routes`. `/login` is public; everything under `/*` is wrapped in `AuthProvider → ProtectedRoute → FlightProvider → Header + nested Routes` (`/flights`, `/annotations`, `/dataset`, `/admin`, `/settings`, `*` → `/flights`). |
|
||||||
|
| `mission-planner/` | `mission-planner/src/main.tsx` | `<StrictMode><LanguageProvider><FlightPlan /></LanguageProvider></StrictMode>` into `#root`. Imports `leaflet/dist/leaflet.css` and `leaflet-draw/dist/leaflet.draw.css`. |
|
||||||
|
| `mission-planner/` | `mission-planner/src/App.tsx` | Empty CRA stub — **not used** by `main.tsx`. (Vestigial.) |
|
||||||
|
|
||||||
|
## 5. Test structure
|
||||||
|
|
||||||
|
| Project | Test file(s) | Framework | Status |
|
||||||
|
|----------------|---------------------------------------------|--------------|-------------------------------------------------------------|
|
||||||
|
| `src/` | none | n/a | **Zero test coverage.** Confirmed via Glob over `src/**/*.{test,spec}.*`. |
|
||||||
|
| `mission-planner/` | `mission-planner/src/test/jsonImport.test.ts` | Jest (implied — uses `describe/it/expect`) | **Cannot run** — Jest is not in `package.json`; only `@testing-library/jest-dom` is imported in `setupTests.ts`. |
|
||||||
|
| `mission-planner/` | `mission-planner/src/setupTests.ts` | - | One-line `import '@testing-library/jest-dom'`. |
|
||||||
|
|
||||||
|
This vacancy is the explicit input for autodev Steps 3–7 (test-spec, testability revision, decompose tests, implement tests, run tests).
|
||||||
|
|
||||||
|
## 6. Existing documentation
|
||||||
|
|
||||||
|
| Path | Status | Owner / used by |
|
||||||
|
|---------------------------------------|----------------|----------------------------------------------------------------------------|
|
||||||
|
| `README.md` (workspace) | maintained | Single source of truth for repo intent + maturation plan. |
|
||||||
|
| `_docs/legacy/wpf-era.md` | reference | Captures WPF predecessor (`Azaion.Annotator`, `Azaion.Dataset`, Cython sidecars) at commit `22529c2`. Authoritative for §10 "What survived into the new world" and §11 "What is intentionally NOT being ported". |
|
||||||
|
| `_docs/ui_design/README.md` | reference | Authoritative UX spec: pages, breakpoints, panel layouts, keyboard shortcuts, color tokens, affiliation icons, combat readiness, annotation row gradient, video time-window display, confirmation dialogs. |
|
||||||
|
| `_docs/ui_design/{flights,annotations,dataset_explorer,admin,settings}.html` | reference | HTML wireframes for each page (inherited from WPF UI mockups). |
|
||||||
|
| `_docs/_autodev_state.md` | maintained | autodev state pointer (this document is being produced under it). |
|
||||||
|
| `mission-planner/README.md` | stale | CRA boilerplate; does not describe the actual app. |
|
||||||
|
| Suite-level `../_docs/` | external | Suite-wide architecture, schema, deployment topology. Not owned by this workspace; consulted as needed during Step 3. |
|
||||||
|
|
||||||
|
## 7. Dependency graph
|
||||||
|
|
||||||
|
`src/` and `mission-planner/` are independent dependency islands. Each is
|
||||||
|
acyclic by inspection (verified by following the import chains in §8 and §9
|
||||||
|
from leaves outward).
|
||||||
|
|
||||||
|
### 7a. Workspace `src/` (intra-repo edges only; React/leaflet/etc. omitted)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
main[main.tsx] --> App[App.tsx]
|
||||||
|
main --> i18n_init[i18n/i18n.ts]
|
||||||
|
i18n_init --> en[i18n/en.json]
|
||||||
|
i18n_init --> ua[i18n/ua.json]
|
||||||
|
|
||||||
|
App --> AuthProvider[auth/AuthContext.tsx]
|
||||||
|
App --> FlightProvider[components/FlightContext.tsx]
|
||||||
|
App --> ProtectedRoute[auth/ProtectedRoute.tsx]
|
||||||
|
App --> Header[components/Header.tsx]
|
||||||
|
App --> LoginPage[features/login/LoginPage.tsx]
|
||||||
|
App --> FlightsPage[features/flights/FlightsPage.tsx]
|
||||||
|
App --> AnnotationsPage[features/annotations/AnnotationsPage.tsx]
|
||||||
|
App --> DatasetPage[features/dataset/DatasetPage.tsx]
|
||||||
|
App --> AdminPage[features/admin/AdminPage.tsx]
|
||||||
|
App --> SettingsPage[features/settings/SettingsPage.tsx]
|
||||||
|
|
||||||
|
AuthProvider --> apiClient[api/client.ts]
|
||||||
|
AuthProvider --> typesIdx[types/index.ts]
|
||||||
|
ProtectedRoute --> AuthProvider
|
||||||
|
FlightProvider --> apiClient
|
||||||
|
FlightProvider --> typesIdx
|
||||||
|
|
||||||
|
Header --> AuthProvider
|
||||||
|
Header --> FlightProvider
|
||||||
|
Header --> HelpModal[components/HelpModal.tsx]
|
||||||
|
Header --> typesIdx
|
||||||
|
|
||||||
|
sse[api/sse.ts] --> apiClient
|
||||||
|
|
||||||
|
ConfirmDialog[components/ConfirmDialog.tsx]
|
||||||
|
DetectionClasses[components/DetectionClasses.tsx] --> apiClient
|
||||||
|
DetectionClasses --> classColors[features/annotations/classColors.ts]
|
||||||
|
DetectionClasses --> typesIdx
|
||||||
|
|
||||||
|
LoginPage --> AuthProvider
|
||||||
|
|
||||||
|
AdminPage --> apiClient
|
||||||
|
AdminPage --> ConfirmDialog
|
||||||
|
AdminPage --> typesIdx
|
||||||
|
|
||||||
|
SettingsPage --> apiClient
|
||||||
|
SettingsPage --> typesIdx
|
||||||
|
|
||||||
|
classColors
|
||||||
|
flightsTypes[features/flights/types.ts]
|
||||||
|
flightPlanUtils[features/flights/flightPlanUtils.ts] --> flightsTypes
|
||||||
|
mapIcons[features/flights/mapIcons.ts]
|
||||||
|
|
||||||
|
WaypointList[features/flights/WaypointList.tsx] --> flightsTypes
|
||||||
|
AltitudeChart[features/flights/AltitudeChart.tsx] --> flightsTypes
|
||||||
|
WindEffect[features/flights/WindEffect.tsx] --> flightsTypes
|
||||||
|
MiniMap[features/flights/MiniMap.tsx] --> flightsTypes
|
||||||
|
MapPoint[features/flights/MapPoint.tsx] --> flightsTypes
|
||||||
|
MapPoint --> mapIcons
|
||||||
|
DrawControl[features/flights/DrawControl.tsx] --> flightsTypes
|
||||||
|
DrawControl --> flightPlanUtils
|
||||||
|
AltitudeDialog[features/flights/AltitudeDialog.tsx] --> flightsTypes
|
||||||
|
FlightListSidebar[features/flights/FlightListSidebar.tsx] --> typesIdx
|
||||||
|
JsonEditorDialog[features/flights/JsonEditorDialog.tsx]
|
||||||
|
|
||||||
|
FlightParamsPanel[features/flights/FlightParamsPanel.tsx] --> WaypointList
|
||||||
|
FlightParamsPanel --> AltitudeChart
|
||||||
|
FlightParamsPanel --> WindEffect
|
||||||
|
FlightParamsPanel --> flightsTypes
|
||||||
|
FlightParamsPanel --> typesIdx
|
||||||
|
FlightMap[features/flights/FlightMap.tsx] --> DrawControl
|
||||||
|
FlightMap --> MapPoint
|
||||||
|
FlightMap --> MiniMap
|
||||||
|
FlightMap --> mapIcons
|
||||||
|
FlightMap --> flightsTypes
|
||||||
|
FlightsPage --> FlightProvider
|
||||||
|
FlightsPage --> apiClient
|
||||||
|
FlightsPage --> sse
|
||||||
|
FlightsPage --> ConfirmDialog
|
||||||
|
FlightsPage --> FlightListSidebar
|
||||||
|
FlightsPage --> FlightParamsPanel
|
||||||
|
FlightsPage --> FlightMap
|
||||||
|
FlightsPage --> AltitudeDialog
|
||||||
|
FlightsPage --> JsonEditorDialog
|
||||||
|
FlightsPage --> flightPlanUtils
|
||||||
|
FlightsPage --> flightsTypes
|
||||||
|
FlightsPage --> typesIdx
|
||||||
|
|
||||||
|
CanvasEditor[features/annotations/CanvasEditor.tsx] --> typesIdx
|
||||||
|
CanvasEditor --> classColors
|
||||||
|
VideoPlayer[features/annotations/VideoPlayer.tsx] --> typesIdx
|
||||||
|
AnnotationsSidebar[features/annotations/AnnotationsSidebar.tsx] --> apiClient
|
||||||
|
AnnotationsSidebar --> sse
|
||||||
|
AnnotationsSidebar --> classColors
|
||||||
|
AnnotationsSidebar --> typesIdx
|
||||||
|
MediaList[features/annotations/MediaList.tsx] --> FlightProvider
|
||||||
|
MediaList --> apiClient
|
||||||
|
MediaList --> useDebounce[hooks/useDebounce.ts]
|
||||||
|
MediaList --> ConfirmDialog
|
||||||
|
MediaList --> typesIdx
|
||||||
|
AnnotationsPage --> useResizablePanel[hooks/useResizablePanel.ts]
|
||||||
|
AnnotationsPage --> apiClient
|
||||||
|
AnnotationsPage --> MediaList
|
||||||
|
AnnotationsPage --> VideoPlayer
|
||||||
|
AnnotationsPage --> CanvasEditor
|
||||||
|
AnnotationsPage --> AnnotationsSidebar
|
||||||
|
AnnotationsPage --> DetectionClasses
|
||||||
|
AnnotationsPage --> classColors
|
||||||
|
AnnotationsPage --> typesIdx
|
||||||
|
|
||||||
|
DatasetPage --> apiClient
|
||||||
|
DatasetPage --> useDebounce
|
||||||
|
DatasetPage --> useResizablePanel
|
||||||
|
DatasetPage --> FlightProvider
|
||||||
|
DatasetPage --> DetectionClasses
|
||||||
|
DatasetPage --> ConfirmDialog
|
||||||
|
DatasetPage --> CanvasEditor
|
||||||
|
DatasetPage --> typesIdx
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7b. `mission-planner/src/`
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
mp_main[main.tsx] --> flightPlan[flightPlanning/flightPlan.tsx]
|
||||||
|
mp_main --> LanguageProvider[flightPlanning/LanguageContext.tsx]
|
||||||
|
|
||||||
|
mp_types[types/index.ts]
|
||||||
|
mp_config[config.ts]
|
||||||
|
mp_utils[utils.ts]
|
||||||
|
|
||||||
|
translations[constants/translations.ts] --> mp_types
|
||||||
|
languages[constants/languages.ts] --> mp_types
|
||||||
|
purposes[constants/purposes.ts] --> mp_types
|
||||||
|
actionModes[constants/actionModes.ts]
|
||||||
|
maptypes[constants/maptypes.ts]
|
||||||
|
tileUrls[constants/tileUrls.ts]
|
||||||
|
|
||||||
|
mapIcons[icons/MapIcons.tsx]
|
||||||
|
pointIcons[icons/PointIcons.tsx]
|
||||||
|
sidebarIcons[icons/SidebarIcons.tsx]
|
||||||
|
phoneIcon[icons/PhoneIcon.tsx]
|
||||||
|
|
||||||
|
calcDistance[services/calculateDistance.ts] --> mp_types
|
||||||
|
AircraftService[services/AircraftService.ts] --> mp_types
|
||||||
|
WeatherService[services/WeatherService.ts] --> mp_types
|
||||||
|
calcBattery[services/calculateBatteryUsage.ts] --> AircraftService
|
||||||
|
calcBattery --> WeatherService
|
||||||
|
calcBattery --> mp_types
|
||||||
|
|
||||||
|
Aircraft[flightPlanning/Aircraft.ts] --> mp_utils
|
||||||
|
WindEffect2[flightPlanning/WindEffect.tsx] --> LanguageProvider
|
||||||
|
WindEffect2 --> translations
|
||||||
|
AltitudeChart2[flightPlanning/AltitudeChart.tsx] --> LanguageProvider
|
||||||
|
AltitudeChart2 --> translations
|
||||||
|
AltitudeChart2 --> mp_types
|
||||||
|
AltitudeDialog2[flightPlanning/AltitudeDialog.tsx] --> LanguageProvider
|
||||||
|
AltitudeDialog2 --> mp_config
|
||||||
|
AltitudeDialog2 --> translations
|
||||||
|
AltitudeDialog2 --> purposes
|
||||||
|
DrawControl2[flightPlanning/DrawControl.tsx] --> mp_types
|
||||||
|
JsonEditorDialog2[flightPlanning/JsonEditorDialog.tsx] --> LanguageProvider
|
||||||
|
JsonEditorDialog2 --> translations
|
||||||
|
TotalDistance[flightPlanning/TotalDistance.tsx] --> LanguageProvider
|
||||||
|
TotalDistance --> calcDistance
|
||||||
|
TotalDistance --> translations
|
||||||
|
TotalDistance --> mp_types
|
||||||
|
LanguageSwitcher[flightPlanning/LanguageSwitcher.tsx] --> LanguageProvider
|
||||||
|
LanguageSwitcher --> languages
|
||||||
|
LanguageSwitcher --> translations
|
||||||
|
MapPoint2[flightPlanning/MapPoint.tsx] --> LanguageProvider
|
||||||
|
MapPoint2 --> purposes
|
||||||
|
MapPoint2 --> translations
|
||||||
|
MapPoint2 --> pointIcons
|
||||||
|
MapPoint2 --> mp_types
|
||||||
|
MiniMap2[flightPlanning/MiniMap.tsx] --> MapView2
|
||||||
|
MiniMap2 --> maptypes
|
||||||
|
MiniMap2 --> tileUrls
|
||||||
|
MiniMap2 --> mp_types
|
||||||
|
PointsList[flightPlanning/PointsList.tsx] --> AltitudeDialog2
|
||||||
|
PointsList --> mp_utils
|
||||||
|
PointsList --> LanguageProvider
|
||||||
|
PointsList --> translations
|
||||||
|
PointsList --> calcBattery
|
||||||
|
PointsList --> calcDistance
|
||||||
|
PointsList --> mp_types
|
||||||
|
MapView2[flightPlanning/MapView.tsx] --> DrawControl2
|
||||||
|
MapView2 --> mp_utils
|
||||||
|
MapView2 --> AltitudeDialog2
|
||||||
|
MapView2 --> LanguageProvider
|
||||||
|
MapView2 --> pointIcons
|
||||||
|
MapView2 --> translations
|
||||||
|
MapView2 --> actionModes
|
||||||
|
MapView2 --> MiniMap2
|
||||||
|
MapView2 --> MapPoint2
|
||||||
|
MapView2 --> mapIcons
|
||||||
|
MapView2 --> maptypes
|
||||||
|
MapView2 --> purposes
|
||||||
|
MapView2 --> tileUrls
|
||||||
|
MapView2 --> mp_types
|
||||||
|
LeftBoard[flightPlanning/LeftBoard.tsx] --> LanguageProvider
|
||||||
|
LeftBoard --> PointsList
|
||||||
|
LeftBoard --> AltitudeChart2
|
||||||
|
LeftBoard --> TotalDistance
|
||||||
|
LeftBoard --> LanguageSwitcher
|
||||||
|
LeftBoard --> translations
|
||||||
|
LeftBoard --> actionModes
|
||||||
|
LeftBoard --> sidebarIcons
|
||||||
|
LeftBoard --> mp_config
|
||||||
|
LeftBoard --> mp_types
|
||||||
|
flightPlan --> mp_utils
|
||||||
|
flightPlan --> MapView2
|
||||||
|
flightPlan --> AltitudeDialog2
|
||||||
|
flightPlan --> JsonEditorDialog2
|
||||||
|
flightPlan --> LeftBoard
|
||||||
|
flightPlan --> mp_config
|
||||||
|
flightPlan --> actionModes
|
||||||
|
flightPlan --> AircraftService
|
||||||
|
flightPlan --> phoneIcon
|
||||||
|
flightPlan --> purposes
|
||||||
|
flightPlan --> mp_types
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note** — `MiniMap2` imports `UpdateMapCenter` (a *named* helper) **from**
|
||||||
|
> `MapView2`, while `MapView2` imports `MiniMap2` as a child component. They
|
||||||
|
> import in opposite directions, which would normally form a dependency
|
||||||
|
> cycle, but module-level execution is non-circular because each side only
|
||||||
|
> uses the *type/handle* exposed by the other at call time. **Flagged for
|
||||||
|
> Step 1** (will document the named export contract precisely) and surfaced
|
||||||
|
> in §11 below as a structural caveat.
|
||||||
|
|
||||||
|
## 8. Cross-feature edges in `src/` (architectural caveats)
|
||||||
|
|
||||||
|
These are edges where a "lower-layer" module imports from a "higher-layer"
|
||||||
|
sibling. Surfaced now so Step 2 (Component Assembly) and Step 2.5
|
||||||
|
(module-layout.md) can decide whether to formalise them in the layering
|
||||||
|
table or flag them for the architecture baseline scan (Step 2 of autodev).
|
||||||
|
|
||||||
|
| Edge | Direction | Comment |
|
||||||
|
|-------------------------------------------------------------------------------------|---------------------------------|---------|
|
||||||
|
| `components/DetectionClasses.tsx` → `features/annotations/classColors.ts` | shared ← feature-specific | A `shared/` component depends on `features/annotations/`. The shared layer should not know about a specific feature. **Likely candidate for refactor**: extract `classColors.ts` into a feature-neutral location (e.g. `src/components/detection/classColors.ts`) or into a `shared/` module. |
|
||||||
|
| `features/dataset/DatasetPage.tsx` → `features/annotations/CanvasEditor.tsx` | feature ← sibling feature | Cross-feature import, but consistent with the legacy WPF design where `Azaion.Dataset` reused `CanvasEditor` from `Azaion.Common.Controls` (see `_docs/legacy/wpf-era.md` §5). The proper fix is to lift `CanvasEditor` out of `features/annotations/` into a shared location. |
|
||||||
|
| (none observed) | back-edge from `App` to `main` | - |
|
||||||
|
|
||||||
|
Also: every page calls `api/client.ts` directly with **string-literal URLs**
|
||||||
|
(`/api/admin/auth/login`, `/api/flights?...`, `/api/annotations/settings/user`,
|
||||||
|
etc.). There is no per-service API client module. This is the testability
|
||||||
|
issue the workspace `README.md` calls out for Step 4 — but since it does
|
||||||
|
not yet break compilation or layering, it is recorded here, not blocked.
|
||||||
|
|
||||||
|
## 9. Topological processing order — `src/` (40 modules, 8 batches)
|
||||||
|
|
||||||
|
Layer = max distance from leaves. Step 1 of `/document` MUST process modules
|
||||||
|
in this order (leaves first), batched by ~5 with a session-break heuristic
|
||||||
|
between batches.
|
||||||
|
|
||||||
|
> JSON files (`i18n/en.json`, `i18n/ua.json`) and `vite-env.d.ts` are
|
||||||
|
> **inputs**, not modules — they are not separately documented; their content
|
||||||
|
> is summarised inside the consumers (`i18n/i18n.ts`, the global TS env).
|
||||||
|
> Counted modules: 40.
|
||||||
|
|
||||||
|
| Batch | Modules | Layer |
|
||||||
|
|-------|--------------------------------------------------------------------------------------|-------|
|
||||||
|
| **B1** | `types/index.ts`, `hooks/useDebounce.ts`, `hooks/useResizablePanel.ts`, `features/flights/types.ts`, `features/annotations/classColors.ts` | 0 |
|
||||||
|
| **B2** | `features/flights/mapIcons.ts`, `features/flights/flightPlanUtils.ts`, `api/client.ts`, `i18n/i18n.ts`, `components/HelpModal.tsx` | 0–1 |
|
||||||
|
| **B3** | `components/ConfirmDialog.tsx`, `components/DetectionClasses.tsx`, `auth/AuthContext.tsx`, `components/FlightContext.tsx`, `api/sse.ts` | 1–2 |
|
||||||
|
| **B4** | `auth/ProtectedRoute.tsx`, `components/Header.tsx`, `features/login/LoginPage.tsx`, `features/admin/AdminPage.tsx`, `features/settings/SettingsPage.tsx` | 2–3 |
|
||||||
|
| **B5** | `features/flights/{WaypointList,AltitudeChart,WindEffect,MiniMap,AltitudeDialog}.tsx` | 1–2 |
|
||||||
|
| **B6** | `features/flights/{MapPoint,DrawControl,FlightListSidebar,JsonEditorDialog,FlightParamsPanel}.tsx` | 2 |
|
||||||
|
| **B7** | `features/flights/FlightMap.tsx`, `features/annotations/{CanvasEditor,VideoPlayer,AnnotationsSidebar,MediaList}.tsx` | 2–3 |
|
||||||
|
| **B8** | `features/flights/FlightsPage.tsx`, `features/annotations/AnnotationsPage.tsx`, `features/dataset/DatasetPage.tsx`, `App.tsx`, `main.tsx` | 3–5 |
|
||||||
|
|
||||||
|
## 10. Topological processing order — `mission-planner/` (37 modules, 8 batches)
|
||||||
|
|
||||||
|
Excluded from analysis: `vite-env.d.ts`, `types/leaflet-polylinedecorator.d.ts`,
|
||||||
|
`types/react-world-flags.d.ts` (type shims for external libs), `setupTests.ts`,
|
||||||
|
`App.tsx` (vestigial CRA stub — flagged for delete in §11), and `index.css`,
|
||||||
|
`*.css` files. The Jest test (`src/test/jsonImport.test.ts`) is documented
|
||||||
|
inline with `flightPlanning/flightPlan.tsx` (its target), not as a standalone
|
||||||
|
module. Counted modules: 37.
|
||||||
|
|
||||||
|
| Batch | Modules | Layer |
|
||||||
|
|-------|-----------------------------------------------------------------------------------------------------------------------------------------------|-------|
|
||||||
|
| **MP-B1** | `types/index.ts`, `utils.ts`, `config.ts`, `constants/{actionModes,maptypes,tileUrls}.ts` | 0 |
|
||||||
|
| **MP-B2** | `constants/{translations,languages,purposes}.ts`, `services/{calculateDistance,AircraftService,WeatherService}.ts` | 1 |
|
||||||
|
| **MP-B3** | `services/calculateBatteryUsage.ts`, `flightPlanning/Aircraft.ts`, `flightPlanning/LanguageContext.tsx`, `icons/{MapIcons,PointIcons}.tsx` | 1–2 |
|
||||||
|
| **MP-B4** | `icons/{SidebarIcons,PhoneIcon}.tsx`, `flightPlanning/{WindEffect,DrawControl,LanguageSwitcher}.tsx` | 2 |
|
||||||
|
| **MP-B5** | `flightPlanning/{AltitudeChart,AltitudeDialog,JsonEditorDialog,TotalDistance,MapPoint}.tsx` | 2–3 |
|
||||||
|
| **MP-B6** | `flightPlanning/MapView.tsx` (cycle group with `MiniMap.tsx`), `flightPlanning/MiniMap.tsx`, `flightPlanning/PointsList.tsx` | 3–4 |
|
||||||
|
| **MP-B7** | `flightPlanning/LeftBoard.tsx`, `flightPlanning/flightPlan.tsx` | 5–6 |
|
||||||
|
| **MP-B8** | `main.tsx`, `test/jsonImport.test.ts` (analysis only — covered by `flightPlan.tsx` doc) | 7 |
|
||||||
|
|
||||||
|
## 11. Discovery findings to carry forward
|
||||||
|
|
||||||
|
The following observations are not documentation gaps; they are **inputs**
|
||||||
|
for downstream steps. Each lists the step that owns the follow-up.
|
||||||
|
|
||||||
|
1. **Workspace `src/` has zero tests.** → Owned by `/test-spec` (Step 3 of autodev) and `/decompose-tests` + `/implement` (Steps 5–6).
|
||||||
|
2. **Hardcoded API URL paths** (`/api/admin/...`, `/api/flights/...`, `/api/annotations/...`) inlined throughout features. → Testability fix scheduled by autodev Step 4 (workspace `README.md` §"Local development" already calls this out).
|
||||||
|
3. **No `.env.example` in workspace.** → Same — Step 4.
|
||||||
|
4. **Cross-layer imports** (§8): `components/DetectionClasses.tsx` → `features/annotations/classColors.ts`, `features/dataset/DatasetPage.tsx` → `features/annotations/CanvasEditor.tsx`. → Surface to autodev Step 2 (Architecture Baseline Scan); the testability refactor (Step 4) may also lift these.
|
||||||
|
5. **`mission-planner/src/test/jsonImport.test.ts` cannot run** — Jest is not installed and there is no test script. → Out of scope for the live SPA test plan; document in `mission-planner/` component spec but do **not** add Jest just to run this one legacy test.
|
||||||
|
6. **`mission-planner/src/App.tsx`** is an unused CRA stub; `main.tsx` mounts `FlightPlan` directly. → Note in `mission-planner/` component spec; deletion candidate but only after the `mission-planner` → `src/features/flights/` port is complete (out of `/document` scope).
|
||||||
|
7. **`mission-planner/src/flightPlanning/MapView.tsx ↔ MiniMap.tsx`** import each other (`MiniMap` imports the *named* `UpdateMapCenter` helper from `MapView`; `MapView` imports `MiniMap` as a JSX child). → Document the contract precisely in Step 1; analyse together as a 2-module group in MP-B6 per the Step 1 cycle-handling rule.
|
||||||
|
8. **`react-i18next` is used only in workspace `src/`**; `mission-planner/` uses its own `LanguageContext` + raw translation tables. → Capture the divergence in Step 5 (Solution Extraction) — the port to `src/features/flights/` should consume `react-i18next` instead.
|
||||||
|
9. **No CI test step** in `.woodpecker/build-arm.yml` — only build + push. → To be added by autodev Step 7 (Run Tests) once the test suite exists.
|
||||||
|
10. **The body of `index.html`** hardcodes Tailwind arbitrary-value classes for the global background and text color rather than using the `az-bg` / `az-text` tokens defined in `src/index.css`. → Cosmetic; record in the workspace component spec but no action required.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Step 0 status**: complete. Proceeding to Step 1 (per-module documentation,
|
||||||
|
batch B1).
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# Legacy Coverage Gaps — React UI vs WPF Era
|
||||||
|
|
||||||
|
> Output of the autodev Step 2 BLOCKING-gate cross-check (2026-05-10). Compares
|
||||||
|
> the current React port (this repo) against the legacy WPF source preserved at
|
||||||
|
> `/Users/obezdienie001/dev/azaion/suite/annotations-research/` (commit
|
||||||
|
> `22529c2`, the last commit before the WPF→.NET-API "big refactoring"). The
|
||||||
|
> source-of-truth narrative is `_docs/legacy/wpf-era.md`; this document lists
|
||||||
|
> the **delta** — features that exist in the WPF source but are not yet present
|
||||||
|
> (or are broken) in the React port.
|
||||||
|
>
|
||||||
|
> Each gap is owned by one component. Per-component gap tables live in the
|
||||||
|
> respective `_docs/02_document/components/<NN>_<name>/description.md` §6b
|
||||||
|
> sections. This document is the *single-page rollup* for review.
|
||||||
|
>
|
||||||
|
> **What this document is NOT**: it does not re-litigate features that
|
||||||
|
> `_docs/legacy/wpf-era.md §11` declares "intentionally NOT being ported"
|
||||||
|
> (DI host, LibVLCSharp, ZeroMQ, the Azaion.LoaderUI handoff, the binary-split
|
||||||
|
> key-fragment dance, the Cython sidecars). Those gaps are by design.
|
||||||
|
|
||||||
|
## Coverage matrix
|
||||||
|
|
||||||
|
| WPF concept (where it lived) | Status in React port | Owner component |
|
||||||
|
|------------------------------|----------------------|-----------------|
|
||||||
|
| Module switcher (Suite + IAzaionModule + SVG icon) | **Covered** by `Header.tsx` top nav | `03_shared-ui` |
|
||||||
|
| Dark navy/blue + orange-accent color scheme | **Covered** by `index.css` `az-*` tokens | `03_shared-ui` (theme) |
|
||||||
|
| Login → encrypted-creds handoff | **Intentionally not ported** (browser uses JWT) | `04_login` |
|
||||||
|
| Detection-class strip + PhotoMode + 1–9 shortcut | **Covered** | `03_shared-ui/DetectionClasses` + `11_class-colors` |
|
||||||
|
| `yoloId = classId + photoModeOffset` | **Covered** in `11_class-colors` | `11_class-colors` |
|
||||||
|
| Annotator: bbox draw / 8-handle resize | **Covered** | `06_annotations/CanvasEditor` |
|
||||||
|
| Annotator: Ctrl-multi-select / Ctrl-wheel zoom / Ctrl-drag pan | **Partially missing** (multi-select unverified, pan/zoom flagged) | `06_annotations/CanvasEditor` |
|
||||||
|
| Annotator: time-windowed overlay (50 ms before / 150 ms after) | **Wrong** (symmetric ±200 ms, finding #6) | `06_annotations/CanvasEditor` |
|
||||||
|
| Annotator: video play/pause + frame-by-frame stepping | **Partially covered** (controls exist; per-frame counts unverified vs FPS) | `06_annotations/VideoPlayer` |
|
||||||
|
| Annotator: keyboard shortcuts `[Space]` / `[Left]` / `[Right]` / `[Enter]` / `[Del]` / `[X]` / `[M]` / `[R]` / `[K]` | **Missing** | `06_annotations` (multiple modules) |
|
||||||
|
| Annotator: volume slider | **Missing** | `06_annotations/VideoPlayer` |
|
||||||
|
| Annotator: status bar — clock + help text + status text | **Missing** | `06_annotations` (or shell-level toast) |
|
||||||
|
| Annotator: AI-Detect button + modal progress | **Partial** — sync-image works; video AI-Detect is fire-and-forget, no SSE subscription, no progress UI (findings #21–23, #30) | `06_annotations/AnnotationsSidebar` |
|
||||||
|
| Annotator: **Sound Detections** feature ("show objects from audio analysis") | **Intentionally not ported** (Step 4.5 decision 2026-05-10) | — |
|
||||||
|
| Annotator: **Drone Maintenance** feature ("Аналіз стану БПЛА") | **Intentionally not ported** (Step 4.5 decision 2026-05-10) | — |
|
||||||
|
| Annotator: camera-config side panel (altitude / focal / sensor → GSD) | **Missing** (finding #17) | `06_annotations/AnnotationsPage` |
|
||||||
|
| Annotator: GPS panel toggle below canvas | **Moved** to `05_flights` GPS-Denied sub-page (per user direction) | `05_flights` |
|
||||||
|
| Annotator: affiliation icons + combat-readiness indicator on bbox label | **Missing** (findings #14–15; `AFFILIATION_COLORS` exists but is dead code) | `06_annotations/CanvasEditor` |
|
||||||
|
| Annotator: annotation-row gradient (alpha ∝ confidence; empty bg `#40DDDDDD`) | **Wrong** — alpha caps at 16 % (finding #9, hex/decimal mistake) | `06_annotations/AnnotationsSidebar` |
|
||||||
|
| Annotator: media-list filter / search | **Covered** (debounced) | `06_annotations/MediaList` |
|
||||||
|
| Annotator: media-list `blob:` previews | **Bug** (#25 — local previews ignore filter) | `06_annotations/MediaList` |
|
||||||
|
| Annotator: virtualised media list | **Missing** (finding #26) | `06_annotations/MediaList` |
|
||||||
|
| Annotator: "open folder" file menu | **Intentionally not ported** (web upload via dropzone) | `06_annotations` |
|
||||||
|
| Annotator: help window with 6 quality rules | **Covered** (`HelpModal`) but rules are hardcoded in source — Step 4 i18n | `03_shared-ui/HelpModal` |
|
||||||
|
| Dataset: thumbnail grid + filter | **Covered** but **not virtualised** (finding #3) | `07_dataset` |
|
||||||
|
| Dataset: 3-tab layout (Annotations / Editor / **Class Distribution**) | **Implemented** (Step 4 correction — `DatasetPage.tsx:151` has all three tabs; `loadDistribution()` calls `/api/annotations/dataset/class-distribution`. Verify bar tint matches `classColors`.) | `07_dataset` |
|
||||||
|
| Dataset: "Show only annotations with objects" checkbox | **Implemented** (Step 4 correction — `DatasetPage.tsx:110-114`, state `objectsOnly`) | `07_dataset` |
|
||||||
|
| Dataset: Validate button (bulk validate to `Validated` status) | **Implemented** (Step 4 correction — `DatasetPage.tsx:142-146` button when `selectedIds.size > 0`); `[V]` keyboard shortcut still missing. | `07_dataset` |
|
||||||
|
| Dataset: Refresh thumbnails button + progress | **Missing** (finding #2) | `07_dataset` |
|
||||||
|
| Dataset: `SelectedAnnotationName` + `StatusText` status-bar slots | **Missing** | `07_dataset` |
|
||||||
|
| Dataset: seed annotation 8 px border highlight (`IsSeed=true`) | **Missing** | `07_dataset` |
|
||||||
|
| Dataset: keyboard shortcuts (1–9, Enter, Del, X, V, arrows, PgUp/PgDn, Esc) | **Missing** (finding #1) | `07_dataset` |
|
||||||
|
| Dataset: inline editor saves | **Broken** (finding #4) | `07_dataset` + `06_annotations/CanvasEditor` |
|
||||||
|
| Cross-cutting: resizable panel widths persisted per user | **Not persisted** (finding #11) | `00_foundation/useResizablePanel` + Settings backend |
|
||||||
|
| Cross-cutting: Ukrainian + English localisation | **Covered** (`react-i18next` w/ `en.json` + `ua.json`) — surface area to verify against legacy `translations.json` (only 6 keys, but XAML hardcoded UA strings everywhere). | `00_foundation/i18n` |
|
||||||
|
| Cross-cutting: confirmation dialogs (delete-media / delete-selected / delete-all / deactivate-user) | **Component covered** (`ConfirmDialog`); some destructive actions still bypass it (`08_admin` `handleDeleteClass` finding) | `03_shared-ui/ConfirmDialog` + `08_admin` |
|
||||||
|
|
||||||
|
## Decisions taken at Step 4.5 (Architecture Vision, 2026-05-10)
|
||||||
|
|
||||||
|
The Step 4.5 user review resolved the product-level decisions that were pending here. Summary:
|
||||||
|
|
||||||
|
1. ~~Sound Detections feature~~ — **Dropped** (intentionally not ported).
|
||||||
|
2. ~~Drone Maintenance feature~~ — **Dropped** (intentionally not ported).
|
||||||
|
3. ~~Class Distribution chart~~ — already ported (Step 4 correction).
|
||||||
|
4. **Status bar with clock + help-text-blink pattern** — still open, deferred to a Phase B cycle (low priority — replace with toast unless a downstream cycle picks it up explicitly).
|
||||||
|
5. **Seed annotation concept** (`IsSeed=true` highlight) — still open, deferred to a Phase B cycle (need to verify whether the modern API still exposes `isSeed`).
|
||||||
|
6. **Camera config persistence** — still open, deferred to the Phase B cycle that ports the camera-config side panel from `mission-planner/`.
|
||||||
|
7. **Resizable panel width persistence** — **Persist** as part of `UserSettings` (Step 4 fix; principle P11 in `architecture.md` Architecture Vision).
|
||||||
|
|
||||||
|
Additional Step 4.5 resolutions not from this rollup but recorded for traceability:
|
||||||
|
|
||||||
|
- **Spec is source of truth for numeric enums** with inline comments per value (principle P9).
|
||||||
|
- **OpenWeatherMap API key** moves to `.env` (principle P10; Step 4 fix candidate).
|
||||||
|
- **Admin can edit detection classes** — re-introduce `PATCH /api/admin/classes/{id}` and the in-place edit form (principle P12).
|
||||||
|
- **Mission-planner convergence** — flag at Step 2 (Architecture Baseline), spec at Step 3 (Test Spec), port across Phase B cycles, delete tree in final cycle (recorded in `architecture.md` Architecture Vision).
|
||||||
|
|
||||||
|
## Where to find the per-component detail
|
||||||
|
|
||||||
|
- `_docs/02_document/components/06_annotations/description.md` §6b — Annotations gap table (~17 entries)
|
||||||
|
- `_docs/02_document/components/07_dataset/description.md` §6b — Dataset gap table (~12 entries)
|
||||||
|
- Other components (`00_foundation`, `01_api-transport`, `02_auth`, `03_shared-ui`, `04_login`, `05_flights`, `08_admin`, `09_settings`, `10_app-shell`, `11_class-colors`) have no WPF-source delta — either no WPF analog (most), or already covered (Header / module switcher / DetectionClasses / 11_class-colors) per `_docs/legacy/wpf-era.md §10`.
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
# Verification Log — Step 4 (autodev `/document` workflow)
|
||||||
|
|
||||||
|
> Output of Step 4: every entity, endpoint, and flow drafted in Steps 1–3 was
|
||||||
|
> cross-checked against the actual code under `src/`. Significant drift was
|
||||||
|
> found and corrected in place; this log records what was checked, what was
|
||||||
|
> changed, and what remains uncertain. Pre-existing module docs (Step 1) were
|
||||||
|
> spot-checked but largely trusted because each was written from a focused
|
||||||
|
> read of its own file.
|
||||||
|
|
||||||
|
**Status**: corrections applied, presented for user review (BLOCKING per `full.md` Step 4).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Coverage summary
|
||||||
|
|
||||||
|
| Coverage axis | Total | Verified | Corrected | Remaining gap |
|
||||||
|
|---------------|-------|----------|-----------|---------------|
|
||||||
|
| Modules (Step 1 docs) | 22 | 22 | 0 (spot-checked) | 0 — all source files have a module doc |
|
||||||
|
| Components (Step 2 specs) | 11 (`00`–`11`) | 11 | 4 corrected (`05`, `06`, `07`, `09` cross-link verifications) | 0 |
|
||||||
|
| System flows (Step 3b) | 14 (was 12 — F13/F14 added at Step 4) | 14 | 7 of 14 corrected | 0 critical-path gaps; F12 remains target-only by design (planned feature) |
|
||||||
|
| API endpoints in `architecture.md` Internal Comm. table | ~24 | 24 | 6 endpoints corrected | 0 — every `api.*()` and `createSSE()` call in `src/` is now reflected |
|
||||||
|
| Cross-references (component → flow → architecture) | n/a | sampled | aligned | n/a |
|
||||||
|
| Completeness score | — | **22/22 modules covered, 11/11 components covered, all `src/` endpoint calls accounted for** | — | Tests directory (`src/**/*.test.*`) has zero files — verified via Grep — so no test-doc completeness expectation. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Corrections applied (by document)
|
||||||
|
|
||||||
|
### 2a. `system-flows.md`
|
||||||
|
|
||||||
|
| Flow | What changed | Source-of-truth file |
|
||||||
|
|------|--------------|----------------------|
|
||||||
|
| F2 (refresh) | Was: "single GET `/api/admin/auth/refresh` on 401". Corrected to **two paths**: bootstrap GET in `AuthContext.tsx:24` (no `credentials:'include'` — bug) and 401-retry POST in `api/client.ts:44` (correct). | `src/auth/AuthContext.tsx`, `src/api/client.ts` |
|
||||||
|
| F3 (select flight) | Was: "PUT `/api/flights/select`". Corrected to **PUT `/api/annotations/settings/user` with `{selectedFlightId}`** — selection is a `UserSettings` field, not a dedicated endpoint. Added the bootstrap-time `GET /api/annotations/settings/user` + `GET /api/flights/{selectedFlightId}` re-hydration. | `src/components/FlightContext.tsx:24,31,34,44` |
|
||||||
|
| F5 (annotation save) | Was: `POST /api/annotations`. Corrected to **`POST /api/annotations/annotations`** (doubly-prefixed: suite-service + resource path). | `src/features/annotations/AnnotationsPage.tsx:39` |
|
||||||
|
| F6 (sync detect) | Confirmed as `POST /api/detect/${mediaId}` — used for **both** images and videos in current code (silent UX hazard for long videos). | `src/features/annotations/AnnotationsSidebar.tsx:39` |
|
||||||
|
| F7 (async video detect) | Re-titled "**NOT WIRED TODAY**". The async path is entirely target-only — `/api/detect/video/{id}` and `/api/detect/stream/{jobId}` are not called anywhere in `src/`. The SSE that **does** exist is a different stream (annotation-status events, see F14). Originally finding #21 said "doesn't stream progress"; the corrected reading is "the async flow does not exist at all". | grep on `src/` — zero matches for `detect/video/` and `detect/stream/` |
|
||||||
|
| F9 (bulk-validate) | Was: "validate UI is missing". **Corrected** — the Validate button **is** wired (`DatasetPage.tsx:142-146` shows the button when items are selected, `handleValidate()` POSTs to `/api/annotations/dataset/bulk-status`). Only the `[V]` keyboard shortcut is missing. | `src/features/dataset/DatasetPage.tsx:65-73,142-146` |
|
||||||
|
| F10 (admin classes) | Was: documented a `PUT /api/admin/classes/{id}` edit-class endpoint. **Corrected** — there is **no edit endpoint**. Code only does `POST /api/admin/classes` (add) and `DELETE /api/admin/classes/{id}` (delete). Surfaced as a Step 4 product gap (admins cannot edit existing classes today). | `src/features/admin/AdminPage.tsx:24,31` |
|
||||||
|
| F11 (settings persist) | Was: "PUTs go to `admin/`". **Corrected** — they go to **`annotations/`** (`/api/annotations/settings/system`, `/api/annotations/settings/directories`); aircraft default-toggle goes to `flights/` (`PATCH /api/flights/aircrafts/${id}`). | `src/features/settings/SettingsPage.tsx:22,29,34` |
|
||||||
|
| F13 (live-GPS SSE) | **Newly added** — discovered at Step 4 (`createSSE('/api/flights/${flightId}/live-gps', ...)` in `FlightsPage.tsx:67`). Was not in the original Step 3 inventory. | `src/features/flights/FlightsPage.tsx:67` |
|
||||||
|
| F14 (annotation-status SSE) | **Newly added** — `createSSE('/api/annotations/annotations/events', ...)` in `AnnotationsSidebar.tsx:25`. Originally conflated with detect-progress SSE (F7); these are different streams. | `src/features/annotations/AnnotationsSidebar.tsx:25` |
|
||||||
|
|
||||||
|
### 2b. `architecture.md`
|
||||||
|
|
||||||
|
The "Internal Communication (UI → suite)" table was rewritten to reflect every actual `api.*()` and `createSSE()` call. The most consequential corrections:
|
||||||
|
|
||||||
|
| Component | Was | Now |
|
||||||
|
|-----------|-----|-----|
|
||||||
|
| `02_auth/AuthContext` | "GET `/admin/auth/refresh` (cookie-only)" | Two refresh paths documented (bootstrap GET — broken, finding B3 — vs. 401-retry POST — correct). |
|
||||||
|
| `03_shared-ui/FlightContext` | "PUT `/api/flights/select`" | `PUT /api/annotations/settings/user`; `GET /api/annotations/settings/user`; `GET /api/flights/{id}` for hydration. |
|
||||||
|
| `06_annotations/AnnotationsSidebar` | "POST `/api/detect/video/{id}` + SSE on `/api/detect/stream/{jobId}`" | `POST /api/detect/${mediaId}` (sync, used for both); SSE is `/api/annotations/annotations/events` (annotation-status, NOT detect progress). |
|
||||||
|
| Section 5 of architecture.md ("AI Detect (async video)") | "The UI does not subscribe today" | Stronger: the async flow does not exist at all — no `/api/detect/video/...` and no `/api/detect/stream/...` are called anywhere in `src/`. |
|
||||||
|
|
||||||
|
### 2c. Component descriptions
|
||||||
|
|
||||||
|
| Component | Change |
|
||||||
|
|-----------|--------|
|
||||||
|
| `07_dataset/description.md` §6b (WPF gap analysis) | Three rows reclassified from "Missing" to **"Implemented"** after re-reading `DatasetPage.tsx`: **Class Distribution chart tab**, **"Show only annotations with objects" checkbox**, **Validate button**. The originating WPF cross-check had been correct about the WPF source but wrong about the React port. Step 4 fix entries narrowed to: `[V]` keyboard shortcut missing, `Refresh thumbnails` button missing, `StatusText` slots missing, `IsSeed` highlight missing. |
|
||||||
|
| `01_legacy_coverage_gaps.md` (rollup) | Same three rows re-classified to "Implemented (Step 4 correction)". Item 3 in the "Decisions required at Step 4.5" list (Class Distribution chart) struck through — already ported. |
|
||||||
|
| `06_annotations` / `05_flights` | No code changes; existing gap analyses remain accurate (spot-checked). |
|
||||||
|
|
||||||
|
### 2d. Verification of pre-existing findings (no changes needed)
|
||||||
|
|
||||||
|
These earlier findings were re-checked against code and confirmed accurate:
|
||||||
|
|
||||||
|
- **Finding B3** (Auth bootstrap missing `credentials:'include'`) — `AuthContext.tsx:24` confirmed as `api.get(...)` without `credentials:'include'`; `api/client.ts` does not auto-attach credentials on GET.
|
||||||
|
- **Finding B3** (Flight pagination ceiling 1000) — `FlightContext.tsx:24` confirmed.
|
||||||
|
- **Finding #11** (panel widths typed but not persisted) — `useResizablePanel` confirmed to write nothing back.
|
||||||
|
- **Finding #6** (annotation overlay window symmetric ±200 ms instead of `[-50ms, +150ms]`) — confirmed by reading the WPF source `Azaion.Annotator/Annotator.xaml.cs`.
|
||||||
|
- **Finding B4** (`AdminPage` lacks ConfirmDialog on destructive class delete) — confirmed; only confirms on user-deactivation, not on `handleDeleteClass`.
|
||||||
|
- **Hardcoded OpenWeatherMap API key** in `flightPlanUtils.ts` — confirmed (will surface in Step 6 problem-extraction security_approach).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Remaining uncertainties / deferred to Step 4.5
|
||||||
|
|
||||||
|
These could not be resolved at Step 4 because they require product-level decisions, not code reading. They are queued for Step 4.5 (Architecture Vision).
|
||||||
|
|
||||||
|
1. **Admin can no longer edit existing detection classes** (only add + delete). Was that an intentional simplification or a regression vs. WPF's in-place edit?
|
||||||
|
2. **Sync `/api/detect/${mediaId}` for video** — is this an interim hack pending the async pipeline, or the deliberate design for short videos? Either way it produces silent failures for long videos.
|
||||||
|
3. **Dataset Refresh-thumbnails / `StatusText` slots** — port the WPF status bar, or accept the simplified React surface?
|
||||||
|
4. **Seed annotation visual** (`IsSeed=true` 8 px IndianRed border) — port or drop?
|
||||||
|
5. **Camera-config side panel** (altitude / focal / sensor → GSD) — finding #17, missing entirely; per-flight, per-job, or per-user?
|
||||||
|
6. **Resizable panel width persistence** — per-user (Settings) or per-device (LocalStorage)?
|
||||||
|
7. **Sound Detections** + **Drone Maintenance** features — port from WPF or drop?
|
||||||
|
8. **Status-bar clock + help-text-blink pattern** — port WPF UX or replace with toast notifications?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Module-layout verification
|
||||||
|
|
||||||
|
The 8 questions surfaced in `module-layout.md` §"Verification Needed" remain open for the user to decide:
|
||||||
|
|
||||||
|
1. `classColors` move (currently in `06_annotations/`, owned by `11_class-colors`) — schedule a file move now or treat as a layout-doc-only mapping?
|
||||||
|
2. `CanvasEditor` cross-feature import from `07_dataset` — accept the edge or lift to a shared `components/canvas/`?
|
||||||
|
3. Barrel `index.ts` exports per component — add now (closer to module-layout's documented Public API) or defer?
|
||||||
|
4. `mission-planner/` ownership — code currently sits at repo root, treated as a port-source by `05_flights`. Move under `src/features/flights/` once port is complete, or keep as a sibling reference?
|
||||||
|
5. `00_foundation` multi-directory shape (`src/types/`, `src/hooks/`, `src/i18n/`, `src/components/DetectionClasses.tsx`) — consolidate under `src/foundation/` or accept the split layout?
|
||||||
|
6. `10_app-shell` files (`src/App.tsx`, `src/main.tsx`, `src/index.css`) — leave at repo root or move under `src/app/`?
|
||||||
|
7. Test layout — `src/**/*.test.tsx` has zero files today; greenfield decision required.
|
||||||
|
8. `11_class-colors` — currently `src/features/annotations/classColors.ts`; move to `src/shared/classColors/` or accept the in-feature placement and make the layout-doc the only source of truth?
|
||||||
|
|
||||||
|
These are NOT blocking Step 4 correctness; they are blocking the **module layout's "confirmed-by-user" status** per `module-layout.md` BLOCKING gate.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Recommendation
|
||||||
|
|
||||||
|
The corrections in §2 are mechanical and grounded in source — nothing in this log requires code changes today. Step 4 BLOCKING gate (`full.md` line 252): present this log to the user, and on confirmation proceed to Step 4.5 (Glossary & Architecture Vision).
|
||||||
|
|
||||||
|
**User decision required**:
|
||||||
|
|
||||||
|
- **A** — Corrections look good; proceed to Step 4.5.
|
||||||
|
- **B** — Some corrections look wrong / need a second look (specify which).
|
||||||
|
- **C** — Hold here; resolve the open questions in §3/§4 before Step 4.5.
|
||||||
@@ -0,0 +1,358 @@
|
|||||||
|
# Azaion UI — Final Documentation Report
|
||||||
|
|
||||||
|
> Output of `/document` Step 7. Integrates all artifacts produced in Steps
|
||||||
|
> 0–6. This is the single entry point for downstream skills (`/code-review`,
|
||||||
|
> `/test-spec`, `/refactor`, `/decompose`, `/new-task`) and for human readers
|
||||||
|
> who want the executive view before diving into per-component detail.
|
||||||
|
|
||||||
|
**Status**: complete
|
||||||
|
**Date**: 2026-05-10
|
||||||
|
**Scope**: full codebase (`src/` + `mission-planner/` port-source)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Executive summary
|
||||||
|
|
||||||
|
Azaion UI is the **operator-facing browser SPA** of the Azaion UAV operations
|
||||||
|
suite — a static-bundle React 19 application served by `nginx:alpine`
|
||||||
|
inside an ARM64 container. It is the in-progress **React rewrite** of the
|
||||||
|
legacy WPF stack (`Azaion.Annotator` + `Azaion.Dataset` + `MapMatcher`); the
|
||||||
|
heavyweight machinery (LibVLC, Cython sidecars, SQLite outbox, DI host,
|
||||||
|
binary-split key-fragment loader) moved server-side into the parent suite.
|
||||||
|
The UI's narrowed responsibility is to render the suite's typed REST + SSE
|
||||||
|
contract with no in-browser persistence beyond a bearer in memory and a
|
||||||
|
`Secure HttpOnly` refresh cookie.
|
||||||
|
|
||||||
|
The codebase is **77 modules across 11 components**, fully documented and
|
||||||
|
verified against source. The architecture is **2-context state**
|
||||||
|
(`AuthContext` + `FlightContext`), **no Redux / Zustand / TanStack Query**
|
||||||
|
(P4), **REST + SSE only** (P1), **bilingual** (en + ua, P6). State of
|
||||||
|
production-correctness sits at **~85% of WPF parity**: the operator's
|
||||||
|
primary loop (login → flights → annotate → bulk-validate) works end to end;
|
||||||
|
async video detect (`F7`) and GPS-Denied Test Mode (`F12`) are target-only;
|
||||||
|
**zero test coverage** today. The Step 4 verification pass found and
|
||||||
|
corrected significant drift in the original Step 3 drafts; the resulting
|
||||||
|
docs are now grounded in real code paths.
|
||||||
|
|
||||||
|
The most consequential findings — **enum drift** (`AnnotationStatus`,
|
||||||
|
`MediaStatus`, `Affiliation`, `CombatReadiness`), the **broken bootstrap
|
||||||
|
refresh** in `AuthContext.tsx:24`, the **hardcoded OpenWeatherMap API key**
|
||||||
|
in `mission-planner/src/utils/flightPlanUtils.ts:60`, the **non-functional
|
||||||
|
Save buttons** in `AdminPage` AI/GPS Settings, the **lossy Waypoint POST**
|
||||||
|
shape, and the **missing `/admin` route role-gate** — collectively block
|
||||||
|
production-correctness and are queued for autodev Step 4 (Code Testability
|
||||||
|
Revision). The deeper structural concerns (mission-planner convergence,
|
||||||
|
Camera-config side panel, async-detect wiring) are sized for **Phase B
|
||||||
|
feature cycles** rather than a single Step 8 refactor.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Problem statement
|
||||||
|
|
||||||
|
The Azaion suite operates UAV / aerial-imagery missions for military and
|
||||||
|
defense use cases. Operators need a **single browser surface** to plan
|
||||||
|
flights, review and annotate captured media, run AI object detection,
|
||||||
|
curate datasets, administer detection classes / users / aircraft, and
|
||||||
|
operate the GPS-Denied positioning workflow (including a planned Test
|
||||||
|
Mode driven by `.tlog` + video pairs through SITL).
|
||||||
|
|
||||||
|
This SPA is that surface. It serves:
|
||||||
|
|
||||||
|
- **Operator** — primary persona, default `/flights` route, bilingual UI.
|
||||||
|
- **Admin** — privileged operator at `/admin`; class CRUD, user / aircraft
|
||||||
|
management, AI / GPS settings.
|
||||||
|
- **System integrator** — uses GPS-Denied Test Mode and Settings to validate
|
||||||
|
end-to-end pipelines.
|
||||||
|
|
||||||
|
It is **internal**, not public. RBAC is server-enforced; the browser is
|
||||||
|
treated as untrusted; no SEO; no mobile-first design (Header has a bottom-
|
||||||
|
nav variant for ≥ 768 px, mobile is a P2 use-case). Three legacy WPF
|
||||||
|
features are explicitly **not ported**: encrypted-creds command-line
|
||||||
|
handoff (P8 — security infra moved server-side), Sound Detections, and
|
||||||
|
Drone Maintenance / "Аналіз стану БПЛА" (Step 4.5 decisions — dropped).
|
||||||
|
|
||||||
|
Full statement: `_docs/00_problem/problem.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Architecture overview
|
||||||
|
|
||||||
|
**Tech stack** (one-liner): React 19 + TypeScript 5.7 strict + Vite 6 +
|
||||||
|
Bun 1.3.11 + Tailwind 4 (`az-*` design tokens) + `react-router-dom@7` +
|
||||||
|
`leaflet@1.9.4` + `react-leaflet@5` + `chart.js@4` + `i18next` (en + ua) →
|
||||||
|
multi-stage Dockerfile → `nginx:alpine` static serve, ARM64-only, pushed
|
||||||
|
by Woodpecker CI to `${REGISTRY_HOST}/azaion/ui:${branch}-arm`.
|
||||||
|
|
||||||
|
**Layering** (`module-layout.md`):
|
||||||
|
|
||||||
|
- **L0 — Foundation**: `00_foundation`, `11_class-colors`
|
||||||
|
- **L1 — Transport**: `01_api-transport` (`fetch` + `EventSource` wrappers)
|
||||||
|
- **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`
|
||||||
|
|
||||||
|
**Cross-cutting principles** (binding constraints — `architecture.md`
|
||||||
|
§ Architecture Vision):
|
||||||
|
|
||||||
|
P1 REST + SSE only · P2 Static bundle + nginx · P3 Bearer in memory +
|
||||||
|
HttpOnly refresh cookie · P4 Two-context state · P5 ARM-first edge ·
|
||||||
|
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 · P11 Persist what you type ·
|
||||||
|
P12 Admin can edit existing detection classes.
|
||||||
|
|
||||||
|
Full architecture: `_docs/02_document/architecture.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Component summary
|
||||||
|
|
||||||
|
| # | Component | Purpose | Direct deps (components) | Files / size |
|
||||||
|
|---|-----------|---------|--------------------------|--------------|
|
||||||
|
| 00 | `00_foundation` | Types, hooks (`useDebounce`, `useResizablePanel`), i18n bundles | — | `src/types/index.ts`, `src/hooks/*`, `src/i18n/*`, `src/components/DetectionClasses.tsx` |
|
||||||
|
| 01 | `01_api-transport` | `fetch` wrapper (`client.ts`) + `EventSource` wrapper (`sse.ts`); 401-retry refresh | 00 | `src/api/client.ts`, `src/api/sse.ts` |
|
||||||
|
| 02 | `02_auth` | `AuthContext` + `ProtectedRoute` + login/logout/bootstrap-refresh | 00, 01 | `src/auth/AuthContext.tsx`, `src/auth/ProtectedRoute.tsx` |
|
||||||
|
| 03 | `03_shared-ui` | Header + flight dropdown + `FlightContext` + `ConfirmDialog` + `HelpModal` + `DetectionClasses` strip | 00, 01, 11 | `src/components/Header.tsx`, `FlightContext.tsx`, `ConfirmDialog.tsx`, `HelpModal.tsx`, `DetectionClasses.tsx` |
|
||||||
|
| 04 | `04_login` | Public `/login` route | 00, 02 | `src/features/login/LoginPage.tsx` |
|
||||||
|
| 05 | `05_flights` | Flight CRUD + waypoints + altitude + GPS-Denied + planned Test Mode; `mission-planner/` port-source | 00, 01, 03 | `src/features/flights/*` (15 modules) + `mission-planner/*` (37 modules, NOT deployed) |
|
||||||
|
| 06 | `06_annotations` | Bbox editor (`CanvasEditor`), `VideoPlayer`, AI Detect (sync), `AnnotationsSidebar`, `MediaList`, `AnnotationsPage` | 00, 01, 03, 11 | `src/features/annotations/*` (5 modules) |
|
||||||
|
| 07 | `07_dataset` | Dataset Explorer (3 tabs: annotations / editor / class-distribution); bulk-validate; class-distribution chart | 00, 01, 03, 06, 11 | `src/features/dataset/DatasetPage.tsx` |
|
||||||
|
| 08 | `08_admin` | Class CRUD (add+delete; edit P12 to be re-introduced); user mgmt; AI/GPS Settings forms (broken save); aircraft default-toggle | 00, 01, 03 | `src/features/admin/AdminPage.tsx` |
|
||||||
|
| 09 | `09_settings` | System / Directory / Camera / User settings; aircraft default-toggle | 00, 01, 03 | `src/features/settings/SettingsPage.tsx` |
|
||||||
|
| 10 | `10_app-shell` | `App.tsx` + `main.tsx` + routing tree + global CSS | 00, 02, 03, 04, 05, 06, 07, 08, 09 | `src/App.tsx`, `src/main.tsx`, `src/index.css`, `index.html` |
|
||||||
|
| 11 | `11_class-colors` | Class → color + text mapping; `getPhotoModeSuffix`; `yoloId = classId + photoModeOffset` | 00 | `src/features/annotations/classColors.ts` (file move pending) |
|
||||||
|
|
||||||
|
Full per-component specs: `_docs/02_document/components/*/description.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. System flows
|
||||||
|
|
||||||
|
| # | Flow | Trigger | Status today |
|
||||||
|
|---|------|---------|--------------|
|
||||||
|
| F1 | Login | `/login` form submit | Works |
|
||||||
|
| F2 | Bearer auto-refresh on 401 | Authenticated fetch returns 401 | **Two paths**: 401-retry POST works; bootstrap GET broken (Step 4 fix) |
|
||||||
|
| F3 | Select active flight | Flight dropdown in Header | Works (corrected at Step 4 — persists via `UserSettings`, not `/flights/select`) |
|
||||||
|
| F4 | Create / save flight + waypoints | Save in `FlightsPage` | Works but lossy: delete-then-recreate waypoint cycle; POST shape mismatches spec (Step 4 fix) |
|
||||||
|
| F5 | Annotate media (manual bbox) | Drag on canvas | Works; save body missing `Source`, `WaypointId`; uses `time` not `videoTime` (Step 4 fix) |
|
||||||
|
| F6 | AI Detect — image (sync) | Click AI Detect with image | Works |
|
||||||
|
| F7 | AI Detect — video (async) | Click AI Detect with video | **NOT WIRED** today; sync `F6` is used as a bridge for short videos. Phase B target. |
|
||||||
|
| F8 | Dataset browse + filter | Open `/dataset` | Works; status filter conflates `None` with `All` (Step 4 fix) |
|
||||||
|
| F9 | Dataset bulk-validate | Select + Validate | Works (Step 4 correction — button is wired); `[V]` keyboard shortcut missing |
|
||||||
|
| F10 | Admin detection class CRUD | Edit in `/admin` | Add + delete only; **edit (P12) to be re-introduced** |
|
||||||
|
| F11 | Settings: persist user prefs | Save in `/settings` | Works (corrected at Step 4 — endpoints route to `annotations/`, not `admin/`); panel widths NOT persisted (P11 fix) |
|
||||||
|
| F12 | GPS-Denied Test Mode | Upload `.tlog` + video | **NOT WIRED** today; Phase B target. |
|
||||||
|
| F13 | Live-GPS SSE | FlightsPage with selected flight | Works (newly added at Step 4) |
|
||||||
|
| F14 | Annotation-status SSE | AnnotationsPage with media | Works (newly added at Step 4 — separate stream from F7 detect-progress) |
|
||||||
|
|
||||||
|
Full sequence diagrams + error scenarios: `_docs/02_document/system-flows.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Risk observations (from `04_verification_log.md`)
|
||||||
|
|
||||||
|
### High-priority — block production-correctness (Step 4 fix candidates)
|
||||||
|
|
||||||
|
| # | Risk | Component | Source |
|
||||||
|
|---|------|-----------|--------|
|
||||||
|
| R1 | **Bootstrap refresh missing `credentials:'include'`** — cold load fails to refresh, forces unnecessary re-login | `02_auth/AuthContext` | `AuthContext.tsx:24`; F2 |
|
||||||
|
| R2 | **Numeric enum drift** (`AnnotationStatus`, `MediaStatus`, `Affiliation`, `CombatReadiness`) — wire payloads will be wrong | `00_foundation` | `src/types/index.ts`; AC-04 |
|
||||||
|
| R3 | **Hardcoded OpenWeatherMap API key** in `mission-planner/src/utils/flightPlanUtils.ts:60` — secret in bundle | `05_flights` (mission-planner) | P10 violation; AC-20 |
|
||||||
|
| R4 | **AdminPage AI/GPS Settings Save buttons do nothing** — defaultValue forms with no state, no submit | `08_admin` | `AdminPage.tsx`; finding B4 |
|
||||||
|
| R5 | **Lossy Waypoint POST shape** — UI sends `{name, latitude, longitude, order}`; spec wants `{Geopoint:{Lat,Lon,MGRS}, Source, Objective, OrderNum, Height}` — likely 400s on a strict server | `05_flights` | finding #20 |
|
||||||
|
| R6 | **Annotation save body missing `Source`, `WaypointId`; field renamed `time → videoTime`** | `06_annotations` | finding #32 |
|
||||||
|
| R7 | **`/admin` route lacks client-side role-gate** — non-admin sees broken admin UI flicker before server 403 | `10_app-shell` + `08_admin` | AC-22 |
|
||||||
|
| R8 | **Async video detect (`F7`) NOT WIRED** — sync `/api/detect/${id}` is silently used for video; long videos blow access-token TTL (no `X-Refresh-Token`) | `06_annotations` | F7; finding #29 |
|
||||||
|
|
||||||
|
### Medium-priority — quality / a11y / hygiene
|
||||||
|
|
||||||
|
| # | Risk | Component | Source |
|
||||||
|
|---|------|-----------|--------|
|
||||||
|
| R9 | Annotation overlay window symmetric ±200 ms instead of asymmetric `[-50, +150]` ms (matches WPF) | `06_annotations/CanvasEditor` | finding #6; AC-28 |
|
||||||
|
| R10 | `useResizablePanel` reads `UserSettings.panelWidths` but never writes back — P11 violation | `00_foundation` + `06_annotations`/`07_dataset` | AC-21 |
|
||||||
|
| R11 | `MediaList` uses `alert()` instead of toast / dialog | `06_annotations` | AC-14 |
|
||||||
|
| R12 | `ConfirmDialog` lacks `aria-modal` / `role=dialog` / focus-trap / Esc | `03_shared-ui` | AC-15 |
|
||||||
|
| R13 | Header flight dropdown lacks `role=combobox` / `aria-expanded` / Esc-to-close / focus-trap; outside-click handler always attached | `03_shared-ui` | AC-16 |
|
||||||
|
| R14 | `AdminPage.handleDeleteClass` lacks `ConfirmDialog` despite being destructive | `08_admin` | AC-30 |
|
||||||
|
| R15 | `09_settings` numeric inputs use `parseInt(v) \|\| 0` — empty silently writes 0 | `09_settings` | AC-26 |
|
||||||
|
| R16 | `09_settings` save handlers lack `try/finally` — PUT failure leaves `saving:true` permanently | `09_settings` | AC-27 |
|
||||||
|
| R17 | `i18next.lng` hardcoded `'en'` — no detector / no persistence | `00_foundation` | AC-13 |
|
||||||
|
| R18 | Hardcoded English strings in `AdminPage`, `HelpModal` | `08_admin` + `03_shared-ui` | P6 violation |
|
||||||
|
| R19 | `mapIcons.ts` defaultIcon CDN URL pinned to `leaflet@1.7.1` while package uses 1.9.4 | `05_flights` | finding |
|
||||||
|
| R20 | `magic mediaType=1` literal in `06_annotations` and `07_dataset` | `06_annotations` + `07_dataset` | AC-29 |
|
||||||
|
| R21 | `classNum=0` sentinel collides with real class 0 in dataset filters | `07_dataset` | finding #9 |
|
||||||
|
| R22 | Dataset status filter conflates `None` with `All` | `07_dataset` | finding |
|
||||||
|
| R23 | DatasetPage editor tab does not save | `07_dataset` | finding #4 |
|
||||||
|
| R24 | `AnnotationsPage.handleDownload` tainted-canvas risk | `06_annotations` | finding |
|
||||||
|
| R25 | `AnnotationsPage.handleSave` fallback hides save loss | `06_annotations` | finding |
|
||||||
|
| R26 | `mission-planner/flightPlanUtils.ts` silently swallows weather errors; sequential `await` per segment (perf trap); ambiguous battery-capacity unit (Wh vs Ws); km vs m altitude mixing | `05_flights` (port-source) | findings |
|
||||||
|
|
||||||
|
### Low-priority — surface / polish
|
||||||
|
|
||||||
|
| # | Risk | Component | Source |
|
||||||
|
|---|------|-----------|--------|
|
||||||
|
| R27 | No `ErrorBoundary` at the app root | `10_app-shell` | finding |
|
||||||
|
| R28 | No lazy code-splitting / chunked routes — bundle bloat | `10_app-shell` | finding (`AltitudeChart` lazy-load opportunity) |
|
||||||
|
| R29 | `index.html` body class hardcodes hex literals instead of `az-*` tokens | `10_app-shell` | discovery #11.10 |
|
||||||
|
| R30 | `runUnlockSequence` 4×600 ms theatrical animation in `LoginPage` | `04_login` | finding B4 |
|
||||||
|
| R31 | `FlightContext.selectFlight` is fire-and-forget — no error path | `03_shared-ui` | finding B3 |
|
||||||
|
| R32 | `FlightContext.GET /api/flights?pageSize=1000` hardcoded ceiling | `03_shared-ui` | finding B3 |
|
||||||
|
|
||||||
|
### CI / infra-level (Step 6 surface — track at suite level)
|
||||||
|
|
||||||
|
| # | Risk | Source |
|
||||||
|
|---|------|--------|
|
||||||
|
| R33 | No CSP / hardening headers in `nginx.conf` | `security_approach.md` § 9 |
|
||||||
|
| R34 | No vulnerability scan / SBOM emission / image signing in CI | `architecture.md` § 3 "Missing from the pipeline today" |
|
||||||
|
| R35 | No test step in CI today | `.woodpecker/build-arm.yml` |
|
||||||
|
| R36 | No AMD64 build (ARM64-only image) | `.woodpecker/build-arm.yml` |
|
||||||
|
| R37 | No browser-list config (browser support matrix not enforced) | `architecture.md` § 6 |
|
||||||
|
| R38 | No bundle-size budget gate | `architecture.md` § 6; AC-11 |
|
||||||
|
| R39 | EventSource refresh-rotation breaks open SSE; no reconnect logic | `architecture.md` § Architecture Vision; AC-24 |
|
||||||
|
| R40 | No centralized client telemetry (only browser-console logging) | `deployment/observability.md` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Open questions
|
||||||
|
|
||||||
|
These were flagged during analysis and remain undecided. They do NOT block
|
||||||
|
downstream skills; each is owned by a specific phase per `architecture.md`
|
||||||
|
§ Architecture Vision Open Questions.
|
||||||
|
|
||||||
|
| # | Question | Owner / planned resolution |
|
||||||
|
|---|----------|---------------------------|
|
||||||
|
| Q1 | Test framework choice (Vitest / Jest / Playwright / Bun:test) | Autodev Step 5 — Decompose Tests |
|
||||||
|
| Q2 | `IsSeed` annotation visual (8 px IndianRed border) — does the modern API still expose `isSeed`? | Phase B feature cycle |
|
||||||
|
| Q3 | Camera-config side panel (GSD = altitude × focal × sensor) — per-user, per-flight, or per-detect-job? | Phase B feature cycle |
|
||||||
|
| Q4 | Status-bar clock + help-text-blink — port WPF UX or replace with toasts? | Phase B feature cycle |
|
||||||
|
| Q5 | OpenWeatherMap routing — `.env` (interim) or proxy via `flights/` (preferred long-term)? | Step 4 (interim); Phase B (proxy) |
|
||||||
|
| Q6 | `mission-planner/` end-state — delete after parity port (preferred per Step 4.5) or keep as continuously-vendored reference? | Final Phase B cycle |
|
||||||
|
| Q7 | Sync `/api/detect/${id}` for video — when in Phase B does the async pipeline (F7) ship? | Phase B feature cycle |
|
||||||
|
| Q8 | Module-layout Verification Needed (8 items) — class-colors file move, CanvasEditor cross-feature import, barrel exports, `mission-planner/` ownership timing, foundation multi-dir, app-shell location, test layout, `11_class-colors` location | Step 4 / Phase B (per item) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Artifact index
|
||||||
|
|
||||||
|
All paths are workspace-relative. Each artifact's `Status` field declares
|
||||||
|
who confirmed it (derived-from-code / synthesised-from-verified-docs /
|
||||||
|
confirmed-by-user).
|
||||||
|
|
||||||
|
### `_docs/02_document/` — Documentation skill outputs
|
||||||
|
|
||||||
|
| Artifact | Step | Purpose |
|
||||||
|
|----------|------|---------|
|
||||||
|
| `00_discovery.md` | 0 | Tech stack, dep graph, topological order, entry points, leaves |
|
||||||
|
| `modules/*.md` (22 files) | 1 | Per-module documentation — covers all 77 source modules |
|
||||||
|
| `components/00_foundation/description.md` | 2 | Foundation component spec |
|
||||||
|
| `components/01_api-transport/description.md` | 2 | API transport spec |
|
||||||
|
| `components/02_auth/description.md` | 2 | Auth spec |
|
||||||
|
| `components/03_shared-ui/description.md` | 2 | Shared UI spec |
|
||||||
|
| `components/04_login/description.md` | 2 | Login spec |
|
||||||
|
| `components/05_flights/description.md` | 2 | Flights spec (incl. mission-planner port-source) |
|
||||||
|
| `components/06_annotations/description.md` | 2 | Annotations spec (incl. WPF gap analysis §6b) |
|
||||||
|
| `components/07_dataset/description.md` | 2 | Dataset spec (incl. WPF gap analysis §6b) |
|
||||||
|
| `components/08_admin/description.md` | 2 | Admin spec |
|
||||||
|
| `components/09_settings/description.md` | 2 | Settings spec |
|
||||||
|
| `components/10_app-shell/description.md` | 2 | App-shell spec |
|
||||||
|
| `components/11_class-colors/description.md` | 2 | Class-colors spec (lifted at Step 2) |
|
||||||
|
| `diagrams/components.md` | 2 | Mermaid component dependency graph |
|
||||||
|
| `module-layout.md` | 2.5 | File-ownership map (consumed by `/implement`, `/code-review`, `/refactor`) |
|
||||||
|
| `architecture.md` | 3a + 4.5 | System architecture + § Architecture Vision (12 principles, mission-planner convergence plan) |
|
||||||
|
| `system-flows.md` | 3b | F1–F14 sequence diagrams + error scenarios |
|
||||||
|
| `data_model.md` | 3c | Entity-relationship + numeric-enum drift map |
|
||||||
|
| `deployment/containerization.md` | 3d | Multi-stage Dockerfile, ARM64, nginx static serve |
|
||||||
|
| `deployment/ci_cd_pipeline.md` | 3d | Woodpecker pipeline structure |
|
||||||
|
| `deployment/environment_strategy.md` | 3d | Dev / Stage / Production |
|
||||||
|
| `deployment/observability.md` | 3d | Current state (no centralized telemetry) |
|
||||||
|
| `04_verification_log.md` | 4 | Coverage summary, corrections by document, pre-existing findings re-checked |
|
||||||
|
| `glossary.md` | 4.5 | Confirmed terminology + synonym/drift pairs |
|
||||||
|
| `01_legacy_coverage_gaps.md` | (Step 2 → Step 4.5 update) | WPF parity rollup; Sound Detections + Drone Maintenance marked "Intentionally not ported" |
|
||||||
|
| `FINAL_report.md` | 7 | **This document** |
|
||||||
|
| `state.json` | (skill-internal) | Document skill resumability state |
|
||||||
|
|
||||||
|
### `_docs/01_solution/` — Solution synthesis
|
||||||
|
|
||||||
|
| Artifact | Step | Purpose |
|
||||||
|
|----------|------|---------|
|
||||||
|
| `solution.md` | 5 | Retrospective solution: per-component table (Solution / Tools / Advantages / Limitations / Requirements / Security / Cost / Fit) for all 11 components; testing strategy; references |
|
||||||
|
|
||||||
|
### `_docs/00_problem/` — Problem extraction
|
||||||
|
|
||||||
|
| Artifact | Step | Purpose |
|
||||||
|
|----------|------|---------|
|
||||||
|
| `problem.md` | 6a | High-level problem statement + users + how-it-works + non-goals |
|
||||||
|
| `restrictions.md` | 6b | Hardware (4) / Software (14) / Environment (10) / Operational (15) restrictions |
|
||||||
|
| `acceptance_criteria.md` | 6c | 34 measurable ACs + 5 anti-criteria; coverage status |
|
||||||
|
| `input_data/data_parameters.md` | 6d | Typed REST entities + SSE payloads + env vars + static assets |
|
||||||
|
| `security_approach.md` | 6e | 13 sections covering auth, authz, tokens, SSE bearer, secrets, CORS, validation, XSS, nginx hardening, audit, supply-chain, fix-map |
|
||||||
|
|
||||||
|
### `_docs/02_document/_autodev_state.md` (parent: `_docs/_autodev_state.md`)
|
||||||
|
|
||||||
|
| Artifact | Purpose |
|
||||||
|
|----------|---------|
|
||||||
|
| `_docs/_autodev_state.md` | Autodev resume pointer — flow / step / sub_step / cycle / retry_count |
|
||||||
|
|
||||||
|
### Reference inputs (existing, not produced by `/document`)
|
||||||
|
|
||||||
|
| Artifact | Purpose |
|
||||||
|
|----------|---------|
|
||||||
|
| `_docs/legacy/wpf-era.md` | Legacy WPF reference — used as the WPF parity baseline |
|
||||||
|
| `_docs/how_to_test.md` | GPS-Denied Test Mode reference |
|
||||||
|
| `_docs/ui_design/*` | UI design wireframes — used in Step 4 cross-check |
|
||||||
|
| `suite/_docs/*` (parent suite) | Service contracts — used for enum drift cross-check |
|
||||||
|
| `suite/annotations-research` (detached @ `22529c2`) | Read-only legacy WPF source — used in Step 2 / Step 4 cross-check |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Coverage / completeness
|
||||||
|
|
||||||
|
| Axis | Result |
|
||||||
|
|------|--------|
|
||||||
|
| Source modules | 77 / 77 covered (22 doc files; some consolidated) |
|
||||||
|
| Components | 11 / 11 specs |
|
||||||
|
| System flows | 14 (F1–F14; F7 + F12 are target-only by design) |
|
||||||
|
| Endpoint inventory | Every `api.*()` and `createSSE()` call in `src/` is reflected in `architecture.md` § 5 |
|
||||||
|
| Test coverage | **0 tests in `src/`** — no test framework configured today |
|
||||||
|
| BLOCKING gates passed | Step 2 (components), Step 2.5 (module-layout), Step 4 (verification), Step 4.5 (glossary + vision), Step 6 (problem docs) — all confirmed by user |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. What comes next (autodev existing-code flow)
|
||||||
|
|
||||||
|
`/document` Step 1 of the autodev flow is now **complete**. The next steps
|
||||||
|
are state-driven from `_docs/_autodev_state.md`:
|
||||||
|
|
||||||
|
- **Step 2 — Architecture Baseline Scan** (`code-review` skill in baseline
|
||||||
|
mode) → produces `_docs/02_document/architecture_compliance_baseline.md`
|
||||||
|
marking pre-existing architecture violations (cycles, cross-component
|
||||||
|
private imports, mission-planner convergence as a Critical Architecture
|
||||||
|
finding).
|
||||||
|
- **Step 3 — Test Spec** (`test-spec` skill) → produces
|
||||||
|
`_docs/02_document/tests/traceability-matrix.md` and per-AC scenario files.
|
||||||
|
- **Step 4 — Code Testability Revision** (`refactor` skill in guided mode)
|
||||||
|
→ addresses the High-priority risks above as a minimal, surgical
|
||||||
|
testability prep.
|
||||||
|
- **Step 5 — Decompose Tests** (`decompose` skill in tests-only mode) →
|
||||||
|
selects test framework (Q1) and creates per-AC test tasks.
|
||||||
|
- **Steps 6–8** → Implement Tests / Run Tests / optional Refactor.
|
||||||
|
- **Phase B (Steps 9–17, looping)** → Mission-planner convergence happens
|
||||||
|
one feature group per cycle.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Ownership map (downstream-skill consumers)
|
||||||
|
|
||||||
|
| Downstream skill | Artifacts it reads first |
|
||||||
|
|------------------|--------------------------|
|
||||||
|
| `/code-review` (baseline mode) | `architecture.md` § Architecture Vision, `module-layout.md`, `00_discovery.md` § 8 (cross-layer imports) |
|
||||||
|
| `/test-spec` | `acceptance_criteria.md`, `system-flows.md`, `architecture.md` § 6 NFRs |
|
||||||
|
| `/refactor` (guided mode for Step 4) | `04_verification_log.md` (high-priority risks), `acceptance_criteria.md`, `module-layout.md` |
|
||||||
|
| `/decompose` (tests-only mode) | `tests/traceability-matrix.md` (produced by `/test-spec`), `module-layout.md`, `restrictions.md` |
|
||||||
|
| `/implement` | per-task spec files in `_docs/02_tasks/todo/`, `module-layout.md` (file ownership) |
|
||||||
|
| `/new-task` (Phase B) | `architecture.md` § Architecture Vision, `glossary.md`, `01_legacy_coverage_gaps.md` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**End of report.**
|
||||||
@@ -0,0 +1,443 @@
|
|||||||
|
# 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 directly by the SPA — security finding) |
|
||||||
|
| Woodpecker CI pipeline (`.woodpecker/build-arm.yml`) | Map tile providers (OpenStreetMap, satellite tile URL via env) |
|
||||||
|
| | 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) |
|
||||||
|
| OpenStreetMap tile servers | HTTPS (Leaflet TileLayer) | Outbound | Map raster tiles (browser-direct, not via nginx proxy) |
|
||||||
|
| Satellite tile provider | HTTPS (Leaflet TileLayer with env-configured URL) | Outbound | Satellite imagery (only consumed by mission-planner today) |
|
||||||
|
| OpenWeatherMap | HTTPS (`api.openweathermap.org/data/2.5/onecall`) | Outbound | Wind data for flight planning. **Hardcoded API key in `flightPlanUtils.ts:60` — security finding to fix at Step 4.** |
|
||||||
|
|
||||||
|
## 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; only OpenWeatherMap and map tiles require internet. Field deployments will need an offline tile cache (not implemented).
|
||||||
|
- **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 **hardcoded API key** — security finding. |
|
||||||
|
|
||||||
|
### External Integrations
|
||||||
|
|
||||||
|
| External System | Protocol | Auth | Rate Limits | Failure Mode |
|
||||||
|
|----------------|----------|------|-------------|--------------|
|
||||||
|
| OpenStreetMap tiles | HTTPS (Leaflet TileLayer) | None | OSM Tile Usage Policy | Map renders blank / stale; no fallback today |
|
||||||
|
| OpenWeatherMap | HTTPS | **Hardcoded API key in source** | Free-tier 60 calls/min | Errors silently swallowed in `flightPlanUtils.ts` (finding) — wind data missing → battery/duration estimates wrong, no UI surface |
|
||||||
|
| 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.
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
# Architecture Compliance Baseline
|
||||||
|
|
||||||
|
**Scope**: full existing codebase (`src/` deployed tree + `mission-planner/` port-source).
|
||||||
|
**Date**: 2026-05-10
|
||||||
|
**Mode**: code-review baseline (Phase 1 + Phase 7 only).
|
||||||
|
**Verdict**: **FAIL** — 1 Critical, 4 High, 2 Medium, 2 Low. Verdict drives Step 2 → Step 4 / Step 8 routing per `flows/existing-code.md`; it does NOT block any pipeline.
|
||||||
|
|
||||||
|
Inputs read in Phase 1:
|
||||||
|
- `_docs/02_document/architecture.md` (incl. Architecture Vision P1–P12, ADR-001..010, Mission-planner convergence plan).
|
||||||
|
- `_docs/02_document/module-layout.md` (per-component file ownership, Layer table, Verification Needed #1–#8).
|
||||||
|
- `_docs/00_problem/restrictions.md`, `_docs/01_solution/solution.md` (project context).
|
||||||
|
|
||||||
|
Detection approach: TypeScript `import ... from '...'` parsing across all `.ts` / `.tsx` files; layer resolved via `module-layout.md` "Per-Component Mapping"; layer comparison against the "Allowed Dependencies (layering)" table.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
|
||||||
|
| # | Severity | Category | Location | Title |
|
||||||
|
|----|----------|-------------|--------------------------------------------------------------------------|---------------------------------------------------------------------|
|
||||||
|
| F1 | Critical | Architecture | `mission-planner/**` vs `src/features/flights/**` | Mission-planner duplicates 13+ modules of the deployed flights tree |
|
||||||
|
| F2 | High | Architecture | `src/features/dataset/DatasetPage.tsx:9` → `../annotations/CanvasEditor` | Cross-feature same-layer edge — `07_dataset` reaches into `06_annotations` |
|
||||||
|
| F3 | High | Architecture | `src/features/annotations/classColors.ts` | Physical / logical owner split — `11_class-colors` file lives inside `06_annotations` |
|
||||||
|
| F4 | High | Architecture | every component | No Public API barrels — every internal file is de-facto public |
|
||||||
|
| F5 | High | Architecture | `mission-planner/src/flightPlanning/MapView.tsx ↔ MiniMap.tsx` | Pre-existing import cycle inside port-source |
|
||||||
|
| F6 | Medium | Architecture | (codebase-wide) | No `src/shared/` infrastructure for cross-cutting concerns |
|
||||||
|
| F7 | Medium | Architecture | every `api.*` / `createSSE` call site | Hardcoded `/api/<service>/...` paths instead of env-driven endpoints |
|
||||||
|
| F8 | Low | Architecture | `_docs/02_document/module-layout.md` | Layering-table inconsistency — Header → useAuth is unannotated |
|
||||||
|
| F9 | Low | Architecture | `mission-planner/src/{main,App,setupTests,vite-env}.tsx` | Inert second Vite entry tree at port-source root |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Finding Details
|
||||||
|
|
||||||
|
### F1: Mission-planner duplicates 13+ modules of the deployed flights tree (Critical / Architecture)
|
||||||
|
|
||||||
|
- **Location**: `mission-planner/src/**` vs `src/features/flights/**`.
|
||||||
|
- **Description**: Component `05_flights` has two physical trees (per ADR-009). The port-source duplicates almost every module the deployed tree exposes today AND carries behaviors the deployed tree does not yet have (camera-config side panel, mission JSON I/O, satellite tile provider, richer waypoint-altitude UX). Duplication enumerated below; until convergence completes, the same logic lives in two places and silently drifts. This is the **work-list** the Architecture Vision (Mission-planner convergence plan) asked Step 2 to surface.
|
||||||
|
|
||||||
|
**Duplicated by name** (deployed → port-source):
|
||||||
|
|
||||||
|
| Deployed (`src/features/flights/`) | Port-source (`mission-planner/src/.../`) | Notes |
|
||||||
|
|--------------------------------------------|----------------------------------------------------------------------------|-------|
|
||||||
|
| `AltitudeChart.tsx` | `flightPlanning/AltitudeChart.tsx` | Both consumed by their respective panels. |
|
||||||
|
| `AltitudeDialog.tsx` | `flightPlanning/AltitudeDialog.tsx` | Port-source version uses `useLanguage` (custom i18n); deployed uses Tailwind. |
|
||||||
|
| `DrawControl.tsx` | `flightPlanning/DrawControl.tsx` | Deployed uses `newGuid` from `flightPlanUtils`; port-source from `utils.ts`. |
|
||||||
|
| `JsonEditorDialog.tsx` | `flightPlanning/JsonEditorDialog.tsx` | Mission JSON I/O — deployed version is a stub vs. port-source's full editor. |
|
||||||
|
| `MapPoint.tsx` | `flightPlanning/MapPoint.tsx` | Both import the local `pointIcon*` set. |
|
||||||
|
| `MiniMap.tsx` | `flightPlanning/MiniMap.tsx` | Port-source version is half of the F5 cycle. |
|
||||||
|
| `WindEffect.tsx` | `flightPlanning/WindEffect.tsx` | Port-source pulls `useLanguage`; deployed uses `i18next`. |
|
||||||
|
| `WaypointList.tsx` | `flightPlanning/PointsList.tsx` (renamed) | DnD reorder; same intent, different libs. |
|
||||||
|
| `FlightMap.tsx` | `flightPlanning/MapView.tsx` (renamed) | The cycle partner. |
|
||||||
|
| `FlightParamsPanel.tsx` | `flightPlanning/LeftBoard.tsx` (renamed) | Side-panel composition root. |
|
||||||
|
| `FlightsPage.tsx` | `flightPlanning/flightPlan.tsx` (renamed) | Route component. |
|
||||||
|
| `mapIcons.ts` | `icons/PointIcons.tsx`, `icons/MapIcons.tsx`, `icons/SidebarIcons.tsx` | Port-source uses inline SVG components; deployed uses `L.icon`. |
|
||||||
|
| `flightPlanUtils.ts` (`newGuid`, `calculateDistance`, `calculateAllPoints`, `parseCoordinates`, `getMockAircraftParams`) | `utils.ts` (`newGuid`); `services/calculateDistance.ts`; `services/calculateBatteryUsage.ts`; `services/AircraftService.ts` (`mockGetAirplaneParams`) | Same five symbols, scattered across four files in the port-source. |
|
||||||
|
| `types.ts` (`FlightPoint`, `CalculatedPointInfo`, `MapRectangle`, `ActionMode`, `WindParams`, `AircraftParams`, `PURPOSES`, `COORDINATE_PRECISION`, `TILE_URLS`) | `types/index.ts` + `constants/{actionModes,purposes,tileUrls,maptypes,languages,translations}.ts` | Port-source has richer constants tables (mapTypes, languages, translations) the deployed tree lacks. |
|
||||||
|
|
||||||
|
**Port-source-only** (no deployed counterpart yet — these are the behaviors that must port across Phase B):
|
||||||
|
|
||||||
|
- `flightPlanning/LanguageContext.tsx` + `LanguageSwitcher.tsx` — mission-planner's standalone i18n (deployed uses `i18next`). DO NOT port; consolidate on `i18next`.
|
||||||
|
- `flightPlanning/TotalDistance.tsx` — distance + battery readout in the left panel. Behavior the deployed tree does not yet show in the same form.
|
||||||
|
- `icons/PhoneIcon.tsx` (rotate-phone hint), `icons/SidebarIcons.tsx` (DashedArea / HideSidebar / ShowSidebar).
|
||||||
|
- `services/WeatherService.ts` — wind compute (already partially in deployed `flightPlanUtils.ts:60` with the hardcoded key — P10 violation).
|
||||||
|
- `flightPlanning/Aircraft.ts` — aircraft entity helpers.
|
||||||
|
- `config.ts` — `COORDINATE_PRECISION`, `GOOGLE_GEOCODE_KEY` (second hardcoded-key risk — verify).
|
||||||
|
|
||||||
|
- **Suggestion**: This is the convergence work-list. Per Architecture Vision (Mission-planner convergence plan):
|
||||||
|
- **NOT a Step 4 testability item** — these are behavior ports, not testability surgery.
|
||||||
|
- **NOT a Step 8 refactor item alone** — too large for one mechanical refactor; behavior changes break tests.
|
||||||
|
- **Primary home**: Phase B feature cycles (one cycle per port group: i18n consolidation; flight-planning UI parity; waypoint-altitude UX; mission JSON I/O; satellite tile provider; weather/battery service; camera-config side panel). Each cycle: New Task → Implement → Run Tests → Test-Spec Sync → Update Docs → Deploy → Retrospective.
|
||||||
|
- **Final Phase B cycle**: delete `mission-planner/` (its only consumer becomes zero).
|
||||||
|
- **Task / Epic**: feeds `05_flights` Step 3 (Test Spec) — every port target must have an AC for the converged behavior before its Phase B cycle starts.
|
||||||
|
|
||||||
|
### F2: Cross-feature same-layer edge — `07_dataset` reaches into `06_annotations` (High / Architecture)
|
||||||
|
|
||||||
|
- **Location**: `src/features/dataset/DatasetPage.tsx:9` → `import CanvasEditor from '../annotations/CanvasEditor'`.
|
||||||
|
- **Description**: `07_dataset` and `06_annotations` are both Layer 3 features. Same-layer imports are forbidden except for explicit grandfathered edges. This edge IS grandfathered (`module-layout.md` Allowed-Dependencies table footnote †, Verification Needed #2). It is the only Layer-3 ↔ Layer-3 import in the codebase. Until a proper home is chosen, the implement skill must keep treating `CanvasEditor.tsx` as READ-ONLY for `07_dataset` tasks.
|
||||||
|
- **Suggestion**: Lift `CanvasEditor.tsx` to `src/components/canvas/CanvasEditor.tsx` (under `03_shared-ui`) OR to a new `06b_canvas` component. Both options drop the same-layer edge. Decision should ride a Phase B cycle that already touches `CanvasEditor` — folding the move into a behavior change is cheaper than a standalone refactor.
|
||||||
|
- **Task / Epic**: defer to Phase B (when a `CanvasEditor`-touching feature lands) or Step 8 refactor (optional).
|
||||||
|
|
||||||
|
### F3: Physical / logical owner split for `classColors.ts` (High / Architecture)
|
||||||
|
|
||||||
|
- **Location**: `src/features/annotations/classColors.ts`.
|
||||||
|
- **Description**: The file is under `06_annotations`'s owns-glob (`src/features/annotations/**`) but the component spec assigns it to `11_class-colors` (Layer 0 shared kernel) — three external consumers depend on it (`03_shared-ui/DetectionClasses`, `06_annotations/{CanvasEditor,AnnotationsPage,AnnotationsSidebar}`, future `07_dataset` class-distribution chart). Module-layout Verification #1 records the workaround: `READ-ONLY` for `06_annotations` tasks. The workaround scales poorly — a new `06_annotations` contributor reading only the directory glob will not know the file is off-limits.
|
||||||
|
- **Suggestion**: Move physical file to `src/shared/classColors.ts` (introducing a `src/shared/` layer for true Layer-0 utilities) or to `src/components/detection/classColors.ts` (under `03_shared-ui`). Either move drops the workaround and aligns physical/logical ownership.
|
||||||
|
- **Task / Epic**: Step 4 testability — minimal, surgical move (rename + import-path update across 4 consumers).
|
||||||
|
|
||||||
|
### F4: No Public API barrels — every internal file is de-facto public (High / Architecture)
|
||||||
|
|
||||||
|
- **Location**: every component root (no `src/<component>/index.ts` exists today; only `src/types/index.ts` and `mission-planner/src/types/index.ts` are barrels and they're re-export hubs, not component facades).
|
||||||
|
- **Description**: Cross-component imports use file-name granularity (`import { api } from '../api/client'`, `import { useFlight } from '../../components/FlightContext'`, etc.). Consequence: there is **no enforceable Public API surface**. Any internal refactor inside a component (split a file, rename an export) is a breaking change to every importer. Phase 7 Check #2 ("Public API respect") cannot meaningfully fail in this codebase because everything is public. Module-layout Verification #3 records the same observation.
|
||||||
|
- **Suggestion**: Step 4 testability candidate — add `src/<component>/index.ts` for every component, re-exporting only the symbols listed in module-layout's "Public API (de-facto)" line for that component. Then a future Phase 7 invocation can flag deep imports as Architecture findings instead of folding into background noise.
|
||||||
|
- **Task / Epic**: Step 4 testability (single mechanical change per component; ~11 new files + ~30 import-path edits).
|
||||||
|
|
||||||
|
### F5: Pre-existing import cycle inside port-source (High / Architecture)
|
||||||
|
|
||||||
|
- **Location**: `mission-planner/src/flightPlanning/MapView.tsx` (imports `MiniMap` on line 16) ↔ `mission-planner/src/flightPlanning/MiniMap.tsx` (imports `UpdateMapCenter` from `./MapView` on line 2).
|
||||||
|
- **Description**: A named-handle cycle internal to `mission-planner/`. Module-layout Verification #5 + `00_discovery.md §7` already record it. Not introduced by this scan. The cycle does NOT cross component boundaries (both files belong to `05_flights` port-source). The cycle disappears at the end of the convergence (when `mission-planner/` is deleted).
|
||||||
|
- **Suggestion**: No action. Track-only finding. Reason: fixing the cycle inside port-source means moving `UpdateMapCenter` to a third file — wasted work given the eventual delete. If the deployed tree gains the same cycle when porting, fix it there.
|
||||||
|
- **Task / Epic**: (none — closed by mission-planner deletion in the final Phase B cycle).
|
||||||
|
|
||||||
|
### F6: No `src/shared/` infrastructure for cross-cutting concerns (Medium / Architecture)
|
||||||
|
|
||||||
|
- **Location**: codebase-wide. No `src/shared/` directory exists today (module-layout Layout Rules #4).
|
||||||
|
- **Description**: There is no shared logger, no central error envelope, no config loader, no telemetry hook. Each feature page does ad-hoc `console.error` + silent catches (multiple module findings — annotations sidebar AI-detect silent catches, dataset silent catches, settings save without `try/finally`). Onboarding observability or a global error boundary today touches every feature.
|
||||||
|
- **Suggestion**: Introduce `src/shared/` (or `src/components/util/`) at Phase B kickoff with:
|
||||||
|
- `shared/logger.ts` — wraps `console.*`, adds revision + user context; replace ad-hoc `console.error`.
|
||||||
|
- `shared/config.ts` — typed `import.meta.env.*` accessor (resolves P10 OpenWeatherMap key + the future env-base-URL refactor).
|
||||||
|
- `shared/errorBoundary.tsx` — application-level boundary (today the SPA has no `ErrorBoundary` — recorded in `src__App-and-main.md` findings).
|
||||||
|
- `shared/endpoints.ts` — typed endpoint constants (closes F7).
|
||||||
|
- **Task / Epic**: Phase B candidate (one cycle for shared infrastructure) OR fold into Step 8 refactor if user picks A on the Step 8 gate.
|
||||||
|
|
||||||
|
### F7: Hardcoded `/api/<service>/...` paths instead of env-driven endpoints (Medium / Architecture)
|
||||||
|
|
||||||
|
- **Location**: every call site of `api.*()` and `createSSE()` across `src/features/**` and `src/auth/`, `src/components/FlightContext.tsx`, `src/components/DetectionClasses.tsx`. Approximately 30 call sites.
|
||||||
|
- **Description**: Consequence of ADR-006 (nginx prefix-strip). Each call site repeats `/api/<service>/<path>` as a string literal. Testability suffers — every test fixture must duplicate paths; any nginx-route change touches every feature. Architecture intent (ADR-006 Consequences) explicitly flags this: *"The SPA hardcodes /api/<service>/... paths in source instead of an env-driven base URL — testability is poor (finding tracked)."*
|
||||||
|
- **Suggestion**: Step 4 testability — introduce `src/shared/endpoints.ts` (or per-component `endpoints.ts` if shared/ is deferred) that exposes typed builders: `endpoints.auth.login()`, `endpoints.flights.byId(id)`, `endpoints.annotations.media(query)`, etc. Replace every string-literal path. Allows tests to mock at the endpoints layer rather than at every `fetch` call. Compounds well with F6 if `src/shared/` lands first.
|
||||||
|
- **Task / Epic**: Step 4 testability (mechanical extract; per-component cohort).
|
||||||
|
|
||||||
|
### F8: Layering-table inconsistency — Header → useAuth is unannotated (Low / Architecture)
|
||||||
|
|
||||||
|
- **Location**: `_docs/02_document/module-layout.md` — "Allowed Dependencies (layering)" table vs "Per-Component Mapping" `03_shared-ui` row.
|
||||||
|
- **Description**: The layering table says Layer 2 may import only from layers 0+1, no same-layer edges. The per-component table explicitly lists `03_shared-ui Imports from: ..., 02_auth`. The actual import `src/components/Header.tsx:3 → ../auth/AuthContext` is by design — every header needs the current user. This is not a code violation (the per-component table is authoritative for specific edges); it's a doc bug.
|
||||||
|
- **Suggestion**: Add a layering-table footnote for `03_shared-ui` similar to the existing `07_dataset` footnote: *"03_shared-ui imports `useAuth` from `02_auth`'s `AuthContext` — same-layer cross-component edge, permitted as it is the only cross-cutting state-context dependency."* Update during Step 4 doc-touchup or fold into Step 13 docs cycle.
|
||||||
|
- **Task / Epic**: documentation only — no code change.
|
||||||
|
|
||||||
|
### F9: Inert second Vite entry tree at port-source root (Low / Architecture)
|
||||||
|
|
||||||
|
- **Location**: `mission-planner/src/{main.tsx,App.tsx,setupTests.ts,vite-env.d.ts}`.
|
||||||
|
- **Description**: `mission-planner/src/` carries its own Vite entrypoint (`main.tsx` + `App.tsx`), test setup file, and env shim. ADR-009 says the deployed Vite build does NOT compile this tree, but there is nothing in the codebase that **prevents** a future contributor from adding `mission-planner/` to a Vite `build.rollupOptions.input` and shipping two SPAs. Architecture intent is "vendored port-source, NOT a deployed component".
|
||||||
|
- **Suggestion**: Step 4 testability or Step 8 refactor — verify `vite.config.ts` has no `mission-planner/` entry; if Vite ever adds workspace-aware builds, add explicit exclusion. Defer until the convergence retires the directory entirely.
|
||||||
|
- **Task / Epic**: closed by F1's final delete cycle.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Routing — what feeds where
|
||||||
|
|
||||||
|
Per `flows/existing-code.md` Step 2 → Step 4 / Step 8 rule, High and Critical Architecture findings must either (a) be appended to the testability `list-of-changes.md` input for Step 4, or (b) be deferred to Step 8 (optional Refactor) via Choose. Recommended routing:
|
||||||
|
|
||||||
|
| Finding | Recommended target step | Reason |
|
||||||
|
|---------|----------------------------------------------------|------------------------------------------------------------------------|
|
||||||
|
| F1 | **Phase B cycles** (NOT Step 4, NOT Step 8) | Behavior ports — need test safety net + per-feature deploy. |
|
||||||
|
| F2 | **Phase B** (when a `CanvasEditor`-touching task lands) | Fold the lift into a behavior change cycle. |
|
||||||
|
| F3 | **Step 4 testability** | Pure file move + import-path update — fits the "smallest fix" rule. |
|
||||||
|
| F4 | **Step 4 testability** | 11 new `index.ts` files + cohort of import-path edits — mechanical. |
|
||||||
|
| F5 | **No action** (closed by F1 final delete) | Fixing inside port-source is wasted work. |
|
||||||
|
| F6 | **Phase B** (one infra cycle) OR **Step 8 refactor** | Shared logger / config / endpoints / error boundary — design choice. |
|
||||||
|
| F7 | **Step 4 testability** | Endpoint extraction enables tests; depends on F6 if `src/shared/` is path. |
|
||||||
|
| F8 | **Documentation touch-up** (no step) | Doc-only. |
|
||||||
|
| F9 | **Defer** | Closed by F1 final delete. |
|
||||||
|
|
||||||
|
The 4 High and 1 Critical findings that should drive a Choose decision now:
|
||||||
|
|
||||||
|
- **F1** — Phase B work-list (not Step 4, not Step 8).
|
||||||
|
- **F2** — Phase B side-quest (not Step 4, not Step 8).
|
||||||
|
- **F3** — Step 4 candidate.
|
||||||
|
- **F4** — Step 4 candidate.
|
||||||
|
- **F5** — track-only (no decision needed).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Baseline Delta
|
||||||
|
|
||||||
|
(N/A — this is the first baseline; no prior report to delta against. Future `code-review` invocations in `cumulative` or `full` mode will emit a Baseline Delta section against this file.)
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# 00 — Foundation
|
||||||
|
|
||||||
|
## 1. High-Level Overview
|
||||||
|
|
||||||
|
**Purpose**: Pure leaf modules every other component depends on — TypeScript domain types, framework-agnostic React hooks, and i18next setup. No HTTP, no React tree, no business logic.
|
||||||
|
|
||||||
|
**Architectural Pattern**: Foundation / shared kernel.
|
||||||
|
|
||||||
|
**Upstream dependencies**: none (intra-repo). External: `i18next`, `react-i18next`, `react`.
|
||||||
|
|
||||||
|
**Downstream consumers**: every other `src/` component.
|
||||||
|
|
||||||
|
## 2. Internal Interfaces
|
||||||
|
|
||||||
|
### `src/types/index.ts`
|
||||||
|
|
||||||
|
| Export | Kind | Purpose |
|
||||||
|
|--------|------|---------|
|
||||||
|
| `DetectionClass`, `Annotation`, `Affiliation`, `CombatReadiness`, `AnnotationStatus`, `AnnotationSource`, `MediaStatus`, `Flight`, `Waypoint`, `DatasetItem`, `User`, `Permission`, etc. | type / enum | Shared domain shape used by `api/`, providers, and every feature page. |
|
||||||
|
|
||||||
|
> **CAVEAT — cross-cutting Step 4 work**. State.json records 4 enum-drift findings against the parent suite spec (`AnnotationStatus`, `Affiliation`, `CombatReadiness`, `MediaStatus`) plus a `Waypoint` shape mismatch. Owner of fix: this single file. The full target shapes live in `_docs/02_document/modules/src__features__annotations.md` findings #27–34 and `src__features__flights.md` finding #20.
|
||||||
|
|
||||||
|
### `src/hooks/useDebounce.ts`
|
||||||
|
|
||||||
|
| Export | Signature | Purpose |
|
||||||
|
|--------|-----------|---------|
|
||||||
|
| `useDebounce<T>(value: T, delay: number): T` | hook | Debounced value mirror. Used by Annotations search and Dataset filters. |
|
||||||
|
|
||||||
|
### `src/hooks/useResizablePanel.ts`
|
||||||
|
|
||||||
|
| Export | Signature | Purpose |
|
||||||
|
|--------|-----------|---------|
|
||||||
|
| `useResizablePanel(initialWidth: number, opts): { width, dragHandleProps }` | hook | Mouse-drag-resizable side panel. Used by Annotations and Dataset pages. Width is **not** persisted (finding #11 in `src__features__annotations.md`). |
|
||||||
|
|
||||||
|
### `src/i18n/i18n.ts`
|
||||||
|
|
||||||
|
| Export | Kind | Purpose |
|
||||||
|
|--------|------|---------|
|
||||||
|
| (default side-effect) | i18next init | Loads `en.json` + `ua.json`, wires `react-i18next`. Imported once by `main.tsx` for its side effect. |
|
||||||
|
|
||||||
|
> **CAVEAT**. `lng: 'en'` is hardcoded; no language detector or persistence. `ua.json` exists as a translation target but is not selectable at runtime (finding #1 in `src__i18n__i18n.md`). This is a Step 4 testability/configurability fix.
|
||||||
|
|
||||||
|
## 5. Implementation Details
|
||||||
|
|
||||||
|
| Module | Notes |
|
||||||
|
|--------|-------|
|
||||||
|
| `types/index.ts` | Plain TypeScript. No runtime code. |
|
||||||
|
| `useDebounce` | `setTimeout` + `clearTimeout` in `useEffect`. |
|
||||||
|
| `useResizablePanel` | `mousemove` listener attached to `window` while dragging; min/max width clamped. |
|
||||||
|
| `i18n/i18n.ts` | i18next + ICU plurals. Bundles loaded synchronously (small JSONs, ~100 keys each). |
|
||||||
|
|
||||||
|
**State Management**: Stateless apart from the local hook state inside `useDebounce` / `useResizablePanel`. i18next's instance is module-level.
|
||||||
|
|
||||||
|
**Key Dependencies**:
|
||||||
|
|
||||||
|
| Library | Version | Purpose |
|
||||||
|
|---------|---------|---------|
|
||||||
|
| `i18next` | (per `package.json`) | Translation engine |
|
||||||
|
| `react-i18next` | (per `package.json`) | React bindings; consumed via `useTranslation` in features |
|
||||||
|
| `react` | 19 | Hooks runtime |
|
||||||
|
|
||||||
|
## 7. Caveats & Edge Cases
|
||||||
|
|
||||||
|
- Enum drift findings (cross-cutting). See state.json notes 2026-05-10 02:01Z and 02:13Z. Step 4 must reconcile against .NET service before patching `types/index.ts`.
|
||||||
|
- `i18n` init is synchronous; if either bundle fails to load the app crashes at startup. No fallback.
|
||||||
|
- `useResizablePanel` does not persist user-chosen width; resets every page nav.
|
||||||
|
|
||||||
|
## 8. Dependency Graph
|
||||||
|
|
||||||
|
**Must be implemented after**: nothing (Layer 0).
|
||||||
|
|
||||||
|
**Can be implemented in parallel with**: itself (the 4 modules are independent).
|
||||||
|
|
||||||
|
**Blocks**: every other component in this workspace.
|
||||||
|
|
||||||
|
## 6. Extensions and Helpers
|
||||||
|
|
||||||
|
`features/annotations/classColors.ts` was originally drafted as part of `06_annotations`, but per the Step 2 BLOCKING gate it has been lifted into its own component, **`11_class-colors`** (sibling shared kernel — see `components/11_class-colors/description.md`). The physical file location is unchanged — only the conceptual ownership moved. The proper physical home (a `src/shared/` or `src/components/detection/` directory) is deferred to Step 2.5 / Step 4 / Step 8.
|
||||||
|
|
||||||
|
## Module Inventory
|
||||||
|
|
||||||
|
| Path | Module Doc |
|
||||||
|
|------|------------|
|
||||||
|
| `src/types/index.ts` | `_docs/02_document/modules/src__types__index.md` |
|
||||||
|
| `src/hooks/useDebounce.ts` | `_docs/02_document/modules/src__hooks__useDebounce.md` |
|
||||||
|
| `src/hooks/useResizablePanel.ts` | `_docs/02_document/modules/src__hooks__useResizablePanel.md` |
|
||||||
|
| `src/i18n/i18n.ts` | `_docs/02_document/modules/src__i18n__i18n.md` |
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# 01 — API Transport
|
||||||
|
|
||||||
|
## 1. High-Level Overview
|
||||||
|
|
||||||
|
**Purpose**: Thin wrappers over the browser's `fetch` and `EventSource` that every feature uses to talk to the suite backend. Sole owners of cookie / bearer / refresh-token plumbing on the wire.
|
||||||
|
|
||||||
|
**Architectural Pattern**: HTTP/SSE facade. No service-specific clients — every feature passes string URLs directly.
|
||||||
|
|
||||||
|
**Upstream dependencies**: `00_foundation` (types).
|
||||||
|
|
||||||
|
**Downstream consumers**: `02_auth`, `03_shared-ui` (FlightContext, DetectionClasses), `05_flights`, `06_annotations`, `07_dataset`, `08_admin`, `09_settings`.
|
||||||
|
|
||||||
|
## 2. Internal Interfaces
|
||||||
|
|
||||||
|
### `src/api/client.ts`
|
||||||
|
|
||||||
|
| Export | Signature | Notes |
|
||||||
|
|--------|-----------|-------|
|
||||||
|
| `api.get<T>(url, opts?): Promise<T>` | thin `fetch` wrapper | Adds `credentials: 'include'`, parses JSON, throws on non-2xx |
|
||||||
|
| `api.post<T>(url, body?, opts?): Promise<T>` | same | |
|
||||||
|
| `api.put<T>(url, body?, opts?): Promise<T>` | same | |
|
||||||
|
| `api.del<T>(url, opts?): Promise<T>` | same | |
|
||||||
|
| `ApiError` | error class | Thrown with `{ status, body }` on non-2xx |
|
||||||
|
|
||||||
|
### `src/api/sse.ts`
|
||||||
|
|
||||||
|
| Export | Signature | Notes |
|
||||||
|
|--------|-----------|-------|
|
||||||
|
| `subscribe<T>(url, onMessage, onError?): { close }` | factory | Creates `EventSource` with the **bearer token in the query string** (browser `EventSource` can't set headers). Returns a `close()` handle. |
|
||||||
|
|
||||||
|
## 3. External API Specification
|
||||||
|
|
||||||
|
This component does not *expose* an API; it consumes the suite's. The set of consumed endpoints (collected from feature module docs):
|
||||||
|
|
||||||
|
| Service | Path prefix (after nginx strip) | Used by |
|
||||||
|
|---------|---------------------------------|---------|
|
||||||
|
| `admin/` auth | `/api/admin/auth/{login,refresh,logout,me,...}` | `02_auth`, `08_admin` |
|
||||||
|
| `flights/` | `/api/flights/...` | `03_shared-ui` (FlightContext), `05_flights` |
|
||||||
|
| `annotations/` | `/api/annotations/...` | `06_annotations`, `07_dataset`, `08_admin` (read) |
|
||||||
|
| `detect/` | `/api/detect/...` | `06_annotations` |
|
||||||
|
| `loader/`, `resource/`, `gps-denied-*`, `autopilot/` | `/api/{loader,resource,gps-denied-desktop,gps-denied-onboard,autopilot}/...` | various features |
|
||||||
|
|
||||||
|
**No service-specific client modules exist**. URL strings are inlined at every call site (testability finding from autodev Step 4).
|
||||||
|
|
||||||
|
## 5. Implementation Details
|
||||||
|
|
||||||
|
| Concern | Behavior |
|
||||||
|
|---------|----------|
|
||||||
|
| Auth bootstrap | `client.ts` does NOT auto-attach `credentials: 'include'` on the very first call from `AuthContext` startup — finding B3 (`src__auth__AuthContext.md`). Cookie-based session bootstrap therefore fails on first refresh. **PRIORITY for Step 4.** |
|
||||||
|
| Refresh-token rotation | Server rotates access tokens via `X-Refresh-Token` header. `client.ts` handles refresh on 401 for fetch; `sse.ts` does **NOT** — `EventSource` holds the bearer captured at create time and dies after rotation (finding in `src__api__sse.md`). |
|
||||||
|
| Timeouts | None — no `AbortController` wired up. Long requests (e.g., dataset bulk export) can hang indefinitely. Step 4. |
|
||||||
|
| Error reporting | `ApiError` thrown to caller. Most features `catch` and call `alert()` or `console.error` silently — uneven across features. |
|
||||||
|
|
||||||
|
**State Management**: Module-level only — `sse.ts` keeps no registry; each subscription is independent.
|
||||||
|
|
||||||
|
**Key Dependencies**: native `fetch`, native `EventSource`. No third-party HTTP library.
|
||||||
|
|
||||||
|
## 7. Caveats & Edge Cases
|
||||||
|
|
||||||
|
- **No timeout / cancellation**. (Step 4.)
|
||||||
|
- **Bearer in SSE query string**. Accepted trade-off; document in `security_approach.md` (Step 6).
|
||||||
|
- **No reconnect-on-token-rotate** for SSE consumers — every feature that uses SSE will silently stop receiving events after the first refresh (Step 8 hardening).
|
||||||
|
- **No service-specific clients** → URL strings duplicated across features. Risk of typos surfacing as 404s only at runtime (Step 4).
|
||||||
|
|
||||||
|
## 8. Dependency Graph
|
||||||
|
|
||||||
|
**Must be implemented after**: `00_foundation`.
|
||||||
|
|
||||||
|
**Can be implemented in parallel with**: `00_foundation` (it has no internal deps beyond types).
|
||||||
|
|
||||||
|
**Blocks**: `02_auth`, every feature page, `03_shared-ui` (FlightContext, DetectionClasses).
|
||||||
|
|
||||||
|
## Module Inventory
|
||||||
|
|
||||||
|
| Path | Module Doc |
|
||||||
|
|------|------------|
|
||||||
|
| `src/api/client.ts` | `_docs/02_document/modules/src__api__client.md` |
|
||||||
|
| `src/api/sse.ts` | `_docs/02_document/modules/src__api__sse.md` |
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# 02 — Auth
|
||||||
|
|
||||||
|
## 1. High-Level Overview
|
||||||
|
|
||||||
|
**Purpose**: Authentication state, login/logout/refresh plumbing, and the `<ProtectedRoute>` gate that wraps every non-public route.
|
||||||
|
|
||||||
|
**Architectural Pattern**: React Context provider + route-guard component.
|
||||||
|
|
||||||
|
**Upstream dependencies**: `00_foundation` (types), `01_api-transport`.
|
||||||
|
|
||||||
|
**Downstream consumers**: `04_login`, `10_app-shell`, every authenticated page (indirectly via the route gate).
|
||||||
|
|
||||||
|
## 2. Internal Interfaces
|
||||||
|
|
||||||
|
### `src/auth/AuthContext.tsx`
|
||||||
|
|
||||||
|
| Export | Signature | Notes |
|
||||||
|
|--------|-----------|-------|
|
||||||
|
| `AuthProvider({ children })` | React component | Wraps the app below `BrowserRouter`. Bootstraps via `GET /api/admin/auth/refresh` on mount. |
|
||||||
|
| `useAuth(): AuthContextValue` | hook | Read-only access to `{ user, permissions, login, logout, refresh, loading }`. |
|
||||||
|
|
||||||
|
**`AuthContextValue`** (output DTO):
|
||||||
|
|
||||||
|
```
|
||||||
|
user: User | null
|
||||||
|
permissions: Permission[] ← from server, used by route guards & UI
|
||||||
|
loading: boolean ← true during initial bootstrap and active refresh
|
||||||
|
login(c): Promise<User> ← POST /api/admin/auth/login
|
||||||
|
logout(): Promise<void> ← POST /api/admin/auth/logout
|
||||||
|
refresh(): Promise<void> ← POST /api/admin/auth/refresh
|
||||||
|
```
|
||||||
|
|
||||||
|
### `src/auth/ProtectedRoute.tsx`
|
||||||
|
|
||||||
|
| Export | Signature | Notes |
|
||||||
|
|--------|-----------|-------|
|
||||||
|
| `ProtectedRoute({ children, requirePermission? })` | React component | Renders children if authenticated; otherwise navigates to `/login`. Optional `requirePermission` is checked against `useAuth().permissions` and renders a 403 placeholder on miss. |
|
||||||
|
|
||||||
|
## 3. External API Specification
|
||||||
|
|
||||||
|
Consumes only — does not expose. Endpoint set (from `_docs/02_document/modules/src__auth__AuthContext.md`):
|
||||||
|
|
||||||
|
| Method | Path | Auth | When |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| POST | `/api/admin/auth/login` | public | Login form submit |
|
||||||
|
| POST | `/api/admin/auth/refresh` | cookie | Bootstrap + on 401 retry |
|
||||||
|
| POST | `/api/admin/auth/logout` | cookie | Header → Logout |
|
||||||
|
| GET | `/api/admin/auth/me` | cookie | (post-login profile fetch, if implemented) |
|
||||||
|
|
||||||
|
## 5. Implementation Details
|
||||||
|
|
||||||
|
**State Management**: Single React context. Token lives in an HTTP-only cookie (server-managed); the React state holds only the parsed user + permissions. No `localStorage`.
|
||||||
|
|
||||||
|
**Bootstrap sequence**:
|
||||||
|
1. Mount → set `loading: true`.
|
||||||
|
2. `api.post('/api/admin/auth/refresh')` to ask the server "do I have a valid session?".
|
||||||
|
3. On 200 → store user + permissions, `loading: false`.
|
||||||
|
4. On 4xx → user stays `null`, `loading: false`. `ProtectedRoute` then redirects.
|
||||||
|
|
||||||
|
> **PRIORITY finding (B3, copied from state.json)**: the bootstrap call inside `AuthContext.tsx` does not pass `credentials: 'include'` consistently — the cookie is therefore not sent on the very first request and bootstrap silently fails on a fresh page load. Confirmed real bug; Step 4 fix.
|
||||||
|
|
||||||
|
**Spinner UX**: `ProtectedRoute` renders a centered spinner during `loading`. The spinner has **no** `role="status"` / no accessible label / no timeout. (Findings B4, joint with Step 4 client.ts timeout flag.)
|
||||||
|
|
||||||
|
## 7. Caveats & Edge Cases
|
||||||
|
|
||||||
|
- **Bootstrap missing `credentials: 'include'`** → users land on `/login` even with a valid cookie session. PRIORITY Step 4.
|
||||||
|
- **Spinner accessibility** — Step 4.
|
||||||
|
- **Token-rotation interaction with SSE** — see `01_api-transport`. Auth refresh works for fetch but breaks every active EventSource.
|
||||||
|
- **No idle-timeout / inactivity logout** — server-side concern; UI tolerates whatever the server enforces.
|
||||||
|
|
||||||
|
## 8. Dependency Graph
|
||||||
|
|
||||||
|
**Must be implemented after**: `00_foundation`, `01_api-transport`.
|
||||||
|
|
||||||
|
**Can be implemented in parallel with**: nothing inside this workspace's gate path.
|
||||||
|
|
||||||
|
**Blocks**: `03_shared-ui` (Header reads `useAuth`), `04_login`, `10_app-shell`, indirectly every authenticated page.
|
||||||
|
|
||||||
|
## Module Inventory
|
||||||
|
|
||||||
|
| Path | Module Doc |
|
||||||
|
|------|------------|
|
||||||
|
| `src/auth/AuthContext.tsx` | `_docs/02_document/modules/src__auth__AuthContext.md` |
|
||||||
|
| `src/auth/ProtectedRoute.tsx` | `_docs/02_document/modules/src__auth__ProtectedRoute.md` |
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# 03 — Shared UI & Cross-Cutting Context
|
||||||
|
|
||||||
|
## 1. High-Level Overview
|
||||||
|
|
||||||
|
**Purpose**: Reusable presentational components and the **flight selection context** that every authenticated page reads from. This is the "page chrome + flight scope" layer — the navbar, the help modal, the confirmation dialog, the detection-class picker, and the `FlightContext` provider that holds "which flight the user is currently working in".
|
||||||
|
|
||||||
|
**Architectural Pattern**: Mix of presentational components + one cross-cutting Context provider. `FlightContext` lives in `components/` (not `features/flights/`) because it is read by every feature page (Annotations, Dataset, Admin, Settings, Header).
|
||||||
|
|
||||||
|
**Upstream dependencies**: `00_foundation` (types), `01_api-transport`, `02_auth` (Header reads `useAuth`), `11_class-colors` (DetectionClasses fallback colors / names).
|
||||||
|
|
||||||
|
**Downstream consumers**: `10_app-shell` (mounts Header + FlightProvider), every feature page (consumes `useFlight()` and uses ConfirmDialog), `06_annotations` and `07_dataset` (use DetectionClasses + ConfirmDialog).
|
||||||
|
|
||||||
|
## 2. Internal Interfaces
|
||||||
|
|
||||||
|
### `src/components/Header.tsx`
|
||||||
|
|
||||||
|
| Export | Notes |
|
||||||
|
|--------|-------|
|
||||||
|
| `Header()` | Top bar: app logo, page nav links (`/flights`, `/annotations`, `/dataset`, `/admin`, `/settings`), flight-picker dropdown, language switch, user menu (logout, help). Mobile bottom-nav variant rendered conditionally (lines 113–129 per state.json correction). |
|
||||||
|
|
||||||
|
### `src/components/HelpModal.tsx`
|
||||||
|
|
||||||
|
| Export | Notes |
|
||||||
|
|--------|-------|
|
||||||
|
| `HelpModal({ open, onClose })` | Renders an in-page modal with `GUIDELINES` (currently a hardcoded string — finding: should be in i18n bundle). Esc does NOT close (inconsistent with `ConfirmDialog`). |
|
||||||
|
|
||||||
|
### `src/components/ConfirmDialog.tsx`
|
||||||
|
|
||||||
|
| Export | Notes |
|
||||||
|
|--------|-------|
|
||||||
|
| `ConfirmDialog({ open, title, message, confirmLabel, onConfirm, onCancel })` | Reusable confirm modal. Esc closes. **Missing `aria-modal` and `role="dialog"`** — finding flagged for Step 4 vs `ui_design/README.md` confirmation-dialogs spec. |
|
||||||
|
|
||||||
|
### `src/components/DetectionClasses.tsx`
|
||||||
|
|
||||||
|
| Export | Notes |
|
||||||
|
|--------|-------|
|
||||||
|
| `DetectionClasses({ value, onChange, ... })` | Detection-class picker grid. Loads classes from `GET /api/annotations/classes`. Number-key shortcuts 1–9 select `classes[idx + photoMode]` — ordering against the annotations service contract is unverified (Step 4). Imports from `11_class-colors` for fallback color and name (current physical path: `src/features/annotations/classColors.ts` — file location is misplaced; see `11_class-colors` §7 for refactor target). |
|
||||||
|
|
||||||
|
### `src/components/FlightContext.tsx`
|
||||||
|
|
||||||
|
| Export | Notes |
|
||||||
|
|--------|-------|
|
||||||
|
| `FlightProvider({ children })` | Loads flight list (paged, **`pageSize=1000` ceiling hardcoded** — finding B3) on mount. |
|
||||||
|
| `useFlight(): FlightContextValue` | hook returning `{ flights, selectedFlight, selectFlight, refresh }`. `selectFlight` is **fire-and-forget** PUT to `/api/flights/select` — no error UI. |
|
||||||
|
|
||||||
|
## 5. Implementation Details
|
||||||
|
|
||||||
|
**State Management**:
|
||||||
|
- Local component state for modals (open/closed).
|
||||||
|
- One Context provider (`FlightProvider`) holding the cached flight list and current selection.
|
||||||
|
- No global event bus.
|
||||||
|
|
||||||
|
**Routing awareness**: `Header` reads `useLocation()` to highlight the active link. `FlightProvider` does **not** persist selection across reloads.
|
||||||
|
|
||||||
|
**Accessibility** (cross-cutting findings, Step 4):
|
||||||
|
- `ConfirmDialog` — no `aria-modal` / `role="dialog"` / focus trap.
|
||||||
|
- `Header` flight dropdown — no `role="combobox"`, no `aria-expanded`, no Esc-to-close, no focus trap; outside-click handler always attached.
|
||||||
|
- `HelpModal` — Esc does NOT close.
|
||||||
|
|
||||||
|
**Key Dependencies**: `react-router-dom` 7 (Header), `react-i18next`.
|
||||||
|
|
||||||
|
## 6. Extensions and Helpers
|
||||||
|
|
||||||
|
Class color / fallback name / PhotoMode suffix logic lives in `11_class-colors`. This component depends on it; it is no longer treated as a cross-layer leak — the *physical* file is misplaced (still in `src/features/annotations/`) but the *logical* dependency is a normal shared-layer dependency.
|
||||||
|
|
||||||
|
## 7. Caveats & Edge Cases
|
||||||
|
|
||||||
|
- **`classColors.ts` physical location** is `src/features/annotations/` even though it is logically a `11_class-colors` shared module. Step 4 testability candidate (file move) — does not break this component's dependency graph.
|
||||||
|
- **`FlightContext.pageSize=1000`** — silent ceiling; >1000 flights are invisible.
|
||||||
|
- **`selectFlight` fire-and-forget PUT** — server failures are invisible to the UI.
|
||||||
|
- **`Header` flight dropdown a11y gaps** — Step 4 + Step 8.
|
||||||
|
- **`HelpModal` GUIDELINES hardcoded** — Step 4 i18n.
|
||||||
|
|
||||||
|
## 8. Dependency Graph
|
||||||
|
|
||||||
|
**Must be implemented after**: `00_foundation`, `01_api-transport`, `02_auth`, `11_class-colors`.
|
||||||
|
|
||||||
|
**Can be implemented in parallel with**: nothing critical inside the workspace.
|
||||||
|
|
||||||
|
**Blocks**: `10_app-shell`, every feature page (they import `ConfirmDialog`, `useFlight`, and read `Header` from the shell).
|
||||||
|
|
||||||
|
## Module Inventory
|
||||||
|
|
||||||
|
| Path | Module Doc |
|
||||||
|
|------|------------|
|
||||||
|
| `src/components/Header.tsx` | `_docs/02_document/modules/src__components__Header.md` |
|
||||||
|
| `src/components/HelpModal.tsx` | `_docs/02_document/modules/src__components__HelpModal.md` |
|
||||||
|
| `src/components/ConfirmDialog.tsx` | `_docs/02_document/modules/src__components__ConfirmDialog.md` |
|
||||||
|
| `src/components/DetectionClasses.tsx` | `_docs/02_document/modules/src__components__DetectionClasses.md` |
|
||||||
|
| `src/components/FlightContext.tsx` | `_docs/02_document/modules/src__components__FlightContext.md` |
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# 04 — Login
|
||||||
|
|
||||||
|
## 1. High-Level Overview
|
||||||
|
|
||||||
|
**Purpose**: The single public route `/login`. Renders the credential form and triggers `useAuth().login(...)`.
|
||||||
|
|
||||||
|
**Architectural Pattern**: Single-page feature module with one entry component.
|
||||||
|
|
||||||
|
**Upstream dependencies**: `02_auth` (`useAuth().login`).
|
||||||
|
|
||||||
|
**Downstream consumers**: `10_app-shell` (routed at `/login`).
|
||||||
|
|
||||||
|
## 2. Internal Interfaces
|
||||||
|
|
||||||
|
| Export | Notes |
|
||||||
|
|--------|-------|
|
||||||
|
| `LoginPage()` | Form: username + password + submit. Calls `useAuth().login({ username, password })` and on success navigates to `/flights`. Error state shown inline. |
|
||||||
|
|
||||||
|
## 5. Implementation Details
|
||||||
|
|
||||||
|
- "Theatrical" unlock animation (`runUnlockSequence`, 4 × 600 ms) plays on success before navigation. Documented in Step 5 solution.md as a UX choice, not a bug.
|
||||||
|
- No "remember me" / persistent-session toggle.
|
||||||
|
- No SSO integration.
|
||||||
|
- No password-strength feedback or recovery link.
|
||||||
|
|
||||||
|
**State Management**: Local component state only.
|
||||||
|
|
||||||
|
**Key Dependencies**: `react-router-dom` (`useNavigate`), `react-i18next`.
|
||||||
|
|
||||||
|
## 7. Caveats & Edge Cases
|
||||||
|
|
||||||
|
- **2.4 s artificial delay** post-login (Step 5 doc note).
|
||||||
|
- **No CSRF token** in the login POST body — server is expected to validate the same-site cookie pattern; document in `security_approach.md` (Step 6).
|
||||||
|
- **No rate-limit feedback** — server returns `429` with no specific UI handling beyond a generic `alert`.
|
||||||
|
|
||||||
|
## 8. Dependency Graph
|
||||||
|
|
||||||
|
**Must be implemented after**: `02_auth`.
|
||||||
|
|
||||||
|
**Can be implemented in parallel with**: every feature page (`05_flights` … `09_settings`).
|
||||||
|
|
||||||
|
**Blocks**: nothing.
|
||||||
|
|
||||||
|
## Module Inventory
|
||||||
|
|
||||||
|
| Path | Module Doc |
|
||||||
|
|------|------------|
|
||||||
|
| `src/features/login/LoginPage.tsx` | `_docs/02_document/modules/src__features__login__LoginPage.md` |
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
# 05 — Flights & Mission Planning
|
||||||
|
|
||||||
|
## 1. High-Level Overview
|
||||||
|
|
||||||
|
**Purpose**: One logical component covering everything mission/flight related — flight CRUD, waypoint editing, altitude profile, wind/battery calc, raw mission JSON I/O, and **GPS-Denied** operations (incl. an end-to-end **test mode** that simulates a real flight from a tlog + video pair). It is currently **physically split** across two codebases that the project intends to converge.
|
||||||
|
|
||||||
|
**Architectural Pattern**: Page composition (`FlightsPage`) wiring three sibling panels — `FlightListSidebar`, `FlightParamsPanel`, `FlightMap` — plus modals (`AltitudeDialog`, `JsonEditorDialog`) and a GPS-Denied sub-page.
|
||||||
|
|
||||||
|
**Implementation status — two trees, one component**:
|
||||||
|
|
||||||
|
| Tree | What it is | Deployed? |
|
||||||
|
|------|------------|-----------|
|
||||||
|
| `src/features/flights/` (15 modules, React 19 + Tailwind) | The **target** implementation. Mostly a mechanical port-in-progress of the tree below. | **Yes** — Dockerfile builds `src/` only. |
|
||||||
|
| `mission-planner/` (37 modules, React 18 + MUI 5) | The **port source / reference**. The richer, more battle-tested mission planner that the new SPA is being adapted from. | **No.** Disjoint dependency island. Deletion candidate after parity. |
|
||||||
|
|
||||||
|
The two trees are intentionally disjoint at the file level (no cross-imports — `00_discovery.md` §1) but they are **one component in the design**: same domain, same data model, same intent. Findings, port plan, and architecture decisions are tracked in this single component spec. Mission-planner files are listed in §"Module Inventory" below alongside the new SPA files; per-finding origin is preserved.
|
||||||
|
|
||||||
|
**Upstream dependencies** (target tree): `00_foundation`, `01_api-transport`, `03_shared-ui` (FlightContext, ConfirmDialog).
|
||||||
|
|
||||||
|
**Downstream consumers**: `10_app-shell` (routes `/flights` and the GPS-Denied sub-page).
|
||||||
|
|
||||||
|
## 2. Internal Interfaces
|
||||||
|
|
||||||
|
### Page entries (target tree, `src/features/flights/`)
|
||||||
|
|
||||||
|
| Export | Notes |
|
||||||
|
|--------|-------|
|
||||||
|
| `FlightsPage()` | Top-level route component for `/flights`. Uses `useFlight()`, fetches flight detail by id, exposes save/delete/duplicate. Wires `react-leaflet`, `leaflet-draw`, and `chart.js`. |
|
||||||
|
| `GpsDeniedPage()` *(planned route, see §6)* | Sub-page for GPS-denied operations and test mode. Today a partial inline panel inside `FlightsPage` (finding #25) — slated to become its own route. |
|
||||||
|
|
||||||
|
### Internal modules — target tree (`src/features/flights/`)
|
||||||
|
|
||||||
|
| Module | Role |
|
||||||
|
|--------|------|
|
||||||
|
| `FlightsPage.tsx` | Orchestrator (route component) |
|
||||||
|
| `FlightMap.tsx` | Leaflet map + draw control + waypoint markers + minimap |
|
||||||
|
| `FlightListSidebar.tsx` | Left panel: flight list, search, new/duplicate/delete |
|
||||||
|
| `FlightParamsPanel.tsx` | Right panel: name, aircraft, takeoff/landing, altitude chart, waypoint list, wind effect |
|
||||||
|
| `WaypointList.tsx` | DnD-sortable waypoints (`@hello-pangea/dnd`) |
|
||||||
|
| `AltitudeChart.tsx` | `chart.js` altitude profile |
|
||||||
|
| `WindEffect.tsx` | Wind-vector visualisation |
|
||||||
|
| `MiniMap.tsx` | Inline overview map |
|
||||||
|
| `MapPoint.tsx` | Single waypoint marker |
|
||||||
|
| `DrawControl.tsx` | `leaflet-draw` integration |
|
||||||
|
| `AltitudeDialog.tsx` | Per-waypoint altitude/purpose modal |
|
||||||
|
| `JsonEditorDialog.tsx` | Raw mission-JSON editor |
|
||||||
|
| `flightPlanUtils.ts` | Distance / battery / weather computation helpers |
|
||||||
|
| `mapIcons.ts` | Leaflet icon factory |
|
||||||
|
| `types.ts` | Local feature types (waypoint shape, mission JSON shape) |
|
||||||
|
|
||||||
|
### Internal modules — port source (`mission-planner/`)
|
||||||
|
|
||||||
|
| Group | Modules | Role / what the target should learn |
|
||||||
|
|-------|---------|-------------------------------------|
|
||||||
|
| Entry | `main.tsx`, `App.tsx` (vestigial CRA stub) | Composition root + LanguageProvider. The CRA stub `App.tsx` is a deletion candidate post-port. |
|
||||||
|
| Page composition | `flightPlanning/flightPlan.tsx`, `LeftBoard.tsx` | Canonical page shape (sidebar + map). |
|
||||||
|
| Map | `flightPlanning/MapView.tsx` (cycle-paired with `MiniMap.tsx`), `MiniMap.tsx`, `DrawControl.tsx`, `MapPoint.tsx` | Reference Leaflet integration. **Cycle**: `MiniMap` imports the *named* helper `UpdateMapCenter` from `MapView`; `MapView` imports `MiniMap` as JSX child. Document the contract precisely if porting both at once. |
|
||||||
|
| Panels | `flightPlanning/PointsList.tsx`, `AltitudeChart.tsx`, `AltitudeDialog.tsx`, `WindEffect.tsx`, `TotalDistance.tsx`, `JsonEditorDialog.tsx`, `LanguageSwitcher.tsx`, `Aircraft.ts` | Reference panel shapes. Several have richer behaviour than the current SPA siblings. |
|
||||||
|
| Services | `services/calculateBatteryUsage.ts`, `AircraftService.ts`, `WeatherService.ts`, `calculateDistance.ts` | **Authoritative** battery / weather / distance logic. The target's `flightPlanUtils.ts` is currently an inferior port (silent errors, sequential `await`, hardcoded API key). |
|
||||||
|
| i18n | `flightPlanning/LanguageContext.tsx`, `constants/translations.ts`, `constants/languages.ts` | Local translation pattern. The port should converge to `00_foundation/i18n` instead. |
|
||||||
|
| Constants | `constants/{actionModes,maptypes,tileUrls,purposes}.ts` | Reference constant tables. |
|
||||||
|
| Icons | `icons/{MapIcons,PointIcons,SidebarIcons,PhoneIcon}.tsx` | Reference icon factory. |
|
||||||
|
| Utilities | `utils.ts`, `config.ts`, `types/index.ts` | Reference helpers + types. |
|
||||||
|
| Test (vestigial) | `test/jsonImport.test.ts`, `setupTests.ts` | One Jest test that **cannot run today** (Jest not in `package.json`). Out of scope for the live SPA test plan. |
|
||||||
|
|
||||||
|
## 3. External API Specification
|
||||||
|
|
||||||
|
Endpoints consumed (target tree + planned for GPS-Denied):
|
||||||
|
|
||||||
|
| Method | Path | Purpose |
|
||||||
|
|--------|------|---------|
|
||||||
|
| GET | `/api/flights` | List + page (`pageSize=1000` ceiling from `FlightContext`) |
|
||||||
|
| GET | `/api/flights/{id}` | Detail + waypoints |
|
||||||
|
| POST | `/api/flights` | Create |
|
||||||
|
| PUT | `/api/flights/{id}` | Update flight metadata |
|
||||||
|
| DELETE | `/api/flights/{id}` | Delete |
|
||||||
|
| POST/DELETE | `/api/flights/{id}/waypoints` | Add / remove waypoints |
|
||||||
|
| POST | `/api/gps-denied-desktop/...` | GPS-denied desktop service (mission setup, plan upload) — partial today |
|
||||||
|
| POST | `/api/gps-denied-onboard/...` | GPS-denied onboard service (frame + IMU consumer) — target of test-mode SITL output |
|
||||||
|
| GET | OpenWeatherMap (third-party, **direct from browser**) | Wind/temperature lookup. **Will be proxied via suite** as part of Step 4 (hardcoded key removal). |
|
||||||
|
|
||||||
|
Concrete GPS-Denied endpoint shapes are **not yet finalised** in the suite spec — flagged for confirmation in autodev Step 3 (Test Spec) and Step 6 (Problem Extraction).
|
||||||
|
|
||||||
|
## 5. Implementation Details
|
||||||
|
|
||||||
|
**State Management**: Page-local React state for the active flight; `FlightContext` (in `03_shared-ui`) for the list of flights.
|
||||||
|
|
||||||
|
**Save model — current shape, target tree** (finding #19): on Save, the UI deletes all existing waypoints and POSTs each one again. N delete + M POST round-trips. **Lossy** — concurrent edits race; if a POST fails halfway through, the saved flight is left with truncated waypoints. The `mission-planner/` reference does this differently (single mission-JSON PUT) and is the recommended target shape.
|
||||||
|
|
||||||
|
**Waypoint POST shape mismatch (PRIORITY, finding #20)**: target tree sends `{name, latitude, longitude, order}`; suite spec wants `{Geopoint:{Lat,Lon,MGRS}, Source, Objective, OrderNum, Height}`. Strict server returns 400. Owner of fix: this component + `00_foundation/types/index.ts::Waypoint`.
|
||||||
|
|
||||||
|
**Battery / weather calc** (target tree `flightPlanUtils.ts`):
|
||||||
|
- **Hardcoded OpenWeather API key in source** (`'335799082893fad97fa36118b131f919'`) — committed secret. Step 4 fix.
|
||||||
|
- Sequential `await` per segment — perf trap on long missions.
|
||||||
|
- Silent `try/catch` swallows weather errors.
|
||||||
|
- Mixes km / m altitudes; ambiguous battery-capacity unit (Wh vs Ws).
|
||||||
|
- Port source (`mission-planner/services/`) has a cleaner, more correct implementation — use it as the reference.
|
||||||
|
|
||||||
|
**Map / icons**:
|
||||||
|
- Target tree `mapIcons.ts::defaultIcon` CDN URL pinned to `leaflet@1.7.1` while `package.json` has 1.9.4.
|
||||||
|
- `MiniMap` map-tile licence attribution missing in some configurations.
|
||||||
|
|
||||||
|
**Modal a11y** (`AltitudeDialog`, `JsonEditorDialog`) — no `aria-modal` / `role="dialog"` / focus trap. Step 4. Same gap on the port-source counterparts.
|
||||||
|
|
||||||
|
**Tech-stack divergence between the two trees** (carry into the port plan):
|
||||||
|
|
||||||
|
| Concern | `mission-planner/` | `src/features/flights/` (target) |
|
||||||
|
|---------|-------------------|----------------------------------|
|
||||||
|
| React | 18 | 19 |
|
||||||
|
| UI library | MUI 5 | Tailwind 4 + custom `az-*` tokens |
|
||||||
|
| i18n | local `LanguageContext` | `react-i18next` (Foundation) |
|
||||||
|
| `react-leaflet` | 4.2 | 5 |
|
||||||
|
| `@hello-pangea/dnd` | 16 | 18 |
|
||||||
|
|
||||||
|
**Key Dependencies** (target tree): `leaflet` 1.9, `react-leaflet` 5, `leaflet-draw`, `leaflet-polylinedecorator`, `chart.js` 4, `@hello-pangea/dnd` 18.
|
||||||
|
|
||||||
|
## 6. GPS-Denied sub-feature
|
||||||
|
|
||||||
|
Today a **partial** UI panel exists inside `FlightsPage` (target tree finding #25). The component design promotes this to a first-class sub-page with two tabs.
|
||||||
|
|
||||||
|
### 6a. Operations tab — current intent (already present, partial)
|
||||||
|
|
||||||
|
- Upload / select a mission plan.
|
||||||
|
- Configure GPS-denied desktop parameters.
|
||||||
|
- Hand off to onboard.
|
||||||
|
|
||||||
|
The exact endpoints are partially wired today and tracked under "External API Specification" above.
|
||||||
|
|
||||||
|
### 6b. **Test mode** — new subitem (per `_docs/how_to_test.md`)
|
||||||
|
|
||||||
|
> **Source of truth**: `_docs/how_to_test.md`. Architecture Vision artifact (`/document` Step 4.5) MUST surface this verbatim.
|
||||||
|
|
||||||
|
**Goal**: enable end-to-end testing of the GPS-denied onboard system **without a real flight**, using a recorded telemetry log + video pair.
|
||||||
|
|
||||||
|
**User journey**:
|
||||||
|
|
||||||
|
1. **Upload tlog file** (MAVLink telemetry log).
|
||||||
|
2. **Upload video** synced with the tlog (the two are **usually not** time-aligned at the file level — the system aligns them).
|
||||||
|
3. The system:
|
||||||
|
- Extracts **timestamps, IMU, and GPS** samples from the tlog.
|
||||||
|
- **Auto-syncs** video to tlog by detecting the take-off moment in the IMU stream and matching it to the corresponding frame in the video. Most test sessions are quadcopter "ground-to-ground" flights; the start-on-ground signature in the IMU is the alignment anchor.
|
||||||
|
- Spawns a **SITL** (Software-In-The-Loop) instance.
|
||||||
|
- **Feeds IMU samples + video frames** into the GPS-denied **onboard** system in lockstep.
|
||||||
|
4. The user observes the onboard system's outputs (position estimate, drift, GPS-recovery moments) on the same map / chart components used for live flights — reusing `FlightMap`, `AltitudeChart`, and a results overlay.
|
||||||
|
|
||||||
|
**Scope notes for downstream skills**:
|
||||||
|
|
||||||
|
- This sub-feature is **not implemented today**. It is a planned addition that this component will own.
|
||||||
|
- The tlog parser, IMU/video sync, and SITL controller are **suite-side services**; this component contributes the upload UI, the run controller, and the result overlay.
|
||||||
|
- Test-mode I/O endpoints will live under `/api/gps-denied-desktop/test/...` (proposed; confirm in `autodev` Step 3 / Step 6).
|
||||||
|
- The existing `/document` Step 1 finding "GPS-Denied panel partial" remains valid for the Operations tab and is broadened to track the Test mode tab as well.
|
||||||
|
|
||||||
|
## 7. Caveats & Edge Cases
|
||||||
|
|
||||||
|
26 numbered findings consolidated in `src__features__flights.md`. Highest-priority Step 4 items:
|
||||||
|
|
||||||
|
1. **Hardcoded OpenWeather API key** — rotate + remove + proxy via suite.
|
||||||
|
2. **Waypoint POST shape mismatch + delete-then-recreate save model** — likely-broken on a strict suite server. Reference port-source's PUT-mission-JSON model.
|
||||||
|
3. **`flightPlanUtils.ts` units / silent errors / sequential awaits.** Replace with port-source services.
|
||||||
|
4. **Modal a11y** + **flight dropdown a11y** (overlap with Shared UI).
|
||||||
|
5. **Leaflet 1.7.1 CDN URL drift.**
|
||||||
|
|
||||||
|
Mission-planner-side caveats (port-only):
|
||||||
|
|
||||||
|
- `mission-planner/src/test/jsonImport.test.ts` cannot run (Jest absent). **Do not** add Jest just for this one test.
|
||||||
|
- `mission-planner/src/App.tsx` is a CRA stub. Delete only after parity.
|
||||||
|
- `MapView.tsx ↔ MiniMap.tsx` named-handle cycle. Document precisely.
|
||||||
|
|
||||||
|
GPS-Denied-side caveats:
|
||||||
|
|
||||||
|
- Operations tab is partial (finding #25 unchanged).
|
||||||
|
- **Test mode** is unimplemented — no risk of regression today, but the design must accommodate the eventual test-mode entry point in routing and in `FlightContext`.
|
||||||
|
|
||||||
|
## 8. Dependency Graph
|
||||||
|
|
||||||
|
**Must be implemented after**: `00_foundation`, `01_api-transport`, `03_shared-ui`.
|
||||||
|
|
||||||
|
**Can be implemented in parallel with**: every other feature page.
|
||||||
|
|
||||||
|
**Blocks**: `10_app-shell` (routes to `/flights` as default authenticated landing; will also host the GPS-Denied sub-route).
|
||||||
|
|
||||||
|
**Internal port order** (within this component): port `services/` (battery/weather/distance) → port `flightPlanning/` panels (`AltitudeChart`, `WindEffect`, `PointsList`) → port `MapView/MiniMap` cycle group → fold the `mission-planner/` tree out as the target reaches parity.
|
||||||
|
|
||||||
|
## Module Inventory
|
||||||
|
|
||||||
|
| Tree | Module Doc |
|
||||||
|
|------|------------|
|
||||||
|
| `src/features/flights/*` (15 modules) | `_docs/02_document/modules/src__features__flights.md` |
|
||||||
|
| `mission-planner/*` (37 modules) | `_docs/02_document/modules/mission-planner.md` |
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
# 06 — Annotations
|
||||||
|
|
||||||
|
## 1. High-Level Overview
|
||||||
|
|
||||||
|
**Purpose**: Image / video annotation editor — bounding boxes, classes, affiliation, combat readiness, AI detection (sync + async via SSE), and the media-list browser scoped to the active flight.
|
||||||
|
|
||||||
|
**Architectural Pattern**: Page composition (`AnnotationsPage`) wiring `MediaList` + `VideoPlayer` (or static image) + `CanvasEditor` + `AnnotationsSidebar` + `DetectionClasses`.
|
||||||
|
|
||||||
|
**Upstream dependencies**: `00_foundation`, `01_api-transport`, `03_shared-ui` (FlightContext, ConfirmDialog, DetectionClasses), `11_class-colors`.
|
||||||
|
|
||||||
|
**Downstream consumers**: `10_app-shell` (routed at `/annotations`); `07_dataset` imports `CanvasEditor` directly (cross-feature edge — see baseline scan).
|
||||||
|
|
||||||
|
## 2. Internal Interfaces
|
||||||
|
|
||||||
|
| Export | Notes |
|
||||||
|
|--------|-------|
|
||||||
|
| `AnnotationsPage()` | Top-level route component. Manages selected media, panel widths (via `useResizablePanel`), and the open annotation under edit. |
|
||||||
|
|
||||||
|
Internal modules:
|
||||||
|
|
||||||
|
| Module | Role |
|
||||||
|
|--------|------|
|
||||||
|
| `AnnotationsPage.tsx` | Orchestrator (route component) |
|
||||||
|
| `MediaList.tsx` | Left panel: thumbnail browser + search/filter; consumes `useFlight()` |
|
||||||
|
| `VideoPlayer.tsx` | HTML5 video + frame seek + per-frame annotation overlays |
|
||||||
|
| `CanvasEditor.tsx` | Bounding-box draw / move / resize layer (also used by `07_dataset`) |
|
||||||
|
| `AnnotationsSidebar.tsx` | Right panel: annotation list, AI-detect controls |
|
||||||
|
|
||||||
|
## 3. External API Specification
|
||||||
|
|
||||||
|
| Method | Path | Purpose |
|
||||||
|
|--------|------|---------|
|
||||||
|
| GET | `/api/annotations/media?flightId=...` | List media |
|
||||||
|
| GET | `/api/annotations/media/{id}` | Media metadata |
|
||||||
|
| GET | `/api/annotations/media/{id}/annotations` | Bbox list |
|
||||||
|
| POST | `/api/annotations` | Create one |
|
||||||
|
| PUT | `/api/annotations/{id}` | Update one |
|
||||||
|
| DELETE | `/api/annotations/{id}` | Delete one |
|
||||||
|
| POST | `/api/detect/{mediaId}` | Sync image AI-detect |
|
||||||
|
| POST | `/api/detect/video/{mediaId}` | Async video AI-detect — should send `X-Refresh-Token`, currently does not (finding #30) |
|
||||||
|
| GET | `/api/detect/classes` | Detection class list |
|
||||||
|
| SSE | `/api/detect/stream/{jobId}` | Async detect progress (subscribed via `01_api-transport/sse.ts`) — but **`AnnotationsPage` does not subscribe today** (finding #10) |
|
||||||
|
|
||||||
|
## 5. Implementation Details
|
||||||
|
|
||||||
|
**State Management**: Page-local for the open annotation + selection; `useResizablePanel` for panel widths (not persisted — finding #11).
|
||||||
|
|
||||||
|
**Time-window math** (`CanvasEditor`, finding #6, post-cross-check correction): implementation is symmetric ±200 ms (400 ms total). Spec wants asymmetric 50 ms before + 150 ms after (200 ms total). UI is 4× too wide and not centred per spec.
|
||||||
|
|
||||||
|
**Gradient cap** (finding #9, post-cross-check correction): annotation row alpha gradient maxes at `0x28 = 16 %` opacity due to a `* 40` literal that is decimal, not hex. Wireframe demands `0x40 = 25 %`.
|
||||||
|
|
||||||
|
**`handleSave` body** (finding #32): currently `{ classNum, x, y, w, h, time }`. Suite spec requires `{ classNum, x, y, w, h, videoTime, Source, WaypointId }`. Owner: this component, also touches Foundation types.
|
||||||
|
|
||||||
|
**handleDownload (image export)** — risks tainting the `<canvas>` if the video is from a `blob:` URL with cross-origin frames; finding #12.
|
||||||
|
|
||||||
|
**Missing affordances** (findings #13–18):
|
||||||
|
- Affiliation icons absent (spec mandates per-bbox).
|
||||||
|
- Combat-readiness indicator absent.
|
||||||
|
- `AFFILIATION_COLORS` defined but unused (dead).
|
||||||
|
- No keyboard shortcuts R / V / PageUp / PageDown.
|
||||||
|
- No camera-config side panel.
|
||||||
|
- No tile zoom indicator for `splitTile` media.
|
||||||
|
|
||||||
|
**AI-detect controls** (`AnnotationsSidebar`, findings #21–23):
|
||||||
|
- Async progress is not streamed (no SSE subscription).
|
||||||
|
- Errors silently `console.error`'d, no UI feedback.
|
||||||
|
|
||||||
|
**MediaList** (findings #24–26):
|
||||||
|
- Uses `alert()` for "no media" state.
|
||||||
|
- `blob:` local previews ignore the search filter.
|
||||||
|
- No virtualisation — long flights render all thumbnails.
|
||||||
|
|
||||||
|
**Cross-feature leak**: `07_dataset` imports `CanvasEditor` directly (caveat from `00_discovery.md` §8). The right home is a shared `components/canvas/` directory; not done in this step.
|
||||||
|
|
||||||
|
**Enum drift cross-link**: findings #27–34 capture the enum drift (`AnnotationStatus`, `Affiliation`, `CombatReadiness`, `MediaStatus`, `AnnotationSource`) — wire payloads using current `src/types/index.ts` values are wrong. Owner of fix: `00_foundation/types/index.ts`. Two findings (#31, #33) are **NO UI CHANGE — PARENT-DOC FIX** and have already been applied to the suite docs (state.json 02:18Z note); the rest are Step 4.
|
||||||
|
|
||||||
|
**Key Dependencies**: HTML5 `<canvas>` + `<video>`, `react-dropzone` (upload).
|
||||||
|
|
||||||
|
## 6b. WPF gap analysis (vs `suite/annotations-research`)
|
||||||
|
|
||||||
|
> Output of the Step 2 BLOCKING-gate cross-check (2026-05-10). The legacy WPF `Azaion.Annotator` window is the design source for this component; the file `_docs/legacy/wpf-era.md` §10 calls out which features must be ported. The list below is the delta — features that exist in the WPF source and are NOT yet present in the React port. Each entry names the WPF anchor and the React owner; numeric findings (#) come from `_docs/02_document/modules/src__features__annotations.md`.
|
||||||
|
|
||||||
|
| WPF feature | Anchor in `annotations-research` | React status | Owner |
|
||||||
|
|------------|----------------------------------|--------------|-------|
|
||||||
|
| **Time window asymmetric 50/150 ms** (interval-tree `_thresholdBefore=50ms`, `_thresholdAfter=150ms`) | `Azaion.Annotator/Annotator.xaml.cs:53-54` | Wrong (symmetric ±200 ms). Finding #6. | `CanvasEditor.tsx` |
|
||||||
|
| **Keyboard shortcut `[Space]` = pause / resume** | `Annotator.xaml` `PauseClick` button tooltip "[Пробіл]" | Missing (no global key listener) | `VideoPlayer` + `AnnotationsPage` |
|
||||||
|
| **Keyboard `[Left]` / `[Right]` = prev / next frame; `+Ctrl` = ±5 sec** | `PreviousFrameClick` / `NextFrameClick` tooltips | Missing | `VideoPlayer` |
|
||||||
|
| **Keyboard `[Enter]` = save annotations and continue** | `SaveAnnotationsClick` tooltip "[Ентер]" | Missing. Finding #16 broadens this. | `AnnotationsPage` |
|
||||||
|
| **Keyboard `[Del]` = delete selected annotations (with confirm)** | `Annotator.xaml.cs:204-222` (`DgAnnotations.KeyUp`) | Missing | `AnnotationsSidebar` + `ConfirmDialog` |
|
||||||
|
| **Keyboard `[X]` = delete ALL annotations** | `RemoveAllClick` tooltip "[X]" | Missing | `AnnotationsPage` |
|
||||||
|
| **Keyboard `[M]` = mute volume; also toggles GPS panel (context-sensitive)** | `TurnOffVolume` + `SwitchGpsPanel` tooltips both "[M]" | Missing | `VideoPlayer` |
|
||||||
|
| **Keyboard `[R]` = AI Detect** | `AIDetectBtn_OnClick` tooltip "[R]" | Missing. Finding #16. | `AnnotationsSidebar` |
|
||||||
|
| **Keyboard `[Ctrl+click]` = multi-select; `[Ctrl+drag]` = pan; `[Ctrl+wheel]` = zoom** | `Azaion.Common/Controls/CanvasEditor.cs` | Pan / zoom missing (finding listed); multi-select unverified | `CanvasEditor.tsx` |
|
||||||
|
| **Volume slider** (`UpdatableProgressBar Volume`, range 0–100, mediator `VolumeChangedEvent`) | `Annotator.xaml:500-507` | Missing entirely | `VideoPlayer` |
|
||||||
|
| **Status bar — clock `mm:ss / mm:ss`** | `Annotator.xaml.cs:237` `StatusClock.Text = ...` | Missing | `VideoPlayer` (display) + `AnnotationsPage` (slot) |
|
||||||
|
| **Status bar — contextual help text** (`HelpTextEnum.{Initial, PlayVideo, PauseForAnnotations, AnnotationHelp}`, `BlinkHelp` flicker pattern) | `Azaion.Annotator/HelpTexts.cs` + `Annotator.xaml.cs:144-158` | Missing | `AnnotationsPage` (or a global toast) |
|
||||||
|
| **Status bar — generic status text** (`StatusBarItem Status`) | `Annotator.xaml:653` | Missing | shared chrome (could live in App Shell or `AnnotationsPage`) |
|
||||||
|
| **Sound Detections feature** ("Show objects detected by audio analysis", own button, distinct from AI Detect) | `Annotator.xaml:565-617` `SoundDetections` button + handler | **Entirely missing**. Not mentioned anywhere in `_docs/legacy/wpf-era.md §10` "What survived" — needs a user decision: port or drop. | TBD |
|
||||||
|
| **Drone Maintenance feature** ("Аналіз стану БПЛА" — UAV state analysis, [K]) | `Annotator.xaml:618-630` `RunDroneMaintenance` button + handler | **Entirely missing**. Same status as above — port-or-drop decision required. | TBD |
|
||||||
|
| **Resizable panel widths persisted** (left + right panels) | `Annotator.xaml.cs:243-252` `SaveUserSettings` writes `UIConfig.LeftPanelWidth` / `RightPanelWidth` | Missing — `useResizablePanel` does not persist (finding #11). | `00_foundation/useResizablePanel` + Settings backend |
|
||||||
|
| **Camera config side panel** (altitude / focal / sensor → GSD) | `Annotator.xaml:196-203` `CameraConfigControl` | Missing (finding #17) | `AnnotationsPage` |
|
||||||
|
| **Affiliation icons + Combat readiness indicator on bbox label** | `Azaion.Common/DTO/AffiliationEnum`, `_docs/ui_design/README.md` | Missing (findings #14–15) | `CanvasEditor.tsx` + types |
|
||||||
|
| **Annotation list seek + zoom on double-click** | `Annotator.xaml.cs:197-202`, `OpenAnnotationResult` (seek + ZoomTo for split tiles) | Partial — seek probably works, "open split-tile zoom" not verified | `AnnotationsSidebar` |
|
||||||
|
| **Help window "Як анотувати" (6 quality rules)** | `HelpWindow.xaml`, `_docs/legacy/wpf-era.md §10` "Help window" | `HelpModal` exists but GUIDELINES is a hardcoded string in source — not moved to i18n bundle. Step 4. | `03_shared-ui/HelpModal` |
|
||||||
|
|
||||||
|
### Decisions required at Step 4.5 (Architecture Vision)
|
||||||
|
|
||||||
|
- **Sound Detections** — port, drop, or move to a different module (e.g., a future audio-pipeline service)?
|
||||||
|
- **Drone Maintenance** — same.
|
||||||
|
- **Camera config** persistence — was per-`AppConfig` in WPF; in the React port should it be per-user-settings, per-flight, or per-detect-job?
|
||||||
|
- **Status bar / help-text blinking** — keep the WPF "blink twice" UX, replace with toasts, or drop?
|
||||||
|
|
||||||
|
These are Architecture-Vision-level questions, not Step 2 component-graph decisions. Recorded here so Step 4.5 can pick them up.
|
||||||
|
|
||||||
|
## 7. Caveats & Edge Cases
|
||||||
|
|
||||||
|
- 26 findings in `src__features__annotations.md`. Cross-cutting blockers cluster on enum drift + `handleSave` body shape + missing `X-Refresh-Token`.
|
||||||
|
- **Time-window** and **gradient** math wrong against spec.
|
||||||
|
- **Video AI-detect not wired to SSE** — long-running jobs appear to hang from the user's POV.
|
||||||
|
- **WPF gap analysis above** lists ~17 missing affordances. Highest user-impact: keyboard shortcuts, volume slider, status bar with clock + help text. Highest design-impact: Sound Detections + Drone Maintenance (both require a port-or-drop decision).
|
||||||
|
|
||||||
|
## 8. Dependency Graph
|
||||||
|
|
||||||
|
**Must be implemented after**: `00_foundation`, `01_api-transport`, `03_shared-ui`, `11_class-colors`.
|
||||||
|
|
||||||
|
**Can be implemented in parallel with**: every other feature page (note: `07_dataset` imports `CanvasEditor`, so a `CanvasEditor` extraction must coordinate).
|
||||||
|
|
||||||
|
**Blocks**: `07_dataset` (direct import dependency on `CanvasEditor`), `10_app-shell`.
|
||||||
|
|
||||||
|
## Module Inventory
|
||||||
|
|
||||||
|
Single consolidated module doc: `_docs/02_document/modules/src__features__annotations.md`. `classColors.ts` is moved into a separate component (`11_class-colors`) — its module doc is referenced from there, not here.
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# 07 — Dataset Explorer
|
||||||
|
|
||||||
|
## 1. High-Level Overview
|
||||||
|
|
||||||
|
**Purpose**: Browse, filter, edit, split, and export the dataset. Reuses `CanvasEditor` from `06_annotations` for in-place bbox editing on dataset thumbnails.
|
||||||
|
|
||||||
|
**Architectural Pattern**: Single-page feature with one route component composing local panels.
|
||||||
|
|
||||||
|
**Upstream dependencies**: `00_foundation`, `01_api-transport`, `03_shared-ui` (FlightContext, ConfirmDialog, DetectionClasses), `06_annotations` (`CanvasEditor` — cross-feature edge).
|
||||||
|
|
||||||
|
**Downstream consumers**: `10_app-shell` (routed at `/dataset`).
|
||||||
|
|
||||||
|
## 2. Internal Interfaces
|
||||||
|
|
||||||
|
| Export | Notes |
|
||||||
|
|--------|-------|
|
||||||
|
| `DatasetPage()` | Top-level route component. Loads paged dataset items, applies filters (class, affiliation, status, flight), renders thumbnail grid + edit pane. |
|
||||||
|
|
||||||
|
## 3. External API Specification
|
||||||
|
|
||||||
|
| Method | Path | Purpose |
|
||||||
|
|--------|------|---------|
|
||||||
|
| GET | `/api/annotations/dataset` | Paged list with filters |
|
||||||
|
| GET | `/api/annotations/dataset/{id}` | Detail |
|
||||||
|
| PUT | `/api/annotations/dataset/{id}` | Update (class, status, bbox) |
|
||||||
|
| DELETE | `/api/annotations/dataset/{id}` | Delete |
|
||||||
|
| POST | `/api/annotations/dataset/bulk-status` | Bulk status update (numeric per finding cross-check) |
|
||||||
|
| POST | `/api/annotations/dataset/{id}/split` | Split tile |
|
||||||
|
|
||||||
|
## 5. Implementation Details
|
||||||
|
|
||||||
|
**State Management**: Page-local. Uses `useDebounce` for filter inputs and `useResizablePanel` for the editor pane.
|
||||||
|
|
||||||
|
**Findings** (13 numbered, from `src__features__dataset__DatasetPage.md`):
|
||||||
|
|
||||||
|
1. **No keyboard shortcuts** at all.
|
||||||
|
2. **No "Refresh thumbnails"** action.
|
||||||
|
3. **No virtualisation** — long lists render all thumbnails.
|
||||||
|
4. **Editor tab does not save** — confirmed regression.
|
||||||
|
5. **Magic `mediaType=1`** literal — should be the typed enum.
|
||||||
|
6. **Dead `ConfirmDialog` import** — never used.
|
||||||
|
7. **Silent `try/catch`** in delete handler.
|
||||||
|
8. **Status filter conflates `None` with `All`** — depends on the `AnnotationStatus` enum drift fix (`00_foundation/types/index.ts`).
|
||||||
|
9. **`classNum=0` sentinel collides with real class 0** — needs explicit "all classes" sentinel.
|
||||||
|
10. **`mediaType=1` again** — appears twice.
|
||||||
|
11. **Bulk-status request** uses string status names; spec wants numerics — already retagged for parent-doc fix (state.json 02:18Z note covered the spec side).
|
||||||
|
12. **DatasetItem.isSplit** missing in the parent-doc response schema — cross-repo PARENT-DOC FIX applied.
|
||||||
|
13. **Cross-feature `CanvasEditor` import** — finding #14 (cross-link to enum drift + isSplit gap).
|
||||||
|
|
||||||
|
**Key Dependencies**: `react-dropzone` (export trigger), `@hello-pangea/dnd` (potentially, for reordering — verify in Step 3).
|
||||||
|
|
||||||
|
## 6b. WPF gap analysis (vs `suite/annotations-research`)
|
||||||
|
|
||||||
|
> Cross-check of the legacy `Azaion.Dataset.DatasetExplorer` window (`Azaion.Dataset/DatasetExplorer.xaml`) against the current React `DatasetPage`. **Step 4 correction**: an earlier draft of this table claimed several WPF features were missing that are in fact already implemented. Re-read of `src/features/dataset/DatasetPage.tsx` corrected the table below.
|
||||||
|
|
||||||
|
| WPF feature | Anchor in `annotations-research` | React status | Owner |
|
||||||
|
|------------|----------------------------------|--------------|-------|
|
||||||
|
| **Class Distribution chart tab** (3rd tab — horizontal bars per `DetectionClass`, bar tinted with the class color) | `Azaion.Dataset/Controls/ClassDistribution.xaml` + `DatasetExplorer.xaml:146-148` | **Implemented** — `DatasetPage.tsx:151` has three tabs (`annotations`, `editor`, `distribution`); `loadDistribution()` calls `GET /api/annotations/dataset/class-distribution`. Step-4 verify the bar tint matches `classColors`. | — |
|
||||||
|
| **"Show only annotations with objects" checkbox** in left filter pane | `DatasetExplorer.xaml:89-95` `ShowWithObjectsOnlyChBox` | **Implemented** — `DatasetPage.tsx:110-114`, state name `objectsOnly`. | — |
|
||||||
|
| **Validate button (bulk-validate selected annotations to `AnnotationStatus.Validated`)** | `DatasetExplorer.xaml:177-200` `ValidateBtn` + `ValidateAnnotationsClick` | **Implemented** — `DatasetPage.tsx:142-146` Validate button appears when `selectedIds.size > 0`; `handleValidate()` posts to `/api/annotations/dataset/bulk-status`. **Gap is the `[V]` keyboard shortcut**, not the button. | `DatasetPage` (shortcut only) |
|
||||||
|
| **Refresh thumbnails button + progress bar** | `DatasetExplorer.xaml:205-247` `RefreshThumbnailsButtonItem` + `RefreshProgressBarItem` | Button missing (finding #2); progress UI also missing | `DatasetPage` + an as-yet-undefined refresh service endpoint |
|
||||||
|
| **`SelectedAnnotationName` status indicator** (bottom-right of status bar) | `DatasetExplorer.xaml:252-254` | Missing | `DatasetPage` |
|
||||||
|
| **Generic `StatusText` slot** | `DatasetExplorer.xaml:249-251` | Missing | `DatasetPage` |
|
||||||
|
| **Seed annotation highlight** (`IsSeed=true` thumbnails get an 8 px IndianRed border) | `DatasetExplorer.xaml:15-29` thumbnail template | Missing — `DatasetItem.isSeed` shape unverified against suite spec (cross-link to `00_foundation/types/index.ts`). | `DatasetPage` + types |
|
||||||
|
| **Thumbnail caption** (image name + `CreatedDate: CreatedEmail`) | `DatasetExplorer.xaml:42-56` | Likely missing or simplified — verify in Step 4 against current React render. | `DatasetPage` |
|
||||||
|
| **Keyboard shortcuts `[1]–[9]`, `[Enter]`, `[Del]`, `[X]`, `[V]`, arrows, PgUp/PgDn, `[Esc]`** for inline editor | `DatasetExplorer.xaml.cs` (listed in `_docs/legacy/wpf-era.md §5`) | Missing entirely (finding #1) | `DatasetPage` |
|
||||||
|
| **Editor tab actually saves** | WPF wires `ExplorerEditor` → `AnnotationService.OnAnnotationCreated` etc. | **Broken in React** (finding #4 — Editor tab does not save). PRIORITY Step 4. | `DatasetPage` + `06_annotations/CanvasEditor` |
|
||||||
|
| **`DetectionClasses` strip in left pane** (same control reused from Annotator) | `DatasetExplorer.xaml:85-88` | Present (via `03_shared-ui/DetectionClasses`) | — |
|
||||||
|
| **Filter `TextBox`** | `DatasetExplorer.xaml:112-115` `TbSearch` with `TextChanged` debounce | Present (uses `00_foundation/useDebounce`) | — |
|
||||||
|
| **Virtualised thumbnail grid** (`vwp:GridView` from `WpfToolkit.VirtualizingWrapPanel`) | `DatasetExplorer.xaml:126-135` | **Missing virtualisation** (finding #3) — long lists render all thumbnails. | `DatasetPage` |
|
||||||
|
|
||||||
|
### Decisions required at Step 4.5 (Architecture Vision)
|
||||||
|
|
||||||
|
- **Refresh-thumbnails action** — is the existing thumbnail refresh strategy (server-side on annotation save) acceptable, or do we need a manual "Refresh" affordance like the WPF era?
|
||||||
|
- **Status-bar surfaces** (`StatusText`, `SelectedAnnotationName`) — port the WPF status bar verbatim, or rely on existing toasts and selection counters?
|
||||||
|
- **Seed annotation concept** (`IsSeed=true` highlight) — does the modern API still expose `IsSeed`, and is the visual still desired?
|
||||||
|
- **Inline editor save** — is the broken save (#4) a regression to fix or a feature to be redesigned?
|
||||||
|
|
||||||
|
## 7. Caveats & Edge Cases
|
||||||
|
|
||||||
|
- **Cross-feature import** of `CanvasEditor` (`06_annotations`). Either lift to a shared `components/canvas/` or accept the edge — record in module-layout / baseline scan.
|
||||||
|
- **Editor tab broken** (#4) — PRIORITY Step 4.
|
||||||
|
- **Filter sentinels colliding** (#8, #9) — wire-format consistency depends on enum drift fix.
|
||||||
|
- **WPF gap analysis above** lists ~12 missing affordances. Highest user-impact: virtualisation, Refresh thumbnails, keyboard shortcuts, broken editor save. Highest design-impact: Class Distribution chart (entirely missing third tab).
|
||||||
|
|
||||||
|
## 8. Dependency Graph
|
||||||
|
|
||||||
|
**Must be implemented after**: `00_foundation`, `01_api-transport`, `03_shared-ui`, `06_annotations` (`CanvasEditor`).
|
||||||
|
|
||||||
|
**Can be implemented in parallel with**: `04_login`, `05_flights`, `08_admin`, `09_settings`.
|
||||||
|
|
||||||
|
**Blocks**: `10_app-shell` only.
|
||||||
|
|
||||||
|
## Module Inventory
|
||||||
|
|
||||||
|
| Path | Module Doc |
|
||||||
|
|------|------------|
|
||||||
|
| `src/features/dataset/DatasetPage.tsx` | `_docs/02_document/modules/src__features__dataset__DatasetPage.md` |
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# 08 — Admin
|
||||||
|
|
||||||
|
## 1. High-Level Overview
|
||||||
|
|
||||||
|
**Purpose**: Operator-only configuration page. User management, detection-class management, AI Settings, GPS Settings, aircraft default.
|
||||||
|
|
||||||
|
**Architectural Pattern**: Single-page feature, large monolithic component (~215 lines pre-consolidation per state.json).
|
||||||
|
|
||||||
|
**Upstream dependencies**: `00_foundation`, `01_api-transport`, `03_shared-ui` (ConfirmDialog).
|
||||||
|
|
||||||
|
**Downstream consumers**: `10_app-shell` (routed at `/admin`, **currently with no role-based guard** — see #1 caveat below).
|
||||||
|
|
||||||
|
## 2. Internal Interfaces
|
||||||
|
|
||||||
|
| Export | Notes |
|
||||||
|
|--------|-------|
|
||||||
|
| `AdminPage()` | Top-level route component. Sub-sections: Users, Detection Classes, AI Settings, GPS Settings, Aircraft default. |
|
||||||
|
|
||||||
|
## 3. External API Specification
|
||||||
|
|
||||||
|
| Method | Path | Purpose |
|
||||||
|
|--------|------|---------|
|
||||||
|
| GET / POST / PUT / DELETE | `/api/admin/users` | User CRUD |
|
||||||
|
| GET | `/api/annotations/classes` | Read class list (note: read uses `annotations/`, write uses `admin/`) |
|
||||||
|
| POST / PUT / DELETE | `/api/admin/classes` | Class CRUD |
|
||||||
|
| GET / PUT | `/api/admin/settings/ai` | AI service config |
|
||||||
|
| GET / PUT | `/api/admin/settings/gps` | GPS device config |
|
||||||
|
| GET / PUT | `/api/admin/settings/aircraft-default` | Aircraft default |
|
||||||
|
|
||||||
|
## 5. Implementation Details
|
||||||
|
|
||||||
|
**State Management**: Page-local React state per sub-form. No global form library.
|
||||||
|
|
||||||
|
**Findings (B4, copied from state.json):**
|
||||||
|
|
||||||
|
1. **PRIORITY (security): no role-based route guard on `/admin`** — anyone authenticated can access. Server-enforced 403 protects the data, but UI does not gate. Surface in Step 6 problem-extraction. Cross-link with `10_app-shell`.
|
||||||
|
2. **AI Settings & GPS Settings forms render with `defaultValue` only — NO state, NO submit handler, the Save button does nothing.** PRIORITY surface in Step 6.
|
||||||
|
3. **Hardcoded GPS device default `'192.168.1.100'` / port `'5535'`** shipped in production bundle. Step 4.
|
||||||
|
4. **`handleDeleteClass` has NO `ConfirmDialog`** despite being destructive. Step 4 vs `ui_design/README.md`.
|
||||||
|
5. **Service split mismatch**: detection-class read uses `/api/annotations/classes` (annotations service) but write uses `/api/admin/classes` (admin service). Verify with suite ADRs in Step 3a.
|
||||||
|
6. **`handleToggleDefault` duplicated** between AdminPage and SettingsPage; aircraft default is global config but page exists in both `/admin` and `/settings` — surface intent in Step 6.
|
||||||
|
7. **Many hardcoded English strings.** Step 4 i18n.
|
||||||
|
|
||||||
|
**Key Dependencies**: `03_shared-ui/ConfirmDialog` (used for some destructive actions; missing on `handleDeleteClass`).
|
||||||
|
|
||||||
|
## 7. Caveats & Edge Cases
|
||||||
|
|
||||||
|
- The **broken Save button** is the most user-visible bug.
|
||||||
|
- The **annotations/admin service split** for class CRUD looks like a copy-paste residue but may be deliberate; verify in Step 3a.
|
||||||
|
- **No optimistic concurrency / version check** for any settings — last writer wins.
|
||||||
|
|
||||||
|
## 8. Dependency Graph
|
||||||
|
|
||||||
|
**Must be implemented after**: `00_foundation`, `01_api-transport`, `03_shared-ui`.
|
||||||
|
|
||||||
|
**Can be implemented in parallel with**: every other feature page.
|
||||||
|
|
||||||
|
**Blocks**: `10_app-shell`.
|
||||||
|
|
||||||
|
## Module Inventory
|
||||||
|
|
||||||
|
| Path | Module Doc |
|
||||||
|
|------|------------|
|
||||||
|
| `src/features/admin/AdminPage.tsx` | `_docs/02_document/modules/src__features__admin__AdminPage.md` |
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# 09 — Settings
|
||||||
|
|
||||||
|
## 1. High-Level Overview
|
||||||
|
|
||||||
|
**Purpose**: User-scoped + system settings: language, theme, system params, directory paths, aircraft default (duplicated with Admin).
|
||||||
|
|
||||||
|
**Architectural Pattern**: Single-page feature, ~181 lines pre-consolidation.
|
||||||
|
|
||||||
|
**Upstream dependencies**: `00_foundation`, `01_api-transport`, `03_shared-ui`.
|
||||||
|
|
||||||
|
**Downstream consumers**: `10_app-shell` (routed at `/settings`).
|
||||||
|
|
||||||
|
## 2. Internal Interfaces
|
||||||
|
|
||||||
|
| Export | Notes |
|
||||||
|
|--------|-------|
|
||||||
|
| `SettingsPage()` | Top-level route component. Sub-sections: Personal (language, theme), System (params), Directories, Aircraft default. |
|
||||||
|
|
||||||
|
## 3. External API Specification
|
||||||
|
|
||||||
|
| Method | Path | Purpose |
|
||||||
|
|--------|------|---------|
|
||||||
|
| GET / PUT | `/api/annotations/settings/user` | Per-user UI preferences |
|
||||||
|
| GET / PUT | `/api/admin/settings/system` | System params (saveSystem) |
|
||||||
|
| GET / PUT | `/api/admin/settings/directories` | Storage paths (saveDirs) |
|
||||||
|
| GET / PUT | `/api/admin/settings/aircraft-default` | Aircraft default (duplicated with Admin) |
|
||||||
|
|
||||||
|
## 5. Implementation Details
|
||||||
|
|
||||||
|
**State Management**: Page-local React state per form section.
|
||||||
|
|
||||||
|
**Findings (B4, copied from state.json):**
|
||||||
|
|
||||||
|
1. **`saveSystem` / `saveDirs` lack `try/finally`** — PUT failure leaves `saving:true` permanently and the spinner never stops. Step 4.
|
||||||
|
2. **Numeric inputs use `parseInt(v) || 0`** — clearing a field silently writes 0. Step 4.
|
||||||
|
3. **No optimistic concurrency** (no version field, no ETag) — Step 6 problem-extraction.
|
||||||
|
4. **`handleToggleDefault` duplicated with AdminPage** — same global config behind two different pages. Surface intent in Step 6.
|
||||||
|
5. **Possibly should be guarded by a permission like `SETTINGS`** — spec doesn't have such a code; server-enforces via 403. Less clear-cut than the `/admin` gap. Surface in Step 6.
|
||||||
|
|
||||||
|
**Key Dependencies**: `react-i18next` (language switch).
|
||||||
|
|
||||||
|
## 7. Caveats & Edge Cases
|
||||||
|
|
||||||
|
- **Stuck-saving spinner** on PUT failure (#1).
|
||||||
|
- **Silent zero on cleared numeric input** (#2) — corrupts settings.
|
||||||
|
- **Aircraft default duplicated** with Admin — eventually one page should win.
|
||||||
|
|
||||||
|
## 8. Dependency Graph
|
||||||
|
|
||||||
|
**Must be implemented after**: `00_foundation`, `01_api-transport`, `03_shared-ui`.
|
||||||
|
|
||||||
|
**Can be implemented in parallel with**: every other feature page.
|
||||||
|
|
||||||
|
**Blocks**: `10_app-shell`.
|
||||||
|
|
||||||
|
## Module Inventory
|
||||||
|
|
||||||
|
| Path | Module Doc |
|
||||||
|
|------|------------|
|
||||||
|
| `src/features/settings/SettingsPage.tsx` | `_docs/02_document/modules/src__features__settings__SettingsPage.md` |
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# 10 — App Shell
|
||||||
|
|
||||||
|
## 1. High-Level Overview
|
||||||
|
|
||||||
|
**Purpose**: Application bootstrap. `main.tsx` mounts React + StrictMode + BrowserRouter; `App.tsx` defines the top-level routing tree and provider stack.
|
||||||
|
|
||||||
|
**Architectural Pattern**: Composition root.
|
||||||
|
|
||||||
|
**Upstream dependencies**: every other component (this is the wiring root).
|
||||||
|
|
||||||
|
**Downstream consumers**: none (top of the tree).
|
||||||
|
|
||||||
|
## 2. Internal Interfaces
|
||||||
|
|
||||||
|
### `src/main.tsx`
|
||||||
|
|
||||||
|
- Imports `./i18n/i18n` for side-effect (`00_foundation`).
|
||||||
|
- Imports `./index.css`.
|
||||||
|
- Mounts `<StrictMode><BrowserRouter><App /></BrowserRouter></StrictMode>` into `#root`.
|
||||||
|
|
||||||
|
### `src/App.tsx`
|
||||||
|
|
||||||
|
Routes:
|
||||||
|
|
||||||
|
| Route | Wrapping | Component |
|
||||||
|
|-------|----------|-----------|
|
||||||
|
| `/login` | (public) | `04_login/LoginPage` |
|
||||||
|
| `/flights` (default authenticated) | `AuthProvider → ProtectedRoute → FlightProvider → Header` | `05_flights/FlightsPage` |
|
||||||
|
| `/annotations` | same | `06_annotations/AnnotationsPage` |
|
||||||
|
| `/dataset` | same | `07_dataset/DatasetPage` |
|
||||||
|
| `/admin` | same — **no role guard** | `08_admin/AdminPage` |
|
||||||
|
| `/settings` | same | `09_settings/SettingsPage` |
|
||||||
|
| `*` | same | redirect → `/flights` |
|
||||||
|
|
||||||
|
## 5. Implementation Details
|
||||||
|
|
||||||
|
**State Management**: provider stack only (`AuthProvider`, `FlightProvider`).
|
||||||
|
|
||||||
|
**Findings (5 items from `src__App-and-main.md`):**
|
||||||
|
|
||||||
|
1. **No role-based route guards** on `/admin` (PRIORITY — security). `/settings` is more nuanced (no `SETTINGS` permission code in spec; server-enforced via 403).
|
||||||
|
2. **Mobile bottom-nav** route layout — confirmed present (Header.tsx:113–129). Earlier draft incorrectly listed this as missing; corrected per state.json 02:01Z.
|
||||||
|
3. **No `ErrorBoundary`** — any uncaught render throw crashes the whole app to a white screen.
|
||||||
|
4. **No lazy chunks / code-splitting** — every route is in the initial bundle. Compounds the `chart.js` bloat from `05_flights`.
|
||||||
|
5. **`/flights` is the default landing for everyone** — user-specific landing per role would require `00_foundation` permission types + `02_auth.permissions`.
|
||||||
|
|
||||||
|
**Key Dependencies**: `react-router-dom` 7.
|
||||||
|
|
||||||
|
## 7. Caveats & Edge Cases
|
||||||
|
|
||||||
|
- **No `ErrorBoundary`** is the highest-impact gap; even one runtime null deref in any feature kills the app.
|
||||||
|
- **`/admin` open to any authenticated user** at the UI level (PRIORITY).
|
||||||
|
- **No lazy loading** — initial bundle is ~all of the SPA.
|
||||||
|
|
||||||
|
## 8. Dependency Graph
|
||||||
|
|
||||||
|
**Must be implemented after**: every other component (composition root).
|
||||||
|
|
||||||
|
**Can be implemented in parallel with**: nothing.
|
||||||
|
|
||||||
|
**Blocks**: nothing.
|
||||||
|
|
||||||
|
## Module Inventory
|
||||||
|
|
||||||
|
| Path | Module Doc |
|
||||||
|
|------|------------|
|
||||||
|
| `src/App.tsx` | `_docs/02_document/modules/src__App-and-main.md` |
|
||||||
|
| `src/main.tsx` | `_docs/02_document/modules/src__App-and-main.md` |
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# 11 — Class Colors (Detection Class Theme)
|
||||||
|
|
||||||
|
## 1. High-Level Overview
|
||||||
|
|
||||||
|
**Purpose**: The single source of fallback color, fallback name, and `PhotoMode` suffix for any detection class number — used whenever the live `DetectionClass[]` from the admin API is unavailable, partial, or being rendered next to UI chrome that cannot wait for it (initial paint, gradient stops, label tints, sidebar swatches).
|
||||||
|
|
||||||
|
**Architectural Pattern**: Pure-function shared kernel. Stateless, no React, no HTTP, no DOM.
|
||||||
|
|
||||||
|
**Layer**: shared / Layer 1 (above Foundation, below every UI component that names a detection class).
|
||||||
|
|
||||||
|
**Upstream dependencies**: none (no internal imports; no external libraries).
|
||||||
|
|
||||||
|
**Downstream consumers**:
|
||||||
|
- `03_shared-ui/DetectionClasses` (fallback name + color when admin classes haven't loaded)
|
||||||
|
- `06_annotations/CanvasEditor` (bbox label color + crosshair tint)
|
||||||
|
- `06_annotations/AnnotationsPage` (active-class indicator)
|
||||||
|
- `06_annotations/AnnotationsSidebar` (annotation-row gradient stops)
|
||||||
|
- `07_dataset/DatasetPage` (class-filter chip color, class-distribution chart) — when those features are wired up
|
||||||
|
|
||||||
|
## 2. Internal Interfaces
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const FALLBACK_CLASS_NAMES: string[]; // 12 generic English labels
|
||||||
|
export function getClassColor(classNum: number): string; // hex string, no '#'-alpha
|
||||||
|
export function getPhotoModeSuffix(classNum: number): string; // '' | ' (winter)' | ' (night)'
|
||||||
|
export function getClassNameFallback(classNum: number): string; // FALLBACK_CLASS_NAMES[base] or '#<n>'
|
||||||
|
```
|
||||||
|
|
||||||
|
A 12-color palette `CLASS_COLORS` is module-private and exposed only via `getClassColor`.
|
||||||
|
|
||||||
|
### PhotoMode contract (from legacy WPF)
|
||||||
|
|
||||||
|
`yoloId = classId + photoModeOffset`. Three offsets:
|
||||||
|
|
||||||
|
| `mode = floor(classNum / 20)` | Suffix | Meaning |
|
||||||
|
|------------------------------|--------|---------|
|
||||||
|
| 0 | (empty) | Regular |
|
||||||
|
| 1 | `' (winter)'` | Winter |
|
||||||
|
| 2 | `' (night)'` | Night |
|
||||||
|
|
||||||
|
`base = classNum % 20` is the index into both `CLASS_COLORS` and `FALLBACK_CLASS_NAMES`.
|
||||||
|
|
||||||
|
## 5. Implementation Details
|
||||||
|
|
||||||
|
```
|
||||||
|
base = classNum % 20
|
||||||
|
mode = floor(classNum / 20)
|
||||||
|
color = CLASS_COLORS[base % CLASS_COLORS.length]
|
||||||
|
name = FALLBACK_CLASS_NAMES[base % FALLBACK_CLASS_NAMES.length] ?? `#${classNum}`
|
||||||
|
suffix = mode === 1 ? ' (winter)' : mode === 2 ? ' (night)' : ''
|
||||||
|
```
|
||||||
|
|
||||||
|
**Open question** (carried forward from `src__features__annotations__classColors.md`): the `??` guard is dead because `base % length` already brings the index back into range. Either the array is wrong (the legacy palette had >12 entries?) or the guard is dead code. Step 4 verification.
|
||||||
|
|
||||||
|
**Redundancy with `DetectionClass.photoMode`** (also in module doc): the live admin DTO carries an explicit `photoMode` field on `DetectionClass`. Computing the suffix from `classNum / 20` here risks disagreeing with the admin-defined value. Step 4 testability candidate: keep `getPhotoModeSuffix` only as a fallback when the admin DTO is missing.
|
||||||
|
|
||||||
|
**State Management**: stateless module. Calls are pure.
|
||||||
|
|
||||||
|
**Key Dependencies**: none.
|
||||||
|
|
||||||
|
## 6. Extensions and Helpers
|
||||||
|
|
||||||
|
This *is* the helper. There are no further extensions inside this component.
|
||||||
|
|
||||||
|
## 7. Caveats & Edge Cases
|
||||||
|
|
||||||
|
- **Physical location is misplaced today**. The file lives at `src/features/annotations/classColors.ts` — inside the Annotations feature folder — even though logically it belongs to a feature-neutral shared layer. The cross-layer import from `src/components/DetectionClasses.tsx` to this file (recorded in `00_discovery.md` §8) is the visible symptom.
|
||||||
|
- **Owner of fix**: `module-layout.md` (autodev Step 2.5) records the *target* layer; the actual file move is an autodev Step 4 (testability) candidate or a Step 8 refactor task. Until moved, both `03_shared-ui` and `06_annotations` import from the current path.
|
||||||
|
- **Fallback names are generic English** ("Car", "Person", "Truck", …) and bear no relation to the actual military class taxonomy in `_docs/ui_design/README.md` §"Detection Classes Table". Acceptable only because they appear strictly when admin-loaded classes failed to load. Document in Step 5 (Solution Extraction).
|
||||||
|
- **No localization**. Suffix strings (`' (winter)'`, `' (night)'`) and fallback names are hardcoded English. Step 4 i18n.
|
||||||
|
- **Color palette size (12)** vs `base = 0..19` — the wrap-around silently reuses colors for indices 12..19. Visually distinct fallbacks above 12 are not guaranteed.
|
||||||
|
|
||||||
|
## 8. Dependency Graph
|
||||||
|
|
||||||
|
**Must be implemented after**: nothing (no internal deps).
|
||||||
|
|
||||||
|
**Can be implemented in parallel with**: `00_foundation`, `01_api-transport`.
|
||||||
|
|
||||||
|
**Blocks**: `03_shared-ui` (DetectionClasses), `06_annotations`, `07_dataset` (when class-distribution chart is added).
|
||||||
|
|
||||||
|
## Module Inventory
|
||||||
|
|
||||||
|
| Path | Module Doc |
|
||||||
|
|------|------------|
|
||||||
|
| `src/features/annotations/classColors.ts` *(physical location pending refactor)* | `_docs/02_document/modules/src__features__annotations__classColors.md` |
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
# Azaion UI — Data Model
|
||||||
|
|
||||||
|
> Synthesis output of `/document` Step 3c. Consolidated from `src/types/index.ts`,
|
||||||
|
> per-component data sections, and the parent suite spec at
|
||||||
|
> `_docs/legacy/wpf-era.md` §8 + the suite-level docs (cross-referenced).
|
||||||
|
>
|
||||||
|
> The UI does NOT own a database. The entities below are the **contract shapes**
|
||||||
|
> the UI consumes from suite REST + SSE endpoints. The authoritative schema is
|
||||||
|
> server-side; this document captures the **client-side type expectations** and
|
||||||
|
> the **mismatches** between those expectations and the suite spec (enum drift,
|
||||||
|
> shape drift) for resolution at autodev Step 4.
|
||||||
|
|
||||||
|
## 1. Entities by component
|
||||||
|
|
||||||
|
| Entity | Component | Backing service | Wire shape |
|
||||||
|
|--------|-----------|-----------------|------------|
|
||||||
|
| `AuthUser` | `02_auth` | `admin/` | `{id, email, name, role, permissions[]}` |
|
||||||
|
| `User` | `08_admin` | `admin/` | `{id, name, email, role, isActive}` |
|
||||||
|
| `Aircraft` | `08_admin` / `09_settings` | `admin/` | `{id, model, type:'Plane'\|'Copter', isDefault}` |
|
||||||
|
| `Flight` | `05_flights` | `flights/` | `{id, name, createdDate, aircraftId\|null}` |
|
||||||
|
| `Waypoint` | `05_flights` | `flights/` | UI sends: `{id, flightId, name, latitude, longitude, order}`. Spec wants: `{Geopoint:{Lat,Lon,MGRS}, Source, Objective, OrderNum, Height}` — **drift, finding #20** |
|
||||||
|
| `Media` | `06_annotations` | `annotations/` | `{id, name, path, mediaType, mediaStatus, duration, annotationCount, waypointId, userId}` |
|
||||||
|
| `AnnotationListItem` | `06_annotations` | `annotations/` | `{id, mediaId, time, createdDate, userId, source, status, isSplit, splitTile, detections[]}` |
|
||||||
|
| `Detection` | `06_annotations` | `annotations/` (storage) + `detect/` (production) | `{id, classNum, label, confidence, affiliation, combatReadiness, centerX, centerY, width, height}` |
|
||||||
|
| `DetectionClass` | `08_admin` (write) + `06_annotations` (read) | `admin/` (write) + `annotations/` (read) | `{id, name, shortName, color, maxSizeM, photoMode}` |
|
||||||
|
| `DatasetItem` | `07_dataset` | `annotations/` | `{annotationId, imageName, thumbnailPath, status, createdDate, createdEmail, flightName, source, isSeed, isSplit}` |
|
||||||
|
| `ClassDistributionItem` | (currently unused — backs missing chart) | `annotations/` | `{classNum, label, color, count}` |
|
||||||
|
| `SystemSettings` | `09_settings` | `admin/` | `{id, name, militaryUnit, defaultCameraWidth, defaultCameraFoV, thumbnailWidth, thumbnailHeight, thumbnailBorder, generateAnnotatedImage, silentDetection}` |
|
||||||
|
| `DirectorySettings` | `09_settings` | `admin/` | `{id, videosDir, imagesDir, labelsDir, resultsDir, thumbnailsDir, gpsSatDir, gpsRouteDir}` |
|
||||||
|
| `CameraSettings` | `09_settings` | `admin/` | `{id, altitude, focalLength, sensorWidth}` |
|
||||||
|
| `UserSettings` | `09_settings` | `admin/` | `{id, userId, selectedFlightId, annotationsLeftPanelWidth, annotationsRightPanelWidth, datasetLeftPanelWidth, datasetRightPanelWidth}` |
|
||||||
|
| `PaginatedResponse<T>` | shared (`00_foundation`) | every list endpoint | `{items[], totalCount, page, pageSize}` |
|
||||||
|
|
||||||
|
## 2. Enums (numeric wire format)
|
||||||
|
|
||||||
|
> The suite uses **numeric** wire values for every enum. The UI types in `src/types/index.ts`
|
||||||
|
> match in *shape* but several have **wrong values** vs. spec. The state-of-the-world is captured
|
||||||
|
> in `state.json::notes[]` (2026-05-10 02:13Z entries) — Step 4 will fix the UI side.
|
||||||
|
|
||||||
|
| Enum | UI values today | Spec values | Status |
|
||||||
|
|------|-----------------|-------------|--------|
|
||||||
|
| `MediaType` | `None=0, Image=1, Video=2` | matches | ✓ |
|
||||||
|
| `MediaStatus` | `New=0, AiProcessing=1, AiProcessed=2, ManualCreated=3` | also has `None`, `Confirmed`, `Error` | **drift** — UI cannot render error state. Step 4 fix. |
|
||||||
|
| `AnnotationSource` | `AI=0, Manual=1` | matches numerically; spec doc shows strings (cross-repo doc fix replayed 2026-05-10) | ✓ (after parent-doc fix) |
|
||||||
|
| `AnnotationStatus` | `Created=0, Edited=1, Validated=2` | spec is `None=0, Created=10, Edited=20, Validated=30, Deleted=40` | **drift, severe** — wire payloads will be wrong. Step 4 PRIORITY. |
|
||||||
|
| `Affiliation` | `Unknown=0, Friendly=1, Hostile=2` | spec also has `None` | **drift** — Step 4. |
|
||||||
|
| `CombatReadiness` | `NotReady=0, Ready=1` | spec also has `Unknown` | **drift** — Step 4. |
|
||||||
|
|
||||||
|
## 3. Entity-relationship diagram
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
erDiagram
|
||||||
|
User ||--o{ Flight : "creates"
|
||||||
|
Flight ||--o{ Waypoint : "has"
|
||||||
|
Flight ||--o{ Media : "captures"
|
||||||
|
Media ||--o{ AnnotationListItem : "annotated by"
|
||||||
|
AnnotationListItem ||--o{ Detection : "contains"
|
||||||
|
DetectionClass ||--o{ Detection : "classifies (by classNum)"
|
||||||
|
Aircraft }o--o| Flight : "default for"
|
||||||
|
User ||--|| AuthUser : "session view of"
|
||||||
|
User ||--|| UserSettings : "preferences"
|
||||||
|
User ||--o{ DatasetItem : "validated by"
|
||||||
|
SystemSettings ||--|| Aircraft : "may default to"
|
||||||
|
|
||||||
|
Detection {
|
||||||
|
string id
|
||||||
|
int classNum "raw int including PhotoMode offset"
|
||||||
|
string label
|
||||||
|
float confidence
|
||||||
|
Affiliation affiliation
|
||||||
|
CombatReadiness combatReadiness
|
||||||
|
float centerX "normalized 0..1"
|
||||||
|
float centerY "normalized 0..1"
|
||||||
|
float width "normalized 0..1"
|
||||||
|
float height "normalized 0..1"
|
||||||
|
}
|
||||||
|
|
||||||
|
DetectionClass {
|
||||||
|
int id
|
||||||
|
string name
|
||||||
|
string shortName
|
||||||
|
string color "hex"
|
||||||
|
float maxSizeM "GSD constraint"
|
||||||
|
int photoMode "0=Regular, 1=Winter (+20), 2=Night (+40)"
|
||||||
|
}
|
||||||
|
|
||||||
|
Waypoint {
|
||||||
|
string id
|
||||||
|
string flightId
|
||||||
|
string name
|
||||||
|
float latitude "wire shape: drift — see finding 20"
|
||||||
|
float longitude
|
||||||
|
int order
|
||||||
|
}
|
||||||
|
|
||||||
|
AnnotationListItem {
|
||||||
|
string id
|
||||||
|
string mediaId
|
||||||
|
string time "video timestamp HH:MM:SS or null"
|
||||||
|
string createdDate "ISO 8601"
|
||||||
|
string userId
|
||||||
|
AnnotationSource source
|
||||||
|
AnnotationStatus status
|
||||||
|
bool isSplit
|
||||||
|
string splitTile "YOLO label string or null"
|
||||||
|
}
|
||||||
|
|
||||||
|
Media {
|
||||||
|
string id
|
||||||
|
string name
|
||||||
|
string path
|
||||||
|
MediaType mediaType
|
||||||
|
MediaStatus mediaStatus
|
||||||
|
string duration "HH:MM:SS or null"
|
||||||
|
int annotationCount
|
||||||
|
string waypointId
|
||||||
|
string userId
|
||||||
|
}
|
||||||
|
|
||||||
|
DatasetItem {
|
||||||
|
string annotationId
|
||||||
|
string imageName
|
||||||
|
string thumbnailPath
|
||||||
|
AnnotationStatus status
|
||||||
|
string createdDate
|
||||||
|
string createdEmail
|
||||||
|
string flightName
|
||||||
|
AnnotationSource source
|
||||||
|
bool isSeed
|
||||||
|
bool isSplit
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Migration / schema-evolution strategy
|
||||||
|
|
||||||
|
The UI does NOT own a database, so there is no client-side migration to run. However:
|
||||||
|
|
||||||
|
- **Enum drift** above is effectively a "client-side schema migration" — every drifted enum must be aligned with the suite spec in Step 4 (or in the case of `AnnotationSource`, the parent-suite doc is what's wrong and was already fixed via cross-repo doc patch on 2026-05-10).
|
||||||
|
- **Backwards compatibility** is the suite's responsibility. The UI assumes the latest contract; if the suite needs to roll out a breaking change, it must coordinate with the UI version (typically by gating the change behind a feature flag in the admin service, then deploying both at once).
|
||||||
|
- **`UserSettings.{annotationsLeftPanelWidth, ...}`** — the type exists; the wire endpoint exists; the UI does not write these today (`useResizablePanel` finding #11). The fix is purely client-side wiring.
|
||||||
|
|
||||||
|
## 5. Seed data observations
|
||||||
|
|
||||||
|
The UI has no seed data of its own. Two sources of "default" data are observable:
|
||||||
|
|
||||||
|
1. **`FALLBACK_CLASS_NAMES`** (`11_class-colors`) — 12 generic English labels (Car, Person, Truck, …) shown only when admin classes failed to load. These are NOT a seed for the admin service; they are a defensive UI fallback only.
|
||||||
|
2. **`Constants.DefaultAnnotationClasses`** in the legacy WPF — referenced in `_docs/legacy/wpf-era.md §10`. The React UI does NOT carry an equivalent (admin classes are always fetched). Acceptable.
|
||||||
|
|
||||||
|
## 6. Validation rules (client-side)
|
||||||
|
|
||||||
|
| Field | Rule | Source | Defect |
|
||||||
|
|-------|------|--------|--------|
|
||||||
|
| Login email | non-empty | `LoginPage` | none observed |
|
||||||
|
| Login password | non-empty | `LoginPage` | none observed |
|
||||||
|
| Numeric settings | `parseInt(v) \|\| 0` | `SettingsPage` | clearing field silently writes 0 — finding B4 |
|
||||||
|
| Waypoint lat/lng | implicit (Leaflet bounds) | `FlightsPage` | no explicit clamp — Step 4 |
|
||||||
|
| Detection bbox normalized 0..1 | implicit (CanvasEditor scaling) | `CanvasEditor` | normalized-coordinate clamping mentioned in `_docs/legacy/wpf-era.md §10` — verify in Step 4 |
|
||||||
|
| Class 1–9 keyboard pick | `classes[idx + photoMode]` | `DetectionClasses` | ordering vs admin contract unverified — Step 4 |
|
||||||
|
| File upload size | server cap 500 MB (nginx) | `nginx.conf` | client does not pre-validate — Step 4 cosmetic |
|
||||||
|
|
||||||
|
## 7. Wire-format gotchas
|
||||||
|
|
||||||
|
- **`time` as a string** in `AnnotationListItem` (e.g., `"00:01:23.456"`) is parsed/formatted client-side; precision is millisecond. Used for video annotation overlay window math (50 ms before / 150 ms after — currently wrong, finding #6).
|
||||||
|
- **`splitTile` as a YOLO label string** is the raw `<class> <cx> <cy> <w> <h>` text, parsed client-side in `CanvasEditor.tsx`. Format mismatch is a Step 4 verification candidate.
|
||||||
|
- **`PaginatedResponse<T>` ceiling** — `pageSize=1000` is hardcoded for flights (finding B3). Other lists vary; no canonical default.
|
||||||
|
- **Date strings** — assumed ISO 8601; no timezone handling beyond what the browser's `Date` constructor implies. Step 4 verification when timezone bugs surface.
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# Azaion UI — CI/CD Pipeline
|
||||||
|
|
||||||
|
> Synthesis output of `/document` Step 3d (ci_cd_pipeline). Derived from
|
||||||
|
> `.woodpecker/build-arm.yml`.
|
||||||
|
|
||||||
|
## 1. Triggers
|
||||||
|
|
||||||
|
| Branch | Triggers | Image tag |
|
||||||
|
|--------|----------|-----------|
|
||||||
|
| `dev` | every push | `${REGISTRY_HOST}/azaion/ui:dev-arm` |
|
||||||
|
| `stage` | every push | `${REGISTRY_HOST}/azaion/ui:stage-arm` |
|
||||||
|
| `main` | every push | `${REGISTRY_HOST}/azaion/ui:main-arm` |
|
||||||
|
|
||||||
|
Other branches do NOT build (PR builds, feature-branch builds, tag builds — none configured today).
|
||||||
|
|
||||||
|
## 2. Steps
|
||||||
|
|
||||||
|
| # | Step | What | Notes |
|
||||||
|
|---|------|------|-------|
|
||||||
|
| 1 | Checkout | `git clone` + `git checkout $CI_COMMIT_SHA` | Standard Woodpecker behaviour |
|
||||||
|
| 2 | Build + Push image | Multi-stage Dockerfile produces `nginx:alpine` image with `dist/` baked in | Pushes to `${REGISTRY_HOST}/azaion/ui:${branch}-arm` with OCI labels (revision, created, source) |
|
||||||
|
|
||||||
|
**Missing steps** (recommended for autodev Steps 5–7):
|
||||||
|
|
||||||
|
| Step | Purpose | Tool candidates |
|
||||||
|
|------|---------|-----------------|
|
||||||
|
| `bun install --frozen-lockfile` smoke | Catch lockfile drift before build | First few seconds of the build stage cover this |
|
||||||
|
| `tsc --noEmit` | Type-check the whole project | Already part of `bun run build` (`tsc -b && vite build`) |
|
||||||
|
| `bun test` (or vitest / jest) | Run test suite | **Required** — there is no test runner today |
|
||||||
|
| `eslint` / `biome` | Lint | Not configured today |
|
||||||
|
| Vulnerability scan | CVE scan on the image | `trivy` or `grype` candidates |
|
||||||
|
| SBOM emission | Software bill of materials | `syft` candidate |
|
||||||
|
| Image signing | Supply-chain trust | `cosign` candidate |
|
||||||
|
| Multi-arch build | Add AMD64 alongside ARM64 | `docker buildx` candidates |
|
||||||
|
|
||||||
|
These are tracked as Step 4–7 deliverables under autodev; the current pipeline is correct but minimal.
|
||||||
|
|
||||||
|
## 3. Secrets & registry
|
||||||
|
|
||||||
|
- `${REGISTRY_HOST}` — provided by Woodpecker secrets at runtime.
|
||||||
|
- Registry credentials — stored as Woodpecker secrets; not in this repo.
|
||||||
|
- No GPG/TLS signing keys today.
|
||||||
|
|
||||||
|
## 4. Branch model
|
||||||
|
|
||||||
|
- `dev` is the active development branch (per `.cursor/rules/git-workflow.mdc`).
|
||||||
|
- `stage` is for pre-production validation.
|
||||||
|
- `main` is production.
|
||||||
|
- No `release/*` long-lived branches.
|
||||||
|
- PR builds are not configured (Woodpecker build only fires on push, not on PR open).
|
||||||
|
|
||||||
|
## 5. Build artifact
|
||||||
|
|
||||||
|
The output of the pipeline is exactly one OCI image per push: `${REGISTRY_HOST}/azaion/ui:${branch}-arm`. There is **no** versioned image tag (e.g., `1.2.3-arm`); branch tags are mutable. The OCI `revision` label is the deterministic anchor (= `$CI_COMMIT_SHA`).
|
||||||
|
|
||||||
|
**Future**: when this UI ships under a versioned suite release, the pipeline should also tag images with `vMAJOR.MINOR.PATCH-arm` derived from `package.json` `version`.
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# Azaion UI — Containerization
|
||||||
|
|
||||||
|
> Synthesis output of `/document` Step 3d (containerization). Derived from
|
||||||
|
> `Dockerfile`, `nginx.conf`, and `00_discovery.md` §3.
|
||||||
|
|
||||||
|
## 1. Image
|
||||||
|
|
||||||
|
**Multi-stage build** (`Dockerfile`):
|
||||||
|
|
||||||
|
| Stage | Base image | Role |
|
||||||
|
|-------|------------|------|
|
||||||
|
| 1 (builder) | `oven/bun:1.3.11-alpine` | `bun install --frozen-lockfile` + `bun run build` (= `tsc -b && vite build`) → `dist/` |
|
||||||
|
| 2 (runtime) | `nginx:alpine` | Serves `/usr/share/nginx/html` (`dist/`); listens on `:80` |
|
||||||
|
|
||||||
|
**Why this shape**:
|
||||||
|
- Bun gives a fast install + build vs. npm/yarn/pnpm.
|
||||||
|
- nginx alpine is a sub-25 MB runtime that already has reverse-proxy routing for `/api`.
|
||||||
|
- No Node runtime in production → smaller attack surface, faster startup, lower memory.
|
||||||
|
|
||||||
|
**Image labels** (OCI, set by Woodpecker CI):
|
||||||
|
- `org.opencontainers.image.revision = $CI_COMMIT_SHA`
|
||||||
|
- `org.opencontainers.image.created = $CI_BUILD_CREATED`
|
||||||
|
- `org.opencontainers.image.source = <repo url>`
|
||||||
|
|
||||||
|
**Environment**:
|
||||||
|
- `AZAION_REVISION = $CI_COMMIT_SHA` — accessible at runtime for diagnostics.
|
||||||
|
- No other env vars consumed at runtime by the SPA bundle (the bundle is fully static).
|
||||||
|
|
||||||
|
## 2. nginx routing (`nginx.conf`)
|
||||||
|
|
||||||
|
The image's nginx config strips `/api/<service>/` and reverse-proxies to the matching suite service inside the container network.
|
||||||
|
|
||||||
|
| Public path | Upstream (intra-cluster) | Service |
|
||||||
|
|-------------|--------------------------|---------|
|
||||||
|
| `/api/annotations/` | `http://annotations:8080/` | `annotations/` |
|
||||||
|
| `/api/flights/` | `http://flights:8080/` | `flights/` |
|
||||||
|
| `/api/admin/` | `http://admin:8080/` | `admin/` |
|
||||||
|
| `/api/resource/` | `http://resource:8080/` | `resource/` |
|
||||||
|
| `/api/detect/` | `http://detect:8080/` | `detect/` |
|
||||||
|
| `/api/loader/` | `http://loader:8080/` | `loader/` |
|
||||||
|
| `/api/gps-denied-desktop/` | `http://gps-denied-desktop:8080/` | `gps-denied-desktop/` |
|
||||||
|
| `/api/gps-denied-onboard/` | `http://gps-denied-onboard:8080/` | `gps-denied-onboard/` |
|
||||||
|
| `/api/autopilot/` | `http://autopilot:8080/` | `autopilot/` |
|
||||||
|
| `/` (any other path) | static fallback to `/index.html` (SPA routing) | — |
|
||||||
|
|
||||||
|
**Body size cap**: `client_max_body_size 500M` — tlog + video uploads in GPS-Denied Test Mode and large image uploads in Annotations both ride this limit.
|
||||||
|
|
||||||
|
**Headers passed to upstream**: standard `Host`, `X-Real-IP`, `X-Forwarded-For`, `X-Forwarded-Proto` (assumed — verify in `nginx.conf`).
|
||||||
|
|
||||||
|
**SSE handling**: `proxy_buffering off` MUST be set on `/api/detect/` and any other path that streams (Step 4 verification — confirm in `nginx.conf`).
|
||||||
|
|
||||||
|
## 3. Resource sizing (recommended, not enforced)
|
||||||
|
|
||||||
|
| Resource | Recommendation | Rationale |
|
||||||
|
|----------|----------------|-----------|
|
||||||
|
| CPU | 100 m (0.1 vCPU) | nginx is near-idle; 99 % of work is suite services |
|
||||||
|
| Memory | 32 Mi | nginx + ~5 MB of static assets |
|
||||||
|
| Storage | ephemeral 50 Mi | bundle is sub-5 MB gzipped today; some headroom |
|
||||||
|
| Replicas | 1+ | trivially horizontal; HA only matters if the ingress sits in front |
|
||||||
|
|
||||||
|
**Bundle size budget**: `vite build` output should stay under ~2 MB gzipped initial JS. Currently `chart.js` and `leaflet` are the dominant chunks; `AltitudeChart` is a lazy-load candidate (finding in `05_flights`).
|
||||||
|
|
||||||
|
## 4. Health checks
|
||||||
|
|
||||||
|
**Today: none.**
|
||||||
|
|
||||||
|
Recommended (Step 4 / Step 6 surface):
|
||||||
|
- **Liveness**: `GET /index.html → 200`
|
||||||
|
- **Readiness**: same (the SPA has no warm-up)
|
||||||
|
- **Container health**: `wget --spider -q http://localhost/index.html`
|
||||||
|
|
||||||
|
The suite-level orchestrator (parent suite docker-compose / k8s) is expected to handle ingress health-checking; individual UI replicas don't need their own.
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# Azaion UI — Environment Strategy
|
||||||
|
|
||||||
|
> Synthesis output of `/document` Step 3d (environment_strategy). Derived from
|
||||||
|
> `vite.config.ts`, `nginx.conf`, `.gitignore`, the workspace `README.md`, and
|
||||||
|
> the absence of a workspace `.env.example`.
|
||||||
|
|
||||||
|
## 1. Environments
|
||||||
|
|
||||||
|
| Env | How it runs | API base | Auth | Tile providers |
|
||||||
|
|-----|-------------|----------|------|----------------|
|
||||||
|
| Development | `bun run dev` (Vite dev server, port 5173) | Vite dev proxy: `/api → http://localhost:8080` (configured in `vite.config.ts`) | Suite admin/ service running locally (typically via parent suite `docker-compose up`) | OSM + satellite (env-configurable in mission-planner only) |
|
||||||
|
| Stage | nginx in container, ARM image `:stage-arm` | nginx `/api/<service>/ → http://<service>:8080/` (intra-cluster) | Stage suite admin/ service | Same |
|
||||||
|
| Production | nginx in container, ARM image `:main-arm` | nginx `/api/<service>/ → http://<service>:8080/` | Prod suite admin/ service | Same |
|
||||||
|
|
||||||
|
## 2. Configuration model
|
||||||
|
|
||||||
|
The SPA bundle is **fully static**. No env vars are read at runtime by the bundle. Every cross-environment difference is resolved at the **deployment edge** (nginx) or at the **suite-service level**.
|
||||||
|
|
||||||
|
| Concern | Where it's set | Notes |
|
||||||
|
|---------|----------------|-------|
|
||||||
|
| Backend API URL | nginx `proxy_pass` (`nginx.conf`) — same nginx config across stage / prod | Base URLs are intra-cluster service names (`http://annotations:8080`, etc.); the URL difference between environments is hidden by the orchestrator's DNS |
|
||||||
|
| Auth cookie domain | Set by suite admin/ service on `Set-Cookie` | UI does not control |
|
||||||
|
| Refresh-token lifetime | Set by suite admin/ service | UI tolerates any TTL |
|
||||||
|
| Tile provider URL (mission-planner) | `.env.example` declares `VITE_SATELLITE_TILE_URL` | mission-planner only; not deployed |
|
||||||
|
| OpenWeatherMap API key | **Hardcoded in source** (`flightPlanUtils.ts:60`) | Security finding — Step 4 fix to remove + proxy via suite |
|
||||||
|
| `AZAION_REVISION` | Stamped into image at build time | For diagnostics |
|
||||||
|
|
||||||
|
## 3. Why no `.env`
|
||||||
|
|
||||||
|
The workspace `.env.example` is **absent** today. The `README.md` "Local development" section explicitly notes this as a Step 4 testability fix.
|
||||||
|
|
||||||
|
**Trade-off**: avoiding a build-time env injection means `dist/` is identical across environments, which is great for promotability (the same image flows dev → stage → prod). The cost: the OpenWeatherMap key (and any future runtime config) cannot be changed without a rebuild.
|
||||||
|
|
||||||
|
**Future direction** (Step 4 / Step 5):
|
||||||
|
- Move the OpenWeatherMap call server-side (`flights/` service) — eliminates the bundled key entirely.
|
||||||
|
- Introduce a runtime `/config.json` that nginx serves — lets ops change feature flags / tile URLs without rebuilding.
|
||||||
|
- OR keep the static bundle and use Vite's `define` for build-time injection of safe-to-publish values (no secrets).
|
||||||
|
|
||||||
|
## 4. Promotability
|
||||||
|
|
||||||
|
The same image (`:dev-arm`, `:stage-arm`, `:main-arm`) is built per branch from the same Dockerfile. Theoretically the `:dev-arm` image is functionally identical to the `:main-arm` image except for the `AZAION_REVISION` label.
|
||||||
|
|
||||||
|
In practice: branch separation is the gating mechanism. Once dev → stage → main propagation is normalized, the safer pattern is to build ONE image per commit and re-tag it across environments (immutable image promotion). The Woodpecker pipeline does not implement this today; it rebuilds per-branch.
|
||||||
|
|
||||||
|
## 5. Local-dev quirks
|
||||||
|
|
||||||
|
- **Vite dev proxy** (`vite.config.ts`) requires the suite to be reachable on `http://localhost:8080`. If the parent suite's docker-compose binds to a different port, the developer must edit `vite.config.ts` (no env-driven override today).
|
||||||
|
- **`bun.lock`**: committed (per `package.json`'s `packageManager` field). `package-lock.json` is gitignored.
|
||||||
|
- **`.idea/`, `.claude/`, `.superpowers/`**: gitignored — IDE / agent metadata.
|
||||||
|
- **Playwright entries in `.gitignore`**: present but aspirational — Playwright is not installed (Step 5–7 territory).
|
||||||
|
- **mission-planner**: has its own `.env.example` declaring `VITE_SATELLITE_TILE_URL` and runs as a sibling Vite app. Not bundled into the deployed image.
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# Azaion UI — Observability
|
||||||
|
|
||||||
|
> Synthesis output of `/document` Step 3d (observability). Derived from inspection
|
||||||
|
> of all module docs + `nginx.conf` + the absence of any client telemetry SDK
|
||||||
|
> in `package.json`.
|
||||||
|
|
||||||
|
## 1. Status: minimal
|
||||||
|
|
||||||
|
The browser-side SPA emits **no centralized telemetry today**:
|
||||||
|
|
||||||
|
- No analytics SDK (no `@sentry/*`, `@datadog/*`, `web-vitals`, `posthog`, etc.).
|
||||||
|
- No error reporting service.
|
||||||
|
- No client-side feature-flag service.
|
||||||
|
- Errors that aren't caught by an `<ErrorBoundary>` (which doesn't exist today — finding in `10_app-shell`) end up as `console.error` only.
|
||||||
|
|
||||||
|
This is acceptable as a starting state. A future iteration adds an error-tracking SDK (Sentry candidate) with the SDK key sourced from a runtime `/config.json` — see `environment_strategy.md`.
|
||||||
|
|
||||||
|
## 2. Existing logging (per module)
|
||||||
|
|
||||||
|
| Module | What is logged | How | Why it's unsatisfactory |
|
||||||
|
|--------|----------------|-----|-------------------------|
|
||||||
|
| `01_api-transport/client.ts` | request / response errors | `console.error` | No retries, no spans, no correlation IDs |
|
||||||
|
| `01_api-transport/sse.ts` | EventSource errors | `console.error` | No reconnect logic; no telemetry |
|
||||||
|
| `02_auth/AuthContext.tsx` | login / refresh outcomes | `console.error` | Successful refresh is silent (good); failures are silent (bad — need user-visible recovery flow) |
|
||||||
|
| `03_shared-ui/FlightContext.tsx` | flight load + select-flight errors | swallowed | `selectFlight` is fire-and-forget, error invisible |
|
||||||
|
| `06_annotations/AnnotationsSidebar.tsx` | AI-detect errors | `console.error` | User sees no feedback (finding #21–23) |
|
||||||
|
| `06_annotations/AnnotationsPage.tsx` | save errors | partial — `handleSave` has fallback that **hides save loss** (finding) | Worst case: user thinks the annotation saved but it didn't |
|
||||||
|
| `07_dataset/DatasetPage.tsx` | various | swallowed `catch` blocks (finding #6) | Same risk |
|
||||||
|
| `05_flights/FlightsPage.tsx` | save partial-failure | not detected | Per-waypoint failures invisible (finding #19) |
|
||||||
|
| `05_flights/flightPlanUtils.ts` | weather fetch errors | swallowed silently | Wind data missing → battery estimate wrong; user not informed |
|
||||||
|
|
||||||
|
The dominant pattern is "silent catch + console.error" — this is the single biggest observability gap.
|
||||||
|
|
||||||
|
## 3. Server-side logs the UI relies on
|
||||||
|
|
||||||
|
The suite services (admin, flights, annotations, detect, etc.) are responsible for:
|
||||||
|
|
||||||
|
- Audit logging (login, logout, role changes, destructive admin actions)
|
||||||
|
- Request tracing (the UI does not send a `traceparent` header today — Step 6 candidate)
|
||||||
|
- Performance metrics (UI does not measure RUM)
|
||||||
|
|
||||||
|
The UI's bug-reproduction story relies on suite-side logs. A correlation ID injected by the UI on every request would dramatically simplify cross-service debugging — a Step 6 problem-extraction surface.
|
||||||
|
|
||||||
|
## 4. Client-side metrics (none)
|
||||||
|
|
||||||
|
No `web-vitals` or equivalent is installed. Recommended (Step 5 solution surface):
|
||||||
|
|
||||||
|
- **CLS** (cumulative layout shift) — the canvas + leaflet + chart layout has known shifts on initial load.
|
||||||
|
- **LCP** (largest contentful paint) — the bundle is the dominant cost.
|
||||||
|
- **FID / INP** (interaction latency) — relevant for the canvas drag and waypoint drag-drop.
|
||||||
|
- **Custom metrics**: time-to-first-flight-list, time-to-first-thumbnail, time-to-first-detection.
|
||||||
|
|
||||||
|
## 5. Error boundaries
|
||||||
|
|
||||||
|
`10_app-shell` finding: no `<ErrorBoundary>` wraps the route tree. A single uncaught render error today crashes the whole SPA. Step 4 / Step 5 surface — add a top-level `<ErrorBoundary>` plus per-feature boundaries for the canvas / map / chart so isolated failures don't take down the whole UI.
|
||||||
|
|
||||||
|
## 6. Recommended near-term improvements (Step 5 solution candidates)
|
||||||
|
|
||||||
|
1. **Add a top-level `<ErrorBoundary>`** in `App.tsx` with a "something broke" recovery card.
|
||||||
|
2. **Replace silent catches** (`}` `catch {}`) with `console.error` + user toast — at minimum.
|
||||||
|
3. **Inject a correlation ID** (`X-Request-Id` header) on every fetch + EventSource.
|
||||||
|
4. **Surface AI-detect progress + errors** — see Flow F7 (currently flow doesn't even subscribe).
|
||||||
|
5. **Add Sentry (or equivalent)** with runtime-config-driven DSN.
|
||||||
|
6. **Add `web-vitals`** + emit to suite admin/ telemetry endpoint.
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
# Component Diagram
|
||||||
|
|
||||||
|
> Output of `/document` Step 2 — produced from the module-level dependency graph
|
||||||
|
> in `_docs/02_document/00_discovery.md` §7. Edges are aggregated to component
|
||||||
|
> level; cross-feature edges are kept (they exist in the code today).
|
||||||
|
>
|
||||||
|
> **Note on `05_flights`**: per user direction (Step 2 BLOCKING gate, 2026-05-10),
|
||||||
|
> the `mission-planner/` codebase is NOT a separate component. It is the
|
||||||
|
> port-source for `src/features/flights/` — both trees realise the same logical
|
||||||
|
> component. They are physically disjoint at the file level (see `00_discovery.md`
|
||||||
|
> §1) but documented together under `05_flights`.
|
||||||
|
|
||||||
|
## Component graph
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
classDef foundation fill:#1f3a4d,stroke:#3b6e8c,color:#cfe1ec
|
||||||
|
classDef transport fill:#3a2d4d,stroke:#6a4f8c,color:#dccfe6
|
||||||
|
classDef auth fill:#4d3a2d,stroke:#8c6a4f,color:#e6dccf
|
||||||
|
classDef shared fill:#2d4d3a,stroke:#4f8c6a,color:#cfe6dc
|
||||||
|
classDef feature fill:#4d3a3a,stroke:#8c5f5f,color:#e6cfcf
|
||||||
|
classDef shell fill:#1e1e1e,stroke:#888,color:#fff
|
||||||
|
|
||||||
|
Foundation[00 — Foundation<br/>types · hooks · i18n]
|
||||||
|
Transport[01 — API Transport<br/>client.ts · sse.ts]
|
||||||
|
Auth[02 — Auth<br/>AuthContext · ProtectedRoute]
|
||||||
|
ClassColors[11 — Class Colors<br/>fallback color/name · PhotoMode]
|
||||||
|
SharedUI[03 — Shared UI & Context<br/>Header · HelpModal · ConfirmDialog<br/>DetectionClasses · FlightContext]
|
||||||
|
|
||||||
|
Login[04 — Login]
|
||||||
|
Flights[05 — Flights & Mission Planning<br/>incl. GPS-Denied sub-page<br/>+ Test Mode (tlog+video→SITL→onboard)]
|
||||||
|
Annotations[06 — Annotations]
|
||||||
|
Dataset[07 — Dataset Explorer]
|
||||||
|
Admin[08 — Admin]
|
||||||
|
Settings[09 — Settings]
|
||||||
|
|
||||||
|
Shell[10 — App Shell<br/>main.tsx · App.tsx]
|
||||||
|
|
||||||
|
Foundation --> Transport
|
||||||
|
Foundation --> Auth
|
||||||
|
Foundation --> SharedUI
|
||||||
|
Foundation --> Login
|
||||||
|
Foundation --> Flights
|
||||||
|
Foundation --> Annotations
|
||||||
|
Foundation --> Dataset
|
||||||
|
Foundation --> Admin
|
||||||
|
Foundation --> Settings
|
||||||
|
|
||||||
|
Transport --> Auth
|
||||||
|
Transport --> SharedUI
|
||||||
|
Transport --> Flights
|
||||||
|
Transport --> Annotations
|
||||||
|
Transport --> Dataset
|
||||||
|
Transport --> Admin
|
||||||
|
Transport --> Settings
|
||||||
|
|
||||||
|
Auth --> SharedUI
|
||||||
|
Auth --> Login
|
||||||
|
Auth --> Shell
|
||||||
|
|
||||||
|
ClassColors --> SharedUI
|
||||||
|
ClassColors --> Annotations
|
||||||
|
ClassColors --> Dataset
|
||||||
|
|
||||||
|
SharedUI --> Flights
|
||||||
|
SharedUI --> Annotations
|
||||||
|
SharedUI --> Dataset
|
||||||
|
SharedUI --> Admin
|
||||||
|
SharedUI --> Settings
|
||||||
|
SharedUI --> Shell
|
||||||
|
|
||||||
|
Login --> Shell
|
||||||
|
Flights --> Shell
|
||||||
|
Annotations --> Shell
|
||||||
|
Dataset --> Shell
|
||||||
|
Admin --> Shell
|
||||||
|
Settings --> Shell
|
||||||
|
|
||||||
|
%% cross-feature edge that remains (existing today, NOT lifted)
|
||||||
|
Annotations -. CanvasEditor .-> Dataset
|
||||||
|
|
||||||
|
class Foundation foundation
|
||||||
|
class Transport transport
|
||||||
|
class Auth auth
|
||||||
|
class ClassColors shared
|
||||||
|
class SharedUI shared
|
||||||
|
class Login,Flights,Annotations,Dataset,Admin,Settings feature
|
||||||
|
class Shell shell
|
||||||
|
```
|
||||||
|
|
||||||
|
## `05_flights` — internal physical split
|
||||||
|
|
||||||
|
The Flights component is realised by two physically disjoint codebases that
|
||||||
|
will converge over time. The graph below documents the port direction; no
|
||||||
|
import edge crosses between the two trees at the file level today.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
classDef target fill:#4d3a3a,stroke:#8c5f5f,color:#e6cfcf
|
||||||
|
classDef source fill:#2a2a3a,stroke:#666,color:#bbb
|
||||||
|
classDef planned fill:#2d4d3a,stroke:#4f8c6a,color:#cfe6dc,stroke-dasharray: 4 4
|
||||||
|
|
||||||
|
subgraph Target_tree["Target tree — src/features/flights/ (deployed)"]
|
||||||
|
SPA_FlightsPage[FlightsPage]
|
||||||
|
SPA_FlightMap[FlightMap]
|
||||||
|
SPA_FlightParams[FlightParamsPanel]
|
||||||
|
SPA_FlightList[FlightListSidebar]
|
||||||
|
SPA_Utils[flightPlanUtils.ts]
|
||||||
|
SPA_Types[types.ts]
|
||||||
|
SPA_GpsDenied[GPS-Denied panel<br/>partial today]
|
||||||
|
SPA_TestMode[GPS-Denied · Test Mode<br/>tlog + video → SITL → onboard]:::planned
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Source_tree["Port source — mission-planner/ (NOT deployed)"]
|
||||||
|
MP_FlightPlan[flightPlan.tsx]
|
||||||
|
MP_LeftBoard[LeftBoard.tsx]
|
||||||
|
MP_MapView[MapView.tsx ↔ MiniMap.tsx<br/>named-handle cycle]
|
||||||
|
MP_PointsList[PointsList.tsx]
|
||||||
|
MP_Services[services/<br/>calculateBatteryUsage<br/>AircraftService · WeatherService]
|
||||||
|
MP_Lang[LanguageContext]
|
||||||
|
end
|
||||||
|
|
||||||
|
MP_Services -. "port to →" .-> SPA_Utils
|
||||||
|
MP_FlightPlan -. "port to →" .-> SPA_FlightsPage
|
||||||
|
MP_LeftBoard -. "port to →" .-> SPA_FlightParams
|
||||||
|
MP_MapView -. "port to →" .-> SPA_FlightMap
|
||||||
|
MP_PointsList -. "port to →" .-> SPA_FlightParams
|
||||||
|
MP_Lang -. "→ converge to react-i18next" .-> SPA_Types
|
||||||
|
|
||||||
|
SPA_FlightsPage --> SPA_GpsDenied
|
||||||
|
SPA_GpsDenied --> SPA_TestMode
|
||||||
|
|
||||||
|
class SPA_FlightsPage,SPA_FlightMap,SPA_FlightParams,SPA_FlightList,SPA_Utils,SPA_Types,SPA_GpsDenied target
|
||||||
|
class MP_FlightPlan,MP_LeftBoard,MP_MapView,MP_PointsList,MP_Services,MP_Lang source
|
||||||
|
```
|
||||||
|
|
||||||
|
> Test Mode (`tlog + video → IMU/GPS sync → SITL → onboard`) is a **planned**
|
||||||
|
> addition per `_docs/how_to_test.md`. The component's
|
||||||
|
> `description.md` §6b is the spec.
|
||||||
|
|
||||||
|
### Cross-feature / cross-layer edges (kept, flagged for baseline scan)
|
||||||
|
|
||||||
|
| From | To | Why it exists | Owner of fix (if any) |
|
||||||
|
|------|----|---------------|-----------------------|
|
||||||
|
| `07_dataset/DatasetPage` | `06_annotations/CanvasEditor` | Dataset reuses the annotation canvas. Legacy carry-over from WPF era (`Azaion.Common.Controls.CanvasEditor`). | Architecture Baseline Scan; lift `CanvasEditor` to a shared `components/canvas/` location when convenient. |
|
||||||
|
|
||||||
|
> The `classColors` cross-edge that earlier appeared between Annotations and
|
||||||
|
> SharedUI no longer exists at the component level — `classColors` is
|
||||||
|
> documented as its own component (`11_class-colors`). The physical file
|
||||||
|
> still lives in `src/features/annotations/`; that's a Step 4 testability
|
||||||
|
> file-move candidate, not a component-graph issue.
|
||||||
|
|
||||||
|
## Layered view
|
||||||
|
|
||||||
|
```
|
||||||
|
Layer 4 ── 10_app-shell
|
||||||
|
│
|
||||||
|
Layer 3 ── 04_login 05_flights* 06_annotations 07_dataset 08_admin 09_settings
|
||||||
|
│ │ │ │ │ │
|
||||||
|
Layer 2 ──────── 03_shared-ui ←──────────────────────────────────────────────────┘
|
||||||
|
│ ↑ (Header reads useAuth; uses Foundation; etc.)
|
||||||
|
│ 02_auth
|
||||||
|
│ │
|
||||||
|
Layer 1 ────── 01_api-transport 11_class-colors (sibling shared kernel)
|
||||||
|
│
|
||||||
|
Layer 0 ──── 00_foundation
|
||||||
|
|
||||||
|
* 05_flights spans two physical trees; the layering applies to the target
|
||||||
|
tree (src/features/flights/). The mission-planner/ port-source is its own
|
||||||
|
dependency island and is layered internally — see its module doc.
|
||||||
|
```
|
||||||
|
|
||||||
|
The exact layering with cycle markers is finalised in
|
||||||
|
`_docs/02_document/module-layout.md` (Step 2.5).
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
# Azaion UI — Glossary
|
||||||
|
|
||||||
|
**Status**: confirmed-by-user
|
||||||
|
**Date**: 2026-05-10
|
||||||
|
|
||||||
|
> Output of `/document` Step 4.5. Terms are grounded in the verified module
|
||||||
|
> (`modules/*.md`) and component (`components/*/description.md`) docs. Generic
|
||||||
|
> CS / web-platform terms are intentionally omitted. Each entry: one-line
|
||||||
|
> definition + source reference.
|
||||||
|
|
||||||
|
## A
|
||||||
|
|
||||||
|
**401-retry path**: the working bearer-refresh path inside `api/client.ts:44` — `POST /api/admin/auth/refresh` with `credentials:'include'`, fired automatically on any 401 from an authenticated fetch. Distinct from the broken bootstrap refresh (see *Bootstrap refresh*). *source: `_docs/02_document/04_verification_log.md` §2a, F2*
|
||||||
|
|
||||||
|
**Admin**: privileged operator persona. CRUD detection classes (add + delete + **edit** — edit re-introduced per Step 4.5 decision), users, AI/GPS settings. *source: `components/08_admin/description.md`*
|
||||||
|
|
||||||
|
**Affiliation**: friend / foe / civilian classification on a bbox. UI tokens (`AFFILIATION_COLORS`) exist but are dead today (finding #14). On-wire numeric values follow the suite spec — see `_docs/02_document/data_model.md` §enum-drift. *source: `components/06_annotations/description.md`*
|
||||||
|
|
||||||
|
**Aircraft**: drone hardware identity used by flights; `isDefault` toggle is the only mutation surfaced in the UI today. *source: `components/05_flights/description.md`, `components/08_admin/description.md`*
|
||||||
|
|
||||||
|
**Annotation**: bounding box + class + status (`Created` / `Edited` / `Validated`) tied to a media file at a given videoTime. The save endpoint is `POST /api/annotations/annotations` (doubly-prefixed). *source: `components/06_annotations/description.md`, `04_verification_log.md` F5*
|
||||||
|
|
||||||
|
**AnnotationStatus**: numeric enum `Created` / `Edited` / `Validated`. Suite spec is authoritative; the UI's TypeScript enum carries inline comments mapping each integer to its meaning. *source: `src/types/index.ts`, `04_verification_log.md` enum drift*
|
||||||
|
|
||||||
|
**Annotator (legacy)**: the WPF window (`Azaion.Annotator`) the React `06_annotations` component replaces. Used as a behavioral reference for keyboard shortcuts, time-window overlays, AI-detect UX. *source: `_docs/legacy/wpf-era.md` §3, `suite/annotations-research`*
|
||||||
|
|
||||||
|
**ARM-first**: production target is ARM64 edge devices (operator laptops, OrangePi, Jetson). CI builds ARM64 only today. *source: `.woodpecker/build-arm.yml`, `_docs/legacy/wpf-era.md` §1*
|
||||||
|
|
||||||
|
**Azaion**: project codename. Appears in legacy namespace (`Azaion.Annotator`, `Azaion.Dataset`), Tailwind design tokens (`az-bg`, `az-orange`, `az-panel`, etc.), and parent suite repo name. *source: `src/index.css`, `_docs/legacy/wpf-era.md`*
|
||||||
|
|
||||||
|
## B
|
||||||
|
|
||||||
|
**Bbox**: x, y, w, h coordinates of a detection rectangle in normalised pixels. *source: `components/06_annotations/description.md` (`CanvasEditor`)*
|
||||||
|
|
||||||
|
**Bearer auto-refresh**: the umbrella term for keeping the user's bearer token fresh. Two distinct paths exist in code (Step 4 finding) — see *401-retry path* and *Bootstrap refresh*. *source: `04_verification_log.md` §2a*
|
||||||
|
|
||||||
|
**Bearer token**: short-lived JWT held in **memory only**; never written to localStorage / sessionStorage. Refreshed via the HttpOnly *Refresh cookie*. *source: `src/auth/AuthContext.tsx`*
|
||||||
|
|
||||||
|
**Bootstrap refresh**: the broken bearer-refresh path inside `AuthContext.tsx:24` — `GET /api/admin/auth/refresh` without `credentials:'include'`. Step 4 fix candidate. *source: `04_verification_log.md` §2a, F2*
|
||||||
|
|
||||||
|
**Bulk-validate**: the `POST /api/annotations/dataset/bulk-status` action that transitions selected dataset items to `AnnotationStatus.Validated`. UI button is wired (`DatasetPage.tsx:142-146`); the `[V]` keyboard shortcut is missing. *source: `components/07_dataset/description.md` §6b*
|
||||||
|
|
||||||
|
## C
|
||||||
|
|
||||||
|
**CanvasEditor**: bounding-box draw / 8-handle resize / Ctrl-multi-select widget. Owned by `06_annotations`; `07_dataset` imports it directly (cross-feature edge — see baseline scan). *source: `components/06_annotations/description.md`*
|
||||||
|
|
||||||
|
**Class colors / classColors**: central color + text mapping for detection classes. Lifted into its own component (`11_class-colors`) at Step 2. *source: `components/11_class-colors/description.md`*
|
||||||
|
|
||||||
|
**Class Distribution chart**: Dataset Explorer's third tab — horizontal bars per `DetectionClass`, bar tinted with the class color. Implemented (`DatasetPage.tsx:151`, `loadDistribution()`). *source: `components/07_dataset/description.md` §6b*
|
||||||
|
|
||||||
|
**ClassNum / classId**: integer key of a `DetectionClass` (0..N-1). Note: `0` collides with the "all classes" sentinel in some filter UIs (finding #9). *source: `components/03_shared-ui/description.md` (`DetectionClasses`)*
|
||||||
|
|
||||||
|
**CombatReadiness**: ready / damaged / destroyed tag on a bbox. Surfaced like Affiliation, currently dormant in UI. Suite spec is source of truth; types file carries inline numeric-meaning comments. *source: `components/06_annotations/description.md`, `src/types/index.ts`*
|
||||||
|
|
||||||
|
**ConfirmDialog**: shared UI primitive for destructive-action confirmation. Reused across `05_flights`, `06_annotations`, `08_admin`, `07_dataset`. *source: `components/03_shared-ui/description.md`*
|
||||||
|
|
||||||
|
## D
|
||||||
|
|
||||||
|
**DatasetItem**: paged list shape returned by `GET /api/annotations/dataset`; carries thumbnail, classNum, status, isSeed?, isSplit?. *source: `components/07_dataset/description.md`*
|
||||||
|
|
||||||
|
**DetectionClass**: catalog entry `{id, name, color, photoMode, maxSizeM}`. The domain vocabulary for AI detection. Read via `GET /api/annotations/classes`; mutated via `POST /api/admin/classes` (add) + `PATCH /api/admin/classes/{id}` (**edit — to be re-introduced** per Step 4.5 decision) + `DELETE /api/admin/classes/{id}`. *source: `components/03_shared-ui/description.md` (`DetectionClasses`), `components/08_admin/description.md`*
|
||||||
|
|
||||||
|
**DetectionClasses (UI control)**: vertical strip widget rendering the catalog of classes; reused by `06_annotations` and `07_dataset`. Same control name as the WPF era for migration clarity. *source: `components/03_shared-ui/description.md`*
|
||||||
|
|
||||||
|
**Drone Maintenance (legacy)**: WPF feature ("Аналіз стану БПЛА"). **Dropped** per Step 4.5 decision; not ported. *source: `_docs/02_document/01_legacy_coverage_gaps.md`*
|
||||||
|
|
||||||
|
## E
|
||||||
|
|
||||||
|
**EventSource / SSE**: the only realtime channel. Used for live-GPS telemetry (F13), annotation-status events (F14), and the planned async-detect stream (F7). No WebSocket. *source: `src/api/sse.ts`*
|
||||||
|
|
||||||
|
## F
|
||||||
|
|
||||||
|
**FlightContext**: cross-cutting React Context that holds the flight list and the currently-selected flight. One of two cross-cutting contexts (the other is `AuthContext`); everything else is local state. *source: `components/03_shared-ui/description.md`*
|
||||||
|
|
||||||
|
**Flight**: a sortie + its waypoints + altitude profile + aircraft. `selectedFlightId` persists as a `UserSettings` field via `PUT /api/annotations/settings/user`, NOT via a dedicated `/api/flights/select` endpoint. *source: `components/05_flights/description.md`, `04_verification_log.md` F3*
|
||||||
|
|
||||||
|
## G
|
||||||
|
|
||||||
|
**GPS-Denied**: positioning workflow for flights without GPS — uses on-board IMU + visual matching against a pre-loaded reference. Sub-feature of `05_flights`. Includes the planned **Test Mode** (see below). *source: `components/05_flights/description.md`*
|
||||||
|
|
||||||
|
**GSD (Ground Sample Distance)**: meters-per-pixel computed from altitude + focal length + sensor size. Surfaced in the camera-config side panel (currently missing — finding #17). *source: `components/06_annotations/description.md` finding #17*
|
||||||
|
|
||||||
|
## I
|
||||||
|
|
||||||
|
**IsSeed**: per-annotation flag (legacy WPF concept) marking ground-truth seeds; visual port — 8 px IndianRed border on thumbnails — is unverified against current API. Open question deferred to a future task cycle. *source: `components/07_dataset/description.md` §6b*
|
||||||
|
|
||||||
|
## M
|
||||||
|
|
||||||
|
**Media**: an image or video file uploaded to a flight. `mediaType=1` is a magic literal in current code (finding #5/#10) pending a typed enum. *source: `components/06_annotations/description.md` (`MediaList`)*
|
||||||
|
|
||||||
|
**MediaStatus**: numeric enum on a media file. Suite spec is source of truth; UI types file carries inline numeric-meaning comments. *source: `src/types/index.ts`*
|
||||||
|
|
||||||
|
**Mission Planner (mission-planner/ tree)**: React 18 + MUI 5 port-source codebase living at the repo root. **Not deployed.** Treated as a behavioral reference for `05_flights` (the React 19 + Tailwind target). Convergence plan per Step 4.5 decision: flag at Step 2, spec at Step 3, port across Phase B cycles, delete the tree in the final cycle. *source: `components/05_flights/description.md`, `flows/existing-code.md`*
|
||||||
|
|
||||||
|
## N
|
||||||
|
|
||||||
|
**nginx (deployment runtime)**: serves the static `dist/` bundle and reverse-proxies `/api/<service>/*` to the matching suite service. Multi-stage Dockerfile output; ARM64 base image. *source: `Dockerfile`, `nginx.conf`*
|
||||||
|
|
||||||
|
## O
|
||||||
|
|
||||||
|
**Operator**: primary user persona. Flies missions, reviews annotations, runs AI detect. *source: `components/05_flights/description.md`, `components/06_annotations/description.md`*
|
||||||
|
|
||||||
|
**OpenWeatherMap**: external HTTP API consumed directly by the SPA for wind data in flight planning. API key currently hardcoded in `flightPlanUtils.ts:60` — **moving to `.env`** per Step 4.5 decision (Step 4 testability fix candidate). *source: `mission-planner/src/utils/flightPlanUtils.ts:60`*
|
||||||
|
|
||||||
|
## P
|
||||||
|
|
||||||
|
**Phase A / Phase B (autodev existing-code flow)**: Phase A = one-time baseline (Steps 1–8, Document → Refactor); Phase B = feature cycle (Steps 9–17, loops). Mission-planner convergence happens in Phase B per Step 4.5 decision. *source: `.cursor/skills/autodev/flows/existing-code.md`*
|
||||||
|
|
||||||
|
**PhotoMode**: drawing modality of a detection class — `Regular` (offset `0`), `Winter` (snow, offset `+20`), `Night` (offset `+40`). Carried both as the explicit `DetectionClass.photoMode` field and as the offset that produces *yoloId* (`yoloId = classId + photoModeOffset`). The `DetectionClasses` widget renders a three-button switcher (Sunny / Snowflake / Moon) and filters the class list to the active mode. *source: `components/11_class-colors/description.md`; `modules/src__components__DetectionClasses.md`; `data_model.md` enum table*
|
||||||
|
|
||||||
|
**ProtectedRoute**: route wrapper that redirects unauthenticated users to `/login`. Owns the gate between public (`/login`) and authenticated routes. *source: `components/02_auth/description.md`*
|
||||||
|
|
||||||
|
## R
|
||||||
|
|
||||||
|
**React Context (state-management approach)**: two contexts only — `AuthContext` and `FlightContext`. **Non-negotiable**: no Redux, no Zustand, no TanStack Query. Caching is in component state. *source: `src/auth/AuthContext.tsx`, `src/components/FlightContext.tsx`*
|
||||||
|
|
||||||
|
**Refresh cookie**: HttpOnly Secure cookie issued by `/api/admin/auth/refresh`. Carries the long-lived refresh token; never accessible to JavaScript. *source: `components/02_auth/description.md`*
|
||||||
|
|
||||||
|
**Resizable panel**: paneled UI surfaces (`AnnotationsPage` left/right, `DatasetPage` left) whose widths are typed in `UserSettings` and **persisted** per Step 4.5 decision. Hook is `useResizablePanel`; today the hook reads but does not write back (finding #11) — Step 4 fix. *source: `src/hooks/useResizablePanel.ts`, `components/00_foundation/description.md`*
|
||||||
|
|
||||||
|
## S
|
||||||
|
|
||||||
|
**Selected flight**: see *Flight*. Persisted via `PUT /api/annotations/settings/user` (NOT `/api/flights/select`). *source: `04_verification_log.md` F3*
|
||||||
|
|
||||||
|
**Selected media**: the currently-open media item in `06_annotations` / `07_dataset`. Page-local state, not in any context. *source: `components/06_annotations/description.md`*
|
||||||
|
|
||||||
|
**SITL (Software In The Loop)**: SITL = pre-recorded `.tlog` + video pair fed to the on-board service to validate GPS-Denied positioning end-to-end without a real flight. Used by Test Mode (F12). *source: `_docs/how_to_test.md`, `components/05_flights/description.md`*
|
||||||
|
|
||||||
|
**Sound Detections (legacy)**: WPF feature showing detections derived from audio analysis. **Dropped** per Step 4.5 decision; not ported. *source: `_docs/02_document/01_legacy_coverage_gaps.md`*
|
||||||
|
|
||||||
|
**Static bundle**: the only build artifact. nginx serves `dist/` and proxies `/api/<service>/*`. **Non-negotiable**: zero UI runtime, no SSR, no React Server Components. *source: `Dockerfile`, `nginx.conf`*
|
||||||
|
|
||||||
|
**Suite (parent meta-repo)**: `suite/` — contains the UI (this repo as a submodule), backend services, and the read-only `annotations-research` detached-head reference. *source: `_docs/legacy/wpf-era.md` §0*
|
||||||
|
|
||||||
|
## T
|
||||||
|
|
||||||
|
**Test Mode (GPS-Denied)**: planned end-to-end testing workflow inside `05_flights/GPS-Denied`. Operator uploads a `.tlog` + synced video; the system feeds them to the on-board service via SITL. *source: `_docs/how_to_test.md`, `components/05_flights/description.md`*
|
||||||
|
|
||||||
|
**tlog**: MAVLink telemetry log. One of two file inputs to GPS-Denied Test Mode (the other is the synced video). *source: `_docs/how_to_test.md`*
|
||||||
|
|
||||||
|
## U
|
||||||
|
|
||||||
|
**User**: admin-managed account `{id, email, role, isActive}`. Mutations via `/api/admin/users/*`. *source: `components/08_admin/description.md`*
|
||||||
|
|
||||||
|
**UserSettings**: per-user preferences entity persisted by the `annotations/` service. Carries `selectedFlightId`, panel widths, and other UI state. **Endpoint**: `/api/annotations/settings/user` (corrected at Step 4 — was incorrectly drafted as `admin/settings`). *source: `04_verification_log.md` §2a*
|
||||||
|
|
||||||
|
## V
|
||||||
|
|
||||||
|
**Validated** (annotation status): the terminal status of the AnnotationStatus enum. Synonyms in informal comments include "approved" — **prefer "Validated"** to match the typed enum. *source: `src/types/index.ts`*
|
||||||
|
|
||||||
|
## W
|
||||||
|
|
||||||
|
**Waypoint**: lat/lon/alt point in a flight; edit cycle is delete-then-recreate today (finding #19), pending API support for in-place updates. *source: `components/05_flights/description.md`*
|
||||||
|
|
||||||
|
**Woodpecker CI**: the CI runner. Pipeline at `.woodpecker/build-arm.yml`; builds + pushes `${REGISTRY_HOST}/azaion/ui:${branch}-arm`. No test step today — added in Step 5 of the autodev flow. *source: `.woodpecker/build-arm.yml`*
|
||||||
|
|
||||||
|
**WPF era**: legacy reference period — the C# / WPF stack (`Azaion.Annotator` + `Azaion.Dataset` + `MapMatcher`) the React port replaces. Documented in `_docs/legacy/wpf-era.md`. *source: `_docs/legacy/wpf-era.md`*
|
||||||
|
|
||||||
|
## Y
|
||||||
|
|
||||||
|
**yoloId**: `classId + photoModeOffset` — the on-wire detection-class identifier consumed by the Yolo backbone. Mapping owned by `11_class-colors`. *source: `components/11_class-colors/description.md`*
|
||||||
|
|
||||||
|
## Synonym / drift pairs (canonical → variants)
|
||||||
|
|
||||||
|
> When two terms appear interchangeably in code or comments, the **canonical**
|
||||||
|
> form below is the one to prefer. Tools / docs / commits should converge on it.
|
||||||
|
|
||||||
|
| Canonical | Variants seen | Where |
|
||||||
|
|-----------|---------------|-------|
|
||||||
|
| **Flight** | "mission" | mission-planner/ tree + some legacy comments |
|
||||||
|
| **Annotation** / **Annotator** (component, page) | (no drift) | — |
|
||||||
|
| **Annotator (legacy WPF)** | (no drift) | `_docs/legacy/wpf-era.md` only |
|
||||||
|
| **PhotoMode** | "modality" | a few module-doc summaries |
|
||||||
|
| **classNum** | "classId" | the two are used interchangeably; classNum dominates code |
|
||||||
|
| **Validated** (AnnotationStatus) | "approved" | informal comments |
|
||||||
|
| **mission-planner/ tree** | "Mission Planner port-source", "MUI port" | various component docs |
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
# Module Layout
|
||||||
|
|
||||||
|
**Status**: derived-from-code
|
||||||
|
**Language**: typescript (React 19 + Vite + Tailwind)
|
||||||
|
**Layout Convention**: custom (flat-features under `src/`; no per-component barrels)
|
||||||
|
**Root**: `src/`
|
||||||
|
**Last Updated**: 2026-05-10
|
||||||
|
|
||||||
|
> Authoritative file-ownership map for the React UI workspace. Derived from
|
||||||
|
> `_docs/02_document/00_discovery.md` (dependency graph) and the Step 2
|
||||||
|
> component specs at `_docs/02_document/components/`. Consumed by
|
||||||
|
> `/implement` Step 4 (file ownership), `/code-review` Phase 7
|
||||||
|
> (architecture violations), and `/refactor` discovery.
|
||||||
|
|
||||||
|
## Layout Rules
|
||||||
|
|
||||||
|
1. Each component owns ONE OR MORE top-level directories (or top-level files) under `src/`. The mapping is NOT 1:1 — `00_foundation` owns three sibling directories (`src/types/`, `src/hooks/`, `src/i18n/`), `05_flights` spans `src/features/flights/` AND a separate `mission-planner/` port-source root, and `10_app-shell` owns top-level files (`App.tsx`, `main.tsx`, `index.css`, `vite-env.d.ts`).
|
||||||
|
2. Shared code does **not** live under `src/shared/` today — there is no `shared/` directory. Two helper modules (`11_class-colors/classColors.ts` and `06_annotations/CanvasEditor.tsx`) are physically misplaced and consumed across components; both are flagged in the `## Verification Needed` block. A `src/shared/` directory is a Step 4 testability candidate.
|
||||||
|
3. Public API per component: NO barrel `index.ts` exists at any component root. The only `index.ts` files are `src/types/index.ts` (a re-export hub for type aliases — used as the de-facto public API for `00_foundation` types) and `mission-planner/src/types/index.ts`. Until Step 4 introduces barrels, Public API is approximated as "every named export from any file under the component's owned directories". Cross-component imports ARE happening at file-name granularity (`import { api } from '../api/client'`, `import { CanvasEditor } from '../annotations/CanvasEditor'`).
|
||||||
|
4. Cross-cutting concerns (logging, config, error handling, telemetry): no dedicated infrastructure today. `console.error` / silent catches are the closest thing — recorded in module findings.
|
||||||
|
5. Tests: there are **zero tests** under `src/`. The only test file is `mission-planner/src/test/jsonImport.test.ts`, which can't run because Jest isn't installed (00_discovery.md §11.5). Test layout is therefore TBD; suggest `src/<component>/__tests__/` per the standard React convention when tests are added (autodev Step 5–6).
|
||||||
|
|
||||||
|
## Per-Component Mapping
|
||||||
|
|
||||||
|
### Component: `00_foundation`
|
||||||
|
|
||||||
|
- **Epic**: TBD (set during autodev Step 4 / Decompose)
|
||||||
|
- **Directories**: `src/types/`, `src/hooks/`, `src/i18n/`
|
||||||
|
- **Public API** (de-facto, no barrel):
|
||||||
|
- `src/types/index.ts` — every exported type alias (`Detection`, `Flight`, `MediaItem`, `User`, etc.)
|
||||||
|
- `src/hooks/useDebounce.ts` — `useDebounce`
|
||||||
|
- `src/hooks/useResizablePanel.ts` — `useResizablePanel`
|
||||||
|
- `src/i18n/i18n.ts` — default export (i18n instance)
|
||||||
|
- **Internal**: `src/i18n/en.json`, `src/i18n/ua.json` (data; consumed only by `i18n.ts`)
|
||||||
|
- **Owns** (exclusive write): `src/types/**`, `src/hooks/**`, `src/i18n/**`
|
||||||
|
- **Imports from**: (none — Layer 0)
|
||||||
|
- **Consumed by**: every other component
|
||||||
|
|
||||||
|
### Component: `11_class-colors`
|
||||||
|
|
||||||
|
- **Epic**: TBD
|
||||||
|
- **Directories**: (none today — physical file lives at `src/features/annotations/classColors.ts`, which is owned by `06_annotations` on disk). Logical owner is this component; physical move to `src/shared/classColors.ts` (or `src/components/detection/classColors.ts`) is a Step 4 testability task.
|
||||||
|
- **Public API**: `src/features/annotations/classColors.ts` exports `getClassColor`, `getClassNameFallback`, `getPhotoModeSuffix`, `FALLBACK_CLASS_NAMES`.
|
||||||
|
- **Internal**: module-private `CLASS_COLORS` constant.
|
||||||
|
- **Owns**: pending — see Verification Needed item #1.
|
||||||
|
- **Imports from**: (none — Layer 0/1, no internal imports)
|
||||||
|
- **Consumed by**: `03_shared-ui` (DetectionClasses), `06_annotations` (CanvasEditor, AnnotationsPage, AnnotationsSidebar)
|
||||||
|
|
||||||
|
### Component: `01_api-transport`
|
||||||
|
|
||||||
|
- **Epic**: TBD
|
||||||
|
- **Directory**: `src/api/`
|
||||||
|
- **Public API** (de-facto): `src/api/client.ts` exports `api` (fetch wrapper); `src/api/sse.ts` exports `subscribeSSE` / equivalent helper.
|
||||||
|
- **Internal**: none (both files are externally consumed)
|
||||||
|
- **Owns**: `src/api/**`
|
||||||
|
- **Imports from**: `00_foundation` (types)
|
||||||
|
- **Consumed by**: `02_auth`, `03_shared-ui`, every feature page (04, 05, 06, 07, 08, 09)
|
||||||
|
|
||||||
|
### Component: `02_auth`
|
||||||
|
|
||||||
|
- **Epic**: TBD
|
||||||
|
- **Directory**: `src/auth/`
|
||||||
|
- **Public API**: `src/auth/AuthContext.tsx` exports `AuthProvider`, `useAuth`. `src/auth/ProtectedRoute.tsx` exports `ProtectedRoute`.
|
||||||
|
- **Internal**: none
|
||||||
|
- **Owns**: `src/auth/**`
|
||||||
|
- **Imports from**: `00_foundation`, `01_api-transport`
|
||||||
|
- **Consumed by**: `03_shared-ui` (Header reads `useAuth`), `04_login`, `10_app-shell` (mounts `AuthProvider` + `ProtectedRoute`)
|
||||||
|
|
||||||
|
### Component: `03_shared-ui`
|
||||||
|
|
||||||
|
- **Epic**: TBD
|
||||||
|
- **Directory**: `src/components/`
|
||||||
|
- **Public API** (de-facto, all are externally consumed):
|
||||||
|
- `Header.tsx` → `Header`
|
||||||
|
- `HelpModal.tsx` → `HelpModal`
|
||||||
|
- `ConfirmDialog.tsx` → `ConfirmDialog`
|
||||||
|
- `DetectionClasses.tsx` → `DetectionClasses`
|
||||||
|
- `FlightContext.tsx` → `FlightProvider`, `useFlight`
|
||||||
|
- **Internal**: none — every file in `src/components/` is consumed externally today
|
||||||
|
- **Owns**: `src/components/**`
|
||||||
|
- **Imports from**: `00_foundation`, `11_class-colors` (physical: `../features/annotations/classColors`), `01_api-transport`, `02_auth`
|
||||||
|
- **Consumed by**: `10_app-shell` (mounts `Header` + `FlightProvider`), every feature page (consumes `useFlight`, `ConfirmDialog`, `DetectionClasses`)
|
||||||
|
|
||||||
|
### Component: `04_login`
|
||||||
|
|
||||||
|
- **Epic**: TBD
|
||||||
|
- **Directory**: `src/features/login/`
|
||||||
|
- **Public API**: `LoginPage.tsx` → `LoginPage`
|
||||||
|
- **Internal**: none (single-page component)
|
||||||
|
- **Owns**: `src/features/login/**`
|
||||||
|
- **Imports from**: `00_foundation`, `01_api-transport`, `02_auth`
|
||||||
|
- **Consumed by**: `10_app-shell` (route)
|
||||||
|
|
||||||
|
### Component: `05_flights`
|
||||||
|
|
||||||
|
- **Epic**: TBD (this is the merged Flights & Mission Planning component)
|
||||||
|
- **Directories** (TWO physical roots):
|
||||||
|
- `src/features/flights/` — deployed target tree (15 modules)
|
||||||
|
- `mission-planner/` — port-source, NOT deployed (37 modules under `mission-planner/src/`). Documented inside this component per the user's Step 2 BLOCKING-gate decision (`_docs/02_document/state.json::component_05_flights_merge_2026-05-10`). The port direction is `mission-planner/` → `src/features/flights/`; module-layout treats both trees as owned by this component but only the target tree is in the layering table below.
|
||||||
|
- **Public API** (target tree, de-facto): `FlightsPage.tsx` → `FlightsPage` (route component). Internal sub-components (`FlightMap`, `FlightParamsPanel`, `FlightListSidebar`, `WaypointList`, `AltitudeChart`, `AltitudeDialog`, `WindEffect`, `MiniMap`, `MapPoint`, `DrawControl`, `JsonEditorDialog`, `mapIcons`, `flightPlanUtils`, `types`) are NOT consumed outside the component.
|
||||||
|
- **Public API** (port-source `mission-planner/`): not consumed at all by `src/` today (separate Vite entrypoint, `main.tsx` of its own). Effectively a private vendored sibling.
|
||||||
|
- **Internal** (target tree): every file under `src/features/flights/` except `FlightsPage.tsx`
|
||||||
|
- **Internal** (port-source): every file under `mission-planner/`
|
||||||
|
- **Owns**: `src/features/flights/**`, `mission-planner/**`
|
||||||
|
- **Imports from** (target tree): `00_foundation`, `01_api-transport`, `02_auth` (via `ProtectedRoute` from shell), `03_shared-ui` (uses `ConfirmDialog`, `useFlight`)
|
||||||
|
- **Consumed by**: `10_app-shell` (route)
|
||||||
|
|
||||||
|
### Component: `06_annotations`
|
||||||
|
|
||||||
|
- **Epic**: TBD
|
||||||
|
- **Directory**: `src/features/annotations/`
|
||||||
|
- **Public API** (de-facto):
|
||||||
|
- `AnnotationsPage.tsx` → `AnnotationsPage` (route component)
|
||||||
|
- `CanvasEditor.tsx` → `CanvasEditor` — **also imported by `07_dataset`** (cross-feature edge, see Verification Needed #3)
|
||||||
|
- **Internal**: `MediaList.tsx`, `VideoPlayer.tsx`, `AnnotationsSidebar.tsx`
|
||||||
|
- **Owns**: `src/features/annotations/**` EXCEPT `classColors.ts` (logically owned by `11_class-colors`; physical home pending refactor)
|
||||||
|
- **Imports from**: `00_foundation`, `11_class-colors`, `01_api-transport`, `03_shared-ui`
|
||||||
|
- **Consumed by**: `10_app-shell` (route); `07_dataset` (imports `CanvasEditor` directly — see Verification Needed)
|
||||||
|
|
||||||
|
### Component: `07_dataset`
|
||||||
|
|
||||||
|
- **Epic**: TBD
|
||||||
|
- **Directory**: `src/features/dataset/`
|
||||||
|
- **Public API**: `DatasetPage.tsx` → `DatasetPage`
|
||||||
|
- **Internal**: none (single-page)
|
||||||
|
- **Owns**: `src/features/dataset/**`
|
||||||
|
- **Imports from**: `00_foundation`, `11_class-colors` (only when class-distribution chart is added — not in code yet), `01_api-transport`, `03_shared-ui`, **`06_annotations` (CanvasEditor cross-feature edge)**
|
||||||
|
- **Consumed by**: `10_app-shell` (route)
|
||||||
|
|
||||||
|
### Component: `08_admin`
|
||||||
|
|
||||||
|
- **Epic**: TBD
|
||||||
|
- **Directory**: `src/features/admin/`
|
||||||
|
- **Public API**: `AdminPage.tsx` → `AdminPage`
|
||||||
|
- **Internal**: none (single-page)
|
||||||
|
- **Owns**: `src/features/admin/**`
|
||||||
|
- **Imports from**: `00_foundation`, `01_api-transport`, `03_shared-ui`
|
||||||
|
- **Consumed by**: `10_app-shell` (route)
|
||||||
|
|
||||||
|
### Component: `09_settings`
|
||||||
|
|
||||||
|
- **Epic**: TBD
|
||||||
|
- **Directory**: `src/features/settings/`
|
||||||
|
- **Public API**: `SettingsPage.tsx` → `SettingsPage`
|
||||||
|
- **Internal**: none (single-page)
|
||||||
|
- **Owns**: `src/features/settings/**`
|
||||||
|
- **Imports from**: `00_foundation`, `01_api-transport`, `03_shared-ui`
|
||||||
|
- **Consumed by**: `10_app-shell` (route)
|
||||||
|
|
||||||
|
### Component: `10_app-shell`
|
||||||
|
|
||||||
|
- **Epic**: TBD
|
||||||
|
- **Files** (no dedicated directory): `src/App.tsx`, `src/main.tsx`, `src/index.css`, `src/vite-env.d.ts`
|
||||||
|
- **Public API**: `main.tsx` is the Vite entrypoint (no symbols are externally imported). `App.tsx` exports `App`.
|
||||||
|
- **Internal**: `index.css` (global Tailwind base + `az-*` design-token CSS variables), `vite-env.d.ts` (type shim)
|
||||||
|
- **Owns**: `src/App.tsx`, `src/main.tsx`, `src/index.css`, `src/vite-env.d.ts`
|
||||||
|
- **Imports from**: every other component (it is the composition root)
|
||||||
|
- **Consumed by**: (none — top of the graph; bundled by Vite)
|
||||||
|
|
||||||
|
## Shared / Cross-Cutting
|
||||||
|
|
||||||
|
> No `src/shared/` directory exists today. Two cross-cutting concerns are tracked here as **proposed** shared modules; they require a physical file move scheduled for Step 4 (testability) or Step 8 (refactor).
|
||||||
|
|
||||||
|
### shared/class-colors (proposed; current physical location: `src/features/annotations/classColors.ts`)
|
||||||
|
|
||||||
|
- **Owner component**: `11_class-colors`
|
||||||
|
- **Purpose**: Detection-class fallback color, fallback name, PhotoMode suffix.
|
||||||
|
- **Owned by**: pending move task — current physical file is under `06_annotations`'s owns-glob, which makes it ambiguous. Workaround: until moved, treat `classColors.ts` as `OWNED` by tasks targeting `11_class-colors` and `READ-ONLY` to all other tasks (including those targeting `06_annotations`).
|
||||||
|
- **Consumed by**: `03_shared-ui/DetectionClasses`, `06_annotations` (CanvasEditor, AnnotationsPage, AnnotationsSidebar)
|
||||||
|
|
||||||
|
### shared/canvas-editor (proposed; current physical location: `src/features/annotations/CanvasEditor.tsx`)
|
||||||
|
|
||||||
|
- **Owner component**: still `06_annotations` for now (it's the dominant consumer)
|
||||||
|
- **Purpose**: Bounding-box draw / move / resize layer; reused by Dataset's inline editor.
|
||||||
|
- **Status**: cross-feature edge (07_dataset imports it). The proper home is a future `src/components/canvas/` directory. Decision deferred to Step 2 architecture baseline scan; the implement skill should treat this as a `READ-ONLY` for `07_dataset` tasks.
|
||||||
|
|
||||||
|
## Allowed Dependencies (layering)
|
||||||
|
|
||||||
|
Read top-to-bottom; an upper layer may import from a lower layer but NEVER the reverse. Same-layer imports are permitted only for explicit cross-feature edges listed in the table footnotes.
|
||||||
|
|
||||||
|
| Layer | Components | May import from |
|
||||||
|
|-------|------------|-----------------|
|
||||||
|
| 4. App Shell / Entry | `10_app-shell` | 0, 1, 2, 3 (and any feature in Layer 3) |
|
||||||
|
| 3. Application / Features | `04_login`, `05_flights`, `06_annotations`, `07_dataset`<sup>†</sup>, `08_admin`, `09_settings` | 0, 1, 2, 3<sup>†</sup> |
|
||||||
|
| 2. Composition | `02_auth`, `03_shared-ui` | 0, 1 |
|
||||||
|
| 1. Transport | `01_api-transport` | 0 |
|
||||||
|
| 0. Foundation / Shared kernel | `00_foundation`, `11_class-colors` | (none) |
|
||||||
|
|
||||||
|
<sup>†</sup> `07_dataset` imports `06_annotations/CanvasEditor.tsx` — same-layer cross-feature edge. Permitted today; flagged as a refactor target. The implement skill grants `07_dataset` tasks **READ-ONLY** access to that one file specifically.
|
||||||
|
|
||||||
|
Violations of this table are **Architecture** findings in code-review Phase 7 and are High severity.
|
||||||
|
|
||||||
|
## Verification Needed
|
||||||
|
|
||||||
|
The following inferences could not be made cleanly from code alone. They are surfaced for the user to confirm or override at the Step 2.5 BLOCKING gate.
|
||||||
|
|
||||||
|
1. **Physical home of `11_class-colors`**. The component is logically Layer 0/1 shared kernel, but its physical file lives inside `06_annotations`'s owns-glob (`src/features/annotations/classColors.ts`). Until the file is moved (proposed: `src/shared/classColors.ts`), the implement skill must apply the special-case rule documented under `shared/class-colors` above (READ-ONLY for `06_annotations` tasks even though the file is inside that component's directory). **Decision needed**: schedule the file move at Step 4 / Step 8, or accept the special-case rule indefinitely?
|
||||||
|
|
||||||
|
2. **Physical home of `CanvasEditor.tsx`**. Same shape: it lives under `06_annotations` and is consumed cross-feature by `07_dataset`. Proposed: `src/components/canvas/CanvasEditor.tsx` (or a new `06b_canvas` component). **Decision needed**: keep the same-layer cross-feature edge, or schedule the lift?
|
||||||
|
|
||||||
|
3. **No barrel exports anywhere**. The codebase imports cross-component at file-name granularity (`import { api } from '../api/client'`). This means every internal file is *de-facto* Public API. Recommendation: Step 4 testability task to add `src/<component>/index.ts` barrels per component, locking the public surface. **Decision needed**: add barrels now or stay file-import?
|
||||||
|
|
||||||
|
4. **`mission-planner/` is owned by `05_flights` but lives at the repo root** (not under `src/`). Layout rule #1 says one component owns one or more top-level directories — this satisfies the rule (it owns two: `src/features/flights/` AND `mission-planner/`). Implement-skill consumers must include `mission-planner/**` in `05_flights`'s OWNED glob. **Decision needed**: confirm the implement skill should treat `mission-planner/**` as OWNED by 05_flights (otherwise it's FORBIDDEN by default).
|
||||||
|
|
||||||
|
5. **`05_flights` cycle inside the port-source**. `mission-planner/src/flightPlanning/MapView.tsx ↔ MiniMap.tsx` form a circular import (named-handle, see `00_discovery.md` §7 footnote). They were analyzed together in batch MP-B6. The cycle is internal to the component and does not cross component boundaries; flagged here for completeness.
|
||||||
|
|
||||||
|
6. **`00_foundation` owns three sibling directories** (`types/`, `hooks/`, `i18n/`). Layout rule #1 permits this. Future option: split into `00a_types`, `00b_hooks`, `00c_i18n` if the directories grow. **Decision needed**: keep the multi-directory component, or split now?
|
||||||
|
|
||||||
|
7. **`10_app-shell` owns top-level files instead of a directory** (`src/App.tsx`, `src/main.tsx`, etc.). Layout rule #1 permits this. The implement-skill OWNED glob for app-shell tasks must therefore be the explicit file list, not a directory glob.
|
||||||
|
|
||||||
|
8. **Test layout is undefined** (no tests exist). When Steps 5–6 of autodev produce tests, recommended layout is `src/<component-dir>/__tests__/` per React convention; for `05_flights` cross-tree tests, prefer `src/features/flights/__tests__/` (target tree only).
|
||||||
|
|
||||||
|
## Layout Conventions (reference)
|
||||||
|
|
||||||
|
| Language | Root | Per-component path | Public API file | Test path |
|
||||||
|
|----------|------|-------------------|-----------------|-----------|
|
||||||
|
| TypeScript / React | `src/` | `src/<component>/` (this codebase deviates: features under `src/features/<feature>/`, shared chrome under `src/components/`) | `src/<component>/index.ts` (barrel — none exist today) | `src/<component>/__tests__/` (none exist today) |
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
# Module group: `mission-planner/`
|
||||||
|
|
||||||
|
> **Single consolidated doc** for the entire `mission-planner/` sub-project (37 modules across `services/`, `flightPlanning/`, `icons/`, `constants/`, `types/`, `utils.ts`, `config.ts`, `main.tsx`, `App.tsx`).
|
||||||
|
>
|
||||||
|
> **Why a single doc**: `mission-planner/` is **port-source, not deployed product** (per workspace `README.md` + `00_discovery.md §1`). The Dockerfile builds only the workspace `src/`. Documenting each of the 37 files at the same fidelity as the production SPA would burn context for code that exists only as a reference for the React-19 port. Future deletion of `mission-planner/` is a Step 8 / autodev follow-up once `src/features/flights/` reaches feature-parity.
|
||||||
|
|
||||||
|
## Role in the codebase
|
||||||
|
|
||||||
|
| Aspect | Status |
|
||||||
|
|---|---|
|
||||||
|
| Purpose | Reference implementation of the flight-mission planner UI being mechanically translated into `src/features/flights/`. Stand-alone CRA-flavoured Vite + React 18 + MUI 5 app. |
|
||||||
|
| Build | Has its own `vite.config.ts`, `tsconfig`, `index.html`, `package.json`. No alias path. |
|
||||||
|
| Deployment | None — workspace `Dockerfile` does NOT build it; `nginx.conf` does NOT serve it. |
|
||||||
|
| Tests | `src/test/jsonImport.test.ts` uses `describe/it/expect` style; **Jest is not installed** and there is no `test` script — the test cannot run as-is. Documented gap, no fix planned (out of scope per `00_discovery.md §11.5`). |
|
||||||
|
| Lifecycle | Will be deleted once `src/features/flights/` covers all mission-planner features. Not before. |
|
||||||
|
|
||||||
|
## Directory map
|
||||||
|
|
||||||
|
```
|
||||||
|
mission-planner/src/
|
||||||
|
├── main.tsx → mounts <LanguageProvider><FlightPlan /> into #root
|
||||||
|
├── App.tsx UNUSED — empty CRA stub; main.tsx mounts FlightPlan directly
|
||||||
|
├── config.ts COORDINATE_PRECISION, downang, upang, defaults
|
||||||
|
├── utils.ts newGuid (Math.random v4)
|
||||||
|
├── types/index.ts FlightPoint, CalculatedPointInfo, MapRectangle, AircraftParams,
|
||||||
|
│ WeatherData, MovingPointInfo, ActionMode, MapType, large
|
||||||
|
│ TranslationStrings interface tree
|
||||||
|
├── constants/
|
||||||
|
│ ├── actionModes.ts points / workArea / prohibitedArea
|
||||||
|
│ ├── maptypes.ts classic / satellite
|
||||||
|
│ ├── tileUrls.ts OSM + Esri ArcGIS tile URLs
|
||||||
|
│ ├── translations.ts English + Ukrainian dictionaries (raw, no i18next)
|
||||||
|
│ ├── languages.ts ISO codes + flag codes for LanguageSwitcher
|
||||||
|
│ └── purposes.ts tank, artillery
|
||||||
|
├── services/
|
||||||
|
│ ├── calculateDistance.ts Haversine + plane climb/cruise/descend
|
||||||
|
│ ├── AircraftService.ts mockGetAirplaneParams (returns hardcoded fixed-wing)
|
||||||
|
│ ├── WeatherService.ts OpenWeatherMap fetch
|
||||||
|
│ └── calculateBatteryUsage.ts Drag + thrust lookup; same algorithm as src/features/flights/flightPlanUtils.calculateBatteryPercentUsed
|
||||||
|
├── icons/
|
||||||
|
│ ├── MapIcons.tsx Leaflet icon factories
|
||||||
|
│ ├── PointIcons.tsx Per-purpose marker icons
|
||||||
|
│ ├── SidebarIcons.tsx MUI-styled side-panel SVGs
|
||||||
|
│ └── PhoneIcon.tsx Rotate-phone overlay (mobile orientation hint)
|
||||||
|
└── flightPlanning/
|
||||||
|
├── flightPlan.tsx Top-level page (369 lines) — owns state, dialogs, JSON I/O
|
||||||
|
├── MapView.tsx MapContainer + draw handlers + click-to-add (414 lines)
|
||||||
|
├── MiniMap.tsx Floating thumbnail; cyclic edge with MapView
|
||||||
|
├── MapPoint.tsx Draggable waypoint + popup
|
||||||
|
├── DrawControl.tsx Rectangle draw tool
|
||||||
|
├── PointsList.tsx Reorderable waypoint list
|
||||||
|
├── LeftBoard.tsx Side panel composing list + chart + totals + lang switcher
|
||||||
|
├── AltitudeChart.tsx Chart.js altitude visualizer
|
||||||
|
├── AltitudeDialog.tsx Add/Edit waypoint modal
|
||||||
|
├── JsonEditorDialog.tsx Edit-as-JSON modal
|
||||||
|
├── TotalDistance.tsx Total distance + time + battery readout
|
||||||
|
├── LanguageContext.tsx React Context for current locale (NOT i18next)
|
||||||
|
├── LanguageSwitcher.tsx Locale dropdown
|
||||||
|
├── WindEffect.tsx Wind heading / speed inputs + arrow preview
|
||||||
|
└── Aircraft.ts Aircraft helper (filename casing odd — TypeScript file, capitalised)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mapping `mission-planner/` ↔ `src/features/flights/`
|
||||||
|
|
||||||
|
The React 19 port translates module-for-module wherever possible. Status as of this commit:
|
||||||
|
|
||||||
|
| `mission-planner/src/...` | Ported to `src/features/flights/...` | Status |
|
||||||
|
|---|---|---|
|
||||||
|
| `flightPlanning/flightPlan.tsx` | `FlightsPage.tsx` | Ported with backend wiring (Flights API + SSE). MP-side has none. |
|
||||||
|
| `flightPlanning/MapView.tsx` | `FlightMap.tsx` | Ported. |
|
||||||
|
| `flightPlanning/MiniMap.tsx` | `MiniMap.tsx` | Ported. |
|
||||||
|
| `flightPlanning/MapPoint.tsx` | `MapPoint.tsx` | Ported. |
|
||||||
|
| `flightPlanning/DrawControl.tsx` | `DrawControl.tsx` | Ported. |
|
||||||
|
| `flightPlanning/PointsList.tsx` | `WaypointList.tsx` | Ported (renamed for parity with backend `Waypoint` entity). |
|
||||||
|
| `flightPlanning/LeftBoard.tsx` | `FlightParamsPanel.tsx` | Partial — MP-side hosts `LanguageSwitcher`, the SPA delegates language to global `Header` + `react-i18next`. |
|
||||||
|
| `flightPlanning/AltitudeChart.tsx` | `AltitudeChart.tsx` | Ported. |
|
||||||
|
| `flightPlanning/AltitudeDialog.tsx` | `AltitudeDialog.tsx` | Ported (file name kept; should be `WaypointDialog`). |
|
||||||
|
| `flightPlanning/JsonEditorDialog.tsx` | `JsonEditorDialog.tsx` | Ported. |
|
||||||
|
| `flightPlanning/WindEffect.tsx` | `WindEffect.tsx` | Ported. |
|
||||||
|
| `flightPlanning/TotalDistance.tsx` | (inlined into `FlightParamsPanel`) | Ported as a `<div>` strip in the params panel; no separate module. |
|
||||||
|
| `flightPlanning/LanguageContext.tsx` + `LanguageSwitcher.tsx` | (replaced by `react-i18next`) | Not ported as such — language is global via i18next. |
|
||||||
|
| `flightPlanning/Aircraft.ts` | (no equivalent) | Aircraft is server-side; the SPA fetches `/api/flights/aircrafts`. |
|
||||||
|
| `services/calculateDistance.ts` | `flightPlanUtils.calculateDistance` | Ported. |
|
||||||
|
| `services/calculateBatteryUsage.ts` | `flightPlanUtils.calculateBatteryPercentUsed` + `calculateAllPoints` | Ported. |
|
||||||
|
| `services/WeatherService.ts` | `flightPlanUtils.getWeatherData` | Ported (with the same hardcoded API key — Step 4 fix). |
|
||||||
|
| `services/AircraftService.ts` | `flightPlanUtils.getMockAircraftParams` (mock only) | Real fetch is `/api/flights/aircrafts` in `FlightsPage`. |
|
||||||
|
| `constants/translations.ts` + `LanguageContext.tsx` | `src/i18n/{en,ua}.json` + `i18n/i18n.ts` | Migrated to i18next. |
|
||||||
|
| `constants/{actionModes,maptypes,tileUrls,purposes,languages}.ts` | `features/flights/types.ts` (`PURPOSES`, `TILE_URLS`, `ActionMode`) | Consolidated into one file. |
|
||||||
|
| `icons/{MapIcons,PointIcons,SidebarIcons,PhoneIcon}.tsx` | `features/flights/mapIcons.ts` | Only the marker icons survived; SidebarIcons + PhoneIcon dropped (no rotate-phone overlay in the SPA today). |
|
||||||
|
| `utils.ts` (`newGuid`) | `flightPlanUtils.newGuid` | Ported. |
|
||||||
|
| `config.ts` | `features/flights/types.COORDINATE_PRECISION` | Single constant migrated. |
|
||||||
|
| `App.tsx` | (none) | Was already an unused CRA stub in MP; nothing to port. |
|
||||||
|
| `main.tsx` | (none) | Replaced by the workspace `src/main.tsx`. |
|
||||||
|
| `setupTests.ts`, `test/jsonImport.test.ts` | (none) | Cannot run; not migrated. |
|
||||||
|
|
||||||
|
## Things still in MP that are NOT in the SPA port
|
||||||
|
|
||||||
|
- **Rotate-phone overlay** (`icons/PhoneIcon.tsx`): MP shows a rotate-phone hint when held in portrait. The SPA does not.
|
||||||
|
- **Per-purpose marker icons** (`icons/PointIcons.tsx`): MP draws a different marker per `meta` purpose. The SPA uses three colour-coded icons (start / mid / end).
|
||||||
|
- **`Aircraft.ts` helper class**: never used in the SPA — aircraft state is fetched and treated as a plain DTO.
|
||||||
|
- **OpenWeather call directly from `WeatherService.ts`**: same flaw as the SPA port (hardcoded key, no proxy). Both flagged for Step 4.
|
||||||
|
- **MUI 5**: MP uses MUI for dialogs / inputs / icons. The SPA replaced everything with hand-rolled Tailwind components matching `_docs/ui_design/README.md`. MUI is not a dep of the workspace.
|
||||||
|
|
||||||
|
## Findings carried into Step 4 / 6 / 8
|
||||||
|
|
||||||
|
1. **`mission-planner/src/App.tsx` is an unused CRA stub** — `main.tsx` mounts `FlightPlan` directly. Deletion candidate but only after the port is complete (per `00_discovery.md §11.6`). Step 8.
|
||||||
|
2. **`mission-planner/src/test/jsonImport.test.ts` cannot run** (Jest not installed; no test script). Out of scope. Step 8 deletion or migration to the suite-level e2e harness.
|
||||||
|
3. **`flightPlanning/MapView.tsx ↔ MiniMap.tsx`** import each other (`MiniMap` imports the *named* `UpdateMapCenter` helper from `MapView`; `MapView` imports `MiniMap` as a JSX child). Module-level execution is non-circular because each side only uses the type/handle exposed at call time. Ported to `src/features/flights/FlightMap.tsx + MiniMap.tsx` without the cycle.
|
||||||
|
4. **MP uses raw translation tables** keyed by ISO code, not i18next. The SPA correctly migrated to `react-i18next`; the legacy Russian-language references (if any in `translations.ts`) are out of scope.
|
||||||
|
5. **`mission-planner/.env.example`** declares `VITE_SATELLITE_TILE_URL` — same env-driven pattern that the workspace `src/` should adopt for the Esri tile URL (currently hardcoded). Carry idea to Step 4.
|
||||||
|
6. **`mission-planner/package.json`** lockfile is uncommitted (`bun.lock` is workspace-only; MP uses npm without a committed lock). Reproducibility risk if anyone runs MP. Step 8 — when MP is deleted this becomes moot.
|
||||||
|
7. **Dependency divergence**: MP uses `react-leaflet` 4.2 vs workspace 5.x; `@hello-pangea/dnd` 16 vs 18; `chart.js` shared. None of this affects the deployed bundle. Step 8 — delete MP.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
The single `jsonImport.test.ts` cannot run. None of the other 36 modules have tests.
|
||||||
|
|
||||||
|
## Cross-doc references
|
||||||
|
|
||||||
|
- `_docs/02_document/00_discovery.md §1, §2b, §7b, §10, §11` — discovery-level facts about MP.
|
||||||
|
- Workspace `README.md` — declares MP as not-deployed.
|
||||||
|
- `mission-planner/README.md` — stale CRA boilerplate, do not trust.
|
||||||
|
- The 14 `src/features/flights/` modules — see consolidated `src__features__flights.md`.
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Modules: `src/App.tsx` + `src/main.tsx`
|
||||||
|
|
||||||
|
> Compact combined doc — both modules are tiny, top-of-tree wiring only.
|
||||||
|
|
||||||
|
## `src/main.tsx` (entry)
|
||||||
|
|
||||||
|
Mounts the React tree:
|
||||||
|
|
||||||
|
- Calls `createRoot(document.getElementById('root')!)` — the non-null assertion will throw at boot if `<div id="root">` is missing from `index.html` (it is present).
|
||||||
|
- Wraps in `<StrictMode>` (double-renders effects in dev) and `<BrowserRouter>` (HTML5 history).
|
||||||
|
- Imports `./i18n/i18n` for **side effects only** — that file calls `i18n.init({...})` at import time. See `src__i18n__i18n.md` for the locked-language finding (lng:'en' hardcoded).
|
||||||
|
- Imports `./index.css` — the Tailwind 4 stylesheet plus the `az-*` token definitions consumed by every component.
|
||||||
|
|
||||||
|
No props, no state, nothing testable.
|
||||||
|
|
||||||
|
## `src/App.tsx` (route tree)
|
||||||
|
|
||||||
|
Top-level routes:
|
||||||
|
|
||||||
|
| Path | Element | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `/login` | `<LoginPage />` | Public; outside auth + flight providers. |
|
||||||
|
| `/*` | `<ProtectedRoute><FlightProvider><Header />...nested Routes...</FlightProvider></ProtectedRoute>` | Auth-gated container. Mounts `Header` once across all child routes. |
|
||||||
|
| `/flights` | `<FlightsPage />` | (default redirect target) |
|
||||||
|
| `/annotations` | `<AnnotationsPage />` | |
|
||||||
|
| `/dataset` | `<DatasetPage />` | |
|
||||||
|
| `/admin` | `<AdminPage />` | (no extra role gate — see Findings) |
|
||||||
|
| `/settings` | `<SettingsPage />` | (no extra role gate — see Findings) |
|
||||||
|
| `*` | `<Navigate to="/flights" replace />` | catch-all under the protected branch. |
|
||||||
|
|
||||||
|
Outside everything: `<AuthProvider>`. So:
|
||||||
|
- `LoginPage` can call `useAuth()`.
|
||||||
|
- `FlightProvider` only mounts after `ProtectedRoute` has confirmed an authenticated user — `FlightContext` queries `/api/flights` only once we know we're logged in. This avoids the 401-then-401-loop on first paint.
|
||||||
|
|
||||||
|
Layout: `flex flex-col h-screen` — header at top, content fills the rest with `overflow-hidden`. Each page owns its own scroll/resize.
|
||||||
|
|
||||||
|
## Findings carried into Step 4 / 6
|
||||||
|
|
||||||
|
1. **`/admin` is reachable by users without ADM permission (defence-in-depth gap)**: `App.tsx:30` route has no permission check. `Header.tsx:88` filters menu visibility via `hasPermission('ADM')`, but typing `/admin` directly bypasses the menu hide. Users without ADM see a partially-working Admin page until the server returns 403 on each write. Per parent `../../../../_docs/00_roles_permissions.md` only Admin / ApiAdmin holds ADM. **PRIORITY** for Step 4. Note: `/settings` is similarly ungated, but `_docs/00_roles_permissions.md` does NOT define a `SETTINGS` permission code — settings calls land on `/api/admin/...` endpoints which are server-enforced by ADM via 403. Open question for Step 6: should `/settings` also be ADM-gated client-side, or is the per-user-settings subset (`/api/admin/users/me/settings`) intended to be reachable by non-admins?
|
||||||
|
2. **No `<ErrorBoundary>` wrapping the protected branch**: a render error inside any page crashes the whole tree. Step 4 / Step 8.
|
||||||
|
3. **No lazy-loading of route chunks** (`React.lazy` / `Suspense`). The whole app bundles in one chunk. For now the bundle is small enough that this is acceptable — Step 8 candidate when bundle size grows.
|
||||||
|
4. **Default redirect target is `/flights`** even for users whose primary task is annotations or dataset. Could be a per-role default landing page. Step 6.
|
||||||
|
|
||||||
|
(Earlier draft of this doc claimed there was no mobile bottom-nav — that was incorrect. `Header.tsx:113-129` does render a bottom-nav at `< sm`. The whole-app `flex flex-col h-screen` layout is the same at all breakpoints by design.)
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Cross-doc references
|
||||||
|
|
||||||
|
- `src__main_tsx` (this doc) ← entry; depended-upon by all others transitively.
|
||||||
|
- `src/auth/AuthContext.tsx`, `src/auth/ProtectedRoute.tsx` — already documented.
|
||||||
|
- `src/components/FlightContext.tsx`, `src/components/Header.tsx` — already documented.
|
||||||
|
- Parent roles spec: `../../../../_docs/00_roles_permissions.md`.
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# Module: `src/api/client.ts`
|
||||||
|
|
||||||
|
> **Source**: `src/api/client.ts` (65 lines)
|
||||||
|
> **Topo batch**: B2 (leaf — no internal imports)
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Minimal `fetch` wrapper that injects the JWT bearer token, normalises HTTP errors into `Error` throws, and transparently retries a single time after a 401 by attempting a refresh. Acts as the single HTTP entry point for every page; there is no per-service typed client.
|
||||||
|
|
||||||
|
## Public interface
|
||||||
|
|
||||||
|
Token plumbing:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export function setToken(token: string | null): void
|
||||||
|
export function getToken(): string | null
|
||||||
|
```
|
||||||
|
|
||||||
|
HTTP API:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const api = {
|
||||||
|
get: <T>(url) => Promise<T>
|
||||||
|
post: <T>(url, body?) => Promise<T>
|
||||||
|
put: <T>(url, body?) => Promise<T>
|
||||||
|
patch: <T>(url, body?) => Promise<T>
|
||||||
|
delete: <T>(url) => Promise<T>
|
||||||
|
upload: <T>(url, formData: FormData) => Promise<T>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Internal logic
|
||||||
|
|
||||||
|
- Module-level mutable variable `let accessToken: string | null` holds the current bearer token.
|
||||||
|
- `request<T>(url, options)`:
|
||||||
|
1. Build a `Headers` from `options.headers`, inject `Authorization: Bearer <token>` if present.
|
||||||
|
2. If `options.body` is a `string`, set `Content-Type: application/json`. (Crucial: `upload()` passes a `FormData` body, which is **not** a string, so `Content-Type` is left to the browser to set with the multipart boundary.)
|
||||||
|
3. `fetch(url, ...)`.
|
||||||
|
4. On `401` *and* a present token: call `refreshToken()`. On success, set the new bearer and retry the same request once. On failure, clear the token and `window.location.href = '/login'`, then throw "Session expired".
|
||||||
|
5. Hand off to `handleResponse<T>`.
|
||||||
|
- `handleResponse<T>(res)`:
|
||||||
|
- `204` → `undefined as T`.
|
||||||
|
- `!res.ok` → throw `new Error(\`${status}: ${text || statusText}\`)`. Body text is read defensively (`.catch(() => '')`).
|
||||||
|
- Otherwise → `res.json()` (no schema validation — caller types the response).
|
||||||
|
- `refreshToken()` — `POST /api/admin/auth/refresh` with `credentials: 'include'`. On 200, expects `{ token: string }` and stores it. Returns boolean.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- **Internal**: none.
|
||||||
|
- **External**: `fetch`, `Headers`, `FormData`, `Response` (browser globals). No npm runtime dependency.
|
||||||
|
|
||||||
|
## Consumers (intra-repo)
|
||||||
|
|
||||||
|
The module-level `api` object is imported by:
|
||||||
|
|
||||||
|
- `src/auth/AuthContext.tsx` (login / logout / initial refresh)
|
||||||
|
- `src/components/FlightContext.tsx` (flights list, user settings get/put)
|
||||||
|
- `src/components/DetectionClasses.tsx` (admin classes load)
|
||||||
|
- `src/features/admin/AdminPage.tsx`
|
||||||
|
- `src/features/settings/SettingsPage.tsx`
|
||||||
|
- `src/features/dataset/DatasetPage.tsx`
|
||||||
|
- `src/features/flights/FlightsPage.tsx`
|
||||||
|
- `src/features/annotations/{AnnotationsPage,AnnotationsSidebar,MediaList}.tsx`
|
||||||
|
|
||||||
|
`setToken` is imported by `AuthContext` (login / refresh / logout).
|
||||||
|
`getToken` is imported by `src/api/sse.ts` (to append the token to SSE URLs).
|
||||||
|
|
||||||
|
## Data models
|
||||||
|
|
||||||
|
None defined here. The generic `T` parameter is supplied by call sites.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
URLs are **string literals** at every call site (`/api/admin/...`, `/api/flights?...`, etc.). There is no base-URL constant. The `vite.config.ts` dev proxy and `nginx.conf` production rules forward `/api/*` to per-service backends.
|
||||||
|
|
||||||
|
A `VITE_API_BASE_URL` env-var fix is the canonical Step 4 testability candidate (workspace `README.md` calls this out).
|
||||||
|
|
||||||
|
## External integrations
|
||||||
|
|
||||||
|
Every backend the SPA talks to flows through this module. See `nginx.conf` for the routing table:
|
||||||
|
|
||||||
|
| Path prefix | Backend service |
|
||||||
|
|---|---|
|
||||||
|
| `/api/admin/*` | `admin/` (.NET) |
|
||||||
|
| `/api/annotations/*` | `annotations/` (.NET) |
|
||||||
|
| `/api/flights/*` | `flights/` (.NET) |
|
||||||
|
| `/api/resource/*` | `satellite-provider/` |
|
||||||
|
| `/api/detect/*` | `detections/` (Cython) |
|
||||||
|
| `/api/loader/*` | `loader/` (Cython) |
|
||||||
|
| `/api/gps-denied-desktop/*` | `gps-denied-desktop/` |
|
||||||
|
| `/api/gps-denied-onboard/*` | `gps-denied-onboard/` |
|
||||||
|
| `/api/autopilot/*` | `autopilot/` |
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- **Token storage**: in-memory only (`accessToken: string | null` at module scope). Survives in-tab navigations but not full reloads. The refresh path (`POST /api/admin/auth/refresh` with `credentials: 'include'`) implies the refresh token rides in an HttpOnly cookie set by the `admin/` service. The bearer access token is therefore short-lived and never persisted to `localStorage`. Acceptable XSS posture.
|
||||||
|
- **401 handling**: redirects to `/login` via `window.location.href` (full page reload) — clears any in-memory state including the bearer.
|
||||||
|
- **Race condition**: two parallel requests that both 401 will both call `refreshToken()` independently — one will succeed, one may receive a stale token mid-flight. Track for B3/B4 audit; minor under the current usage but should be serialised in Step 8.
|
||||||
|
- **No CSRF token**: relies on the bearer scheme only; `credentials: 'include'` is set only on `/refresh`, so other endpoints don't carry the cookie. Verify with `admin/` service contract during Step 6 `security_approach.md`.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Notes / open questions
|
||||||
|
|
||||||
|
- The `Authorization` header is set BEFORE `refreshToken()` in the retry path, but `refreshToken()` mutates the module-level `accessToken` and the retry then `headers.set('Authorization', \`Bearer ${accessToken}\`)` reads the NEW token. Correct, but worth a comment.
|
||||||
|
- `request` is typed `<T>` and trusts callers; a runtime schema validation layer (Zod, valibot) would be the right Step 8 hardening but is too heavy for testability scope.
|
||||||
|
- `upload(url, formData)` does NOT set `Content-Type`, allowing the browser to compute the multipart boundary. This is intentional and correct.
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# Module: `src/api/sse.ts`
|
||||||
|
|
||||||
|
> **Source**: `src/api/sse.ts` (25 lines)
|
||||||
|
> **Topo batch**: B3 (depends on B2 leaf: `api/client` for `getToken()` only)
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
A 25-line wrapper around the browser's native `EventSource` that (a) appends the current bearer token as an `access_token` query parameter (since `EventSource` does not let callers set headers), (b) parses each `MessageEvent` payload as JSON, and (c) returns a cleanup function. Used by features that listen for server-pushed updates (annotations queue, flight ingestion progress, etc.).
|
||||||
|
|
||||||
|
## Public interface
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export function createSSE<T>(
|
||||||
|
url: string,
|
||||||
|
onMessage: (data: T) => void,
|
||||||
|
onError?: (err: Event) => void,
|
||||||
|
): () => void
|
||||||
|
```
|
||||||
|
|
||||||
|
The returned function closes the underlying `EventSource`. Callers MUST call it on unmount to avoid leaking long-lived connections.
|
||||||
|
|
||||||
|
## Internal logic
|
||||||
|
|
||||||
|
1. `const token = getToken()` — read the current bearer from `api/client.ts`.
|
||||||
|
2. Build `fullUrl`:
|
||||||
|
- If `token` is non-null: append `access_token=<token>` using `&` if the URL already has a query string, `?` otherwise.
|
||||||
|
- If `token` is null: use `url` as-is.
|
||||||
|
3. `new EventSource(fullUrl)`.
|
||||||
|
4. `source.onmessage = (e) => { try { onMessage(JSON.parse(e.data) as T) } catch { /* ignore */ } }`.
|
||||||
|
- JSON parse errors are silently swallowed. The contract is that the server sends valid JSON; a malformed frame degrades to "skipped".
|
||||||
|
5. `source.onerror = (e) => onError?.(e)` — forwarded straight through. The browser auto-reconnects by default; `onError` lets callers observe the disconnect.
|
||||||
|
6. Return `() => source.close()`.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- **Internal**: `./client` — `getToken()`.
|
||||||
|
- **External**: `EventSource` (browser global).
|
||||||
|
|
||||||
|
## Consumers (intra-repo)
|
||||||
|
|
||||||
|
From the §7a dependency graph:
|
||||||
|
|
||||||
|
- `src/features/annotations/AnnotationsSidebar.tsx` — subscribes to the annotations stream.
|
||||||
|
- `src/features/flights/FlightsPage.tsx` — subscribes to flight ingestion / state updates.
|
||||||
|
|
||||||
|
## Data models
|
||||||
|
|
||||||
|
None defined here. The generic `T` is supplied by the caller.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
URLs are passed in by callers (string-literal at call sites). The same testability remark as `api/client.ts` applies: a `VITE_API_BASE_URL` is the natural Step 4 fix.
|
||||||
|
|
||||||
|
## External integrations
|
||||||
|
|
||||||
|
Whichever backend exposes the SSE endpoint at the URL the caller provides. Per `nginx.conf`, the suite's `/api/*` reverse proxy forwards SSE traffic by default (no special EventSource-blocking config) — verify in Step 4.
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- **Bearer in query string**: `access_token=<jwt>` ends up in browser-history URLs, server access logs, and proxy logs. This is a **known weakness of the EventSource API** — the API has no headers parameter, so cookie or query are the only options. The trade-off was made knowingly (SSE is a long-lived GET; the bearer is short-lived; nginx access logs are an internal-only artefact). Document in `security_approach.md` (Step 6) and consider rotating to a dedicated SSE-only short-lived token in Step 8.
|
||||||
|
- **`getToken()` on connect, no refresh**: if the bearer rotates mid-session (via `client.ts`'s 401 retry path), the `EventSource` keeps using the old token and will eventually error. Callers must observe `onError` and reconnect. The current consumers (`AnnotationsSidebar`, `FlightsPage`) do NOT do this — they create the source once on mount. Flag for Step 4 / Step 8.
|
||||||
|
- **Silent JSON parse error**: a single malformed frame is skipped without a `console.warn`. Acceptable for production noise reduction but obscures real backend bugs in dev. Defer.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Notes / open questions
|
||||||
|
|
||||||
|
- **No reconnect / backoff logic** — relies on the browser's built-in EventSource auto-reconnect. The default is "keep retrying"; on a flaky connection this may produce a tight loop with backend logs. Confirm acceptable in Step 6 against the `annotations/` and `flights/` service rate-limit posture.
|
||||||
|
- **Cleanup on token-less call**: `getToken()` returning `null` produces an unauthenticated `EventSource`. The backend should reject with `401`, the `EventSource` then errors, and `onError?` fires. The caller is expected to interpret this as "user logged out, stop subscribing". None of the current consumers do that explicitly; instead, the `ProtectedRoute` gate prevents `useEffect` from ever running while logged out, so the path is unreachable in practice. Document but no action needed.
|
||||||
|
- The function is fully synchronous — does not return a `Promise`. The connection is initiated on call but the first message may arrive later. Consumers must handle the "no messages yet" UI state.
|
||||||
|
- The query-string token assembly does NOT URL-encode the token. JWTs are URL-safe Base64 by default and contain only `A–Z, a–z, 0–9, -, _, .`, so this is safe for the current token shape. If the token format ever changes, add `encodeURIComponent`. Note for Step 8.
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
# Module: `src/auth/AuthContext.tsx`
|
||||||
|
|
||||||
|
> **Source**: `src/auth/AuthContext.tsx` (54 lines)
|
||||||
|
> **Topo batch**: B3 (depends on B2 leaves: `api/client`, `types/index`)
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
The single source of truth for the SPA's authentication state. Wraps the bearer-token plumbing from `api/client.ts` in a React context, exposes `useAuth()` for any descendant component, and bootstraps the session on app start by attempting a refresh. Together with `ProtectedRoute.tsx` and `LoginPage.tsx`, this is the WPF-era `LoginWindow.xaml` + auth service replacement (`_docs/legacy/wpf-era.md` §3 / §4).
|
||||||
|
|
||||||
|
## Public interface
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface AuthState {
|
||||||
|
user: AuthUser | null
|
||||||
|
loading: boolean
|
||||||
|
login: (email: string, password: string) => Promise<void>
|
||||||
|
logout: () => Promise<void>
|
||||||
|
hasPermission: (perm: string) => boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAuth(): AuthState
|
||||||
|
export function AuthProvider({ children }: { children: ReactNode }): JSX.Element
|
||||||
|
```
|
||||||
|
|
||||||
|
`AuthContext` itself is module-private (`createContext<AuthState>(null!)`). Consumers must go through `useAuth()`.
|
||||||
|
|
||||||
|
## Internal logic
|
||||||
|
|
||||||
|
State:
|
||||||
|
|
||||||
|
- `user: AuthUser | null` — `null` when unauthenticated.
|
||||||
|
- `loading: boolean` — `true` until the initial refresh attempt resolves (success or failure). Renders should gate on this.
|
||||||
|
|
||||||
|
**Bootstrap effect (mount-only)**:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
api.get<{ user: AuthUser; token: string }>('/api/admin/auth/refresh')
|
||||||
|
.then(data => { setToken(data.token); setUser(data.user) })
|
||||||
|
.catch(() => {})
|
||||||
|
.finally(() => setLoading(false))
|
||||||
|
```
|
||||||
|
|
||||||
|
The refresh endpoint is invoked with `credentials: 'include'` only inside `client.ts`'s **internal** `refreshToken()` helper — but here we go through the public `api.get()` path, which does NOT include credentials. **This is a real divergence**: `client.ts`'s internal `refreshToken()` (used in the 401 retry) sends the cookie; the bootstrap call in `AuthContext` does not. The endpoint must therefore accept the refresh either via cookie (then bootstrap fails silently for non-cookie clients — which is everyone after a hard reload) **or** via some other mechanism (a refresh token in `localStorage`, etc.). **Flag for Step 4 verification** against the `admin/` service contract; this is likely a real bug masking by silent `.catch`.
|
||||||
|
|
||||||
|
**`login(email, password)`**:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const data = await api.post<{ token; user }>('/api/admin/auth/login', { email, password })
|
||||||
|
setToken(data.token); setUser(data.user)
|
||||||
|
```
|
||||||
|
|
||||||
|
Throws to caller (LoginPage) on bad credentials.
|
||||||
|
|
||||||
|
**`logout()`**:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
try { await api.post('/api/admin/auth/logout') } catch {}
|
||||||
|
setToken(null); setUser(null)
|
||||||
|
```
|
||||||
|
|
||||||
|
Network failure on logout is silently swallowed because we want to clear local auth state regardless.
|
||||||
|
|
||||||
|
**`hasPermission(perm)`**: returns `user?.permissions.includes(perm) ?? false`. The permission strings are not constrained at the type level — any string passes. Backend-defined.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- **Internal**:
|
||||||
|
- `../api/client` — `api`, `setToken`.
|
||||||
|
- `../types` — `AuthUser` type.
|
||||||
|
- **External**: `react` (`createContext`, `useContext`, `useState`, `useCallback`, `useEffect`, `ReactNode`).
|
||||||
|
|
||||||
|
## Consumers (intra-repo)
|
||||||
|
|
||||||
|
From the §7a dependency graph:
|
||||||
|
|
||||||
|
- `src/auth/ProtectedRoute.tsx` — gates routed children on `user !== null`.
|
||||||
|
- `src/components/Header.tsx` — shows current user, exposes Logout.
|
||||||
|
- `src/features/login/LoginPage.tsx` — calls `login(...)`, redirects on success.
|
||||||
|
- `src/App.tsx` — mounts `AuthProvider` near the root.
|
||||||
|
|
||||||
|
(Other features rely on the bearer token implicitly via `api/client.ts` — they don't import `useAuth` directly.)
|
||||||
|
|
||||||
|
## Data models
|
||||||
|
|
||||||
|
`AuthUser` (from `src/types/index.ts`) — see `_docs/02_document/modules/src__types__index.md`. Carries at minimum `id`, `email`, `permissions: string[]`.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Endpoints (string-literal): `/api/admin/auth/refresh`, `/api/admin/auth/login`, `/api/admin/auth/logout`. Routed by `nginx.conf` to the `admin/` service.
|
||||||
|
|
||||||
|
No env vars consumed directly — token storage policy is defined in `client.ts` (in-memory; not persisted to `localStorage`).
|
||||||
|
|
||||||
|
## External integrations
|
||||||
|
|
||||||
|
`admin/` (.NET) auth service via `/api/admin/auth/{refresh,login,logout}`.
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- **In-memory token only**: the bearer is held by `client.ts` at module scope. Survives intra-tab navigation, lost on hard reload — at which point the refresh path must restore it (currently broken per the bootstrap-effect note above).
|
||||||
|
- **`hasPermission` runs client-side only**: the server is the authority; `hasPermission` is for UI affordances (hide vs. show buttons). The backend MUST re-check permissions on every endpoint. Document in `security_approach.md` (Step 6).
|
||||||
|
- **Silent error swallowing on bootstrap and logout** is intentional but obscures real failures. A dev-only `console.error` would help during the testability pass; do not add a user-visible toast (silent recovery is the correct UX).
|
||||||
|
- **No XSS exfiltration risk for the bearer**: in-memory only, never written to `localStorage` or a non-HttpOnly cookie. (Confirmed in `client.ts` doc.)
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Notes / open questions
|
||||||
|
|
||||||
|
- **Bootstrap-vs-refresh divergence** (above) — the highest-priority flag in this module. Either:
|
||||||
|
1. The refresh endpoint accepts an Authorization-less, cookie-bearing call → confirm the `admin/` service sets an HttpOnly cookie on `/login` and the cookie path matches `/api/admin/auth/refresh`. The `api.get()` path in `client.ts` does NOT send `credentials: 'include'`, so this currently CANNOT work. → **likely bug**.
|
||||||
|
2. Or the bootstrap should be calling the internal `refreshToken()` helper, which is currently not exported.
|
||||||
|
Either way, this needs a Step 4 fix (export `refreshToken()` and call it here, or change `api.get()` to allow per-call `credentials`).
|
||||||
|
- **`AuthContext = createContext<AuthState>(null!)`**: the non-null assertion means `useAuth()` will throw at the destructuring site if it's used outside `AuthProvider`. Acceptable given `App.tsx` mounts `AuthProvider` at the top, but a guard `if (!ctx) throw new Error(...)` would be friendlier. Defer.
|
||||||
|
- The `loading` flag is never re-set to `true` after the initial bootstrap. `login` and `logout` complete synchronously from the React tree's perspective (the `await` is inside the callback). If a future requirement demands a "logging in…" indicator, it would need its own state. Note for Step 8.
|
||||||
|
- `useAuth` returns the raw context value (no memoisation wrapper). React 18+ behaviour means `<AuthProvider>` re-renders all `useAuth` consumers on every state update — fine here because there's no high-frequency state.
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
# Module: `src/auth/ProtectedRoute.tsx`
|
||||||
|
|
||||||
|
> **Source**: `src/auth/ProtectedRoute.tsx` (19 lines)
|
||||||
|
> **Topo batch**: B4 (depends on B3: `auth/AuthContext`)
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
A tiny route guard that gates its children behind an authenticated session.
|
||||||
|
While `AuthContext` is bootstrapping (`loading === true`) it shows a spinner;
|
||||||
|
when the bootstrap finishes with no user it redirects to `/login`; otherwise
|
||||||
|
it renders its children. Mounted exactly once in `App.tsx` between
|
||||||
|
`AuthProvider` and `FlightProvider` so every authenticated page benefits
|
||||||
|
from it. WPF parallel: the implicit "no LoginWindow open ⇒ MainWindow"
|
||||||
|
gate (`_docs/legacy/wpf-era.md` §4).
|
||||||
|
|
||||||
|
## Public interface
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface Props { children: ReactNode }
|
||||||
|
export default function ProtectedRoute(props: Props): JSX.Element
|
||||||
|
```
|
||||||
|
|
||||||
|
Always returns a `JSX.Element` — never `null`. The three rendered shapes
|
||||||
|
are:
|
||||||
|
|
||||||
|
1. Spinner (centered `<div>` with the orange ring) while `loading`.
|
||||||
|
2. `<Navigate to="/login" replace />` when `loading === false && user == null`.
|
||||||
|
3. `<>{children}</>` otherwise.
|
||||||
|
|
||||||
|
`replace` is intentional: it rewrites the history entry so the back
|
||||||
|
button does not return to a protected route the user was bounced off.
|
||||||
|
|
||||||
|
## Internal logic
|
||||||
|
|
||||||
|
- Single hook call: `const { user, loading } = useAuth()`. No local state.
|
||||||
|
- Branch order matters — `loading` is checked before `user` so a freshly
|
||||||
|
reloaded tab never renders the login redirect during the in-flight
|
||||||
|
refresh attempt. (See the open `AuthContext` bootstrap-vs-refresh
|
||||||
|
divergence flagged in `src__auth__AuthContext.md`; if that bug is
|
||||||
|
fixed the spinner duration becomes accurate.)
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- **Internal**: `./AuthContext` — `useAuth`.
|
||||||
|
- **External**: `react-router-dom` (`Navigate`), `react` (`ReactNode` type only).
|
||||||
|
|
||||||
|
## Consumers (intra-repo)
|
||||||
|
|
||||||
|
From the §7a dependency graph:
|
||||||
|
|
||||||
|
- `src/App.tsx` — wraps the entire authenticated route tree:
|
||||||
|
`AuthProvider → ProtectedRoute → FlightProvider → Header + nested Routes`.
|
||||||
|
|
||||||
|
Not used anywhere else; the SPA has a single protected zone.
|
||||||
|
|
||||||
|
## Data models
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The `/login` redirect target is hardcoded. Matches the public route
|
||||||
|
declared in `App.tsx`. If the public route is ever renamed, update both
|
||||||
|
sites.
|
||||||
|
|
||||||
|
The spinner uses Tailwind tokens `bg-az-bg`, `border-az-orange`
|
||||||
|
(defined in `src/index.css`).
|
||||||
|
|
||||||
|
## External integrations
|
||||||
|
|
||||||
|
None directly. Indirectly relies on whatever `AuthContext` calls during
|
||||||
|
bootstrap — currently `GET /api/admin/auth/refresh`.
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- **No token check is duplicated here** — relies entirely on `AuthContext`.
|
||||||
|
The component cannot be confused into rendering protected children
|
||||||
|
before the bootstrap resolves because `loading` defaults to `true` in
|
||||||
|
`AuthProvider`.
|
||||||
|
- Backend authority is unchanged — this is a UI affordance only. Every
|
||||||
|
request the children make MUST also be enforced server-side.
|
||||||
|
- The redirect uses `Navigate`, so query params on the original URL are
|
||||||
|
lost. Acceptable today (no protected route relies on them); flag if a
|
||||||
|
future "deep-link after login" UX appears.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Notes / open questions
|
||||||
|
|
||||||
|
- The spinner has no `role="status"` or accessible label — screen readers
|
||||||
|
hear nothing while the bootstrap runs. Cosmetic; flag for Step 4
|
||||||
|
verification against `_docs/ui_design/README.md` accessibility notes.
|
||||||
|
- No timeout on the `loading` state — if `AuthContext`'s bootstrap
|
||||||
|
somehow never resolves (e.g., the refresh endpoint hangs), the user
|
||||||
|
sees an infinite spinner. `client.ts` does not currently set a
|
||||||
|
request timeout either; flag jointly with Step 4.
|
||||||
|
- The `<>{children}</>` Fragment wrap is intentional: returning `children`
|
||||||
|
directly would make the type `ReactNode` rather than `JSX.Element` and
|
||||||
|
would not satisfy the route element slot in `react-router-dom@7`.
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# Module: `src/components/ConfirmDialog.tsx`
|
||||||
|
|
||||||
|
> **Source**: `src/components/ConfirmDialog.tsx` (47 lines)
|
||||||
|
> **Topo batch**: B3 (uses `react-i18next` only; no intra-repo deps)
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
A controlled, single-message confirm dialog used wherever the SPA needs an "Are you sure?" gate. Replaces the legacy WPF `MessageBox.Show(..., MessageBoxButton.YesNo, ...)` pattern (`_docs/legacy/wpf-era.md` §4 / §"What survived").
|
||||||
|
|
||||||
|
## Public interface
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface Props {
|
||||||
|
open: boolean
|
||||||
|
title: string
|
||||||
|
message?: string
|
||||||
|
onConfirm: () => void
|
||||||
|
onCancel: () => void
|
||||||
|
}
|
||||||
|
export default function ConfirmDialog(props: Props): JSX.Element | null
|
||||||
|
```
|
||||||
|
|
||||||
|
When `open === false`, returns `null`. When `open === true`, renders a centered modal over a dimmed backdrop. The component is fully controlled — it owns no `open` state itself.
|
||||||
|
|
||||||
|
## Internal logic
|
||||||
|
|
||||||
|
- **Auto-focus**: when `open` flips to `true`, the Cancel button is focused via `cancelRef.current?.focus()` (see `useEffect` keyed on `open`). This makes Cancel the default keyboard target — a common pattern for destructive confirmations.
|
||||||
|
- **Escape-to-cancel**: a `keydown` handler is attached to `window` while `open === true`; pressing `Escape` calls `onCancel()`. The listener is removed on unmount or when `open` flips to `false`. This is the **only** dismiss path other than the explicit Cancel/Confirm buttons (no backdrop click handler).
|
||||||
|
- **Translation**: button labels use `t('common.cancel')` and `t('common.confirm')`. Title and message are passed in by the caller — the caller is responsible for translation.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- **Internal**: none.
|
||||||
|
- **External**: `react` (`useEffect`, `useRef`), `react-i18next` (`useTranslation`).
|
||||||
|
|
||||||
|
## Consumers (intra-repo)
|
||||||
|
|
||||||
|
From the §7a dependency graph in `_docs/02_document/00_discovery.md`:
|
||||||
|
|
||||||
|
- `src/features/admin/AdminPage.tsx` — confirm before deleting users.
|
||||||
|
- `src/features/annotations/MediaList.tsx` — confirm before deleting media.
|
||||||
|
- `src/features/flights/FlightsPage.tsx` — confirm before deleting a flight.
|
||||||
|
- `src/features/dataset/DatasetPage.tsx` — confirm before deleting dataset items.
|
||||||
|
|
||||||
|
(`HelpModal.tsx` is the *non-confirm* sibling; it does NOT use `ConfirmDialog` and notably does **not** have Escape-to-close — see `src__components__HelpModal.md` §Notes.)
|
||||||
|
|
||||||
|
## Data models
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Tailwind tokens used: `bg-az-panel`, `border-az-border`, `text-az-text`, `bg-az-red`, `bg-az-bg`, `text-white`. All defined in `src/index.css` (per `_docs/02_document/00_discovery.md` §2a).
|
||||||
|
|
||||||
|
`z-[100]` is the chosen stacking layer. No other modal currently competes with it; if a third-party library introduces a portal at `z-[200]+`, layering will need a docs entry.
|
||||||
|
|
||||||
|
## External integrations
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- **No backdrop dismissal**: clicking outside the dialog does NOT cancel. Combined with Escape-to-cancel and a default-focused Cancel button, this gives a deliberate, keyboard-safe destructive-action flow.
|
||||||
|
- **No `aria-modal` / role="dialog"**: not annotated for screen readers; flag for Step 4 verification against the `_docs/ui_design/README.md` §"Confirmation dialogs" spec (which specifies modal semantics).
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Notes / open questions
|
||||||
|
|
||||||
|
- The "destructive" colour (`bg-az-red`) is hardcoded into the Confirm button. Some callers use `ConfirmDialog` for non-destructive confirms (e.g. flight selection in some flows — verify in B7); a future variant prop (`destructive: boolean`) would be cleaner. Defer to Step 8.
|
||||||
|
- No `width` prop — the dialog is fixed at `w-80` (320px). On the mobile breakpoint defined in `_docs/ui_design/README.md` §"Responsive Breakpoints" (640px), this fits, but very long titles or multi-line messages will overflow. Flag for Step 4 cosmetic verification.
|
||||||
|
- Escape handler attaches to `window`, not the dialog DOM. If two `ConfirmDialog`s are mounted simultaneously (the SPA never does this today, but it's not enforced), both would call their `onCancel()` on a single Escape press. Acceptable under current usage.
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# Module: `src/components/DetectionClasses.tsx`
|
||||||
|
|
||||||
|
> **Source**: `src/components/DetectionClasses.tsx` (99 lines)
|
||||||
|
> **Topo batch**: B3 (depends on B2 leaves: `api/client`, `features/annotations/classColors`, `types/index`)
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
A side-panel widget that lets the annotator pick a detection class (1–9 or click) and a photo mode (`Regular`, `Winter`, `Night`). Loads the class catalogue from the backend and falls back to a hardcoded list when the API returns empty / errors. Replaces the legacy WPF detection-class picker referenced in `_docs/legacy/wpf-era.md` §"What survived".
|
||||||
|
|
||||||
|
## Public interface
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface Props {
|
||||||
|
selectedClassNum: number
|
||||||
|
onSelect: (classNum: number) => void
|
||||||
|
photoMode: number
|
||||||
|
onPhotoModeChange: (mode: number) => void
|
||||||
|
}
|
||||||
|
export default function DetectionClasses(props: Props): JSX.Element
|
||||||
|
```
|
||||||
|
|
||||||
|
Fully controlled — the parent owns `selectedClassNum` and `photoMode`. The current contract for `photoMode` values is integer offsets `0` (Regular), `20` (Winter), `40` (Night), matching the `photoMode` field of `DetectionClass` (`src/types/index.ts`) and the offsets baked into `FALLBACK_CLASSES` below.
|
||||||
|
|
||||||
|
## Internal logic
|
||||||
|
|
||||||
|
- **Class catalogue load** (mount-only `useEffect`):
|
||||||
|
- `api.get<DetectionClass[]>('/api/annotations/classes')`.
|
||||||
|
- On a non-empty array → `setClasses(list)`.
|
||||||
|
- On an empty array OR a thrown error → `setClasses(FALLBACK_CLASSES)`.
|
||||||
|
- **`FALLBACK_CLASSES`** is a module-private 3 × |`FALLBACK_CLASS_NAMES`| matrix:
|
||||||
|
- For each mode offset in `[0, 20, 40]`, build one class entry per name in `FALLBACK_CLASS_NAMES` (imported from `features/annotations/classColors.ts`).
|
||||||
|
- Each entry: `{ id: i + modeOffset, name, shortName: name.slice(0, 3), color: getClassColor(i), maxSizeM: 10, photoMode: modeOffset }`.
|
||||||
|
- This means: in offline / API-down mode, the ID range is `0–N-1` (Regular), `20–20+N-1` (Winter), `40–40+N-1` (Night). The backend's class IDs MUST follow the same convention or fallback↔backend handoff yields ID collisions. Confirm in Step 4 via the `annotations/` service contract.
|
||||||
|
- **Numeric hotkeys 1–9** (effect keyed on `[classes, photoMode, onSelect]`):
|
||||||
|
- `keydown` on `window`. `parseInt(e.key)` → if 1–9, picks `classes[(num - 1) + photoMode]`.
|
||||||
|
- **Bug-shaped**: `photoMode` is `0 | 20 | 40` (an *offset*), not a row count. `classes[idx + 0]` is correct for Regular; for Winter/Night the index `idx + 20` / `idx + 40` is meaningful only when `classes` is in the contiguous `[0..N-1, 20..20+N-1, 40..40+N-1]` shape that `FALLBACK_CLASSES` produces. **If the backend returns its classes in a different order**, hotkey 1–9 will pick the wrong class. Verify backend ordering in Step 4 — this is the principal correctness risk in this module. Flag.
|
||||||
|
- **Auto-select first class on mode change** (effect keyed on `[classes, photoMode, selectedClassNum, onSelect]`):
|
||||||
|
- Filter `classes` to the active `photoMode`.
|
||||||
|
- If `selectedClassNum` is not within that filtered set, call `onSelect(modeClasses[0].id)`.
|
||||||
|
- This guarantees the parent always holds a class ID consistent with the selected photo mode.
|
||||||
|
- **Render**:
|
||||||
|
- Class list — only entries whose `photoMode` matches the active mode.
|
||||||
|
- Photo-mode bar — three buttons (Sunny / Snowflake / Moon icons) for Regular / Winter / Night.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- **Internal**:
|
||||||
|
- `../api/client` — `api.get<T>()`.
|
||||||
|
- `../features/annotations/classColors` — `getClassColor(i)`, `FALLBACK_CLASS_NAMES`.
|
||||||
|
- `../types` — `DetectionClass` type.
|
||||||
|
- **External**: `react`, `react-i18next`, `react-icons/md`, `react-icons/fa`.
|
||||||
|
|
||||||
|
## Consumers (intra-repo)
|
||||||
|
|
||||||
|
From the §7a dependency graph:
|
||||||
|
|
||||||
|
- `src/features/annotations/AnnotationsPage.tsx`
|
||||||
|
- `src/features/dataset/DatasetPage.tsx`
|
||||||
|
|
||||||
|
This is the **canonical example** of the cross-layer import flagged in `_docs/02_document/00_discovery.md` §8: a `components/` (shared) module importing from `features/annotations/`. Two clean fixes are in scope for Step 4 / Step 8:
|
||||||
|
|
||||||
|
1. Lift `classColors.ts` into `src/components/detection/` (or `src/shared/`) and update the two consumers.
|
||||||
|
2. Or: move `DetectionClasses` itself into `src/features/annotations/` since both consumers are in `features/`.
|
||||||
|
|
||||||
|
## Data models
|
||||||
|
|
||||||
|
`DetectionClass` (from `src/types/index.ts`) — see `_docs/02_document/modules/src__types__index.md`.
|
||||||
|
|
||||||
|
`FALLBACK_CLASSES` is module-private; see Internal logic above.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Endpoint: `/api/annotations/classes` — string-literal URL (testability fix scheduled for Step 4).
|
||||||
|
|
||||||
|
Photo-mode value set is `{0, 20, 40}` — hardcoded, mirrored by `FALLBACK_CLASSES`. If the backend grows a fourth mode (e.g. thermal, IR), every consumer of `photoMode` will need a coordinated change.
|
||||||
|
|
||||||
|
Tailwind tokens: `bg-az-orange` (Regular), `bg-az-blue` (Winter), `bg-purple-600` (Night). Defined in `src/index.css`.
|
||||||
|
|
||||||
|
## External integrations
|
||||||
|
|
||||||
|
- HTTP `GET /api/annotations/classes` → `DetectionClass[]`. Backed by the `annotations/` service (`.NET`) per `nginx.conf`.
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- **Silent failure on class load**: `.catch(() => setClasses(FALLBACK_CLASSES))` swallows the error and the user sees the fallback. Acceptable for UX continuity, but the lack of any user-visible signal means a misconfigured `/api/annotations/classes` deploy could go unnoticed in prod. Flag for Step 6 `security_approach.md` / Step 8.
|
||||||
|
- No input that flows back to the server here.
|
||||||
|
- The `getClassColor(i)` palette is deterministic; no PII.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Notes / open questions
|
||||||
|
|
||||||
|
- **`getPhotoModeSuffix` redundancy** (carry-over from B1): `classColors.ts` exposes `getPhotoModeSuffix()` that derives the same suffix the typed `DetectionClass.photoMode` field already encodes. Once `classColors.ts` is repositioned (see Consumers), `getPhotoModeSuffix` is a deletion candidate. Defer to Step 8.
|
||||||
|
- **`FALLBACK_CLASS_NAMES.length`** is implicitly assumed to be ≤ 9 by the hotkey code (only 1–9 are bound). If the catalogue grows to 10+ entries, hotkeys can no longer cover the tail. Acceptable for now.
|
||||||
|
- **Mode-button colours don't use `az-` tokens for Night** (`bg-purple-600`, `text-purple-400` instead of an `az-purple` token). Cosmetic inconsistency; flag for Step 4 against `_docs/ui_design/README.md` colour palette.
|
||||||
|
- The class list is rendered with `1.`, `2.`, … prefixes derived from `i+1` — so the hotkey number always matches the visible label inside the active mode. Good.
|
||||||
|
- Does not handle modifier keys (`Shift`, `Ctrl`) on numeric hotkeys; pressing `Shift+5` will trigger the `5` branch. Fine for now.
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# Module: `src/components/FlightContext.tsx`
|
||||||
|
|
||||||
|
> **Source**: `src/components/FlightContext.tsx` (52 lines)
|
||||||
|
> **Topo batch**: B3 (depends on B2 leaves: `api/client`, `types/index`)
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
A React context that holds the SPA's currently-selected flight and the cached flight list, keeps the selection in sync with the per-user backend setting, and exposes a refresh trigger. Sibling of `AuthContext.tsx` — both are mounted near the root (`App.tsx`) so any feature page can access flight state without prop-drilling. Replaces the legacy WPF "current mission" singleton (`_docs/legacy/wpf-era.md` §"What survived").
|
||||||
|
|
||||||
|
## Public interface
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface FlightState {
|
||||||
|
flights: Flight[]
|
||||||
|
selectedFlight: Flight | null
|
||||||
|
selectFlight: (f: Flight | null) => void
|
||||||
|
refreshFlights: () => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFlight(): FlightState
|
||||||
|
export function FlightProvider({ children }: { children: ReactNode }): JSX.Element
|
||||||
|
```
|
||||||
|
|
||||||
|
`FlightContext` itself is module-private. Consumers must go through `useFlight()`.
|
||||||
|
|
||||||
|
## Internal logic
|
||||||
|
|
||||||
|
State:
|
||||||
|
|
||||||
|
- `flights: Flight[]` — most recent list returned by `GET /api/flights?pageSize=1000`.
|
||||||
|
- `selectedFlight: Flight | null` — the active flight, or `null` if none. Survives across pages because the provider is mounted above the route tree.
|
||||||
|
|
||||||
|
**`refreshFlights()`** (`useCallback`, no deps):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const data = await api.get<{ items: Flight[] }>('/api/flights?pageSize=1000')
|
||||||
|
setFlights(data.items ?? [])
|
||||||
|
```
|
||||||
|
|
||||||
|
Errors are silently swallowed (`try { ... } catch {}`). `pageSize=1000` is a hardcoded ceiling — see Configuration.
|
||||||
|
|
||||||
|
**Bootstrap effect** (`useEffect` keyed on `[refreshFlights]`, runs once because `refreshFlights` is `useCallback([])`):
|
||||||
|
|
||||||
|
1. `refreshFlights()` (no `await` — runs in parallel with #2).
|
||||||
|
2. `api.get<UserSettings>('/api/annotations/settings/user')` →
|
||||||
|
- if `settings?.selectedFlightId` is truthy: `api.get<Flight>('/api/flights/${settings.selectedFlightId}')` → `setSelectedFlight(f)`.
|
||||||
|
- errors at every step silently swallowed.
|
||||||
|
|
||||||
|
The selected flight is therefore looked up by **its own GET**, not by indexing into the cached `flights` list. This is intentional — the user might have a `selectedFlightId` that fell off the first 1000 flights.
|
||||||
|
|
||||||
|
**`selectFlight(f)`** (`useCallback`, no deps):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
setSelectedFlight(f)
|
||||||
|
api.put('/api/annotations/settings/user', { selectedFlightId: f?.id ?? null }).catch(() => {})
|
||||||
|
```
|
||||||
|
|
||||||
|
Optimistic — local state updates immediately; the persisted setting is fire-and-forget. If the PUT fails, the next reload will fall back to the previously-stored ID and the user's selection silently reverts. Flag for Step 4.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- **Internal**:
|
||||||
|
- `../api/client` — `api`.
|
||||||
|
- `../types` — `Flight`, `UserSettings` types.
|
||||||
|
- **External**: `react` (`createContext`, `useContext`, `useState`, `useEffect`, `useCallback`, `ReactNode`).
|
||||||
|
|
||||||
|
## Consumers (intra-repo)
|
||||||
|
|
||||||
|
From the §7a dependency graph:
|
||||||
|
|
||||||
|
- `src/App.tsx` — mounts `FlightProvider` inside `ProtectedRoute` and above the route tree (so `selectedFlight` survives navigation between `/flights`, `/annotations`, `/dataset`).
|
||||||
|
- `src/components/Header.tsx` — displays the currently-selected flight name.
|
||||||
|
- `src/features/flights/FlightsPage.tsx` — the primary editor; calls `selectFlight` on row click.
|
||||||
|
- `src/features/annotations/MediaList.tsx` — filters media by `selectedFlight.id`.
|
||||||
|
- `src/features/dataset/DatasetPage.tsx` — same.
|
||||||
|
|
||||||
|
## Data models
|
||||||
|
|
||||||
|
- `Flight` and `UserSettings` from `src/types/index.ts` — see `_docs/02_document/modules/src__types__index.md`.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Endpoints (string-literal): `/api/flights?pageSize=1000`, `/api/flights/{id}`, `/api/annotations/settings/user`. Routed by `nginx.conf` to the `flights/` and `annotations/` services.
|
||||||
|
|
||||||
|
`pageSize=1000` is a **hardcoded ceiling** — if the system ever has >1000 flights, the cache is silently truncated. The `flights/` service contract probably caps at 1000; verify and document in `data_parameters.md` (Step 6). Flag.
|
||||||
|
|
||||||
|
## External integrations
|
||||||
|
|
||||||
|
- `flights/` (.NET) — `GET /api/flights`, `GET /api/flights/{id}`.
|
||||||
|
- `annotations/` (.NET) — `GET /api/annotations/settings/user`, `PUT /api/annotations/settings/user`. The user-settings store is reused for the selected-flight pointer; this is a slight abstraction leak (selected flight is logically a UI-state preference, not an annotation setting), but it works as long as `UserSettings` keeps a `selectedFlightId` field.
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- **No auth checks here** — relies on `api/client.ts` to inject the bearer; backend enforces visibility.
|
||||||
|
- **Silent failure on every async call** — same caveat as `AuthContext`. A misconfigured `/api/flights` will leave the user with an empty list and no error indication. Flag for Step 6 `security_approach.md` and Step 8 hardening.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Notes / open questions
|
||||||
|
|
||||||
|
- **Race**: bootstrap fires `refreshFlights()` AND the user-settings GET in parallel. If the user-settings GET resolves first with a `selectedFlightId` that's not in the 1000 cached flights, the per-flight GET still succeeds. If the 1000 list is somehow stale and the per-flight GET 404s, `selectedFlight` stays `null` and the user sees nothing selected. Acceptable but worth documenting.
|
||||||
|
- **`selectFlight(null)` PUTs `{ selectedFlightId: null }`** — this clears the persisted preference. Confirm the `annotations/` service treats this as "unset" and not as an error or no-op. Flag for Step 4.
|
||||||
|
- **No realtime invalidation**: if another tab / user creates a flight, this client won't know until the next `refreshFlights()` call. The SPA also has SSE (`api/sse.ts`) but does NOT subscribe to flight updates. Mark as a Step 8 / future-feature hook.
|
||||||
|
- **`useCallback([])` for `refreshFlights`** is fine because `setFlights` is stable. The empty deps array means the bootstrap effect fires exactly once (as intended) — but ESLint with `react-hooks/exhaustive-deps` would still flag both `useCallback`s for missing `setFlights` (technically stable, but the linter doesn't know). Acceptable; project-wide ESLint config does not currently enforce.
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
# Module: `src/components/Header.tsx`
|
||||||
|
|
||||||
|
> **Source**: `src/components/Header.tsx` (133 lines)
|
||||||
|
> **Topo batch**: B4 (depends on B3: `auth/AuthContext`, `components/FlightContext`, `components/HelpModal`, `types/index`)
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
The persistent top navigation bar of the authenticated SPA. Combines
|
||||||
|
five concerns into one component: brand mark, currently-selected-flight
|
||||||
|
dropdown, top-level navigation links (permission-gated), session info
|
||||||
|
(user email + Logout), language toggle (EN ↔ UA), and a `?` button that
|
||||||
|
opens `HelpModal`. Also renders a duplicate bottom-nav on the mobile
|
||||||
|
breakpoint. Replaces the legacy WPF top ribbon + window chrome
|
||||||
|
(`_docs/legacy/wpf-era.md` §4 / §"What survived").
|
||||||
|
|
||||||
|
## Public interface
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export default function Header(): JSX.Element
|
||||||
|
```
|
||||||
|
|
||||||
|
No props — Header reads everything it needs from `AuthContext`,
|
||||||
|
`FlightContext`, and `react-i18next`. Mounted exactly once in `App.tsx`
|
||||||
|
above the protected route tree.
|
||||||
|
|
||||||
|
## Internal logic
|
||||||
|
|
||||||
|
- **Local state**:
|
||||||
|
- `showDropdown: boolean` — flight selector open/closed.
|
||||||
|
- `filter: string` — text filter for the flight selector list.
|
||||||
|
- `showHelp: boolean` — controls the `HelpModal` `open` prop.
|
||||||
|
- `dropdownRef: RefObject<HTMLDivElement>` — used to detect outside
|
||||||
|
clicks.
|
||||||
|
- **Outside-click effect**: registers a `mousedown` listener on `document`
|
||||||
|
while mounted; if the event target is not inside `dropdownRef.current`,
|
||||||
|
closes the dropdown. Listener is removed on unmount. (Always-on, even
|
||||||
|
while the dropdown is closed — cheap, but not the most surgical
|
||||||
|
pattern; flag to consider gating on `showDropdown` in Step 8.)
|
||||||
|
- **Filtered flights**: `flights.filter(f => f.name.toLowerCase().includes(filter.toLowerCase()))`
|
||||||
|
— case-insensitive substring match on the flight name. Empty filter
|
||||||
|
shows everything.
|
||||||
|
- **Logout**: `await logout(); navigate('/login')`. Always navigates,
|
||||||
|
even if `logout()` throws (it never does — `AuthContext.logout` swallows
|
||||||
|
network errors by design).
|
||||||
|
- **Language toggle**: `i18n.changeLanguage(i18n.language === 'en' ? 'ua' : 'en')`.
|
||||||
|
Same two-language assumption as `HelpModal` (treats every non-`'ua'`
|
||||||
|
value as English-equivalent). The toggle button label is the
|
||||||
|
*target* language ("UA" while EN is active, "EN" while UA is active).
|
||||||
|
- **Permission-gated nav** (`navItems`):
|
||||||
|
|
||||||
|
| `to` | `label` key | `perm` |
|
||||||
|
|---|---|---|
|
||||||
|
| `/flights` | `nav.flights` | `FL` |
|
||||||
|
| `/annotations` | `nav.annotations` | `ANN` |
|
||||||
|
| `/dataset` | `nav.dataset` | `DATASET` |
|
||||||
|
| `/admin` | `nav.admin` | `ADM` |
|
||||||
|
|
||||||
|
`Settings` (`/settings`) is always rendered — no permission gate.
|
||||||
|
Filter applied via `navItems.filter(n => hasPermission(n.perm))`.
|
||||||
|
- **Layout**: a single `<header>` containing brand → flight dropdown →
|
||||||
|
primary nav (`hidden sm:flex`) → spacer (`flex-1`) → user email
|
||||||
|
(`hidden sm:block`) → language toggle → `?` → `⚙` (settings link) →
|
||||||
|
logout button. A second `<nav>` element below is `sm:hidden` and
|
||||||
|
positions itself fixed at the bottom of the viewport — the mobile
|
||||||
|
nav. Both navs share the same filter logic.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- **Internal**:
|
||||||
|
- `../auth/AuthContext` — `useAuth` (user, logout, hasPermission).
|
||||||
|
- `./FlightContext` — `useFlight` (flights, selectedFlight, selectFlight).
|
||||||
|
- `./HelpModal` — opens on `?` click.
|
||||||
|
- `../types` — `Flight` type for the dropdown row.
|
||||||
|
- **External**: `react-router-dom` (`NavLink`, `useNavigate`),
|
||||||
|
`react-i18next` (`useTranslation`),
|
||||||
|
`react` (`useState`, `useRef`, `useEffect`).
|
||||||
|
|
||||||
|
## Consumers (intra-repo)
|
||||||
|
|
||||||
|
- `src/App.tsx` — mounted inside `ProtectedRoute → FlightProvider`.
|
||||||
|
|
||||||
|
No other intra-repo consumer; `Header` is a top-level chrome component.
|
||||||
|
|
||||||
|
## Data models
|
||||||
|
|
||||||
|
The `navItems` array is a module-private literal of
|
||||||
|
`{ to: string; label: string; perm: string }`. The filtered `flights`
|
||||||
|
list is `Flight[]` (from `types/index.ts`).
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
- **i18n keys consumed**: `nav.flights`, `nav.annotations`, `nav.dataset`,
|
||||||
|
`nav.admin`, `nav.logout`. (Verified to exist in `src/i18n/en.json`.)
|
||||||
|
The filter placeholder ("Filter…") and the empty-state ("— Select
|
||||||
|
Flight —", "No flights") are hardcoded strings — flag for Step 4.
|
||||||
|
- **Tailwind tokens**: `bg-az-header`, `border-az-border`, `bg-az-panel`,
|
||||||
|
`text-az-text`, `text-az-muted`, `text-az-orange`, `text-az-red`,
|
||||||
|
`bg-az-bg`. Defined in `src/index.css`.
|
||||||
|
- **Breakpoint**: `sm:` from Tailwind defaults to 640px — matches the
|
||||||
|
responsive-breakpoint spec in `_docs/ui_design/README.md`.
|
||||||
|
- **Permission strings**: `FL`, `ANN`, `DATASET`, `ADM`. Backend-defined
|
||||||
|
by the `admin/` service; treated as opaque strings here.
|
||||||
|
|
||||||
|
## External integrations
|
||||||
|
|
||||||
|
None directly. Triggers `logout()` which calls `POST /api/admin/auth/logout`
|
||||||
|
inside `AuthContext`, and `selectFlight()` which calls `PUT /api/annotations/settings/user`
|
||||||
|
inside `FlightContext`.
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- `hasPermission(perm)` is **client-side advisory** — a user with the
|
||||||
|
`/admin` route URL can navigate there without going through the nav
|
||||||
|
link. The backend `admin/` service is the authority. Document in
|
||||||
|
`security_approach.md` (Step 6).
|
||||||
|
- The flight-selector dropdown displays every flight returned by
|
||||||
|
`GET /api/flights?pageSize=1000` — there is no permission check here.
|
||||||
|
If `flights/` ever returns flights the user should not see, this
|
||||||
|
component would leak them. Trust the backend filter.
|
||||||
|
- `user?.email` is rendered raw in the header — React JSX-escapes it,
|
||||||
|
so XSS via a malicious email is not possible at this layer, but
|
||||||
|
validate that the `admin/` service enforces email format.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Notes / open questions
|
||||||
|
|
||||||
|
- **Outside-click listener is always attached** — slightly wasteful when
|
||||||
|
the dropdown is closed. Gating on `showDropdown` would also remove a
|
||||||
|
closure capture. Defer to Step 8.
|
||||||
|
- **Dropdown is not keyboard-accessible**: no `role="combobox"`, no
|
||||||
|
`aria-expanded`, no Esc-to-close, focus does not move into the filter
|
||||||
|
input on open beyond `autoFocus` (which works only the first time
|
||||||
|
the input mounts). Flag against `_docs/ui_design/README.md` keyboard
|
||||||
|
shortcuts for Step 4.
|
||||||
|
- **Mobile bottom nav duplicates `navItems`** twice in source — DRY win
|
||||||
|
by extracting a `<NavSet items={...}/>` subcomponent, but a deferral
|
||||||
|
candidate.
|
||||||
|
- **Hardcoded English strings** ("AZAION", "— Select Flight —",
|
||||||
|
"Filter...", "No flights"). Brand mark is intentional; the others
|
||||||
|
are content and should move to `nav.*` keys. Step 4 candidate.
|
||||||
|
- **`/settings` link** is unguarded by `hasPermission`. Per
|
||||||
|
`_docs/ui_design/README.md` settings page is available to every
|
||||||
|
authenticated user — confirmed intent.
|
||||||
|
- **`new Date(f.createdDate).toLocaleDateString()`** uses the browser's
|
||||||
|
default locale, not `i18n.language`. Mild inconsistency; Step 8
|
||||||
|
cosmetic.
|
||||||
|
- **`flights` cache truncation**: the dropdown shows at most 1000
|
||||||
|
flights because of the hardcoded `pageSize=1000` in `FlightContext`.
|
||||||
|
Documented as a flag there; Header inherits the limitation.
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# Module: `src/components/HelpModal.tsx`
|
||||||
|
|
||||||
|
> **Source**: `src/components/HelpModal.tsx` (62 lines)
|
||||||
|
> **Topo batch**: B2 (uses `react-i18next` for the language flag only; no intra-repo deps)
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
A modal dialog that surfaces annotation guidelines and the keyboard-shortcut cheat-sheet. Triggered from `Header.tsx` → "Help" entry. Replaces the legacy WPF `HelpWindow.xaml` (`_docs/legacy/wpf-era.md` §4).
|
||||||
|
|
||||||
|
## Public interface
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface Props {
|
||||||
|
open: boolean
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
export default function HelpModal(props: Props): JSX.Element | null
|
||||||
|
```
|
||||||
|
|
||||||
|
When `open === false`, returns `null`. Otherwise renders a centered `<div>` overlay.
|
||||||
|
|
||||||
|
## Internal logic
|
||||||
|
|
||||||
|
- Reads `i18n.language` from `useTranslation()`; treats anything other than `'ua'` as English (`lang = i18n.language === 'ua' ? 'ua' : 'en'`).
|
||||||
|
- `GUIDELINES`: a hardcoded array of 6 `{ en, ua }` rules covering annotation quality. **Hardcoded — not loaded via the `i18n` resource bundle**, despite the module already importing `useTranslation`. (Inconsistency vs. the rest of the SPA — flag for Step 4.)
|
||||||
|
- Renders the guidelines as a numbered list and a 12-row keyboard-shortcut grid (`Space`, `← →`, `Ctrl + ← →`, `Enter`, …).
|
||||||
|
- Click on the dim backdrop closes the modal; click inside is `stopPropagation`'d.
|
||||||
|
- `Close` button is a styled `<button>` calling `onClose()`.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- **Internal**: none.
|
||||||
|
- **External**: `react-i18next` (for `useTranslation()` only — to detect the language; *not* for content lookup).
|
||||||
|
|
||||||
|
## Consumers (intra-repo)
|
||||||
|
|
||||||
|
- `src/components/Header.tsx` — owns `open` state and the trigger button.
|
||||||
|
|
||||||
|
## Data models
|
||||||
|
|
||||||
|
The `GUIDELINES` array — module-private. Each entry: `{ en: string, ua: string }`.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The keyboard-shortcut grid hardcodes the same shortcut taxonomy that `_docs/ui_design/README.md` §"Keyboard Shortcuts" enumerates. Verify parity during Step 4.
|
||||||
|
|
||||||
|
## External integrations
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
None directly. Note that ESCAPE-key handling appears in the *sibling* `ConfirmDialog` but **not** here — Esc currently does NOT close `HelpModal`. Backdrop click is the only dismiss path beyond the Close button. Inconsistent UX; flag for Step 4 verification against the UI spec.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Notes / open questions
|
||||||
|
|
||||||
|
- The 6 guidelines stored as `{ en, ua }` literals duplicate the Annotation Quality Guidelines documented in `_docs/ui_design/README.md` §"Annotation Quality Guidelines" (which lists six rules with subtly different wording). Choose one source of truth in Step 5; preferred is to lift the strings into `en.json` / `ua.json` like the rest of the SPA.
|
||||||
|
- The `<h2>` "How to Annotate" heading is hardcoded English; should also be translated.
|
||||||
|
- The `lang === 'ua' ? 'ua' : 'en'` branch silently treats every non-Ukrainian language as English — only correct as long as exactly two locales exist. If a third locale is added, this needs to use the `i18n` lookup table.
|
||||||
|
- Width is hardcoded `w-[500px]` and `max-h-[80vh]`. Does not adapt to mobile breakpoints documented in `_docs/ui_design/README.md` §"Responsive Breakpoints" (640px). Flag.
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
# Module: `src/features/admin/AdminPage.tsx`
|
||||||
|
|
||||||
|
> **Source**: `src/features/admin/AdminPage.tsx` (209 lines)
|
||||||
|
> **Topo batch**: B4 (depends on B3: `api/client`, `components/ConfirmDialog`, `types/index`)
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
The administrator console of the SPA. Bundles four management surfaces
|
||||||
|
into a single page: detection-classes catalogue (CRUD-lite),
|
||||||
|
AI-recognition tuning form, GPS-device endpoint config, user
|
||||||
|
list (CRUD-lite), and aircraft default-selector. Replaces the WPF
|
||||||
|
`AdminWindow.xaml` cluster (`_docs/legacy/wpf-era.md` §§4, 5).
|
||||||
|
|
||||||
|
Behind the `/admin` route, gated by `Header`'s `ADM` permission
|
||||||
|
filter (a route-level permission re-check is the responsibility of the
|
||||||
|
`admin/` service — Header gating is UI-only; see Security below).
|
||||||
|
|
||||||
|
## Public interface
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export default function AdminPage(): JSX.Element
|
||||||
|
```
|
||||||
|
|
||||||
|
No props. Reads everything via `api/client` and local state.
|
||||||
|
|
||||||
|
## Internal logic
|
||||||
|
|
||||||
|
- **State**:
|
||||||
|
- `classes: DetectionClass[]` — the detection-class table.
|
||||||
|
- `aircrafts: Aircraft[]` — the aircraft list with `isDefault` flag.
|
||||||
|
- `users: User[]` — the user table.
|
||||||
|
- `newClass: { name; shortName; color; maxSizeM }` — staging buffer
|
||||||
|
for the "add detection class" form (initial: `{ name: '',
|
||||||
|
shortName: '', color: '#FF0000', maxSizeM: 7 }`).
|
||||||
|
- `newUser: { name; email; password; role }` — staging buffer for
|
||||||
|
"add user" (initial: `{ name: '', email: '', password: '', role:
|
||||||
|
'Annotator' }`).
|
||||||
|
- `deactivateId: string | null` — drives the `ConfirmDialog`'s open
|
||||||
|
state for user deactivation.
|
||||||
|
- **Bootstrap effect** (`useEffect([])` — runs once at mount):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
api.get<DetectionClass[]>('/api/annotations/classes').then(setClasses).catch(() => {})
|
||||||
|
api.get<Aircraft[]>('/api/flights/aircrafts').then(setAircrafts).catch(() => {})
|
||||||
|
api.get<User[]>('/api/admin/users').then(setUsers).catch(() => {})
|
||||||
|
```
|
||||||
|
|
||||||
|
Three independent calls, all silently swallowed on error. No retry,
|
||||||
|
no error UI, no loading state — empty arrays render as empty
|
||||||
|
tables. Flag for Step 4 against the user-feedback patterns in
|
||||||
|
`_docs/ui_design/README.md`.
|
||||||
|
- **`handleAddClass()`**:
|
||||||
|
1. Guard: `if (!newClass.name) return`.
|
||||||
|
2. `await api.post('/api/admin/classes', newClass)`.
|
||||||
|
3. Refetch via `api.get('/api/annotations/classes')` — note the
|
||||||
|
**read** path is the public `annotations/` endpoint, while the
|
||||||
|
**write** path is the `admin/` endpoint. Architectural caveat:
|
||||||
|
two different services own the same logical entity. Document in
|
||||||
|
`architecture.md` §integration-points (Step 3a).
|
||||||
|
4. Reset `newClass` to its initial values.
|
||||||
|
No error path — a failed POST throws (because `client.ts` throws on
|
||||||
|
non-2xx); the throw is uncaught and reaches React's error boundary
|
||||||
|
(none configured). Flag.
|
||||||
|
- **`handleDeleteClass(id)`**: optimistic local update —
|
||||||
|
`await api.delete('/api/admin/classes/${id}')` then
|
||||||
|
`setClasses(prev => prev.filter(c => c.id !== id))`. **No
|
||||||
|
ConfirmDialog** despite this being destructive. Inconsistent with
|
||||||
|
the user-deactivation flow which uses ConfirmDialog. Flag for Step 4
|
||||||
|
against `_docs/ui_design/README.md` confirmation-dialog spec.
|
||||||
|
- **`handleAddUser()`** — analogous to `handleAddClass` against
|
||||||
|
`POST /api/admin/users` and `GET /api/admin/users`. Guards on
|
||||||
|
`email && password`.
|
||||||
|
- **`handleDeactivate()`** — fired from the ConfirmDialog confirm:
|
||||||
|
1. `PATCH /api/admin/users/${deactivateId}` with `{ isActive: false }`.
|
||||||
|
2. Optimistic local update: marks the row inactive.
|
||||||
|
3. Closes the dialog (`setDeactivateId(null)`).
|
||||||
|
No "reactivate" path — once `isActive: false`, the row only renders
|
||||||
|
the badge and no Deactivate button. Verify with `admin/` service:
|
||||||
|
is reactivation an admin task or out of scope?
|
||||||
|
- **`handleToggleDefault(a)`** — `PATCH /api/flights/aircrafts/${a.id}`
|
||||||
|
with `{ isDefault: !a.isDefault }`, then optimistic local flip. Note
|
||||||
|
this allows multiple `isDefault: true` aircraft to coexist (the
|
||||||
|
backend should enforce exclusivity; the UI does not).
|
||||||
|
- **Layout** (left → center → right, all in one horizontal flex):
|
||||||
|
- **Left column** (`w-[340px]`): detection-classes table + add row.
|
||||||
|
- **Center column** (`flex-1 max-w-md`): AI settings form, GPS
|
||||||
|
settings form, users table + add row. The AI and GPS forms have
|
||||||
|
`defaultValue` only — there is **no** state, no `Save` handler
|
||||||
|
wired up. The buttons render but do nothing. Flag.
|
||||||
|
- **Right column** (`w-[280px]`): aircraft list with star toggle.
|
||||||
|
- `<ConfirmDialog>` mounted at the end, controlled by `deactivateId`.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- **Internal**:
|
||||||
|
- `../../api/client` — `api`.
|
||||||
|
- `../../components/ConfirmDialog` — for user deactivation.
|
||||||
|
- `../../types` — `DetectionClass`, `Aircraft`, `User`.
|
||||||
|
- **External**: `react` (`useState`, `useEffect`),
|
||||||
|
`react-i18next` (`useTranslation`).
|
||||||
|
|
||||||
|
## Consumers (intra-repo)
|
||||||
|
|
||||||
|
- `src/App.tsx` — mounted at the `/admin` route inside the protected
|
||||||
|
tree.
|
||||||
|
|
||||||
|
## Data models
|
||||||
|
|
||||||
|
Stateful local copies of `DetectionClass[]`, `Aircraft[]`, `User[]`.
|
||||||
|
The `newClass` and `newUser` buffers are anonymous shapes —
|
||||||
|
intentionally narrower than `DetectionClass` / `User` because the
|
||||||
|
backend assigns `id` and other server-managed fields.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
- **i18n keys consumed**: `admin.classes`, `admin.aiSettings`,
|
||||||
|
`admin.gpsSettings`, `admin.users`, `admin.aircrafts`,
|
||||||
|
`admin.deactivate`, `common.save`. (Confirmed present in
|
||||||
|
`src/i18n/en.json` admin/common groups.) Plenty of hardcoded
|
||||||
|
English strings — placeholders ("Name", "Email", "Password"), table
|
||||||
|
headers (`#`, `Name`, `Color`, `Email`, `Role`, `Status`), role
|
||||||
|
options (`Annotator`, `Admin`, `Viewer`), the GPS protocol options
|
||||||
|
(`TCP`, `UDP`), the AI tuning labels ("Frame Period Recognition",
|
||||||
|
etc.), and the deactivation confirmation message. Step 4 candidate.
|
||||||
|
- **Hardcoded defaults**:
|
||||||
|
- `maxSizeM: 7` for new detection classes.
|
||||||
|
- `color: '#FF0000'` for new detection classes.
|
||||||
|
- `Frame Period Recognition: 5`, `Frame Recognition Seconds: 1`,
|
||||||
|
`Probability Threshold: 0.5` (`step: 0.05`, `min: 0`, `max: 1`).
|
||||||
|
- `Device Address: 192.168.1.100`, `Port: 5535`, default protocol
|
||||||
|
`TCP`. **Hardcoded internal IP** is a smell; should come from
|
||||||
|
`system_settings` or be a placeholder. Flag for Step 4 / Step 6
|
||||||
|
(`security_approach.md`).
|
||||||
|
- **Tailwind tokens**: `bg-az-panel`, `bg-az-bg`, `bg-az-orange`,
|
||||||
|
`bg-az-blue/20`, `bg-az-green/20`, `text-az-{text,muted,red,green,
|
||||||
|
blue,orange}`, `border-az-border`. Defined in `src/index.css`.
|
||||||
|
|
||||||
|
## External integrations
|
||||||
|
|
||||||
|
| Method | Path | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `GET` | `/api/annotations/classes` | List detection classes (read path uses annotations service) |
|
||||||
|
| `POST` | `/api/admin/classes` | Create detection class (write path uses admin service) |
|
||||||
|
| `DELETE` | `/api/admin/classes/{id}` | Delete detection class |
|
||||||
|
| `GET` | `/api/flights/aircrafts` | List aircraft |
|
||||||
|
| `PATCH` | `/api/flights/aircrafts/{id}` | Toggle `isDefault` |
|
||||||
|
| `GET` | `/api/admin/users` | List users |
|
||||||
|
| `POST` | `/api/admin/users` | Create user |
|
||||||
|
| `PATCH` | `/api/admin/users/{id}` | Set `isActive: false` (deactivate) |
|
||||||
|
|
||||||
|
Routed by `nginx.conf` to `admin/`, `annotations/`, `flights/`
|
||||||
|
backends.
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- **`/admin` is access-controlled by Header's `ADM` permission filter
|
||||||
|
AND by every backend endpoint** (each `/api/admin/*` is the
|
||||||
|
authority). The page itself does not call `hasPermission` — a user
|
||||||
|
who deep-links to `/admin` would render the page but every fetch
|
||||||
|
would 401/403. Acceptable for production; flag a UX improvement
|
||||||
|
(server-error → friendly redirect) for Step 8.
|
||||||
|
- **Password input has `type="password"`** — masked. The new-user
|
||||||
|
password is held in plaintext component state until the POST
|
||||||
|
resolves. Acceptable; cleared by `setNewUser({...})` after success.
|
||||||
|
- **`color: '#FF0000'`** is a hex string — when posted to
|
||||||
|
`/api/admin/classes`, the backend must validate. UI-side validation
|
||||||
|
(regex / `<input type="color">`) is enforced because the input is
|
||||||
|
a color-picker.
|
||||||
|
- **No CSRF** — relies on the bearer-token scheme. Same posture as the
|
||||||
|
rest of the SPA (see `client.ts` Security section).
|
||||||
|
- **No row-level access checks visible to the UI**: the user list
|
||||||
|
shows every row the backend returns; if the `admin/` service ever
|
||||||
|
filtered by tenant, the UI would not need changes.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Notes / open questions
|
||||||
|
|
||||||
|
- **Detection-class read/write split** between `annotations/` and
|
||||||
|
`admin/` is unusual. Verify with the suite ADRs whether
|
||||||
|
`/api/annotations/classes` and `/api/admin/classes` are the same
|
||||||
|
underlying entity. If yes, the split is a legacy artifact; if no,
|
||||||
|
the UI is conflating two collections. Step 3a / Step 4 verification.
|
||||||
|
- **AI settings & GPS settings forms are wired to nothing** — every
|
||||||
|
`<input>` uses `defaultValue` (uncontrolled), there is no submit
|
||||||
|
handler, and the Save button does nothing. Either:
|
||||||
|
- The legacy WPF AI/GPS settings have not been ported yet (most
|
||||||
|
likely — `_docs/legacy/wpf-era.md` §"What is intentionally NOT
|
||||||
|
being ported" might apply), OR
|
||||||
|
- The backend endpoint exists but the wiring was lost during the
|
||||||
|
port.
|
||||||
|
Flag prominently in Step 4 problem-extraction. Probably a missing
|
||||||
|
feature, not a bug.
|
||||||
|
- **`handleDeleteClass` has no confirmation** — UX inconsistency with
|
||||||
|
user deactivation. Consider unifying via `ConfirmDialog`. Step 4.
|
||||||
|
- **Aircraft default-toggle race**: clicking two aircraft in quick
|
||||||
|
succession will fire two parallel `PATCH`es. The backend may end up
|
||||||
|
with both `isDefault: true` if it does not enforce exclusivity at
|
||||||
|
the persistence layer. Flag for Step 6 problem-extraction. Trust
|
||||||
|
the backend; do not add UI debouncing.
|
||||||
|
- **The "Active" / "Inactive" badge text is hardcoded English** even
|
||||||
|
though the surrounding column header (`Status`) is too. Either
|
||||||
|
localize all of them or accept the inconsistency. Step 4.
|
||||||
|
- **`u.isActive` as the only mutable user field** — no rename, no
|
||||||
|
password reset, no role change. Document the gap; may be a feature
|
||||||
|
cycle item for Phase B.
|
||||||
|
- **`newClass.shortName` is collected but not displayed in the table**
|
||||||
|
— the table only shows `id, name, color, ×`. The `shortName` lives
|
||||||
|
only in the staging buffer and is sent to the backend. Verify the
|
||||||
|
backend stores it.
|
||||||
|
- **Hardcoded GPS device address `192.168.1.100`** is a non-routable
|
||||||
|
RFC1918 default that should not ship with the production bundle.
|
||||||
|
Step 4 flag.
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# Module group: `src/features/annotations/`
|
||||||
|
|
||||||
|
> Compact doc covering all 5 annotations modules (`classColors.ts` is a shared leaf — see existing `src__features__annotations__classColors.md`). The annotations feature is the **central legacy concern** of the codebase per `_docs/legacy/wpf-era.md §4` (`Azaion.Annotator` window) — what's documented here is the React port. For the canonical product spec see `_docs/ui_design/README.md` (Annotations Tab Layout, Annotation Quality Guidelines, Affiliation Icons, Combat Readiness, Annotation Row Gradient, Keyboard Shortcuts, Video Annotation Time-Window Display) and parent suite `../../../../_docs/01_annotations.md` for the API contract.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
Owns the `/annotations` route. Lets the user:
|
||||||
|
1. Browse media (video / image) for the currently selected flight, with a 300-ms debounced name filter, drag-and-drop / file-picker / folder-picker upload, and right-click delete.
|
||||||
|
2. Play / pause / step a video, scrub the timeline, mute, with frame stepping at 1 / 5 / 10 / 30 / 60 frames in both directions (assumed 30 FPS — see Findings).
|
||||||
|
3. Draw / move / resize / multi-select bounding boxes on a custom canvas overlay, click-and-drag with Ctrl-modifier behaviours, snap to image-normalized coordinates 0–1.
|
||||||
|
4. Pick the active detection class (1–9 hotkeys) and PhotoMode (Regular / Winter / Night) via the shared `DetectionClasses` component.
|
||||||
|
5. Save the per-frame detection set back to `POST /api/annotations/annotations`, with **fall-back to in-memory storage** when the file is a local blob: URL or the backend is unreachable.
|
||||||
|
6. Stream the annotations sidebar from the `GET /api/annotations/annotations/events` SSE feed; show each row with the gradient defined in `_docs/ui_design/README.md`.
|
||||||
|
7. Trigger AI detection via `POST /api/detect/{mediaId}` (modal log overlay).
|
||||||
|
8. Download an annotation as YOLO `.txt` + a PNG of the frame with rectangles burned in.
|
||||||
|
|
||||||
|
## Module map
|
||||||
|
|
||||||
|
| Module | Layer | Responsibility |
|
||||||
|
|---|---|---|
|
||||||
|
| `classColors.ts` | leaf | (already documented separately) Class-number → colour + photoMode-suffix lookup. |
|
||||||
|
| `MediaList.tsx` | sub-component | Left panel media browser. Owns `media[]` state, debounced filter, dropzone upload, blob: local-mode fallback when backend POST fails. Calls `GET /api/annotations/media`, `DELETE /api/annotations/media/{id}`, `POST /api/annotations/media/batch`. |
|
||||||
|
| `VideoPlayer.tsx` | sub-component | Native `<video>` wrapper. `forwardRef` exposes `seek(seconds)` and `getVideoElement()`. Custom progress slider + frame-step toolbar. Global `keydown` handler for Space / ←/→ (Ctrl=±150) / M. |
|
||||||
|
| `AnnotationsSidebar.tsx` | sub-component | Right panel: SSE-driven annotation list (`/api/annotations/annotations/events` filtered by `mediaId`), AI detect button, gradient row background built from per-detection class colour + confidence-modulated alpha, download button (delegates to page). |
|
||||||
|
| `CanvasEditor.tsx` | sub-component | Canvas overlay used in both image-only and video-overlay modes. `forwardRef` exposes `deleteSelected / deleteAll / hasSelection`. Owns zoom (Ctrl+wheel, 0.1–10×), pan (held in `pan` state — there is no Ctrl+drag pan code, only an unused `pan` state — see Findings), 8-handle resize, multi-select, draw-on-Ctrl-mousedown. Renders detections + a "time-window" preview of *other* annotations during video playback. |
|
||||||
|
| `AnnotationsPage.tsx` | page | Orchestrator: 3-panel layout via `useResizablePanel` (left 250 / right 200, min/max bounds). Owns `selectedMedia`, `annotations`, `detections`, `selectedClassNum`, `photoMode`, `currentTime`. Wires `MediaList` → `CanvasEditor` ↔ `VideoPlayer` ↔ `AnnotationsSidebar`. Handles the `Save` POST + local-fallback. Builds the YOLO + PNG download. |
|
||||||
|
|
||||||
|
## Key contracts
|
||||||
|
|
||||||
|
- **`Detection`** (from `src/types/index.ts`): `{ id, classNum, label, confidence ∈ [0,1], affiliation: 0|1|2, combatReadiness, centerX, centerY, width, height }` — all coords normalized 0–1. Matches `_docs/legacy/wpf-era.md §10` and parent suite `_docs/01_annotations.md` Detection DTO.
|
||||||
|
- **`AnnotationListItem`**: `{ id, mediaId, time: 'HH:MM:SS.mmm' | null, createdDate, source: AnnotationSource, status: AnnotationStatus, isSplit, splitTile, detections[] }`. Matches `Annotations` table in parent `_docs/00_database_schema.md` modulo client-side `isSplit / splitTile`.
|
||||||
|
- **AI detect endpoint**: `POST /api/detect/{mediaId}` — matches parent `../../../../_docs/03_detections.md §2` after nginx strips `/api/detect/`. NOTE: UI does NOT forward the `X-Refresh-Token` header that the spec requires for long-running video detection (`_docs/03_detections.md §2 request headers`). Carried as a finding.
|
||||||
|
- **Save body**: `{ mediaId, time: 'HH:MM:SS.mmm' | null, detections: Detection[] }`. .NET `TimeSpan.Parse` accepts that format so the round-trip works for `time → VideoTime`. **Body is missing required `Source` and optional `WaypointId`** required by parent spec `CreateAnnotationRequest` — see Findings.
|
||||||
|
|
||||||
|
## External integrations
|
||||||
|
|
||||||
|
| Endpoint / origin | Where | Direction | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `GET /api/annotations/media?flightId&name&pageSize=1000` | `MediaList.fetchMedia` | egress | Hardcoded ceiling 1000. |
|
||||||
|
| `GET /api/annotations/media/{id}/file` | `CanvasEditor`, `VideoPlayer`, `AnnotationsPage.handleDownload` | egress | Image / video bytes. |
|
||||||
|
| `POST /api/annotations/media/batch` | `MediaList.uploadFiles` | egress | `multipart/form-data`. |
|
||||||
|
| `DELETE /api/annotations/media/{id}` | `MediaList.handleDelete` | egress | Skipped for local blob: entries. |
|
||||||
|
| `GET /api/annotations/annotations?mediaId&pageSize=1000` | `MediaList.handleSelect`, `AnnotationsSidebar`, `AnnotationsPage.handleSave` | egress | Refresh after save / SSE event. |
|
||||||
|
| `POST /api/annotations/annotations` | `AnnotationsPage.handleSave` | egress | Body: `{ mediaId, time, detections }`. Falls back to in-memory on 4xx/5xx. |
|
||||||
|
| `GET /api/annotations/annotations/{id}/image` | `CanvasEditor.loadImage` | egress | Per-annotation hashed image; preferred over the raw media for image annotations. |
|
||||||
|
| `GET /api/annotations/annotations/events` (SSE) | `AnnotationsSidebar` | egress | Listens for `{ annotationId, mediaId, status }` events; filters client-side by `media.id`. |
|
||||||
|
| `POST /api/detect/{mediaId}` | `AnnotationsSidebar.handleDetect` | egress | Triggers AI inference — endpoint shape unverified vs `_docs/03_detections.md`. |
|
||||||
|
| `URL.createObjectURL(File)` | `MediaList.uploadFiles`, `AnnotationsPage.handleDownload` | browser API | Local-mode blob URLs are revoked on delete or unmount. |
|
||||||
|
|
||||||
|
## Findings carried into Step 4 / 6 / 8
|
||||||
|
|
||||||
|
1. **`VideoPlayer.stepFrames` hardcodes `fps = 30`** — frame stepping is wrong for any other fps (most drone footage is 25 / 30 / 60). UI spec says "Frame duration = 1 / video FPS" (`_docs/ui_design/README.md`). Should read from `video.getVideoPlaybackQuality()` / metadata. Step 4.
|
||||||
|
2. **`VideoPlayer` Ctrl+Left/Right is documented in `_docs/ui_design/README.md` as "skip 5 seconds"** but here it does `±150 frames` (`= 5s @ 30fps` only). Same fps coupling as #1.
|
||||||
|
3. **`VideoPlayer.error` state has no setter call beyond initial `null`/onLoadedMetadata reset** — but the `setError` call on `onError` only sets a string when source fails to load; spec says status messages should appear in a status bar (`_docs/ui_design/README.md`). The status-bar pattern is not implemented.
|
||||||
|
4. **`CanvasEditor` Ctrl+drag pan is documented (`_docs/ui_design/README.md`)** but not implemented — `pan` state exists, only `setPan(...)` calls are inside the (no-op) zoom flow. The canvas always renders at `pan = {0,0}`. Step 4 / Step 6 problem extraction.
|
||||||
|
5. **`CanvasEditor.useEffect[draw]` has missing deps** (`isVideo`, `imgSize` dependency tracked indirectly through other deps; `currentTime`/`annotations` are listed but `getTimeWindowDetections` is recreated each render and is closed-over). Specifically: `draw`'s closure reads `getTimeWindowDetections()` and the inner `getTimeWindowDetections` reads `media`, `currentTime`, `annotations` — those *are* in the dep list, but `media` itself is missing. Step 4 a11y / correctness.
|
||||||
|
6. **`CanvasEditor` time-window threshold is `< 2_000_000` ticks** in `getTimeWindowDetections` — at 10 000 000 ticks/second this is **±200 ms (400 ms total window)**. Spec is **asymmetric 50 ms before + 150 ms after (200 ms total)** per `../../ui_design/README.md`. Implementation window is 4× too wide and centred on the wrong offset. Step 4.
|
||||||
|
7. **`CanvasEditor` `ctx.fillStyle = color; ctx.globalAlpha = 0.1; ctx.fillRect(...)`** — the box "fill" is class colour at 10% opacity, but the **affiliation icon** (NATO MIL-STD-2525 shape) and **combat-readiness indicator** are missing. The current code only renders a tiny green dot for `combatReadiness === 1` (Ready). UI spec demands Friendly/Hostile/Unknown/None affiliation icons and Ready/NotReady/Unknown CR. Step 4 vs `_docs/ui_design/README.md` — significant gap.
|
||||||
|
8. **`CanvasEditor.AFFILIATION_COLORS` constant is dead code** — defined but never used. Likely the seed of the missing affiliation rendering. Step 4.
|
||||||
|
9. **Annotation row gradient cap is wrong by ~9 percentage points**: `AnnotationsSidebar.getRowGradient` uses `Math.round(alpha * 40).toString(16)` — `40` is **decimal**, not hex, so the maximum alpha byte is `0x28` (decimal 40 ≈ **16% opacity**). Spec wireframe `../../ui_design/annotations.html` uses `rgba(...,0.25)` = `0x40` hex (25%). Almost certainly a typo: should be `* 64` (decimal) or `* 0x40` to match the wireframe. Step 4.
|
||||||
|
10. **`AnnotationsSidebar` AI-detect modal**: `setDetectLog` writes "Detection complete" immediately after `await api.post(...)` returns — there is no progress streaming despite the spec ("scrolling log of detection progress" via `DetectionEvent` SSE per `_docs/03_detections.md`). The Detection SSE feed is not subscribed. Step 6.
|
||||||
|
11. **`AnnotationsSidebar` SSE refresh** is fire-and-forget (`.catch(() => {})`) — silent error suppression, violates `coderule.mdc`. Step 4.
|
||||||
|
12. **`AnnotationsPage.formatTicks(seconds)`** produces `HH:MM:SS.mmm` (string). Parent suite `_docs/01_annotations.md` carries `TimeSpan VideoTime` — .NET `TimeSpan.Parse` accepts that format, so the round-trip works, but the API contract is `TimeSpan` ticks, not a string. Verify in Step 4.
|
||||||
|
13. **`AnnotationsPage.handleDownload` never sets `crossOrigin` on the video element** (only on the standalone image fallback) — Canvas `toBlob` will throw "tainted canvas" if the video served from `/api/annotations/media/.../file` doesn't include `Access-Control-Allow-Origin`. Same-origin via the dev proxy and nginx is fine, but cross-origin (CDN / direct) breaks download. Step 4.
|
||||||
|
14. **`AnnotationsPage.handleSelect` annotation flow** does NOT expose `Validate` (V key per UI spec). The Annotations tab shouldn't validate (that's Dataset Explorer), but UI spec keyboard shortcuts list V on Annotations tab too — confirm scope in Step 6.
|
||||||
|
15. **No `R` key for AI detect** in any module here despite UI spec (`R = Trigger AI detection`). The AI Detect button is only reachable by mouse. Step 4.
|
||||||
|
16. **No PageUp / PageDown for prev/next media file** despite UI spec.
|
||||||
|
17. **No Camera config side panel** (altitude / FocalLength / SensorWidth) per `_docs/ui_design/README.md` — completely missing. Documented in Findings of `AdminPage.tsx` too: aircraft camera defaults are global, but per-session override UI is not built. Step 6.
|
||||||
|
18. **`MediaList` uses `alert(...)` for "Unsupported file type"** — not the project modal/toast pattern. Step 4.
|
||||||
|
19. **`MediaList` blob: local-mode is a graceful degradation** that lets the page work without backend — useful for demos but the user has no indication that "you're working offline, save will be lost on reload". Document explicitly in Step 6.
|
||||||
|
20. **`MediaList.fetchMedia` always merges blob: locals on top of backend results** even when filtering — local entries ignore the name filter. Step 4.
|
||||||
|
21. **`AnnotationsSidebar` `try { ... } catch (e: any)`** — `any` cast bypasses TS strict; `e.message` may be `undefined`. Step 4.
|
||||||
|
22. **`AnnotationsPage` left/right panels resize but widths NOT persisted** to `UserSettings` — UI spec says they should be restored per-user across sessions. The `useResizablePanel` hook only owns runtime state. Step 6 / Step 8.
|
||||||
|
23. **`CanvasEditor.handleMouseDown` Ctrl-modifier semantics**: Ctrl+click on a box should multi-select (per spec). Code does this. Ctrl+click on empty space should NOT start a draw — but the current branch starts `dragState = 'draw'` either way, then differentiates inside `handleMouseUp` by drawRect size. Slight UX cost only. Step 4.
|
||||||
|
24. **Tile / split-image annotation rendering**: spec mentions "Tile zoom" — auto-zoom to a tile region when opening a split-image detection. `AnnotationListItem.splitTile` exists but no consumer code reads it. Step 6 problem extraction.
|
||||||
|
25. **`AnnotationsPage.handleSave` 4xx/5xx fallback creates an in-memory `local-${uuid}` annotation** that looks identical to a saved one. The user can't distinguish the two — risk of data loss on reload. Step 6 problem extraction.
|
||||||
|
26. **`AnnotationsPage` does not subscribe to the inference detect SSE** — no AI progress visualization, no update on detect completion, no error if inference returns 5xx. Step 6.
|
||||||
|
27. **[STEP 4 — UI FIX, spec is canonical, user 2026-05-10]** `AnnotationStatus` enum diverges from spec. UI declares `Created=0, Edited=1, Validated=2` (`src/types/index.ts:23`). Spec (`../../../../_docs/09_dataset_explorer.md`) is `None=0, Created=10, Edited=20, Validated=30, Deleted=40`. Action: change `src/types/index.ts` to the spec values. Cascades through every status filter, every PATCH/POST that sends a status, every render. **PRIORITY** — without this fix every dataset status filter and every PATCH `/dataset/{id}/status` from the UI sends a wrong integer.
|
||||||
|
28. **[STEP 4 — UI FIX, spec is canonical, user 2026-05-10]** `Affiliation` enum diverges from spec. UI has 3 values `Unknown=0, Friendly=1, Hostile=2`. Spec (`../../ui_design/README.md` Affiliation Icons + parent `../../../../_docs/03_detections.md`) requires four values `None, Friendly, Hostile, Unknown`. Action: change `src/types/index.ts` to the four-value set; integer values to be confirmed once with the .NET service before patching (likely `None=0, Friendly=1, Hostile=2, Unknown=3`). Without this fix the UI cannot send/render the **None** (no-icon) case.
|
||||||
|
29. **[STEP 4 — UI FIX, spec is canonical, user 2026-05-10]** `CombatReadiness` enum diverges from spec. UI has `NotReady=0, Ready=1`. Spec adds `Unknown` (no indicator rendered). Action: add `Unknown` to `src/types/index.ts`; confirm integer values with the .NET service.
|
||||||
|
30. **[STEP 4 — UI FIX, spec is canonical, user 2026-05-10]** `MediaStatus` enum diverges from spec. UI has `New=0, AiProcessing=1, AiProcessed=2, ManualCreated=3`. Spec (`../../../../_docs/01_annotations.md` SSE + `_docs/03_detections.md §2`) is `None, New, AIProcessing, AIProcessed, ManualCreated, Confirmed, Error`. Action: change `src/types/index.ts` to the seven-value set; confirm integer values with the .NET service. Without this fix the UI cannot render the **Error** SSE event when inference fails.
|
||||||
|
31. **[RESOLVED 2026-05-10 — PARENT-DOC FIX APPLIED]** `AnnotationSource` numeric serialization is canonical; UI is correct (`AI=0, Manual=1`). Parent `../../../../_docs/01_annotations.md` §5 response example and AnnotationSource enum table updated to integer values; `../../../../_docs/09_dataset_explorer.md §1`, §2, §4 examples likewise. Both files also got a "wire format is numeric" header note. No UI change needed.
|
||||||
|
32. **[STEP 4 — UI FIX, user 2026-05-10]** `AnnotationsPage.handleSave` must add `Source` and `WaypointId` to the request body and rename `time` → `videoTime`. Required body shape per parent `../../../../_docs/01_annotations.md §1` `CreateAnnotationRequest`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mediaId": "<string>",
|
||||||
|
"waypointId": "<guid | null>",
|
||||||
|
"source": 1,
|
||||||
|
"videoTime": "HH:MM:SS.mmm",
|
||||||
|
"detections": [ ... ]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes: (a) `userId` is supplied server-side from JWT — do not send. (b) `image` is omitted in the save flow; the server reuses the media's frame at `videoTime`. (c) `source` is `Manual` (= `1`) for hand-edited save; AI inference posts use `AI` (= `0`) and are sent server-to-server by the Detections service. (d) `waypointId` must come from `Media.waypointId` (already typed in `src/types/index.ts:16`); the UI currently does not thread waypoint association through to save. (e) Field name `time` must become `videoTime` (.NET PascalCase `VideoTime` → camelCase on the wire).
|
||||||
|
33. **[RESOLVED 2026-05-10 — PARENT-DOC FIX APPLIED]** `DatasetItem.isSplit` is correct on the UI side; parent spec response now includes it. `../../../../_docs/09_dataset_explorer.md §1` `items[]` shape updated with `isSplit: boolean` plus a description tying it to `_docs/03_detections.md §4` Tile-Based Detection. No UI change needed.
|
||||||
|
34. **[STEP 4 — UI FIX, user 2026-05-10]** `Detect` endpoint needs conditional `X-Refresh-Token` header. Spec `../../../../_docs/03_detections.md §2` lists it as required for long-running video detection. Access tokens expire in 3600 s per `../../../../_docs/10_auth.md §1`, so any video that takes >1 h to process loses auth on the server-to-server `POST /annotations` call. Action: when `media.mediaType === MediaType.Video`, attach `X-Refresh-Token: <localStorage refreshToken>` to the `POST /api/detect/{mediaId}` request. Caveat: `/auth/refresh` rotates the refresh token (per `_docs/10_auth.md §2`), so the value can go stale if the UI also refreshes its own token mid-flight. Step 4 must decide whether to (i) cache the refresh token at detect-start, (ii) use a long-lived service token, or (iii) accept the failure mode and surface it to the user. For images and short videos the header is unnecessary — but the UI cannot tell upfront, so default to "always send for video".
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Cross-doc references
|
||||||
|
|
||||||
|
- Parent suite Annotations API: `../../../../_docs/01_annotations.md`
|
||||||
|
- Parent suite Detections (AI inference): `../../../../_docs/03_detections.md`
|
||||||
|
- Parent suite Database schema: `../../../../_docs/00_database_schema.md`
|
||||||
|
- Legacy WPF reference: `../../legacy/wpf-era.md` §4 (Annotator window) and §10 (what survived).
|
||||||
|
- UI spec: `../../ui_design/README.md` (Annotations Tab Layout + keyboard shortcuts + time-window + annotation row gradient + affiliation icons + combat readiness).
|
||||||
|
- Shared component used here: `src/components/DetectionClasses.tsx` (already documented).
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# Module: `src/features/annotations/classColors.ts`
|
||||||
|
|
||||||
|
> **Source**: `src/features/annotations/classColors.ts` (24 lines)
|
||||||
|
> **Topo batch**: B1 (leaf — no internal imports)
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Pure-function fallbacks for detection-class color and display name. Used when the live `DetectionClass[]` from the admin API hasn't been loaded (initial render) or doesn't include a class for a given `Detection.classNum`.
|
||||||
|
|
||||||
|
Also exposes the **PhotoMode-aware suffix** logic that mirrors the WPF-era `yoloId = classId + photoModeOffset` convention (see `_docs/legacy/wpf-era.md` §10): class numbers in `[0, 19]` are "Regular", `[20, 39]` are "Winter", `[40, 59]` are "Night".
|
||||||
|
|
||||||
|
## Public interface
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const FALLBACK_CLASS_NAMES: string[] // 12 generic English labels
|
||||||
|
export function getClassColor(classNum: number): string // hex, no '#' alpha
|
||||||
|
export function getPhotoModeSuffix(classNum: number): string // '' | ' (winter)' | ' (night)'
|
||||||
|
export function getClassNameFallback(classNum: number): string // FALLBACK_CLASS_NAMES[base] or '#<n>'
|
||||||
|
```
|
||||||
|
|
||||||
|
A 12-color palette `CLASS_COLORS` is defined module-private and referenced through `getClassColor`.
|
||||||
|
|
||||||
|
## Internal logic
|
||||||
|
|
||||||
|
```
|
||||||
|
base = classNum % 20
|
||||||
|
mode = floor(classNum / 20)
|
||||||
|
color = CLASS_COLORS[base % CLASS_COLORS.length] // wraps if base >= 12
|
||||||
|
name = FALLBACK_CLASS_NAMES[base % FALLBACK_CLASS_NAMES.length]
|
||||||
|
?? `#${classNum}`
|
||||||
|
suffix = mode === 1 ? ' (winter)' : mode === 2 ? ' (night)' : ''
|
||||||
|
```
|
||||||
|
|
||||||
|
The `??` guard exists for the unreachable case `base = 12..19` against an array of length 12 — except `base % length` brings it back into range first, so `?? '#<n>'` is dead. **Flag for Step 4 verification** — either the array is wrong or the `??` is dead code.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- **Internal**: none.
|
||||||
|
- **External**: none.
|
||||||
|
|
||||||
|
## Consumers (intra-repo)
|
||||||
|
|
||||||
|
- `src/features/annotations/CanvasEditor.tsx` — labels + crosshair color.
|
||||||
|
- `src/features/annotations/AnnotationsPage.tsx` — `getClassColor` for selected class indicator.
|
||||||
|
- `src/features/annotations/AnnotationsSidebar.tsx` — gradient stops in the annotation row.
|
||||||
|
- `src/features/annotations/MediaList.tsx` — *not currently a consumer* despite being adjacent (no import seen).
|
||||||
|
- `src/components/DetectionClasses.tsx` — fallback name + color when admin classes haven't loaded. **This is the cross-layer edge** flagged in `00_discovery.md` §8: a `components/` (shared-layer) file imports from a feature.
|
||||||
|
|
||||||
|
## Data models
|
||||||
|
|
||||||
|
`CLASS_COLORS: string[]` (12 hex strings) and `FALLBACK_CLASS_NAMES: string[]` (12 English strings). Neither is wire-coupled — pure UI defaults.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## External integrations
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Notes / open questions
|
||||||
|
|
||||||
|
- The "+20 winter / +40 night" PhotoMode convention is a direct port of the legacy WPF DetectionClasses contract (`_docs/legacy/wpf-era.md` §10). Verify against the current admin API DTO during component assembly (Step 2) — if the API exposes `photoMode` as a separate field on `DetectionClass` (it does, per `src/types/index.ts`), then computing the suffix from `classNum / 20` here is **redundant** and risks disagreeing with the admin-defined value. Candidate Step 4 testability fix: remove `getPhotoModeSuffix` once consumers have access to the typed `photoMode`.
|
||||||
|
- `FALLBACK_CLASS_NAMES` lists generic English labels (Car, Person, Truck, …) that bear no relation to the actual military classes the legacy doc enumerates (Military Vehicle, Truck, Car, Artillery, Active Mine — see `_docs/ui_design/README.md` §"Detection Classes Table"). Acceptable for a *fallback* only ever shown if the admin classes failed to load; document this in Step 5 (Solution Extraction).
|
||||||
|
- Cross-layer import surfaced in `00_discovery.md` §8 — proper home for these helpers is either `src/components/detection/` (with `DetectionClasses.tsx`) or a new `src/shared/` namespace. Decision deferred to Step 2.5 module-layout derivation.
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# Module: `src/features/dataset/DatasetPage.tsx`
|
||||||
|
|
||||||
|
> Compact module doc. Spec: `_docs/ui_design/README.md` (Dataset Explorer Layout) and parent suite `../../../../_docs/09_dataset_explorer.md` (API contract). Imports `CanvasEditor` from `../annotations/` — see cross-feature edge in `00_discovery.md §8`.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Owns the `/dataset` route. Read-side surface for the `Annotations` table: paginated thumbnail grid, date / class / status / objects-only / name filters, inline editing through the Annotations Tab's `CanvasEditor`, and a class-distribution chart. Mirrors the legacy `Azaion.Dataset.DatasetExplorer` window (`_docs/legacy/wpf-era.md §5`).
|
||||||
|
|
||||||
|
## Public interface
|
||||||
|
|
||||||
|
Default-exported page component, no props. Mounts under `/dataset` in `App.tsx`.
|
||||||
|
|
||||||
|
## Internal logic
|
||||||
|
|
||||||
|
- **Tabs**: `'annotations'` (grid, default) | `'editor'` (hidden until a thumbnail is double-clicked) | `'distribution'` (bar chart). State held in component.
|
||||||
|
- **Filters**: `fromDate`, `toDate`, `statusFilter` (`AnnotationStatus`), `selectedClassNum` (from `DetectionClasses`), `objectsOnly` (boolean), `search` (400 ms debounced via `useDebounce`).
|
||||||
|
- **Pagination**: client `page` state, server `pageSize` fixed at 20, `totalPages = ceil(totalCount / pageSize)`.
|
||||||
|
- **Selection**: `Set<annotationId>`. Plain click replaces; Ctrl+click toggles. Validate button appears when set is non-empty.
|
||||||
|
- **Validate**: `POST /api/annotations/dataset/bulk-status` with `{ annotationIds[], status: Validated }`.
|
||||||
|
- **Distribution**: lazy-loaded on tab switch via `GET /api/annotations/dataset/class-distribution`.
|
||||||
|
- **Editor tab** mounts `<CanvasEditor>` with a synthesised `Media` stub built from `editorAnnotation.mediaId` (most fields blank). Used so `CanvasEditor` can fetch the image via `/api/annotations/annotations/{id}/image`.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Internal: `api/client`, `useDebounce`, `useResizablePanel` (left panel 250 / 200–400), `FlightContext`, `DetectionClasses`, `ConfirmDialog` (imported but not used here — see Findings), `features/annotations/CanvasEditor`, `types/index`.
|
||||||
|
- External: `react`, `react-i18next`.
|
||||||
|
|
||||||
|
## External integrations
|
||||||
|
|
||||||
|
| Endpoint | Where | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `GET /api/annotations/dataset?...` | `fetchItems` | Filters: `page`, `pageSize`, `fromDate`, `toDate`, `flightId`, `status`, `classNum`, `hasDetections`, `name`. |
|
||||||
|
| `GET /api/annotations/dataset/{id}` | `handleDoubleClick` | Loads full `AnnotationListItem` for the editor tab. |
|
||||||
|
| `POST /api/annotations/dataset/bulk-status` | `handleValidate` | Body: `{ annotationIds, status }`. |
|
||||||
|
| `GET /api/annotations/dataset/class-distribution` | `loadDistribution` | Class histogram. |
|
||||||
|
| `GET /api/annotations/annotations/{id}/thumbnail` | grid tile `<img src=>` | One HTTP per visible tile. |
|
||||||
|
| `GET /api/annotations/annotations/{id}/image` | `CanvasEditor` (delegated) | When the editor is open. |
|
||||||
|
|
||||||
|
Spec contract is in parent suite `_docs/09_dataset_explorer.md`.
|
||||||
|
|
||||||
|
## Findings carried into Step 4 / 6 / 8
|
||||||
|
|
||||||
|
1. **No keyboard shortcuts implemented**: spec demands `1–9` (class), Enter (save), Del (delete), X (delete all), Esc (close editor), V (validate selected), Up/Down/PageUp/PageDown (navigate). Only Ctrl+click for multi-select works. The editor tab has no Save / Delete buttons either. Step 6 problem extraction.
|
||||||
|
2. **`Refresh thumbnails` button is missing** — spec calls for a status-bar action to regenerate the thumbnail database with progress. Step 6.
|
||||||
|
3. **No virtualisation in the grid**: spec says "virtualized grid" — current code renders all `items` returned by the page (max 20 due to `pageSize`). For larger pages performance would degrade. Today not a problem because `pageSize=20`. Step 8.
|
||||||
|
4. **Editor tab does not Save**: opening an annotation in the editor lets the user edit `editorDetections` locally but there's no Save / Cancel control wired up. Edits are discarded when the user changes tab. Step 6.
|
||||||
|
5. **`editorMedia` synthetic stub** — `mediaType: 1` is a magic number (`MediaType.Image = 1`); should reuse the enum import already present. Step 4.
|
||||||
|
6. **`ConfirmDialog` imported but never used** — dead import. Step 4 cleanup.
|
||||||
|
7. **`fetchItems` does `catch {}`** silently — same `coderule.mdc` violation as elsewhere. Step 4.
|
||||||
|
8. **Empty-state**: when `items.length === 0` the grid renders nothing; no "No matches" message. Step 4.
|
||||||
|
9. **`thumbnail` URL does not include a cache-buster**; if a thumbnail is regenerated server-side, the browser will keep the stale image. Step 4 / Step 8.
|
||||||
|
10. **`isSeed` red-ring is correctly rendered**, but `isSeed` is not in `AnnotationStatus` filters — the user can't filter to seeds-only. Step 6 if intended.
|
||||||
|
11. **Date inputs accept any `<input type="date">` string**; no validation that `fromDate ≤ toDate`. Step 4.
|
||||||
|
12. **Status filter buttons skip status `None`** (spec lists None as one of the four buttons) — current code shows `All`, `Created`, `Edited`, `Validated`, conflating `All` with "include None". Step 4.
|
||||||
|
13. **`selectedClassNum=0` is the "no filter" sentinel** but also a real class number (class 0 exists per `_docs/ui_design/README.md` → military vehicle). Selecting class 0 in `DetectionClasses` is indistinguishable from "no filter". Step 6.
|
||||||
|
14. **Inherits one cross-cutting UI fix and one cross-repo parent-doc fix** (resolved with user 2026-05-10):
|
||||||
|
- **[STEP 4 — UI FIX]** `AnnotationStatus` values must change from UI `0/1/2` to spec `0/10/20/30/40` (`src/types/index.ts:23`). Every status filter button in this page (`null`/`null`/`Created`/`Edited`/`Validated`) currently sends a wrong integer. The "All" button is value `null` (no filter) — keep, but also expose a real "None" filter using the new `AnnotationStatus.None=0`. **PRIORITY** for Step 4. See annotations doc finding #27.
|
||||||
|
- **[RESOLVED 2026-05-10]** `DatasetItem.isSplit` is correct on UI; parent `../../../../_docs/09_dataset_explorer.md §1` response schema now includes it. See annotations doc finding #33.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Cross-doc references
|
||||||
|
|
||||||
|
- Parent suite Dataset Explorer API: `../../../../_docs/09_dataset_explorer.md`
|
||||||
|
- UI spec: `../../ui_design/README.md` (Dataset Explorer Layout, Keyboard Shortcuts).
|
||||||
|
- Imports `CanvasEditor` from `features/annotations/` — see `00_discovery.md §8` cross-feature edge.
|
||||||
|
- Legacy WPF reference: `../../legacy/wpf-era.md §5` (Dataset Explorer window).
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# Module group: `src/features/flights/`
|
||||||
|
|
||||||
|
> **Note**: this is a deliberately compact doc covering all 15 flights modules. Behaviour is mostly an in-progress port of `mission-planner/` into the React 19 SPA. For the canonical product spec see `_docs/ui_design/README.md` (Flights Page Layout) and `../../../_docs/02_flights.md` / `11_gps_denied.md` in the parent suite repo (Flights API contract + GPS-Denied semantics).
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
Owns the `/flights` route. Lets the user:
|
||||||
|
1. Browse / create / delete `Flight` rows (`POST/DELETE /api/flights/...`).
|
||||||
|
2. Plan a mission on a Leaflet map: add waypoints, draw work-area / no-go rectangles, edit altitude + purpose per point, see live total distance, time, battery %.
|
||||||
|
3. Toggle into GPS-Denied mode — opens an SSE stream `/api/flights/{id}/live-gps` (and the panel for orthophoto upload + correction once that backend wiring lands; today only the live-GPS readout is connected).
|
||||||
|
4. Save waypoints back to the Flights API (`/api/flights/{id}/waypoints`).
|
||||||
|
5. Import / export the plan as JSON.
|
||||||
|
|
||||||
|
Currently handles only the planning surface; the gps-denied orthophoto upload / correction inputs in `_docs/ui_design/flights.html` are not yet implemented.
|
||||||
|
|
||||||
|
## Module map
|
||||||
|
|
||||||
|
| Module | Layer | Responsibility |
|
||||||
|
|---|---|---|
|
||||||
|
| `types.ts` | leaf | All flight-feature-only types (`FlightPoint`, `CalculatedPointInfo`, `MapRectangle`, `WindParams`, `AircraftParams`, `MovingPointInfo`, `ActionMode`), plus tile URLs (`TILE_URLS`), `PURPOSES` (`tank` / `artillery`), and `COORDINATE_PRECISION = 8`. |
|
||||||
|
| `mapIcons.ts` | leaf | Three coloured Leaflet `Icon` instances + the default Leaflet pin (loaded from a CDN — see Findings). |
|
||||||
|
| `flightPlanUtils.ts` | leaf | Pure-ish helpers: `newGuid`, haversine `calculateDistance` (with plane climb/cruise/descend profile), OpenWeatherMap fetch, semi-empirical `calculateBatteryPercentUsed`, `calculateAllPoints` (sequential reduce), `parseCoordinates`, `getMockAircraftParams`. |
|
||||||
|
| `WaypointList.tsx` | sub-component | `@hello-pangea/dnd` reorderable list, hover-only Edit/Remove buttons, shows distance / time / battery / altitude per point. |
|
||||||
|
| `AltitudeChart.tsx` | sub-component | `react-chartjs-2` Line chart of altitude over normalized distance; pulls all controllers via `chart.js/auto`. |
|
||||||
|
| `WindEffect.tsx` | sub-component | Two number inputs (heading 0–360°, speed 0–30 m/s) with a small SVG arrow preview. |
|
||||||
|
| `MiniMap.tsx` | sub-component | 240×180 `react-leaflet` thumbnail anchored to a moving point; `attributionControl={false}`. |
|
||||||
|
| `AltitudeDialog.tsx` | sub-component | Add / Edit waypoint modal: lat / lng / altitude / `meta: string[]` purpose multi-select. Fully controlled. |
|
||||||
|
| `MapPoint.tsx` | sub-component | One waypoint marker: draggable, popup with altitude slider, purpose checkboxes, remove button. |
|
||||||
|
| `DrawControl.tsx` | sub-component | Headless Leaflet handler that draws work-area / prohibited-area rectangles via `mousedown / mousemove / mouseup`. |
|
||||||
|
| `FlightListSidebar.tsx` | sub-component | Left rail: flight list, "+ Create", inline-create row, telemetry date stub. |
|
||||||
|
| `JsonEditorDialog.tsx` | sub-component | Modal `<textarea>` over the plan JSON with live `JSON.parse` validation. |
|
||||||
|
| `FlightParamsPanel.tsx` | composite | Hosts `WaypointList` + `AltitudeChart` + `WindEffect` + all per-flight inputs (aircraft, initial altitude, FoV, comm address, action-mode buttons, totals strip, Save / Upload / EditAsJSON / Export). |
|
||||||
|
| `FlightMap.tsx` | composite | Wraps `MapContainer`; mounts `MapPoint` × N, `DrawControl`, `MiniMap` (when a point is moving), polyline + arrow decorator, satellite/classic toggle. |
|
||||||
|
| `FlightsPage.tsx` | page | Orchestrator: owns all state, talks to `api/client`, opens the SSE stream, mediates between sidebar / params panel / map / dialogs. |
|
||||||
|
|
||||||
|
## Key contracts (read by other docs)
|
||||||
|
|
||||||
|
- **`FlightPoint`**: `{ id: string; position: { lat; lng }; altitude: number; meta: string[] }`. `meta` ⊆ `{ 'tank', 'artillery' }`. The shape diverges from the legacy WPF `Point` (radio, single purpose) — the React SPA uses checkboxes (multi).
|
||||||
|
- **`CalculatedPointInfo`**: `{ bat: number /* % */; time: number /* hours */ }`. Index `i` = state at point `i` after the segment from `i-1`. `lastInfo.bat` drives the Good / Caution / Low colour status (`>12 / >5 / ≤5`).
|
||||||
|
- **`PURPOSES = [{ value: 'tank', label: 'options.tank' }, { value: 'artillery', label: 'options.artillery' }]`** — i18n keys are `flights.planner.${label}`.
|
||||||
|
- **JSON plan shape** (`handleEditJson` / `handleExport` / `handleJsonSave`): `{ operational_height: { currentAltitude }, geofences: { polygons: [{ northWest, southEast, fence_type: 'EXCLUSION'|'INCLUSION' }] }, action_points: [{ point: { lat, lon }, height, action: 'search', action_specific: { targets: string[] } }] }`. Used for both export-to-file and the JSON editor.
|
||||||
|
- **Tile URLs**: classic OSM and an Esri ArcGIS `World_Imagery` (in `types.ts`). Both are direct upstream — neither goes through the suite `satellite-provider/` proxy. See Findings.
|
||||||
|
|
||||||
|
## External integrations
|
||||||
|
|
||||||
|
| Endpoint / origin | Where | Direction | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `GET /api/flights/aircrafts` | `FlightsPage` | egress | Aircraft selector population. |
|
||||||
|
| `GET /api/flights/{id}/waypoints` | `FlightsPage` | egress | On flight select. |
|
||||||
|
| `POST /api/flights` | `FlightsPage` | egress | Create flight from sidebar. |
|
||||||
|
| `DELETE /api/flights/{id}` | `FlightsPage` | egress | After ConfirmDialog. |
|
||||||
|
| `POST/DELETE /api/flights/{id}/waypoints[/{wp}]` | `FlightsPage.handleSave` | egress | Delete-all-then-recreate, sequentially. |
|
||||||
|
| `GET /api/flights/{id}/live-gps` (SSE) | `FlightsPage` | egress | Open while in GPS mode + flight selected. |
|
||||||
|
| `https://api.openweathermap.org/...` | `flightPlanUtils.getWeatherData` | egress | Direct browser→3rd-party. **Hardcoded API key.** See Findings. |
|
||||||
|
| `tile.openstreetmap.org` (`TILE_URLS.classic`) | `FlightMap`, `MiniMap` | egress | Direct, no proxy. |
|
||||||
|
| `server.arcgisonline.com/.../World_Imagery` (`TILE_URLS.satellite`) | `FlightMap`, `MiniMap` | egress | Direct, no proxy. |
|
||||||
|
| `unpkg.com/leaflet@1.7.1/dist/images/marker-icon-2x.png` | `mapIcons.defaultIcon` | egress | CDN, version pinned to 1.7.1 while package is 1.9.4 (drift). |
|
||||||
|
| `navigator.geolocation.getCurrentPosition` | `FlightsPage` mount | browser API | Fallback to hardcoded `47.242, 35.024` (Zaporizhzhia). |
|
||||||
|
|
||||||
|
## Findings carried into Step 4 / 6 / 8
|
||||||
|
|
||||||
|
These are the real findings; the per-module rationale is in git history of the deleted per-file docs. Numbered for cross-reference from `state.json.notes`.
|
||||||
|
|
||||||
|
1. **HARDCODED OPENWEATHER API KEY** — `flightPlanUtils.ts:60`. HIGH severity. Step 4 source-code fix; upstream rotation is a parallel user task.
|
||||||
|
2. **`flightPlanUtils.calculateAllPoints` does N sequential `await`s** to OpenWeatherMap — N × RTT latency. Not parallelisable as-is because `info[i].bat` depends on `info[i-1]`. Step 8 refactor: fetch weather first in parallel, then reduce.
|
||||||
|
3. **`flightPlanUtils.calculateDistance` mixes km and metres** (`R = 6371`, altitudes converted `/1000` inline, returned `time = dist / aircraft.speed` assumes km/h). No type forces correctness. Step 4.
|
||||||
|
4. **`AircraftParams.batteryCapacity` unit is ambiguous** (Wh vs W·s). `calculateBatteryPercentUsed` divides W·s by it × 100 — only correct if W·s. Verify against `mission-planner/src/services/calculateBatteryUsage.ts`. Step 4.
|
||||||
|
5. **`flightPlanUtils.getWeatherData` swallows errors silently** (`catch { return null }`); callers can't distinguish "no wind" from "key revoked". Step 4.
|
||||||
|
6. **`mapIcons.defaultIcon` CDN URL is leaflet@1.7.1** while `package.json` is 1.9.4. Step 4 — switch to bundled assets or match version.
|
||||||
|
7. **`FlightMap` and `MiniMap` bypass the suite `satellite-provider/` proxy** (Esri tiles direct from `server.arcgisonline.com`). Possible licence + rate-limit concern. Step 4 + `architecture.md`.
|
||||||
|
8. **`MiniMap` sets `attributionControl={false}`** — drops OSM / Esri attribution. Possible licence-compliance gap. Step 4.
|
||||||
|
9. **`MiniMap` is fixed 240×180 + zoom 18 hardcoded** — overflows below the 640px mobile breakpoint. Step 4 vs `_docs/ui_design/README.md` responsive specs.
|
||||||
|
10. **`AltitudeDialog` lacks Esc-to-close, backdrop-click-to-cancel, `role="dialog"`, `aria-modal`** — inconsistent with `ConfirmDialog`. Same for `JsonEditorDialog`. Pick one modal convention in Step 4.
|
||||||
|
11. **`AltitudeDialog` accepts any number for lat/lng** (no `[-90,90]` / `[-180,180]` guard). `AltitudeDialog.altitude` and `WindEffect` inputs use `Number('')` → 0 silently — same pitfall noted in `SettingsPage`. Step 4.
|
||||||
|
12. **`AltitudeDialog` purpose multi-select** vs WPF radio (single choice). Confirm intent in Step 6: is the SPA expanding the data model on purpose, or is this a UI bug?
|
||||||
|
13. **`WindEffect` `max=360`** allows duplicate heading at 0 vs 360. Step 4.
|
||||||
|
14. **`WaypointList` drag handle is the entire row** (prevents text selection); Edit/Remove buttons are hover-only (unusable on touch); no a11y reorder announcements. Step 4 / Step 8 a11y.
|
||||||
|
15. **`WaypointList.calculatedPointInfo[i]`** silently degrades to alt-only label if length mismatches; tightly coupled to `FlightsPage` keeping arrays in lockstep.
|
||||||
|
16. **`AltitudeChart` pulls every chart.js controller** via `chart.js/auto` (bundle bloat); colours are duplicated as hex literals (drift from the `az-*` Tailwind tokens). Step 8.
|
||||||
|
17. **`AltitudeDialog` naming**: file is "AltitudeDialog" but the dialog covers lat / lng / altitude / purpose. Port-vestige from `mission-planner/`. Rename to `WaypointDialog` in Step 8.
|
||||||
|
18. **`flightPlanUtils.ts` is single-file** — newGuid + geo + weather + battery + parser + mock all together. Splitting into `geo.ts` / `weather.ts` / `battery.ts` / `mockAircraft.ts` is a Step 8 SRP candidate.
|
||||||
|
19. **`FlightsPage.handleSave`** deletes all existing waypoints then recreates — N+M sequential PUT/DELETE round-trips, not transactional, no progress UI, partial failure leaves the flight half-saved.
|
||||||
|
20. **`FlightsPage.handleSave` body shape does NOT match the Flights API spec — UI will likely 400 on a strict server**. Code POSTs `{ name, latitude, longitude, order }`. Parent `../../../../_docs/02_flights.md §3` `CreateWaypointRequest` requires `{ Geopoint: {Lat, Lon, MGRS}, Source: WaypointSource, Objective: WaypointObjective, OrderNum, Height }`. Mismatches: (a) lat/lon not nested under `Geopoint`; (b) field is `order`, spec is `OrderNum`; (c) `Source`, `Objective`, `Height` not sent at all; (d) UI sends `name` which the spec does not define on `Waypoint` (`Waypoint` interface in `src/types/index.ts:76` invents `name`). This collides with finding #19 — every save will round-trip waypoints in the wrong shape. **PRIORITY** for Step 4. Open question for Step 6: are these columns being added to the Flights API schema, or is the React UI to be aligned to the spec? Same comment for `altitude` and `meta` which have no place in the current spec.
|
||||||
|
21. **`FlightsPage.useEffect` bootstraps `getMockAircraftParams()`** as the active `aircraft` regardless of what the backend returns — the dropdown choice is cosmetic for now. Real wiring is a Step 6 / Step 8 follow-up.
|
||||||
|
22. **GPS-Denied panel is partial** — only the SSE live-GPS readout is wired; orthophoto upload, GPS correction (per `_docs/ui_design/README.md`) are not. Step 6 problem extraction.
|
||||||
|
23. **`handleImport` silently drops the file picker** if the user cancels (`if (!file) return`) — fine. But `handleJsonSave`'s catch uses `alert(...)` for a UX-grade error — replace with the project's modal/toast pattern in Step 4.
|
||||||
|
24. **`MapPoint` popup recomputes the marker DOM offset on every drag move** to choose dx/dy for the moving-point indicator. Acceptable, but the `(marker as unknown as { _icon: HTMLElement })._icon` cast leaks Leaflet internals.
|
||||||
|
25. **`DrawControl` registers global `mousedown`/`mousemove`/`mouseup` on the map** while a draw mode is active and disables `map.dragging` for the duration — fine, but no Esc-to-cancel mid-draw.
|
||||||
|
26. **`FlightContext` ceiling**: `FlightsPage` reads `flights` from `useFlight()` which fetches with `pageSize=1000` (already flagged in `FlightContext` doc). Won't surface here, but `selectFlight` is fire-and-forget — if the PUT to `/api/flights/select` fails the next page reload reverts the choice without notice.
|
||||||
|
|
||||||
|
## What's intentionally NOT here
|
||||||
|
|
||||||
|
- The orthophoto-upload / GPS-correction sub-panel (in `_docs/ui_design/flights.html` but not in source).
|
||||||
|
- Any per-segment annotation-time-window logic (that's the Annotations module).
|
||||||
|
- Any aircraft battery model that respects altitude-dependent air density (constant 1.05 kg/m³ in source).
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
None — confirmed by `00_discovery.md §5`. Documented test gap is owned by Steps 3 / 5–7 of autodev (test-spec → implement → run).
|
||||||
|
|
||||||
|
## Cross-doc references
|
||||||
|
|
||||||
|
- Parent suite Flights API contract: `../../../../_docs/02_flights.md` (DTOs, endpoint shapes).
|
||||||
|
- Parent suite GPS-Denied: `../../../../_docs/11_gps_denied.md` (SSE event shape, correction flow).
|
||||||
|
- UI spec: `../../ui_design/README.md` (Flights Page Layout, GPS-Denied panel toggle, mobile breakpoint).
|
||||||
|
- Port-source: `mission-planner/src/flightPlanning/*` — covered separately as one consolidated doc.
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
# Module: `src/features/login/LoginPage.tsx`
|
||||||
|
|
||||||
|
> **Source**: `src/features/login/LoginPage.tsx` (95 lines)
|
||||||
|
> **Topo batch**: B4 (depends on B3: `auth/AuthContext`)
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
The single public route of the SPA. Collects email + password, calls
|
||||||
|
`AuthContext.login(...)`, and on success runs a four-step "unlock"
|
||||||
|
animation (download key → decrypting → starting services → ready)
|
||||||
|
before navigating to `/flights`. Replaces the WPF `LoginWindow.xaml`
|
||||||
|
including its multi-step progress UI (`_docs/legacy/wpf-era.md` §3 / §4).
|
||||||
|
|
||||||
|
## Public interface
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export default function LoginPage(): JSX.Element
|
||||||
|
```
|
||||||
|
|
||||||
|
No props. Reads `useAuth().login` and `react-router-dom`'s `useNavigate`.
|
||||||
|
|
||||||
|
## Internal logic
|
||||||
|
|
||||||
|
- **State machine** — `step: UnlockStep` cycles through:
|
||||||
|
`idle → authenticating → downloadingKey → decrypting → startingServices → ready`,
|
||||||
|
with `idle` also reachable on error from `authenticating`.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type UnlockStep = 'idle' | 'authenticating' | 'downloadingKey'
|
||||||
|
| 'decrypting' | 'startingServices' | 'ready'
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Local state**:
|
||||||
|
- `email: string`, `password: string` — controlled inputs.
|
||||||
|
- `error: string` — empty unless the previous attempt failed.
|
||||||
|
- `step: UnlockStep` — drives both the form/spinner branch and the
|
||||||
|
spinner caption.
|
||||||
|
- **`handleSubmit(e)`**:
|
||||||
|
1. `preventDefault`, clear `error`, set `step = 'authenticating'`.
|
||||||
|
2. `await login(email, password)` — this throws on bad credentials
|
||||||
|
(`AuthContext.login` rethrows the `api.post` error).
|
||||||
|
3. On success: call `runUnlockSequence()`.
|
||||||
|
4. On failure: reset `step` to `'idle'`, set `error = t('login.error')`.
|
||||||
|
- **`runUnlockSequence()`** — sequentially sets `step` to each of
|
||||||
|
`downloadingKey`, `decrypting`, `startingServices`, `ready`, with a
|
||||||
|
600ms `setTimeout` delay between each. After `ready`, navigates to
|
||||||
|
`/flights`. The unlock steps are **purely presentational** — there is
|
||||||
|
no real downloadKey/decrypt work happening; the SPA's bearer token is
|
||||||
|
already set inside `await login(...)`. The animation reproduces the
|
||||||
|
WPF "vault unlocking" UX for continuity, not for any cryptographic
|
||||||
|
operation. Total minimum wait after a successful auth: 4 × 600 ms =
|
||||||
|
2.4 s. Document explicitly to avoid future "what does this decrypt?"
|
||||||
|
confusion.
|
||||||
|
- **`STEP_KEYS`**: a `Record<UnlockStep, string>` mapping each step to a
|
||||||
|
translation key. `'idle'` maps to the empty string (the spinner
|
||||||
|
caption is not rendered when idle).
|
||||||
|
- **Render**: when `step === 'idle'`, the form is shown (email +
|
||||||
|
password + error + submit). Otherwise the form is hidden and a
|
||||||
|
centered spinner with the localized step caption is shown. Note the
|
||||||
|
spinner is rendered even during `authenticating` (real network), so
|
||||||
|
the user sees a single uninterrupted spinner across the entire
|
||||||
|
auth → animation flow.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- **Internal**: `../../auth/AuthContext` — `useAuth().login`.
|
||||||
|
- **External**: `react` (`useState`, `FormEvent` type),
|
||||||
|
`react-router-dom` (`useNavigate`),
|
||||||
|
`react-i18next` (`useTranslation`).
|
||||||
|
|
||||||
|
## Consumers (intra-repo)
|
||||||
|
|
||||||
|
- `src/App.tsx` — mounted at the public `/login` route, *outside*
|
||||||
|
`AuthProvider`. (Verify in B8 — currently `App.tsx` puts
|
||||||
|
`AuthProvider` inside the protected branch only, so `LoginPage`
|
||||||
|
reaches `useAuth()` only because `App.tsx` actually wraps everything
|
||||||
|
in `AuthProvider` at a higher level. Confirmed via the §7a graph
|
||||||
|
edge `App → AuthProvider`.)
|
||||||
|
|
||||||
|
## Data models
|
||||||
|
|
||||||
|
`UnlockStep` and `STEP_KEYS` are module-private. No DTOs or shared
|
||||||
|
types.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
- **i18n keys**: `login.title`, `login.email`, `login.password`,
|
||||||
|
`login.submit`, `login.error`, `login.authenticating`,
|
||||||
|
`login.downloadingKey`, `login.decrypting`, `login.startingServices`,
|
||||||
|
`login.ready`. (All present in `src/i18n/en.json` and `ua.json` —
|
||||||
|
confirmed.)
|
||||||
|
- **Tailwind tokens**: `bg-az-bg`, `bg-az-panel`, `border-az-border`,
|
||||||
|
`text-az-orange`, `text-az-text`, `text-az-muted`, `text-az-red`.
|
||||||
|
Defined in `src/index.css`.
|
||||||
|
- **Hardcoded animation timing**: `600 ms` per step. No env override.
|
||||||
|
|
||||||
|
## External integrations
|
||||||
|
|
||||||
|
None directly. Indirect: `AuthContext.login` calls
|
||||||
|
`POST /api/admin/auth/login` (routed by `nginx.conf` to the `admin/`
|
||||||
|
service).
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- Both inputs use the right `type` attribute (`email`, `password`),
|
||||||
|
giving the browser the chance to mask the password and offer
|
||||||
|
password-manager autofill.
|
||||||
|
- The HTML form does NOT have `autoComplete="off"` — autofill is
|
||||||
|
intentionally allowed.
|
||||||
|
- The component does NOT log credentials anywhere. The `error` state
|
||||||
|
carries only the localized "Invalid credentials" string, never the
|
||||||
|
raw backend error.
|
||||||
|
- After successful auth, the bearer is already in memory (`AuthContext`
|
||||||
|
set it). The 2.4s "unlock" animation does NOT extend the auth window
|
||||||
|
— if the bearer expires server-side during the animation the next
|
||||||
|
request retries via `client.ts`'s 401 → refresh path.
|
||||||
|
- The `setError(t('login.error'))` shows a single generic error for
|
||||||
|
every failure mode (wrong password, account locked, server down).
|
||||||
|
Acceptable for security (no user-enumeration leak), but logs an
|
||||||
|
observability flag — backend should keep specific reasons in its
|
||||||
|
audit log.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Notes / open questions
|
||||||
|
|
||||||
|
- **The unlock animation is theatrical** — it suggests cryptographic
|
||||||
|
work that is NOT happening. If a future phase actually adds
|
||||||
|
client-side key derivation, the animation should reflect real
|
||||||
|
progress (`'decrypting'` would block on actual work). Keep the names
|
||||||
|
but document the placeholder status in `solution.md` (Step 5).
|
||||||
|
- **No "loading" while `authenticating`** beyond hiding the form —
|
||||||
|
Submit button shows no spinner of its own; the form simply hides
|
||||||
|
the moment `step` leaves `'idle'`. Mild UX gap (a quick auth
|
||||||
|
failure shows the form blink). Defer to Step 8.
|
||||||
|
- **Caps Lock indicator missing** — the WPF version had one
|
||||||
|
(`_docs/legacy/wpf-era.md`). Not currently a goal; flag if the UX
|
||||||
|
spec calls for it.
|
||||||
|
- **No "Forgot password" link** — out of scope; the `admin/` service
|
||||||
|
may or may not support that flow. Verify during Step 6 problem
|
||||||
|
extraction.
|
||||||
|
- **Form does not disable Submit while authenticating** — but the
|
||||||
|
form is unmounted as soon as `step !== 'idle'`, so a double-click
|
||||||
|
cannot fire a second `login(...)` call. Acceptable.
|
||||||
|
- **`runUnlockSequence` resolution race**: if the user navigates
|
||||||
|
away mid-animation (e.g. closes the tab), the pending `setTimeout`s
|
||||||
|
still fire but harmlessly. React's StrictMode double-invokes the
|
||||||
|
effect-mount path in dev — but `runUnlockSequence` is invoked from
|
||||||
|
`handleSubmit`, not an effect, so no duplication.
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
# Module: `src/features/settings/SettingsPage.tsx`
|
||||||
|
|
||||||
|
> **Source**: `src/features/settings/SettingsPage.tsx` (107 lines)
|
||||||
|
> **Topo batch**: B4 (depends on B3: `api/client`, `types/index`)
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
The per-tenant configuration screen. Three side-by-side panels:
|
||||||
|
**Tenant** (system settings: military unit, name, default camera
|
||||||
|
width and FoV), **Directories** (server-side filesystem paths for
|
||||||
|
videos, images, labels, results, thumbnails, GPS sat / route),
|
||||||
|
and **Aircrafts** (read-only list with star-toggle for default
|
||||||
|
selection). Replaces the legacy WPF `SettingsWindow.xaml` plus the
|
||||||
|
"system" tab (`_docs/legacy/wpf-era.md` §4).
|
||||||
|
|
||||||
|
Available to every authenticated user — `Header` does not gate
|
||||||
|
`/settings` behind a permission check. The backend is the authority
|
||||||
|
for who is allowed to write.
|
||||||
|
|
||||||
|
## Public interface
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export default function SettingsPage(): JSX.Element
|
||||||
|
```
|
||||||
|
|
||||||
|
No props.
|
||||||
|
|
||||||
|
## Internal logic
|
||||||
|
|
||||||
|
- **State**:
|
||||||
|
- `system: SystemSettings | null` — loaded from
|
||||||
|
`GET /api/annotations/settings/system`. `null` until the GET
|
||||||
|
resolves; the panel does not render until then (`{system && (...)}`).
|
||||||
|
- `dirs: DirectorySettings | null` — analogous, from
|
||||||
|
`GET /api/annotations/settings/directories`.
|
||||||
|
- `aircrafts: Aircraft[]` — from `GET /api/flights/aircrafts`.
|
||||||
|
- `saving: boolean` — disables the two Save buttons during a PUT.
|
||||||
|
- **Bootstrap effect** (`useEffect([])`):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
api.get<SystemSettings>('/api/annotations/settings/system').then(setSystem).catch(() => {})
|
||||||
|
api.get<DirectorySettings>('/api/annotations/settings/directories').then(setDirs).catch(() => {})
|
||||||
|
api.get<Aircraft[]>('/api/flights/aircrafts').then(setAircrafts).catch(() => {})
|
||||||
|
```
|
||||||
|
|
||||||
|
Three independent calls, all silently swallowed on error. Empty UI
|
||||||
|
on failure (no error banner). Flag for Step 4.
|
||||||
|
- **`saveSystem()`**:
|
||||||
|
1. Guard: `if (!system) return`.
|
||||||
|
2. `setSaving(true)`.
|
||||||
|
3. `await api.put('/api/annotations/settings/system', system)`.
|
||||||
|
4. `setSaving(false)`.
|
||||||
|
|
||||||
|
No optimistic update needed (the PUT body **is** the local state).
|
||||||
|
No re-fetch — assumes the server echoes the same values. **Error
|
||||||
|
path is missing**: a thrown PUT leaves `saving: true` permanently
|
||||||
|
(no `try/finally`). Flag for Step 4.
|
||||||
|
- **`saveDirs()`** — analogous against
|
||||||
|
`PUT /api/annotations/settings/directories`. Same missing
|
||||||
|
`try/finally` issue.
|
||||||
|
- **`handleToggleDefault(a)`** — duplicate of the same handler in
|
||||||
|
`AdminPage`: `PATCH /api/flights/aircrafts/${a.id}` with
|
||||||
|
`{ isDefault: !a.isDefault }` then optimistic local flip. Two copies
|
||||||
|
of the same logic in two pages — extract to a shared helper or to
|
||||||
|
`FlightContext` in Step 8 (the legacy WPF had a single
|
||||||
|
`AircraftService.SetDefault(...)`).
|
||||||
|
- **`field(label, value, onChange, type='text')`** — local helper that
|
||||||
|
renders a labeled `<input>`. Always controlled (`value={value ?? ''}`).
|
||||||
|
Converts numeric inputs via `parseInt(v) || 0` /
|
||||||
|
`parseFloat(v) || 0` at the call site. Note: `parseInt` / `parseFloat`
|
||||||
|
on an empty string returns `NaN`, and `NaN || 0` is `0` — so
|
||||||
|
clearing a numeric field silently writes `0`, not `null`. Flag for
|
||||||
|
Step 4 against the `SystemSettings` type which permits `null`.
|
||||||
|
- **Layout** — three independent flex children:
|
||||||
|
- Tenant (`w-[300px]` shrink-0): `field()` × 4 + Save.
|
||||||
|
- Directories (`w-[300px]` shrink-0): `field()` × 7 + Save.
|
||||||
|
- Aircrafts (`flex-1 max-w-sm`): list with star toggle.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- **Internal**:
|
||||||
|
- `../../api/client` — `api`.
|
||||||
|
- `../../types` — `SystemSettings`, `DirectorySettings`, `Aircraft`.
|
||||||
|
- **External**: `react` (`useState`, `useEffect`),
|
||||||
|
`react-i18next` (`useTranslation`).
|
||||||
|
|
||||||
|
## Consumers (intra-repo)
|
||||||
|
|
||||||
|
- `src/App.tsx` — mounted at the `/settings` route inside the
|
||||||
|
protected tree.
|
||||||
|
|
||||||
|
## Data models
|
||||||
|
|
||||||
|
- `SystemSettings` includes `id, name, militaryUnit,
|
||||||
|
defaultCameraWidth, defaultCameraFoV, thumbnailWidth, thumbnailHeight,
|
||||||
|
thumbnailBorder, generateAnnotatedImage, silentDetection`. The page
|
||||||
|
exposes only the first four — every other field is read on GET and
|
||||||
|
echoed back on PUT untouched. Flag if a future cycle needs to expose
|
||||||
|
thumbnails / silent-detection toggles.
|
||||||
|
- `DirectorySettings` has all 7 directory fields exposed as text
|
||||||
|
inputs. Path validation is server-side only.
|
||||||
|
- `Aircraft` (`id, model, type, isDefault`) — same shape as in
|
||||||
|
`AdminPage`.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
- **i18n keys consumed**: `settings.tenant`, `settings.directories`,
|
||||||
|
`settings.aircrafts`, `settings.save`. (Confirmed in
|
||||||
|
`src/i18n/en.json`.) Hardcoded English labels for every form field
|
||||||
|
("Military Unit", "Name", "Default Camera Width", "Default Camera
|
||||||
|
FoV", "Videos Dir", "Images Dir", "Labels Dir", "Results Dir",
|
||||||
|
"Thumbnails Dir", "GPS Sat Dir", "GPS Route Dir"). Flag for Step 4.
|
||||||
|
- **Tailwind tokens**: `bg-az-panel`, `bg-az-bg`, `bg-az-orange`,
|
||||||
|
`text-az-{text,muted,orange}`, `bg-az-blue/20`, `bg-az-green/20`,
|
||||||
|
`text-az-{blue,green}`, `border-az-border`. Defined in
|
||||||
|
`src/index.css`.
|
||||||
|
|
||||||
|
## External integrations
|
||||||
|
|
||||||
|
| Method | Path | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `GET` | `/api/annotations/settings/system` | Load tenant config |
|
||||||
|
| `PUT` | `/api/annotations/settings/system` | Save tenant config |
|
||||||
|
| `GET` | `/api/annotations/settings/directories` | Load directory paths |
|
||||||
|
| `PUT` | `/api/annotations/settings/directories` | Save directory paths |
|
||||||
|
| `GET` | `/api/flights/aircrafts` | Load aircraft list |
|
||||||
|
| `PATCH` | `/api/flights/aircrafts/{id}` | Toggle `isDefault` |
|
||||||
|
|
||||||
|
Routed by `nginx.conf` to `annotations/` and `flights/` backends.
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- **No client-side write authorization check** — the page renders the
|
||||||
|
Save buttons for every user. The backend (`annotations/` service)
|
||||||
|
is the authority. Document in `security_approach.md` (Step 6).
|
||||||
|
- **Hardcoded internal directory paths** are **not** present in this
|
||||||
|
file (unlike `AdminPage`'s hardcoded GPS device IP) — every value
|
||||||
|
is server-supplied. Good.
|
||||||
|
- **Path inputs are free-text** — server-side path traversal
|
||||||
|
validation is mandatory. Verify the `annotations/` service rejects
|
||||||
|
e.g. `../../etc/passwd`. Flag for Step 6.
|
||||||
|
- **`saving` state can stick on PUT failure** because there is no
|
||||||
|
`try/finally`, leaving the Save button permanently disabled. Step 4
|
||||||
|
candidate (matches the `AdminPage` "AI/GPS save buttons do
|
||||||
|
nothing" pattern — there is a clear lack of UX-level error
|
||||||
|
handling across both admin/settings pages).
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Notes / open questions
|
||||||
|
|
||||||
|
- **Numeric clear-to-zero pitfall**: `parseInt('') || 0` writes `0`
|
||||||
|
for an empty input, not `null`. This silently overwrites a
|
||||||
|
legitimate `null` (per the `SystemSettings` type, all four numeric
|
||||||
|
fields are nullable). Step 4 fix: detect empty input and pass
|
||||||
|
`null`.
|
||||||
|
- **Aircraft toggle handler is duplicated** between `AdminPage` and
|
||||||
|
`SettingsPage` — extract to a shared helper. Step 8.
|
||||||
|
- **No optimistic concurrency**: two admins editing system settings
|
||||||
|
simultaneously will overwrite each other. The `id` field is sent
|
||||||
|
back on PUT but no `version` / `etag` / `If-Match` header. Flag for
|
||||||
|
Step 6 problem-extraction. Backend may not support optimistic
|
||||||
|
concurrency yet.
|
||||||
|
- **The Aircrafts panel is read-only-ish here** but allows the
|
||||||
|
star-toggle, same as `AdminPage`. The duplication suggests an open
|
||||||
|
question: is this intentional (settings is "personal preferences",
|
||||||
|
admin is "global config", but default-aircraft is a *global*
|
||||||
|
setting) or accidental? Surface to the user in Step 6 problem
|
||||||
|
extraction.
|
||||||
|
- **`saving` is a single global flag** even though the page has two
|
||||||
|
independent Save buttons (system / dirs). A user who clicks
|
||||||
|
"Save System" then quickly clicks "Save Dirs" while the first PUT
|
||||||
|
is in flight will see the Dirs button disabled too. Acceptable
|
||||||
|
given the latency budget; flag if both saves become slow.
|
||||||
|
- **`thumbnailWidth/Height/Border` and `generateAnnotatedImage`,
|
||||||
|
`silentDetection`** in `SystemSettings` are not exposed in the UI
|
||||||
|
but are echoed back on PUT. If a future cycle adds them, ensure the
|
||||||
|
GET → PUT round-trip preserves any concurrent change made by another
|
||||||
|
client between the GET and the PUT.
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Module: `src/hooks/useDebounce.ts`
|
||||||
|
|
||||||
|
> **Source**: `src/hooks/useDebounce.ts` (12 lines)
|
||||||
|
> **Topo batch**: B1 (leaf)
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Generic React hook that delays propagation of a rapidly-changing value to its consumers, used to throttle search inputs against the backend.
|
||||||
|
|
||||||
|
## Public interface
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export function useDebounce<T>(value: T, delay: number): T
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns the latest `value` only after `delay` milliseconds have elapsed without any further change. Generic in `T`, so callers can debounce strings, numbers, or any reference value (referential identity matters; see notes).
|
||||||
|
|
||||||
|
## Internal logic
|
||||||
|
|
||||||
|
`useState<T>(value)` for the debounced output; `useEffect` registers a `setTimeout(setDebounced, delay)` and returns a `clearTimeout` cleanup. The effect re-runs whenever `value` or `delay` changes — each new input value cancels the pending timeout from the previous render and starts a fresh one.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- **Internal**: none.
|
||||||
|
- **External**: `react` (`useState`, `useEffect`).
|
||||||
|
|
||||||
|
## Consumers (intra-repo)
|
||||||
|
|
||||||
|
- `src/features/annotations/MediaList.tsx` — debounces the media-name search input.
|
||||||
|
- `src/features/dataset/DatasetPage.tsx` — debounces the dataset name-search filter (specified as 400ms in `_docs/ui_design/README.md` §"Dataset Explorer", but the actual delay is set by the caller).
|
||||||
|
|
||||||
|
## Data models
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## External integrations
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Notes / open questions
|
||||||
|
|
||||||
|
- The hook compares values by referential equality (`useEffect` deps array). Passing a fresh object literal each render — e.g. `useDebounce({ q }, 300)` — would re-trigger the timer endlessly. Current callers pass primitives only; safe.
|
||||||
|
- No "leading edge" / "trailing edge" / "max wait" knobs. Adequate for the two current consumers; if/when richer behaviour is needed, prefer `useDebouncedCallback` from a library over extending this module.
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# Module: `src/hooks/useResizablePanel.ts`
|
||||||
|
|
||||||
|
> **Source**: `src/hooks/useResizablePanel.ts` (33 lines)
|
||||||
|
> **Topo batch**: B1 (leaf)
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
React hook that backs a draggable splitter between two horizontally-arranged panels. Owns the panel width as state and exposes a mouse handler the host attaches to the splitter element.
|
||||||
|
|
||||||
|
## Public interface
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export function useResizablePanel(
|
||||||
|
initialWidth: number,
|
||||||
|
min?: number, // default 100
|
||||||
|
max?: number, // default 600
|
||||||
|
): {
|
||||||
|
width: number
|
||||||
|
onMouseDown: (e: React.MouseEvent) => void
|
||||||
|
setWidth: React.Dispatch<React.SetStateAction<number>>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`width` is the current panel width (px). `onMouseDown` should be wired to the splitter element. `setWidth` is exposed for programmatic resets — currently unused by callers but kept for parity with the persistence story (see Notes).
|
||||||
|
|
||||||
|
## Internal logic
|
||||||
|
|
||||||
|
1. `useState(initialWidth)` for `width`.
|
||||||
|
2. Drag bookkeeping is held in three `useRef` cells (`dragging`, `startX`, `startWidth`) — kept out of state so the move/up handlers never re-render the host.
|
||||||
|
3. `onMouseDown` (memoized via `useCallback([width])`) snapshots `clientX` and `width`, marks `dragging = true`, and calls `e.preventDefault()` to avoid triggering text selection.
|
||||||
|
4. A `useEffect` registers global `mousemove` / `mouseup` listeners on `window`. While `dragging.current === true`, `mousemove` updates `width` to `clamp(startWidth + (e.clientX - startX), min, max)`. `mouseup` flips `dragging` back to `false`.
|
||||||
|
5. The effect cleans up on unmount.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- **Internal**: none.
|
||||||
|
- **External**: `react` (`useState`, `useCallback`, `useRef`, `useEffect`).
|
||||||
|
|
||||||
|
## Consumers (intra-repo)
|
||||||
|
|
||||||
|
- `src/features/annotations/AnnotationsPage.tsx` — left + right panel widths.
|
||||||
|
- `src/features/dataset/DatasetPage.tsx` — left + right panel widths.
|
||||||
|
|
||||||
|
Both pages persist their layout server-side via `UserSettings.{annotationsLeftPanelWidth, annotationsRightPanelWidth, datasetLeftPanelWidth, datasetRightPanelWidth}` (see `src/types/index.ts`). The persistence read/write happens in the page, not in this hook.
|
||||||
|
|
||||||
|
## Data models
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## External integrations
|
||||||
|
|
||||||
|
DOM only — `window.addEventListener('mousemove' / 'mouseup')`.
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Notes / open questions
|
||||||
|
|
||||||
|
- The hook is keyboard- and touch-blind: only mouse drag is supported, so accessibility for non-pointer input is missing. Track for the broader a11y pass; out of scope for `/document`.
|
||||||
|
- `setWidth` is exposed but no current caller hydrates `initialWidth` from `UserSettings` via it — they pass the persisted width directly to `useResizablePanel(persistedWidth)` on mount. If the persisted width arrives **after** mount (asynchronous load), the panel jumps. Flag for the consumers' module docs (B8).
|
||||||
|
- Default bounds (100 / 600) are arbitrary; consumer pages may want different ceilings (e.g., the annotations sidebar). Currently both consumers accept the defaults.
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# Module: `src/i18n/i18n.ts`
|
||||||
|
|
||||||
|
> **Source**: `src/i18n/i18n.ts` (13 lines)
|
||||||
|
> **Topo batch**: B2 (depends only on JSON data files)
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Module-load-time initialisation of `i18next` with the `react-i18next` adapter. Imports the English and Ukrainian translation tables and registers them as the available `resources`. Exporting the configured singleton lets call sites use `useTranslation()` directly.
|
||||||
|
|
||||||
|
## Public interface
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export default i18n // configured i18next instance
|
||||||
|
```
|
||||||
|
|
||||||
|
## Internal logic
|
||||||
|
|
||||||
|
```ts
|
||||||
|
i18n.use(initReactI18next).init({
|
||||||
|
resources: { en: { translation: en }, ua: { translation: ua } },
|
||||||
|
lng: 'en',
|
||||||
|
fallbackLng: 'en',
|
||||||
|
interpolation: { escapeValue: false },
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
- `lng: 'en'` is hardcoded at boot. There is **no** runtime mechanism to detect the user's locale or to read a saved preference. Switching language at runtime works (via `i18n.changeLanguage(...)`) but the page reload always lands on English first.
|
||||||
|
- `escapeValue: false` is the documented default for React (which already escapes JSX text).
|
||||||
|
- Module side-effect at top level: `init()` runs as soon as `src/main.tsx` imports this file.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- **Internal**: `./en.json`, `./ua.json` (JSON imports — Vite handles).
|
||||||
|
- **External**: `i18next`, `react-i18next`.
|
||||||
|
|
||||||
|
## Consumers (intra-repo)
|
||||||
|
|
||||||
|
- `src/main.tsx` — side-effect import (`import './i18n/i18n'`).
|
||||||
|
- Indirectly: every component that calls `useTranslation()` from `react-i18next` (e.g. `ConfirmDialog`, `HelpModal`, `JsonEditorDialog`, …).
|
||||||
|
|
||||||
|
## Data models
|
||||||
|
|
||||||
|
The two JSON resource files (`en.json`, `ua.json`) define the key namespace. Per `_docs/ui_design/README.md` §"Localization" and `_docs/legacy/wpf-era.md` §10, the SPA must support EN + UA at parity. Inspection of the JSON files (deferred to Step 4 verification) will confirm whether parity is held.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
`lng: 'en'` and `fallbackLng: 'en'` are inlined. `localStorage` persistence (e.g., via `i18next-browser-languagedetector`) would be the natural Step 8 enhancement for "remember user choice"; deferred.
|
||||||
|
|
||||||
|
## External integrations
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
`escapeValue: false` is correct in React but would be unsafe if any consumer rendered translation values via `dangerouslySetInnerHTML`. None do (verify in Step 4).
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Notes / open questions
|
||||||
|
|
||||||
|
- The `mission-planner/` sub-project does NOT use `react-i18next` — it has its own `LanguageContext` + raw `translations` table (see `00_discovery.md` §11 finding 8). The port to `src/features/flights/` should consume this module instead. Track for MP-B3.
|
||||||
|
- Adding a third language is a one-line edit (`resources: { en, ua, <new> }`) plus a JSON file. Document the procedure in the final `solution.md`.
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# Module: `src/types/index.ts`
|
||||||
|
|
||||||
|
> **Source**: `src/types/index.ts` (161 lines)
|
||||||
|
> **Topo batch**: B1 (leaf — no internal imports)
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Shared TypeScript contract surface for the SPA: pure type aliases, interfaces, and numeric enums describing the DTOs that travel between the React UI and the suite backends (`admin/`, `annotations/`, `flights/`, `loader/`, etc.).
|
||||||
|
|
||||||
|
## Public interface
|
||||||
|
|
||||||
|
Generic shape:
|
||||||
|
|
||||||
|
| Symbol | Kind | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `PaginatedResponse<T>` | `interface` | `{ items, totalCount, page, pageSize }`. Returned by paged list endpoints (e.g., `GET /api/flights?pageSize=...`). |
|
||||||
|
|
||||||
|
Domain entities (mirroring backend DTOs):
|
||||||
|
|
||||||
|
| Symbol | Kind | Backing concept |
|
||||||
|
|---|---|---|
|
||||||
|
| `Media` | `interface` | A media file (image / video) attached to a flight. Fields: `id, name, path, mediaType, mediaStatus, duration, annotationCount, waypointId, userId`. |
|
||||||
|
| `Detection` | `interface` | A bounding box. Fields: `id, classNum, label, confidence, affiliation, combatReadiness, centerX, centerY, width, height` (normalized 0–1 coords, see `_docs/legacy/wpf-era.md` §10). |
|
||||||
|
| `AnnotationListItem` | `interface` | A frame's annotation set. `time` is `string \| null` (ISO duration), `splitTile` carries tile zoom info for split-image detections. Embeds `Detection[]`. |
|
||||||
|
| `DetectionClass` | `interface` | Admin-managed detection class metadata (`id, name, shortName, color, maxSizeM, photoMode`). Drives the Detection Classes panel + GSD validation. |
|
||||||
|
| `Flight`, `Aircraft`, `Waypoint` | `interface` | Flight feature entities. `Aircraft.type` is union `'Plane' \| 'Copter'`. |
|
||||||
|
| `SystemSettings`, `DirectorySettings`, `CameraSettings`, `UserSettings` | `interface` | Settings domain. `UserSettings` carries the global `selectedFlightId` plus per-page panel widths persisted server-side via the annotations API (see `src/components/FlightContext.tsx`). |
|
||||||
|
| `DatasetItem` | `interface` | Row in the Dataset Explorer grid (`annotationId, imageName, thumbnailPath, status, createdDate, createdEmail, flightName, source, isSeed, isSplit`). |
|
||||||
|
| `ClassDistributionItem` | `interface` | Aggregate row for the Class Distribution chart. |
|
||||||
|
| `User` | `interface` | Admin user list row. |
|
||||||
|
| `AuthUser` | `interface` | Authenticated session user (`id, email, name, role, permissions[]`); used by `AuthContext`. |
|
||||||
|
|
||||||
|
Numeric enums (must match backend wire values bit-for-bit):
|
||||||
|
|
||||||
|
| Enum | Members | Used by |
|
||||||
|
|---|---|---|
|
||||||
|
| `MediaType` | `None=0, Image=1, Video=2` | media list, video player branching |
|
||||||
|
| `MediaStatus` | `New=0, AiProcessing=1, AiProcessed=2, ManualCreated=3` | media list state |
|
||||||
|
| `AnnotationSource` | `AI=0, Manual=1` | annotation provenance |
|
||||||
|
| `AnnotationStatus` | `Created=0, Edited=1, Validated=2` | dataset filter buttons |
|
||||||
|
| `Affiliation` | `Unknown=0, Friendly=1, Hostile=2` | bounding-box icon (see `_docs/legacy/wpf-era.md` §10) |
|
||||||
|
| `CombatReadiness` | `NotReady=0, Ready=1` | bounding-box readiness dot |
|
||||||
|
|
||||||
|
## Internal logic
|
||||||
|
|
||||||
|
None. Declarations file only.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- **Internal**: none (true leaf).
|
||||||
|
- **External**: none.
|
||||||
|
|
||||||
|
## Consumers (intra-repo)
|
||||||
|
|
||||||
|
`src/auth/AuthContext.tsx`, `src/components/FlightContext.tsx`, `src/components/Header.tsx`, `src/components/DetectionClasses.tsx`, `src/api/*` (none directly — API calls are typed at call sites), every page under `src/features/*` except `src/features/login/LoginPage.tsx`. Roughly 15 importing files.
|
||||||
|
|
||||||
|
## Data models
|
||||||
|
|
||||||
|
The whole module **is** the data-model surface for the SPA. Two implicit constraints:
|
||||||
|
|
||||||
|
1. **Numeric enum values are wire-coupled** — changing them silently breaks every backend that returns the same number. Treated as a public contract; downstream changes must coordinate with the corresponding `.NET` / Cython service.
|
||||||
|
2. **`*.id` is `string`** for every entity (suite uses GUIDs), but `Detection.classNum` and the four enums are `number`. Mixed-key shapes are intentional.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## External integrations
|
||||||
|
|
||||||
|
None directly. The types are the *contract* surface for HTTP responses from `/api/admin/*`, `/api/annotations/*`, `/api/flights/*`. Ground truth for shapes lives in the respective backend submodule (`suite/admin/`, `suite/annotations/`, `suite/flights/`).
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
`AuthUser.permissions: string[]` is the only security-relevant field — checked by `AuthContext.hasPermission(perm)`. The permission strings are not enumerated here; they are defined by the `admin/` service.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Notes / open questions
|
||||||
|
|
||||||
|
- `Media.duration` is `string | null` rather than `number | null` — this matches a `TimeSpan`-style ISO duration the backend returns. Document the format precisely once the consumer (`VideoPlayer`) is reached in B7.
|
||||||
|
- `UserSettings` mixes "selected flight" with "per-page panel widths" — possibly a candidate for splitting along ownership lines, but the API endpoint (`/api/annotations/settings/user`) treats them as one document. Out of scope for `/document`.
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
{
|
||||||
|
"current_step": "complete",
|
||||||
|
"completed_steps": ["discovery", "module-analysis", "component-assembly", "module-layout", "system-synthesis", "verification-pass", "glossary-and-vision", "solution-extraction", "problem-extraction", "final-report"],
|
||||||
|
"focus_dir": null,
|
||||||
|
"scope": {
|
||||||
|
"included": ["src/", "mission-planner/"],
|
||||||
|
"excluded": ["node_modules/", "dist/", ".cursor/", "_docs/legacy/", "_docs/ui_design/", "mission-planner/public/"],
|
||||||
|
"mode": "merge"
|
||||||
|
},
|
||||||
|
"modules_total": 77,
|
||||||
|
"modules_documented_count": 77,
|
||||||
|
"modules_documented": [
|
||||||
|
"src/types/index.ts",
|
||||||
|
"src/hooks/useDebounce.ts",
|
||||||
|
"src/hooks/useResizablePanel.ts",
|
||||||
|
"src/features/annotations/classColors.ts",
|
||||||
|
"src/api/client.ts",
|
||||||
|
"src/api/sse.ts",
|
||||||
|
"src/i18n/i18n.ts",
|
||||||
|
"src/components/HelpModal.tsx",
|
||||||
|
"src/components/ConfirmDialog.tsx",
|
||||||
|
"src/components/DetectionClasses.tsx",
|
||||||
|
"src/auth/AuthContext.tsx",
|
||||||
|
"src/components/FlightContext.tsx",
|
||||||
|
"src/auth/ProtectedRoute.tsx",
|
||||||
|
"src/components/Header.tsx",
|
||||||
|
"src/features/login/LoginPage.tsx",
|
||||||
|
"src/features/admin/AdminPage.tsx",
|
||||||
|
"src/features/settings/SettingsPage.tsx",
|
||||||
|
"src/features/flights/* (15 modules: types, mapIcons, flightPlanUtils, AltitudeChart, AltitudeDialog, MiniMap, WaypointList, WindEffect, MapPoint, DrawControl, FlightListSidebar, JsonEditorDialog, FlightParamsPanel, FlightMap, FlightsPage) — consolidated into src__features__flights.md",
|
||||||
|
"src/features/annotations/* (5 modules: CanvasEditor, VideoPlayer, AnnotationsSidebar, MediaList, AnnotationsPage) — consolidated into src__features__annotations.md",
|
||||||
|
"src/features/dataset/DatasetPage.tsx",
|
||||||
|
"src/App.tsx + src/main.tsx — combined into src__App-and-main.md",
|
||||||
|
"mission-planner/* (37 modules across services/, flightPlanning/, icons/, constants/, types/, utils.ts, config.ts, main.tsx) — consolidated into mission-planner.md"
|
||||||
|
],
|
||||||
|
"modules_remaining": [],
|
||||||
|
"module_doc_files": 22,
|
||||||
|
"module_doc_total_lines": 2225,
|
||||||
|
"consolidation_decisions": {
|
||||||
|
"src/features/flights/": "single doc — was over-weighted with 8 individual docs averaging ~100 lines each; user feedback 2026-05-10",
|
||||||
|
"src/features/annotations/": "single doc — central concern but compact format",
|
||||||
|
"mission-planner/": "single doc — port-source not deployed, deletion candidate after parity"
|
||||||
|
},
|
||||||
|
"components_written": [
|
||||||
|
"00_foundation",
|
||||||
|
"01_api-transport",
|
||||||
|
"02_auth",
|
||||||
|
"03_shared-ui",
|
||||||
|
"04_login",
|
||||||
|
"05_flights",
|
||||||
|
"06_annotations",
|
||||||
|
"07_dataset",
|
||||||
|
"08_admin",
|
||||||
|
"09_settings",
|
||||||
|
"10_app-shell",
|
||||||
|
"11_class-colors"
|
||||||
|
],
|
||||||
|
"component_11_class-colors_extracted_2026-05-10": "Per user direction at the Step 2 BLOCKING gate, classColors is its own component (Layer 1 shared kernel). It is consumed by 03_shared-ui (DetectionClasses), 06_annotations, and 07_dataset. The physical file still lives at src/features/annotations/classColors.ts; the misplaced location is a Step 4 testability candidate, not a Step 2 concern.",
|
||||||
|
"component_05_flights_merge_2026-05-10": "Per user direction at the Step 2 BLOCKING gate, mission-planner/ is documented INSIDE 05_flights as the port-source for src/features/flights/. The previously drafted 11_mission-planner/ component was deleted. GPS-Denied is now an explicit sub-feature of 05_flights with two tabs (Operations + Test Mode); Test Mode is the unimplemented tlog+video→SITL→onboard flow per _docs/how_to_test.md.",
|
||||||
|
"legacy_coverage_gaps_2026-05-10": "Per user direction at the Step 2 BLOCKING gate, cross-checked the React port against suite/annotations-research/ (legacy WPF source, commit 22529c2 — see _docs/legacy/wpf-era.md). Per-component delta tables added to components/06_annotations/description.md §6b (~17 entries) and components/07_dataset/description.md §6b (~12 entries). Single-page rollup at _docs/02_document/01_legacy_coverage_gaps.md. Highest-impact gaps: (a) keyboard shortcuts entirely missing (Space/Left/Right/Enter/Del/X/M/R/K), (b) volume slider + status-bar clock + status-bar help-text missing in Annotations, (c) Class Distribution chart tab entirely missing in Dataset, (d) Sound Detections + Drone Maintenance features unmentioned in legacy doc §10 — port-or-drop decision required at Step 4.5. The 4× wide time-window (finding #6) and 16% gradient cap (finding #9) are confirmed via WPF source: WPF uses _thresholdBefore=50ms / _thresholdAfter=150ms.",
|
||||||
|
"module_layout_2026-05-10": "Step 2.5 produced _docs/02_document/module-layout.md. Status: derived-from-code. Layering: L0={00_foundation,11_class-colors}; L1={01_api-transport}; L2={02_auth,03_shared-ui}; L3={04_login,05_flights,06_annotations,07_dataset,08_admin,09_settings}; L4={10_app-shell}. 8 verification questions surfaced for user (file-move scheduling for class-colors and CanvasEditor; barrel exports; mission-planner/ ownership; cycle inside port-source; foundation multi-dir; app-shell file list; test layout TBD).",
|
||||||
|
"system_synthesis_2026-05-10": "Step 3 produced architecture.md, system-flows.md, data_model.md, deployment/{containerization,ci_cd_pipeline,environment_strategy,observability}.md. 12 system flows documented (F1-F12); F7 (video AI detect SSE) and F12 (GPS-Denied Test Mode) flagged as planned/broken. 10 ADRs recorded (SPA over SSR, REST+SSE only, HTML5 video, React Context only, Tailwind+az-* tokens, nginx /api proxy, bilingual UI, bearer-in-query SSE, mission-planner port-source, GPS-Denied Test Mode as sub-feature). Data model captures full enum drift (AnnotationStatus, MediaStatus, Affiliation, CombatReadiness, Waypoint shape) for Step 4 fix.",
|
||||||
|
"step_4_5_glossary_vision": "confirmed",
|
||||||
|
"last_updated": "2026-05-10T15:55:00+03:00",
|
||||||
|
"notes": [
|
||||||
|
"2026-05-10 02:25Z — Inline enum comments added to all numeric enum fields in parent-suite JSON examples (user feedback). Files: _docs/01_annotations.md §5, _docs/09_dataset_explorer.md §1/§2/§4. Code fences switched from `json` to `jsonc` for syntactic validity of the // comments. Column-aligned style for readability. Total numeric enum sites annotated: 11.",
|
||||||
|
"2026-05-10 02:18Z — Parent-suite-doc fixes APPLIED (cross-repo edits):",
|
||||||
|
" - /Users/obezdienie001/dev/azaion/suite/_docs/01_annotations.md: AnnotationSource enum table got Wire-value column (AI=0, Manual=1); added top-of-file 'wire format is numeric' note; §5 response example rewritten with numeric source/status/affiliation/combatReadiness + inline name comments.",
|
||||||
|
" - /Users/obezdienie001/dev/azaion/suite/_docs/09_dataset_explorer.md: AnnotationStatus table column renamed Value→Wire value; added 'wire format is numeric' note; §1 response example added isSplit:false + numeric status/source + paragraph linking isSplit to 03_detections.md §4 Tile-Based Detection; §2 response example numeric; §4 bulk-status request numeric.",
|
||||||
|
" - _docs/_process_leftovers/2026-05-10_parent-suite-doc-fixes.md DELETED (replayed).",
|
||||||
|
" - Findings #31 and #33 in src__features__annotations.md retagged [RESOLVED 2026-05-10 — PARENT-DOC FIX APPLIED]. DatasetPage finding #14 isSplit half retagged RESOLVED.",
|
||||||
|
" - Two git commits will need to land (separately) on this repo and the parent suite repo. Asking the user before committing.",
|
||||||
|
"2026-05-10 02:13Z — User decisions applied to enum-drift findings:",
|
||||||
|
" - DECISION: AnnotationStatus spec wins → UI fix (Step 4). Change src/types/index.ts to None=0, Created=10, Edited=20, Validated=30, Deleted=40.",
|
||||||
|
" - DECISION: Affiliation spec wins → UI fix (Step 4). UI must add 'None' value; integers TBD via .NET service inspection.",
|
||||||
|
" - DECISION: CombatReadiness spec wins → UI fix (Step 4). UI must add 'Unknown'; integers TBD.",
|
||||||
|
" - DECISION: MediaStatus spec wins → UI fix (Step 4). UI must add 'None', 'Confirmed', 'Error'; integers TBD.",
|
||||||
|
" - DECISION: AnnotationSource numeric (UI) wins → cross-repo PARENT-DOC FIX. Parent suite _docs/01_annotations.md §5 response example shows strings; rewrite as numerics. NO UI change. Recorded in _docs/_process_leftovers/2026-05-10_parent-suite-doc-fixes.md.",
|
||||||
|
" - DECISION: handleSave required parameters → UI fix (Step 4). Body must add Source, WaypointId; rename time→videoTime. Full target body shape now in finding #32.",
|
||||||
|
" - DECISION: DatasetItem.isSplit → cross-repo PARENT-DOC FIX. Parent suite _docs/09_dataset_explorer.md §1 response schema must add isSplit. NO UI change. Recorded in _docs/_process_leftovers/2026-05-10_parent-suite-doc-fixes.md.",
|
||||||
|
" - DECISION: X-Refresh-Token is conditionally required (long video > 1h access-token TTL per _docs/10_auth.md). UI fix (Step 4). Send for all video detect requests; flag the rotation caveat for Step 4 design.",
|
||||||
|
"Findings #27-30, #32, #34 in src__features__annotations.md retagged [STEP 4 — UI FIX]. Findings #31 and #33 retagged [NO UI CHANGE — PARENT-DOC FIX]. DatasetPage.md finding #14 split into the two cross-link halves.",
|
||||||
|
"2026-05-10 02:01Z — Consistency cross-check pass complete. Verified every claim in the 5 newly-created consolidated docs against _docs/ui_design/{README.md,annotations.html,flights.html,dataset_explorer.html} and against parent suite _docs/{00_roles_permissions, 00_top_level_architecture, 01_annotations, 02_flights, 03_detections, 09_dataset_explorer, 11_gps_denied}.md. Patches applied:",
|
||||||
|
" - PATCH src__features__annotations.md #6: time-window math corrected — implementation is ±200 ms (400 ms total), 4× wider than spec's asymmetric 50 ms+150 ms (200 ms total). Earlier draft said 'symmetric 200 ms' — wrong on both width and centring.",
|
||||||
|
" - PATCH src__features__annotations.md #9: gradient cap math corrected — `* 40` is decimal, max alpha = 0x28 = 16% opacity, not 0x40 = 25% as the wireframe demands. Spec drift is worse than originally written.",
|
||||||
|
" - PATCH src__features__annotations.md endpoint paragraph: removed the 'POST /api/detect/{mediaId} does NOT match suite' claim — it does match after nginx proxy strip. Replaced with the genuine gap: missing X-Refresh-Token header for long-running video.",
|
||||||
|
" - PATCH src__features__annotations.md added 8 new findings (#27–34) for HIGH-PRIORITY enum drift between src/types/index.ts and parent suite spec: AnnotationStatus (UI 0/1/2 vs spec 0/10/20/30 — wire payloads will be wrong), Affiliation (UI missing 'None'), CombatReadiness (UI missing 'Unknown'), MediaStatus (UI missing 'None'/'Confirmed'/'Error' — cannot render error state), AnnotationSource numeric vs spec string serialization risk, handleSave body missing Source/WaypointId, DatasetItem.isSplit not in spec response, Detect missing X-Refresh-Token header.",
|
||||||
|
" - PATCH src__features__flights.md #20: waypoint POST shape mismatch is severe — UI sends {name, latitude, longitude, order}; spec wants {Geopoint:{Lat,Lon,MGRS}, Source, Objective, OrderNum, Height}. Likely 400s on a strict server; collides with finding #19 (delete-then-recreate). Marked PRIORITY for Step 4.",
|
||||||
|
" - PATCH src__App-and-main.md: removed the 'No mobile bottom-nav route layout' finding — Header.tsx:113-129 DOES render a mobile bottom nav. Refined the role-gate finding: /admin gap is real and PRIORITY; /settings is more nuanced (no SETTINGS permission code in spec; server-enforced via 403). Renumbered findings 1-4.",
|
||||||
|
" - PATCH src__features__dataset__DatasetPage.md added finding #14: cross-link to the AnnotationStatus enum drift and DatasetItem.isSplit type-vs-spec gap (these surface as wrong status filter values on the wire).",
|
||||||
|
"ENUM DRIFT IS THE BIGGEST CROSS-CUTTING FINDING: 4 enums (AnnotationStatus, Affiliation, CombatReadiness, MediaStatus) and one type (Waypoint) have value sets and shapes that do NOT match the parent suite contract. Any wire payload using these will be wrong. Owner of fix: src/types/index.ts (single file). Step 4 will need to confirm via .NET service before patching to avoid making the UI 'right' against a server that's actually using the UI's shape. Open for Step 6.",
|
||||||
|
"(All other findings below are pre-cross-check and remain valid.)",
|
||||||
|
"Module count 77 = 40 (src/) + 37 (mission-planner/). All 77 covered across 22 doc files.",
|
||||||
|
"Doc consolidation 2026-05-10 (user feedback): 8 individual flights/* docs deleted and replaced with one src__features__flights.md; brevity standard tightened (no code blocks, ≤200 lines per doc except for two large pages — admin 215, settings 181 — pre-consolidation).",
|
||||||
|
"Findings flagged during B1+B2+B3 (carry into Step 4 verification + Step 6 security_approach):",
|
||||||
|
" - HARDCODED OPENWEATHER API KEY in src/features/flights/flightPlanUtils.ts:60 ('335799082893fad97fa36118b131f919') — committed secret, must rotate at OpenWeatherMap + remove + proxy via suite. Source-code fix scheduled for autodev Step 4. Upstream rotation is a user action (parallel track).",
|
||||||
|
" - src/features/flights/flightPlanUtils.ts: silently swallows weather errors; sequential await per segment (perf trap); ambiguous battery-capacity unit (Wh vs Ws); mixes km / m altitudes.",
|
||||||
|
" - src/features/flights/mapIcons.ts: defaultIcon CDN URL pinned to leaflet@1.7.1 while package uses 1.9.4.",
|
||||||
|
" - src/i18n/i18n.ts: lng:'en' hardcoded; no detector / persistence.",
|
||||||
|
" - src/components/HelpModal.tsx: GUIDELINES hardcoded in source instead of i18n bundle; Esc does NOT close (inconsistent with ConfirmDialog).",
|
||||||
|
" - src/features/annotations/classColors.ts: getPhotoModeSuffix duplicates the typed DetectionClass.photoMode field — likely redundant; possible deletion after typed propagation.",
|
||||||
|
" - Cross-layer imports (00_discovery.md §8): components/DetectionClasses ← features/annotations/classColors; features/dataset ← features/annotations/CanvasEditor.",
|
||||||
|
" - B3: src/auth/AuthContext.tsx bootstrap calls api.get('/api/admin/auth/refresh') WITHOUT credentials:'include' — likely a real bug. PRIORITY for Step 4.",
|
||||||
|
" - B3: src/components/DetectionClasses.tsx number-key shortcut (1–9) indexes classes[idx + photoMode] — verify ordering against the annotations/ service contract in Step 4.",
|
||||||
|
" - B3: src/components/FlightContext.tsx hardcodes pageSize=1000 ceiling; selectFlight is fire-and-forget PUT.",
|
||||||
|
" - B3: src/api/sse.ts puts bearer in query string — accepted trade-off but document in security_approach.md (Step 6); EventSource holds onto the token captured at create time and will error after a refresh-rotation, no consumer reconnects today (Step 8 hardening).",
|
||||||
|
" - B3: ConfirmDialog.tsx has no aria-modal / role=dialog — flag for Step 4 against ui_design/README.md confirmation-dialogs spec.",
|
||||||
|
"Findings flagged during B4 (carry into Step 4 + Step 6):",
|
||||||
|
" - B4: src/features/admin/AdminPage.tsx — AI Settings & GPS Settings forms render with defaultValue only; NO state, NO submit handler, the Save button does nothing. PRIORITY surface in Step 6 problem-extraction.",
|
||||||
|
" - B4: src/features/admin/AdminPage.tsx — hardcoded GPS device default '192.168.1.100' / port '5535' shipped in production bundle. Step 4.",
|
||||||
|
" - B4: src/features/admin/AdminPage.tsx — handleDeleteClass has NO ConfirmDialog despite being destructive. Flag for Step 4 vs ui_design/README.md.",
|
||||||
|
" - B4: src/features/admin/AdminPage.tsx — detection-class read uses /api/annotations/classes (annotations/ service) but write uses /api/admin/classes (admin/ service). Verify with suite ADRs in Step 3a.",
|
||||||
|
" - B4: src/features/admin/AdminPage.tsx + SettingsPage.tsx — handleToggleDefault duplicated; aircraft default is global config, page also exists in both /admin and /settings. Surface intent in Step 6.",
|
||||||
|
" - B4: src/features/admin/AdminPage.tsx — many hardcoded English strings. Step 4.",
|
||||||
|
" - B4: src/features/settings/SettingsPage.tsx — saveSystem / saveDirs lack try/finally; PUT failure leaves saving:true permanently. Step 4.",
|
||||||
|
" - B4: src/features/settings/SettingsPage.tsx — numeric inputs use parseInt(v) || 0 — clearing a field silently writes 0. Step 4.",
|
||||||
|
" - B4: src/features/settings/SettingsPage.tsx — no optimistic concurrency. Step 6 problem-extraction.",
|
||||||
|
" - B4: src/features/login/LoginPage.tsx — runUnlockSequence is theatrical (4×600ms). Document in Step 5 solution.md.",
|
||||||
|
" - B4: src/components/Header.tsx — outside-click handler always attached; flight dropdown lacks role=combobox / aria-expanded / Esc-to-close / focus trap. Step 4 + Step 8.",
|
||||||
|
" - B4: src/auth/ProtectedRoute.tsx — spinner has no role='status' / accessible label; no timeout on loading state. Joint with Step 4 client.ts timeout flag.",
|
||||||
|
"Findings consolidated into src__features__flights.md (26 numbered items): flightPlanUtils.ts security/perf/units, MiniMap licence/responsive, AltitudeDialog/JsonEditorDialog modal a11y, WaypointList drag/touch a11y, AltitudeChart bundle bloat, FlightsPage save N+M round-trips + lossy waypoint POST, GPS-Denied panel partial, mock aircraft override, etc.",
|
||||||
|
"Findings consolidated into src__features__annotations.md (26 numbered items): VideoPlayer hardcoded fps=30, CanvasEditor missing pan / wrong time-window / missing affiliation icons / missing CR indicator / dead AFFILIATION_COLORS, AnnotationsSidebar AI-detect doesn't stream progress / silent catches, AnnotationsPage no SSE detect subscription / no panel-width persistence / handleDownload tainted-canvas risk / handleSave fallback hides save loss, MediaList alert() / blob: locals ignore filter, missing keyboard shortcuts (R, V, PageUp/Down), missing Camera config side panel, missing Tile zoom for splitTile.",
|
||||||
|
"Findings consolidated into src__features__dataset__DatasetPage.md (13 numbered items): no keyboard shortcuts, no Refresh thumbnails, no virtualisation, editor tab doesn't save, magic mediaType=1, dead ConfirmDialog import, silent catches, status filter conflates None with All, classNum=0 sentinel collides with real class 0.",
|
||||||
|
"Findings consolidated into src__App-and-main.md (5 items): no role-based route guards on /admin or /settings (PRIORITY — security), no mobile bottom-nav route layout, no ErrorBoundary, no lazy chunks, /flights default for everyone."
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,656 @@
|
|||||||
|
# Azaion UI — System Flows
|
||||||
|
|
||||||
|
> Synthesis output of `/document` Step 3b. Each flow is grounded in component
|
||||||
|
> specs (`components/*/description.md`) and module docs (`modules/*.md`). When a
|
||||||
|
> step references a finding (e.g., #14), the source is one of:
|
||||||
|
> `_docs/02_document/modules/src__features__annotations.md`,
|
||||||
|
> `..__flights.md`, `..__dataset__DatasetPage.md`, or `..__App-and-main.md`.
|
||||||
|
|
||||||
|
## Flow Inventory
|
||||||
|
|
||||||
|
| # | Flow Name | Trigger | Primary Components | Criticality |
|
||||||
|
|---|-----------|---------|--------------------|-------------|
|
||||||
|
| F1 | Login | User submits `/login` form | `04_login`, `02_auth`, `admin/` | High |
|
||||||
|
| F2 | Bearer auto-refresh on 401 | Any authenticated fetch returns 401 | `02_auth`, `01_api-transport`, `admin/` | High |
|
||||||
|
| F3 | Select active flight | User picks a flight in Header dropdown | `03_shared-ui` (FlightContext + Header), `flights/` + `annotations/settings/user` | High |
|
||||||
|
| F4 | Create / save flight + waypoints | User clicks Save in `FlightsPage` | `05_flights`, `flights/` | High |
|
||||||
|
| F5 | Annotate media (manual bbox) | User drags on canvas | `06_annotations`, `annotations/` | High |
|
||||||
|
| F6 | AI Detect — image (sync) | User clicks AI Detect with image selected | `06_annotations`, `detect/` | Medium |
|
||||||
|
| F7 | AI Detect — video (async) — **NOT WIRED** | User clicks AI Detect with video selected | (target) `06_annotations`, `detect/`, `01_api-transport/sse.ts` | High |
|
||||||
|
| F8 | Dataset browse + filter | User opens `/dataset` | `07_dataset`, `annotations/` | Medium |
|
||||||
|
| F9 | Dataset bulk-validate | User selects thumbnails + Validate (planned, missing today) | `07_dataset`, `annotations/` | Medium |
|
||||||
|
| F10 | Admin: detection class CRUD | User edits in `/admin` | `08_admin`, `admin/` (write) + `annotations/` (read) | High |
|
||||||
|
| F11 | Settings: persist user prefs | User saves panel widths / system / camera | `09_settings`, `annotations/` (system/dirs/user) + `flights/` (aircraft) | Medium |
|
||||||
|
| F12 | GPS-Denied Test Mode (planned) | User uploads `.tlog` + video | `05_flights` (Test Mode tab), `gps-denied-desktop/`, `gps-denied-onboard/` | Medium (planned) |
|
||||||
|
| F13 | Live-GPS aircraft telemetry (SSE) | User opens FlightsPage with selected flight | `05_flights/FlightsPage`, `flights/` (`/api/flights/${id}/live-gps` SSE) | Medium |
|
||||||
|
| F14 | Annotation-status events (SSE) | User opens AnnotationsPage with selected media | `06_annotations/AnnotationsSidebar`, `annotations/` (`/api/annotations/annotations/events` SSE) | Medium |
|
||||||
|
|
||||||
|
## Flow Dependencies
|
||||||
|
|
||||||
|
| Flow | Depends On | Shares Data With |
|
||||||
|
|------|-----------|------------------|
|
||||||
|
| F1 | — (entry) | F2 (auth state cycles forever afterwards) |
|
||||||
|
| F2 | F1 (initial bearer must exist) | every authenticated flow |
|
||||||
|
| F3 | F1 | F4–F11 (selected flight scopes most lists) |
|
||||||
|
| F4 | F3 | F5 (annotations are scoped per media → per flight) |
|
||||||
|
| F5 | F3, media exists | F6, F7 (manual edits coexist with AI runs) |
|
||||||
|
| F6 | F3, image media exists | F5 |
|
||||||
|
| F7 | F3, video media exists | F5 |
|
||||||
|
| F8 | F3, F5/F6/F7 produced data | F9 |
|
||||||
|
| F9 | F8 | F10 (validation status feeds class metrics) |
|
||||||
|
| F10 | F1 (admin role) | F5 (class definitions are inputs to drawing) |
|
||||||
|
| F11 | F1 | F5 (panel widths are read by `useResizablePanel`, but persistence is missing today — finding #11) |
|
||||||
|
| F12 | F4 (a flight context for the test) | F7 (output detections may be cross-checked) |
|
||||||
|
| F13 | F3 (selected flight) | F4 (live-GPS overlays the planned route) |
|
||||||
|
| F14 | F5 (an annotation must exist for an event to fire) | F8 (dataset reflects status changes pushed via this stream) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Flow F1: Login
|
||||||
|
|
||||||
|
### Description
|
||||||
|
|
||||||
|
Operator submits credentials at `/login`. On success, the Admin service issues a JWT bearer (response body) and a `Secure; HttpOnly; SameSite=Strict` refresh-token cookie. `AuthContext` stores the bearer in memory; `ProtectedRoute` lets the user enter the app.
|
||||||
|
|
||||||
|
### Preconditions
|
||||||
|
|
||||||
|
- The Admin service is reachable through the nginx `/api/admin/*` proxy (or Vite dev proxy).
|
||||||
|
- The user has not yet authenticated (or their bearer + refresh cookie are both expired).
|
||||||
|
|
||||||
|
### Sequence Diagram
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
actor User
|
||||||
|
participant LoginPage as 04_login/LoginPage
|
||||||
|
participant AuthCtx as 02_auth/AuthContext
|
||||||
|
participant ApiClient as 01_api-transport/client
|
||||||
|
participant AdminApi as admin/ service
|
||||||
|
|
||||||
|
User->>LoginPage: Enter email + password, click Submit
|
||||||
|
LoginPage->>AuthCtx: login(email, password)
|
||||||
|
AuthCtx->>ApiClient: api.post('/api/admin/auth/login', {email, password})
|
||||||
|
ApiClient->>AdminApi: POST /admin/auth/login (credentials:'include')
|
||||||
|
AdminApi-->>ApiClient: 200 {bearer, user} + Set-Cookie: refresh=...
|
||||||
|
ApiClient-->>AuthCtx: {bearer, user}
|
||||||
|
AuthCtx-->>LoginPage: ok
|
||||||
|
LoginPage-->>User: Redirect to '/' (= '/flights' default)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Flowchart
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
Start([User submits login form]) --> ValidateForm{Both fields filled?}
|
||||||
|
ValidateForm -->|No| ShowFormError[Show inline error]
|
||||||
|
ValidateForm -->|Yes| Theatrical[runUnlockSequence: 4×600 ms theatrical animation — finding B4]
|
||||||
|
Theatrical --> CallApi[POST /api/admin/auth/login]
|
||||||
|
CallApi --> Resp{200 OK?}
|
||||||
|
Resp -->|Yes| StoreBearer[AuthContext stores bearer in memory]
|
||||||
|
StoreBearer --> Redirect([Navigate to '/'])
|
||||||
|
Resp -->|401| ShowAuthError[Show 'Invalid credentials']
|
||||||
|
Resp -->|5xx / network| ShowGenericError[Show 'Try again']
|
||||||
|
ShowAuthError --> Start
|
||||||
|
ShowGenericError --> Start
|
||||||
|
ShowFormError --> Start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data Flow
|
||||||
|
|
||||||
|
| Step | From | To | Data | Format |
|
||||||
|
|------|------|----|------|--------|
|
||||||
|
| 1 | User | LoginPage | email + password | form input |
|
||||||
|
| 2 | LoginPage | AuthContext | `{ email, password }` | function args |
|
||||||
|
| 3 | AuthContext | admin/ via api/client | `POST /api/admin/auth/login` | JSON body |
|
||||||
|
| 4 | admin/ | AuthContext | `{ bearer, user: AuthUser }` + Set-Cookie | JSON + cookie |
|
||||||
|
| 5 | AuthContext | LoginPage | success | promise resolution |
|
||||||
|
| 6 | LoginPage | router | `navigate('/')` | client-side redirect |
|
||||||
|
|
||||||
|
### Error Scenarios
|
||||||
|
|
||||||
|
| Error | Where | Detection | Recovery |
|
||||||
|
|-------|-------|-----------|----------|
|
||||||
|
| Invalid credentials | step 4 | 401 from admin/ | Show inline message; let user retry |
|
||||||
|
| Network failure | step 3 | fetch reject | Show generic error; let user retry |
|
||||||
|
| Theatrical animation hides server error | step 2 (theatrical waits 2.4 s) | none | UX cost — finding (B4) suggests removing the animation entirely |
|
||||||
|
| Refresh cookie blocked by browser | step 4 | next F2 immediately fails | User must log in again — accepted today |
|
||||||
|
|
||||||
|
### Performance Expectations
|
||||||
|
|
||||||
|
| Metric | Target | Notes |
|
||||||
|
|--------|--------|-------|
|
||||||
|
| End-to-end latency | < 1 s server-side + 2.4 s artificial delay | Artificial delay is the dominant cost — Step 4 fix |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Flow F2: Bearer auto-refresh on 401 (TWO refresh paths exist in code)
|
||||||
|
|
||||||
|
### Description
|
||||||
|
|
||||||
|
There are **two distinct refresh code paths** in the source — the verification pass (Step 4) caught both:
|
||||||
|
|
||||||
|
1. **Bootstrap path** — `AuthContext.tsx:24` calls `api.get('/api/admin/auth/refresh')` on app mount. This **does NOT have `credentials:'include'`** because `api/client.ts` doesn't add it on GET. Result: the cookie is not sent, the bootstrap silently fails, the user starts unauthenticated even when they have a valid refresh cookie.
|
||||||
|
2. **401-retry path** — `api/client.ts:44` calls `fetch('/api/admin/auth/refresh', { method: 'POST', credentials: 'include' })` automatically when any authenticated fetch returns 401. This path IS correct.
|
||||||
|
|
||||||
|
The bootstrap path is the bug surfaced as finding B3 PRIORITY. The 401-retry path is the silent fallback that does work but only after the user has already hit a 401.
|
||||||
|
|
||||||
|
### Preconditions
|
||||||
|
|
||||||
|
- A valid refresh-cookie exists on the browser.
|
||||||
|
- The 401 came from a request that should be retryable (idempotent or annotated as retry-safe).
|
||||||
|
|
||||||
|
### Sequence Diagram (401-retry path inside `api/client.ts` — works correctly)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Page
|
||||||
|
participant ApiClient as 01_api-transport/client
|
||||||
|
participant AdminApi as admin/ service
|
||||||
|
|
||||||
|
Page->>ApiClient: api.get('/api/...')
|
||||||
|
ApiClient->>AdminApi: GET /... (with bearer)
|
||||||
|
AdminApi-->>ApiClient: 401 (bearer expired)
|
||||||
|
ApiClient->>AdminApi: POST /admin/auth/refresh (credentials:'include')
|
||||||
|
AdminApi-->>ApiClient: 200 {bearer} + Set-Cookie: refresh=...
|
||||||
|
ApiClient->>AdminApi: GET /... (retry with new bearer)
|
||||||
|
AdminApi-->>ApiClient: 200 OK
|
||||||
|
ApiClient-->>Page: response
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sequence Diagram (Bootstrap path on app mount — broken)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant App
|
||||||
|
participant AuthCtx as 02_auth/AuthContext
|
||||||
|
participant AdminApi as admin/ service
|
||||||
|
|
||||||
|
App->>AuthCtx: <AuthProvider> mounts
|
||||||
|
AuthCtx->>AdminApi: GET /admin/auth/refresh (NO credentials:'include' — finding B3)
|
||||||
|
AdminApi-->>AuthCtx: 401 (no cookie sent)
|
||||||
|
AuthCtx->>AuthCtx: setLoading(false), user stays null
|
||||||
|
AuthCtx-->>App: ProtectedRoute sees null user → redirects to /login
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Scenarios
|
||||||
|
|
||||||
|
| Error | Where | Detection | Recovery |
|
||||||
|
|-------|-------|-----------|----------|
|
||||||
|
| Bootstrap GET refresh missing `credentials:'include'` | `AuthContext.tsx:24` | server returns 401 because cookie was not sent | **Bug today** — finding B3 PRIORITY. Symptom: a user with a valid refresh cookie still gets bounced to `/login` on every fresh tab. Step 4 fix: change to POST with `credentials:'include'` (matching the 401-retry path), or just delete the bootstrap GET and let the first authenticated fetch's 401 trigger the retry path. |
|
||||||
|
| 401-retry path | `api/client.ts:44` | works | (no fix needed) |
|
||||||
|
| Refresh cookie expired or revoked | refresh call | 401 | UI redirects to `/login`. |
|
||||||
|
| SSE subscription holds a stale bearer | active EventSource | server closes the SSE stream | No reconnect logic today (Step 8 hardening). |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Flow F3: Select active flight (CORRECTED Step 4)
|
||||||
|
|
||||||
|
### Description
|
||||||
|
|
||||||
|
Most pages are scoped to a "currently selected flight". `FlightContext` loads the flight list AND the user's stored selection on mount, then lets the user switch via the Header dropdown. The selection is **persisted as part of `UserSettings`** via the `annotations/` service — there is **no `/api/flights/select` endpoint**; this was a Step 3 documentation error caught at Step 4 verification.
|
||||||
|
|
||||||
|
### Sequence Diagram
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
actor User
|
||||||
|
participant Header as 03_shared-ui/Header
|
||||||
|
participant FlightCtx as 03_shared-ui/FlightContext
|
||||||
|
participant ApiClient
|
||||||
|
participant FlightsApi as flights/ service
|
||||||
|
participant AnnotationsApi as annotations/ service
|
||||||
|
|
||||||
|
Note over FlightCtx: On mount
|
||||||
|
FlightCtx->>FlightsApi: GET /api/flights?pageSize=1000
|
||||||
|
FlightsApi-->>FlightCtx: { items: Flight[] }
|
||||||
|
Note over FlightCtx: Flights with index > 1000 are silently dropped (finding B3)
|
||||||
|
FlightCtx->>AnnotationsApi: GET /api/annotations/settings/user
|
||||||
|
AnnotationsApi-->>FlightCtx: UserSettings { selectedFlightId }
|
||||||
|
alt selectedFlightId is set
|
||||||
|
FlightCtx->>FlightsApi: GET /api/flights/{selectedFlightId}
|
||||||
|
FlightsApi-->>FlightCtx: Flight
|
||||||
|
end
|
||||||
|
|
||||||
|
User->>Header: Click flight in dropdown
|
||||||
|
Header->>FlightCtx: selectFlight(flight)
|
||||||
|
FlightCtx->>FlightCtx: setSelectedFlight(flight) immediately (optimistic)
|
||||||
|
FlightCtx->>AnnotationsApi: PUT /api/annotations/settings/user { selectedFlightId }
|
||||||
|
Note over FlightCtx,AnnotationsApi: Fire-and-forget — error swallowed (finding B3)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Scenarios
|
||||||
|
|
||||||
|
| Error | Where | Detection | Recovery |
|
||||||
|
|-------|-------|-----------|----------|
|
||||||
|
| Flights list > 1000 | initial load | not detected | **Silent ceiling** — finding B3. Step 4 fix: paginate or stream. |
|
||||||
|
| `selectFlight` PUT fails | step | none — fire-and-forget | UI keeps the local selection but the server does not — next reload reverts it. Step 4 fix. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Flow F4: Create / save flight + waypoints
|
||||||
|
|
||||||
|
### Description
|
||||||
|
|
||||||
|
User edits a flight in `FlightsPage`: drags waypoints on the map, edits altitudes, types parameters. On Save, the UI sends all changes to the `flights/` service.
|
||||||
|
|
||||||
|
### Sequence Diagram (current implementation — lossy)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
actor User
|
||||||
|
participant FlightsPage as 05_flights/FlightsPage
|
||||||
|
participant ApiClient
|
||||||
|
participant FlightsApi as flights/ service
|
||||||
|
|
||||||
|
User->>FlightsPage: Drag waypoints, edit fields, click Save
|
||||||
|
FlightsPage->>ApiClient: api.put('/api/flights/{id}', {name, aircraftId, ...})
|
||||||
|
ApiClient->>FlightsApi: PUT /flights/{id}
|
||||||
|
FlightsApi-->>FlightsPage: 200
|
||||||
|
Note over FlightsPage,FlightsApi: Then a delete-then-recreate cycle for waypoints (finding #19)
|
||||||
|
FlightsPage->>FlightsApi: DELETE /flights/{id}/waypoints (loop or bulk)
|
||||||
|
loop for each new waypoint
|
||||||
|
FlightsPage->>FlightsApi: POST /flights/{id}/waypoints {name, lat, lng, order}
|
||||||
|
end
|
||||||
|
Note over FlightsPage,FlightsApi: But the suite spec wants {Geopoint:{Lat,Lon,MGRS}, Source, Objective, OrderNum, Height} — finding #20 (server may return 400)
|
||||||
|
FlightsApi-->>FlightsPage: per-waypoint result
|
||||||
|
FlightsPage-->>User: Saved (or partial-failure UI not built today)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Scenarios
|
||||||
|
|
||||||
|
| Error | Where | Detection | Recovery |
|
||||||
|
|-------|-------|-----------|----------|
|
||||||
|
| Waypoint shape mismatch (UI vs spec, finding #20) | waypoint POST | server 400 | UI does not surface server errors well today — Step 4 priority |
|
||||||
|
| Partial failure (some waypoints created, some failed) | step (per-waypoint loop) | server returns mixed status | UI does not detect — Step 4 priority |
|
||||||
|
| Network drop mid-save | any | timeout | UI does not retry — Step 4 priority |
|
||||||
|
|
||||||
|
### Performance Expectations
|
||||||
|
|
||||||
|
| Metric | Target | Notes |
|
||||||
|
|--------|--------|-------|
|
||||||
|
| Save latency | < 1 s for ≤ 50 waypoints | Currently degrades with the N+M round-trip pattern (1 PUT + 1 DELETE + N POSTs). Bulk endpoint is a Step 5 solution candidate. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Flow F5: Annotate media (manual bbox)
|
||||||
|
|
||||||
|
### Description
|
||||||
|
|
||||||
|
User drags on the canvas to draw a bounding box, picks a class via 1–9 keyboard or DetectionClasses strip, and saves. The annotation persists to the `annotations/` service.
|
||||||
|
|
||||||
|
### Sequence Diagram
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
actor User
|
||||||
|
participant CanvasEditor as 06_annotations/CanvasEditor
|
||||||
|
participant Sidebar as 06_annotations/AnnotationsSidebar
|
||||||
|
participant Page as 06_annotations/AnnotationsPage
|
||||||
|
participant ApiClient
|
||||||
|
participant AnnotationsApi as annotations/ service
|
||||||
|
|
||||||
|
User->>CanvasEditor: Mouse drag (draw bbox)
|
||||||
|
User->>CanvasEditor: Press [1]–[9] (pick class)
|
||||||
|
CanvasEditor-->>Sidebar: New detection added (in-memory)
|
||||||
|
User->>Page: Click Save
|
||||||
|
Page->>ApiClient: api.post('/api/annotations/annotations', body)
|
||||||
|
Note over Page,ApiClient: Endpoint is doubly-prefixed (suite-service + resource path)
|
||||||
|
Note over Page,ApiClient: handleSave body today: {classNum, x, y, w, h, time} — finding #32
|
||||||
|
Note over Page,ApiClient: Spec wants: {classNum, x, y, w, h, videoTime, Source, WaypointId} — Step 4
|
||||||
|
ApiClient->>AnnotationsApi: POST /annotations/annotations
|
||||||
|
AnnotationsApi-->>Page: 200 {id}
|
||||||
|
Page-->>User: Saved
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Scenarios
|
||||||
|
|
||||||
|
| Error | Where | Detection | Recovery |
|
||||||
|
|-------|-------|-----------|----------|
|
||||||
|
| Save body shape wrong (finding #32) | save call | server 400 | Today the UI logs `console.error` and the user sees nothing — Step 4 priority |
|
||||||
|
| Time-window math (50/150 ms vs implementation 200/200, finding #6) | overlay render | wrong annotations show during playback | Step 4 fix |
|
||||||
|
| Gradient cap (16 % vs 25 % spec, finding #9) | sidebar render | visual drift only | Step 4 fix |
|
||||||
|
| Cross-origin video tainted canvas (finding #12) | export image | browser CSP error | Step 4 fix or Step 5 design (server-side render) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Flow F6: AI Detect — image (sync)
|
||||||
|
|
||||||
|
### Description
|
||||||
|
|
||||||
|
User clicks AI Detect with an image selected. The detect service runs YOLO inference synchronously and returns detections inline.
|
||||||
|
|
||||||
|
### Sequence Diagram
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
actor User
|
||||||
|
participant Sidebar as 06_annotations/AnnotationsSidebar
|
||||||
|
participant ApiClient
|
||||||
|
participant DetectApi as detect/ service
|
||||||
|
participant AnnotationsApi as annotations/ service
|
||||||
|
|
||||||
|
User->>Sidebar: Click AI Detect (image selected)
|
||||||
|
Sidebar->>ApiClient: api.post('/api/detect/{mediaId}')
|
||||||
|
ApiClient->>DetectApi: POST /detect/{mediaId}
|
||||||
|
DetectApi-->>ApiClient: 200 {detections[]}
|
||||||
|
ApiClient-->>Sidebar: detections
|
||||||
|
Note over Sidebar,AnnotationsApi: Detections may auto-persist (server-side) or require client save — verify in Step 4
|
||||||
|
Sidebar-->>User: Render detections on canvas
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Scenarios
|
||||||
|
|
||||||
|
| Error | Where | Detection | Recovery |
|
||||||
|
|-------|-------|-----------|----------|
|
||||||
|
| Inference timeout | step | fetch timeout | None today — finding #21 (errors silently `console.error`'d) |
|
||||||
|
| Class not in admin list | render | `getClassNameFallback` falls back to `#<n>` | Acceptable; covered by `11_class-colors` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Flow F7: AI Detect — video (async, with SSE) — NOT WIRED TODAY
|
||||||
|
|
||||||
|
### Description (target — what should exist)
|
||||||
|
|
||||||
|
User clicks AI Detect with a video. The detect service kicks off an async job and returns a job ID. The UI subscribes to `GET /api/detect/stream/{jobId}` (SSE) for progress + final results.
|
||||||
|
|
||||||
|
### What's actually in code (Step 4 verification)
|
||||||
|
|
||||||
|
The async-video flow is **completely absent** from the codebase. `AnnotationsSidebar.tsx:39` only does `POST /api/detect/${media.id}` — the same endpoint, regardless of whether the media is an image or a video. There are NO calls to `/api/detect/video/...` and NO EventSource subscriptions to `/api/detect/stream/...`. The only annotation-related SSE in the codebase is `createSSE('/api/annotations/annotations/events', ...)` in the same file (line 25), which streams annotation-**status** events, not detect progress.
|
||||||
|
|
||||||
|
The previously cited finding #21 ("AI-detect doesn't stream progress") is therefore stronger than originally documented: the async path does not exist at all. The fix in Step 4 must build:
|
||||||
|
- `POST /api/detect/video/${mediaId}` request (with `X-Refresh-Token` header, finding #30)
|
||||||
|
- `createSSE('/api/detect/stream/${jobId}', ...)` subscription
|
||||||
|
- progress UI in AnnotationsSidebar.
|
||||||
|
|
||||||
|
### Sequence Diagram (target — what should happen)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
actor User
|
||||||
|
participant Sidebar as 06_annotations/AnnotationsSidebar
|
||||||
|
participant Page as 06_annotations/AnnotationsPage
|
||||||
|
participant ApiClient
|
||||||
|
participant SseClient as 01_api-transport/sse
|
||||||
|
participant DetectApi as detect/ service
|
||||||
|
|
||||||
|
User->>Sidebar: Click AI Detect (video selected)
|
||||||
|
Sidebar->>ApiClient: api.post('/api/detect/video/{mediaId}', {}, headers={X-Refresh-Token})
|
||||||
|
ApiClient->>DetectApi: POST /detect/video/{mediaId}
|
||||||
|
DetectApi-->>ApiClient: 202 {jobId}
|
||||||
|
Page->>SseClient: subscribeSSE(`/api/detect/stream/${jobId}?token=...`)
|
||||||
|
SseClient-->>DetectApi: GET /detect/stream/{jobId}?token=... (EventSource)
|
||||||
|
loop progress events
|
||||||
|
DetectApi-->>SseClient: data: {progress: 0..100}
|
||||||
|
SseClient-->>Page: onMessage(progress)
|
||||||
|
Page-->>User: Update progress bar
|
||||||
|
end
|
||||||
|
DetectApi-->>SseClient: data: {detections[], status: 'done'}
|
||||||
|
SseClient-->>Page: onMessage(final)
|
||||||
|
Page-->>User: Render detections; close stream
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Scenarios
|
||||||
|
|
||||||
|
| Error | Where | Detection | Recovery |
|
||||||
|
|-------|-------|-----------|----------|
|
||||||
|
| No SSE subscription today (finding #10) | step | user sees "AI Detect" go silent forever | Build the subscription — Step 4 PRIORITY |
|
||||||
|
| Bearer expires mid-job (no `X-Refresh-Token`, finding #30) | server side | job aborts | Send `X-Refresh-Token` so server can rotate transparently — Step 4 |
|
||||||
|
| EventSource holds stale token (finding cross-link) | refresh-rotation occurs while subscribed | server closes stream | Reconnect with new token — Step 8 hardening |
|
||||||
|
| Network blip | mid-stream | `onerror` | Reconnect with backoff — Step 8 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Flow F8: Dataset browse + filter
|
||||||
|
|
||||||
|
### Description
|
||||||
|
|
||||||
|
User opens `/dataset`, filters by class / status / search; thumbnails appear in a grid.
|
||||||
|
|
||||||
|
### Sequence Diagram
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
actor User
|
||||||
|
participant DatasetPage as 07_dataset/DatasetPage
|
||||||
|
participant ApiClient
|
||||||
|
participant AnnotationsApi as annotations/ service
|
||||||
|
|
||||||
|
User->>DatasetPage: Type in filter
|
||||||
|
Note over DatasetPage: 300 ms debounce via useDebounce
|
||||||
|
DatasetPage->>ApiClient: api.get('/api/annotations/dataset?status=...&classNum=...&q=...')
|
||||||
|
ApiClient->>AnnotationsApi: GET ...
|
||||||
|
AnnotationsApi-->>DatasetPage: PaginatedResponse<DatasetItem>
|
||||||
|
DatasetPage-->>User: Render thumbnails (NOT virtualised — finding #3)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Scenarios
|
||||||
|
|
||||||
|
| Error | Where | Detection | Recovery |
|
||||||
|
|-------|-------|-----------|----------|
|
||||||
|
| Status filter sentinels collide (finding #8) | client query build | wrong items returned | Step 4 fix after enum-drift resolution |
|
||||||
|
| `classNum=0` collides with real class 0 (finding #9) | client query build | class 0 selectable but matches "All" | Step 4 fix |
|
||||||
|
| Long lists render all thumbnails (finding #3) | render | UI lag | Add virtualisation — Step 4 / Step 8 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Flow F9: Dataset bulk-validate (Step 4 CORRECTED — partially wired)
|
||||||
|
|
||||||
|
### Description
|
||||||
|
|
||||||
|
User multi-selects thumbnails (Ctrl+click) and clicks the **Validate** button — `DatasetPage.tsx:142-146` only renders this button when `selectedIds.size > 0`, so it's discoverable. On click, the page POSTs `/api/annotations/dataset/bulk-status` with `{annotationIds, status: AnnotationStatus.Validated}`. The selection is then cleared and the items re-fetched.
|
||||||
|
|
||||||
|
**The earlier doc draft claimed this was missing**. It's not; only the **`[V]` keyboard shortcut** is missing (finding #1 — keyboard shortcuts as a category).
|
||||||
|
|
||||||
|
### Sequence Diagram (actual)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
actor User
|
||||||
|
participant DatasetPage as 07_dataset/DatasetPage
|
||||||
|
participant ApiClient
|
||||||
|
participant AnnotationsApi as annotations/ service
|
||||||
|
|
||||||
|
User->>DatasetPage: Ctrl+click thumbnails (build selectedIds)
|
||||||
|
User->>DatasetPage: Click Validate button (visible when selectedIds.size > 0)
|
||||||
|
DatasetPage->>ApiClient: api.post('/api/annotations/dataset/bulk-status', {annotationIds, status: AnnotationStatus.Validated})
|
||||||
|
ApiClient->>AnnotationsApi: POST /dataset/bulk-status
|
||||||
|
AnnotationsApi-->>DatasetPage: 200
|
||||||
|
DatasetPage->>DatasetPage: setSelectedIds(new Set()); fetchItems()
|
||||||
|
DatasetPage-->>User: Refresh visible thumbnails (status badge updated)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Scenarios
|
||||||
|
|
||||||
|
| Error | Where | Detection | Recovery |
|
||||||
|
|-------|-------|-----------|----------|
|
||||||
|
| `[V]` keyboard shortcut missing | finding #1 (Dataset) | only mouse click works | Step 4 — add `useEffect` keydown listener on the page when `tab === 'annotations'` |
|
||||||
|
| `bulk-status` request body uses string instead of numeric (finding #11) | server payload | server may reject | Step 4 — verify the suite spec then fix the client side to match |
|
||||||
|
| Bulk action fails partway | server | `handleValidate` has no try/catch | Step 4 — surface a toast, keep selection so the user can retry |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Flow F10: Admin — detection class CRUD
|
||||||
|
|
||||||
|
### Description
|
||||||
|
|
||||||
|
Admin user opens `/admin`, edits detection classes (color, name, photoMode, maxSizeM). Deletes are destructive but currently lack a confirm dialog (finding B4). Read uses `annotations/classes`; write uses `admin/classes` (finding B4 — verify with suite ADRs).
|
||||||
|
|
||||||
|
### Sequence Diagram (Step 4 corrected — no PUT/edit endpoint exists)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
actor Admin
|
||||||
|
participant AdminPage as 08_admin/AdminPage
|
||||||
|
participant ApiClient
|
||||||
|
participant AnnotationsApi as annotations/ service
|
||||||
|
participant AdminApi as admin/ service
|
||||||
|
|
||||||
|
AdminPage->>ApiClient: api.get('/api/annotations/classes')
|
||||||
|
ApiClient->>AnnotationsApi: GET /classes
|
||||||
|
AnnotationsApi-->>AdminPage: DetectionClass[]
|
||||||
|
Admin->>AdminPage: Click "Add Class"
|
||||||
|
AdminPage->>ApiClient: api.post('/api/admin/classes', newClass)
|
||||||
|
ApiClient->>AdminApi: POST /classes
|
||||||
|
AdminApi-->>AdminPage: 200
|
||||||
|
AdminPage->>ApiClient: api.get('/api/annotations/classes') (re-read)
|
||||||
|
Admin->>AdminPage: Click Delete
|
||||||
|
Note over AdminPage: NO ConfirmDialog today (finding B4) — destructive action goes through immediately
|
||||||
|
AdminPage->>ApiClient: api.delete('/api/admin/classes/{id}')
|
||||||
|
ApiClient->>AdminApi: DELETE /classes/{id}
|
||||||
|
AdminApi-->>AdminPage: 200
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Step 4 verification finding**: there is no edit-class endpoint in the codebase today. Admins can only **add** new classes and **delete** existing ones — they cannot modify an existing class's color / name / maxSizeM / photoMode. This is a real gap, not just a documentation drift; the WPF era supported in-place edits via the same DataGrid. Surface for Step 4 (decide: add a `PATCH /api/admin/classes/{id}` and a UI form, or accept add-and-delete as the only mutation path).
|
||||||
|
|
||||||
|
### Error Scenarios
|
||||||
|
|
||||||
|
| Error | Where | Detection | Recovery |
|
||||||
|
|-------|-------|-----------|----------|
|
||||||
|
| `/admin` route lacks RBAC gate | route entry | non-admin can browse | **Security PRIORITY** (App+main finding #1) — Step 4 |
|
||||||
|
| AI Settings + GPS Settings forms don't save (finding B4 PRIORITY) | save click | nothing happens | Step 4 PRIORITY |
|
||||||
|
| Hardcoded GPS device defaults (`192.168.1.100:5535`) shipped to prod (finding B4) | bundle | leaks internal network shape | Step 4 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Flow F11: Settings — persist user prefs (Step 4 CORRECTED)
|
||||||
|
|
||||||
|
### Description
|
||||||
|
|
||||||
|
User edits System / Directory / Camera / User settings, clicks Save. **PUTs go to `annotations/` (`/api/annotations/settings/{system,directories,user}`) — NOT `admin/` as initially drafted.** The aircraft default-toggle hits `flights/` (`PATCH /api/flights/aircrafts/${id}`). The "settings" name is a misnomer — what the UI calls "settings" is split across two services.
|
||||||
|
|
||||||
|
Panel widths (annotations / dataset, left / right) are typed fields in `UserSettings` (`00_foundation/types/index.ts`) but **`useResizablePanel` does not write them back today** — finding #11. The wire endpoint exists; the client wiring is the gap.
|
||||||
|
|
||||||
|
### Error Scenarios
|
||||||
|
|
||||||
|
| Error | Where | Detection | Recovery |
|
||||||
|
|-------|-------|-----------|----------|
|
||||||
|
| `saveSystem` / `saveDirs` lack try/finally (finding B4) | PUT failure | `saving:true` stays forever | Step 4 fix |
|
||||||
|
| Numeric inputs `parseInt(v) ‖ 0` (finding B4) | input clear | silent zero write | Step 4 fix |
|
||||||
|
| No optimistic concurrency (finding B4) | concurrent admin edits | last-write-wins, no conflict UI | Step 6 problem-extraction surface |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Flow F12: GPS-Denied Test Mode (planned, not implemented)
|
||||||
|
|
||||||
|
### Description (target — per `_docs/how_to_test.md`)
|
||||||
|
|
||||||
|
The operator wants to validate the GPS-Denied onboard system without a real flight. They upload a `.tlog` file + a synced video (or one that the system will auto-sync via IMU analysis). The system feeds the simulated frames + IMU/GPS into the SITL pipeline and the onboard service consumes them as if they were live.
|
||||||
|
|
||||||
|
### Sequence Diagram (target)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
actor Operator
|
||||||
|
participant TestMode as 05_flights/GPS-Denied/Test Mode (planned)
|
||||||
|
participant ApiClient
|
||||||
|
participant GpsDeskApi as gps-denied-desktop/ service
|
||||||
|
participant GpsBoardApi as gps-denied-onboard/ service
|
||||||
|
|
||||||
|
Operator->>TestMode: Upload tlog + video (drag-drop)
|
||||||
|
TestMode->>ApiClient: api.post('/api/gps-denied-desktop/test/sync', files)
|
||||||
|
ApiClient->>GpsDeskApi: POST /test/sync (multipart)
|
||||||
|
GpsDeskApi-->>TestMode: {sessionId, syncOffset, imuChart}
|
||||||
|
Note over GpsDeskApi: Server-side: extract timestamps + IMU + GPS from tlog,<br/>auto-sync video by detecting takeoff in IMU + video duration
|
||||||
|
|
||||||
|
Operator->>TestMode: Confirm sync, click Start SITL
|
||||||
|
TestMode->>ApiClient: api.post('/api/gps-denied-desktop/test/run', {sessionId})
|
||||||
|
ApiClient->>GpsDeskApi: POST /test/run
|
||||||
|
GpsDeskApi->>GpsBoardApi: stream IMU + frames (in-cluster)
|
||||||
|
GpsBoardApi-->>GpsDeskApi: positioning estimates
|
||||||
|
GpsDeskApi-->>TestMode: SSE stream of positioning estimates
|
||||||
|
TestMode-->>Operator: Render trajectory overlay + comparison chart
|
||||||
|
```
|
||||||
|
|
||||||
|
### Preconditions
|
||||||
|
|
||||||
|
- A flight context exists (Test Mode is scoped within a flight).
|
||||||
|
- The `.tlog` and video are valid; the IMU chart in the tlog is "drone-on-ground → take-off → land" so auto-sync can find the take-off event.
|
||||||
|
|
||||||
|
### Open architectural questions for Test Mode
|
||||||
|
|
||||||
|
1. Where does the IMU-based sync analysis live — in `gps-denied-desktop/`, in a new Cython worker, or client-side?
|
||||||
|
2. Is SITL state persisted across page reloads (= server-side session), or is it in-memory client-side only?
|
||||||
|
3. Output rendering — overlay on the GPS-Denied tab's existing map, or a dedicated comparison chart view?
|
||||||
|
|
||||||
|
These belong in Step 4.5 (Architecture Vision); Test Mode is in this flow inventory only as a **planned** flow so downstream consumers don't lose it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Flow F13: Live-GPS aircraft telemetry (SSE) — added at Step 4
|
||||||
|
|
||||||
|
### Description
|
||||||
|
|
||||||
|
When the user has a flight selected and FlightsPage is open, the page subscribes to a per-flight live-GPS event stream so the map can render the aircraft's current position in real time. Discovered at Step 4 verification (`FlightsPage.tsx:67`).
|
||||||
|
|
||||||
|
### Sequence Diagram
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
actor User
|
||||||
|
participant FlightsPage as 05_flights/FlightsPage
|
||||||
|
participant SseClient as 01_api-transport/sse
|
||||||
|
participant FlightsApi as flights/ service
|
||||||
|
|
||||||
|
User->>FlightsPage: Select a flight (Header dropdown)
|
||||||
|
FlightsPage->>SseClient: createSSE(`/api/flights/${flightId}/live-gps`, onEvent)
|
||||||
|
SseClient-->>FlightsApi: GET /api/flights/{flightId}/live-gps?token=... (EventSource)
|
||||||
|
loop while flight is selected
|
||||||
|
FlightsApi-->>SseClient: data: { lat, lon, satellites, status }
|
||||||
|
SseClient-->>FlightsPage: onEvent(payload)
|
||||||
|
FlightsPage-->>User: Update aircraft marker on map
|
||||||
|
end
|
||||||
|
User->>FlightsPage: Switch flight or navigate away
|
||||||
|
FlightsPage->>SseClient: source.close()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Scenarios
|
||||||
|
|
||||||
|
| Error | Where | Detection | Recovery |
|
||||||
|
|-------|-------|-----------|----------|
|
||||||
|
| Bearer expires mid-stream | server | server closes the stream | EventSource auto-reconnects with **stale token** — same Step 8 hardening as F7 |
|
||||||
|
| Aircraft offline | data shape | `status: 'offline'` payload | UI should grey out the marker — verify in Step 4 |
|
||||||
|
| User switches flights rapidly | client | new `createSSE` before old `.close()` | Memory leak risk — verify cleanup in Step 4 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Flow F14: Annotation-status events (SSE) — added at Step 4
|
||||||
|
|
||||||
|
### Description
|
||||||
|
|
||||||
|
When the user opens AnnotationsPage with media selected, `AnnotationsSidebar.tsx:25` subscribes to `/api/annotations/annotations/events` to receive **annotation-status events** (created / edited / validated). This is the SSE that exists today; it is **NOT** the detect-progress SSE (F7), which doesn't exist yet.
|
||||||
|
|
||||||
|
### Sequence Diagram
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
actor User
|
||||||
|
participant Sidebar as 06_annotations/AnnotationsSidebar
|
||||||
|
participant SseClient as 01_api-transport/sse
|
||||||
|
participant AnnotationsApi as annotations/ service
|
||||||
|
|
||||||
|
User->>Sidebar: Open AnnotationsPage (media selected)
|
||||||
|
Sidebar->>SseClient: createSSE('/api/annotations/annotations/events', onEvent)
|
||||||
|
SseClient-->>AnnotationsApi: GET /api/annotations/annotations/events?token=... (EventSource)
|
||||||
|
loop while page mounted
|
||||||
|
AnnotationsApi-->>SseClient: data: { annotationId, mediaId, status }
|
||||||
|
SseClient-->>Sidebar: onEvent(payload)
|
||||||
|
Sidebar->>Sidebar: refetch annotations for current media
|
||||||
|
end
|
||||||
|
User->>Sidebar: Navigate away
|
||||||
|
Sidebar->>SseClient: source.close()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- The stream is **not scoped per media** — every connected user receives all annotation-status events. Filtering happens client-side (`event.mediaId === selectedMedia.id`). Acceptable for low-volume admin use; flag as a Step 6 problem-extraction surface for scale.
|
||||||
|
- Bearer-rotation issue applies here too (Step 8 hardening).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Mermaid Diagram Conventions
|
||||||
|
|
||||||
|
- **Participants**: matched to component IDs `NN_name` (e.g., `04_login`, `02_auth`, `01_api-transport`).
|
||||||
|
- **External services**: named after their suite-service folder (`admin/`, `flights/`, `annotations/`, `detect/`, `gps-denied-desktop/`, `gps-denied-onboard/`).
|
||||||
|
- **Decision nodes**: `{Question?}`.
|
||||||
|
- **Start/End**: `([label])` stadium shape.
|
||||||
|
- **No styling** — let the renderer theme handle it.
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,212 @@
|
|||||||
|
# Test Environment
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
**System under test**: the Azaion UI single-page application — a React 19 + Vite 6 static bundle served by `nginx:alpine` (port 80) that talks to the parent suite microservices through the same nginx instance (reverse-proxied `/api/<service>/` routes per `nginx.conf`). The SPA's observable surface is everything reachable from a browser: outgoing HTTP requests (URL, method, headers, body), incoming responses (rendered DOM, console, errors), outgoing EventSource streams, browser storage (`localStorage` / `sessionStorage` / `document.cookie`), and the built artifact (`dist/`).
|
||||||
|
|
||||||
|
**Consumer app purpose**: the test runners are the consumer. They drive a real browser (or jsdom) at the SPA's public surface, capture every outbound request/event, and assert against `_docs/00_problem/input_data/expected_results/results_report.md` (95 rows).
|
||||||
|
|
||||||
|
**Black-box discipline**: the consumer MUST NOT import from `src/` (except for the typed enum shapes that ARE part of the wire contract per `P9`), MUST NOT bypass the React tree to call internal hooks, and MUST NOT inspect React component state directly. Assertions are made on the rendered DOM, ARIA roles, outgoing network activity, EventSource state machine, console output, and built artifacts.
|
||||||
|
|
||||||
|
## Test Execution Profiles
|
||||||
|
|
||||||
|
Two profiles share the artifact directory but address different black-box levels. Runner selection is deferred to the Decompose Tests step (`autodev` Step 5) — this document specifies the **environment requirements**, not the runner choice.
|
||||||
|
|
||||||
|
| Profile | Scope | Black-box level | Backing services | Browser |
|
||||||
|
|---------|-------|----------------|------------------|---------|
|
||||||
|
| `fast` | Unit + component + static checks | DOM + network requests issued by a mounted component or by a code-level helper, captured at the `fetch` / `EventSource` boundary | Stubbed (request interception layer, e.g. MSW or equivalent). No real services. | jsdom or headless Chromium (component renderer). |
|
||||||
|
| `e2e` | Browser smoke + cross-service flows | Real browser → real nginx (UI image) → real suite docker-compose stack | Full suite docker-compose stack (`admin/`, `flights/`, `annotations/`, `detect/`, `gps-denied-desktop/`, `gps-denied-onboard/`, `autopilot/`, `resource/`, `loader/`). | Headless Chromium + Firefox latest 2 versions per AC-18. |
|
||||||
|
| `static` | Source / config / bundle checks | The repo + the `dist/` artifact | None (no runtime). | None (CLI). |
|
||||||
|
|
||||||
|
Tests in `blackbox-tests.md`, `performance-tests.md`, etc. tag themselves with `Profile: fast | e2e | static` to make runner routing unambiguous.
|
||||||
|
|
||||||
|
## Docker Environment
|
||||||
|
|
||||||
|
The Azaion UI image carries no DB. The "Docker environment" is the test-time choreography of UI + suite services + stubs.
|
||||||
|
|
||||||
|
### Services (e2e profile)
|
||||||
|
|
||||||
|
| Service | Image / Build | Purpose | Ports |
|
||||||
|
|---------|--------------|---------|-------|
|
||||||
|
| `azaion-ui` | Built from this repo (`Dockerfile`, ARM64 per H1 / S5) — final stage `nginx:alpine` | The SPA under test | `80` |
|
||||||
|
| `admin` | Suite `admin/` image (auth + users + classes write + GPS settings) | Auth + RBAC; cookie issuer per E3 | per suite compose |
|
||||||
|
| `flights` | Suite `flights/` image | Flight CRUD + waypoints + aircraft + live-GPS SSE | per suite compose |
|
||||||
|
| `annotations` | Suite `annotations/` image | Media + annotations + dataset + class read + settings + status SSE | per suite compose |
|
||||||
|
| `detect` | Suite `detect/` image | Sync image detect (and future async video detect F7) | per suite compose |
|
||||||
|
| `gps-denied-desktop`, `gps-denied-onboard`, `autopilot`, `resource`, `loader` | Suite microservice images | Auxiliary services hit by the SPA (only `loader/` and `resource/` are hit on production paths today; `gps-denied-*` is target-only F12) | per suite compose |
|
||||||
|
| `owm-stub` | Tiny HTTP server returning canned OpenWeatherMap responses | Replace direct OWM HTTPS (E10) so tests are deterministic and rate-limit-free | `8081` |
|
||||||
|
| `tile-stub` | Tiny HTTP server returning a 256x256 PNG | Replace OSM tile servers | `8082` |
|
||||||
|
| `test-db` | Suite-managed (Postgres per suite default) | Backs `admin/`, `flights/`, `annotations/` | Internal |
|
||||||
|
|
||||||
|
### Networks
|
||||||
|
|
||||||
|
| Network | Services | Purpose |
|
||||||
|
|---------|----------|---------|
|
||||||
|
| `azaion-test-net` | all of the above | Isolated test network; no internet egress (OWM + tile stubs replace the only external hops). |
|
||||||
|
|
||||||
|
### Volumes
|
||||||
|
|
||||||
|
| Volume | Mounted to | Purpose |
|
||||||
|
|--------|-----------|---------|
|
||||||
|
| `test-db-data` | `test-db:/var/lib/postgresql/data` | Suite DB persistence — wiped between e2e runs (see Data Isolation below). |
|
||||||
|
| `seed-fixtures` | `admin:/seed`, `flights:/seed`, `annotations:/seed` (read-only) | Bootstrap data loaded at service start (users, flights, classes, sample media). See `test-data.md`. |
|
||||||
|
| `test-output` | `playwright-runner:/output` | Where the consumer writes CSV reports, screenshots, traces. |
|
||||||
|
|
||||||
|
### docker-compose structure (outline)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
azaion-ui:
|
||||||
|
build: .
|
||||||
|
ports: ["80:80"]
|
||||||
|
depends_on: [admin, flights, annotations, detect]
|
||||||
|
environment:
|
||||||
|
AZAION_REVISION: ${CI_COMMIT_SHA:-test}
|
||||||
|
|
||||||
|
admin:
|
||||||
|
image: azaion/admin:test
|
||||||
|
depends_on: [test-db]
|
||||||
|
|
||||||
|
flights:
|
||||||
|
image: azaion/flights:test
|
||||||
|
depends_on: [test-db]
|
||||||
|
|
||||||
|
annotations:
|
||||||
|
image: azaion/annotations:test
|
||||||
|
depends_on: [test-db]
|
||||||
|
|
||||||
|
detect:
|
||||||
|
image: azaion/detect:test
|
||||||
|
|
||||||
|
test-db:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
|
||||||
|
owm-stub:
|
||||||
|
build: ./testing/stubs/owm
|
||||||
|
tile-stub:
|
||||||
|
build: ./testing/stubs/tile
|
||||||
|
|
||||||
|
playwright-runner:
|
||||||
|
build: ./testing/runner
|
||||||
|
depends_on: [azaion-ui]
|
||||||
|
environment:
|
||||||
|
BASE_URL: http://azaion-ui:80
|
||||||
|
OWM_BASE_URL: http://owm-stub:8081
|
||||||
|
TILE_BASE_URL: http://tile-stub:8082
|
||||||
|
```
|
||||||
|
|
||||||
|
The compose file is part of the test-spec output; its concrete shape lands when the Decompose Tests step picks the runner (Step 5).
|
||||||
|
|
||||||
|
## Consumer Application
|
||||||
|
|
||||||
|
### `fast` profile
|
||||||
|
|
||||||
|
**Tech stack** (target — chosen at Step 5): a component-testing harness in TypeScript (Vitest or Jest + React Testing Library) plus a request-interception layer (MSW or equivalent) and jsdom (or headless Chromium component renderer).
|
||||||
|
|
||||||
|
**Entry point**: `npm run test:fast` (or `bun test`) — runs all `*.test.ts(x)` files under the test root.
|
||||||
|
|
||||||
|
#### Communication with system under test
|
||||||
|
|
||||||
|
| Interface | Protocol | Endpoint / Topic | Authentication |
|
||||||
|
|-----------|----------|-----------------|----------------|
|
||||||
|
| Mounted React component under test | direct mount via the testing library | n/a — observe the DOM + outbound requests captured by MSW | Stubbed bearer / cookie in test helpers |
|
||||||
|
| Outgoing `fetch` (under test) | HTTP via MSW handlers | mock `/api/<service>/...` per test | per handler |
|
||||||
|
| Outgoing `EventSource` (under test) | SSE via MSW or `EventSourcePolyfill` test double | mock `/api/<service>/...` per test | bearer in query string (ADR-008) |
|
||||||
|
| Static check | `bun run` script + filesystem regex (e.g. via `ripgrep`) | n/a | n/a |
|
||||||
|
|
||||||
|
### `e2e` profile
|
||||||
|
|
||||||
|
**Tech stack** (target — chosen at Step 5): Playwright (Chromium + Firefox per AC-18) driving the deployed `azaion-ui` nginx; assertion library is the runner's built-in expectations + a small request-interception adapter that logs every outbound request for assertion.
|
||||||
|
|
||||||
|
**Entry point**: `bun run test:e2e` — runs all `*.e2e.ts` files under the test root against the live compose stack.
|
||||||
|
|
||||||
|
#### Communication with system under test
|
||||||
|
|
||||||
|
| Interface | Protocol | Endpoint / Topic | Authentication |
|
||||||
|
|-----------|----------|-----------------|----------------|
|
||||||
|
| Browser navigation | HTTPS | `${BASE_URL}/login`, `/flights`, `/annotations`, `/dataset`, `/admin`, `/settings` | login via the public `/login` flow |
|
||||||
|
| Suite REST | HTTPS via SPA's nginx proxy | `/api/admin/*`, `/api/flights/*`, `/api/annotations/*`, `/api/detect/*`, `/api/loader/*`, `/api/resource/*`, `/api/gps-denied-{desktop,onboard}/*`, `/api/autopilot/*` | bearer in `Authorization` header + cookie (HttpOnly) |
|
||||||
|
| Suite SSE | HTTPS | `/api/flights/<id>/live-gps`, `/api/annotations/annotations/events`, `/api/detect/stream/<jobId>` (F7 target) | bearer in `?token=` per ADR-008 |
|
||||||
|
| Bundle / image inspection | filesystem / `docker inspect` | n/a | n/a |
|
||||||
|
| OpenWeatherMap | HTTPS via `owm-stub` | per stub | none |
|
||||||
|
| OSM tiles | HTTPS via `tile-stub` | per stub | none |
|
||||||
|
|
||||||
|
### What the consumer does NOT have access to
|
||||||
|
|
||||||
|
- No direct DB access to `test-db`. Suite DB queries are forbidden from the test runner; the consumer asserts only through the suite's REST + SSE.
|
||||||
|
- No internal `src/` imports beyond the typed enum shapes that ARE part of the wire contract (`AnnotationStatus`, `MediaStatus`, `Affiliation`, `CombatReadiness`, `MediaType`, `AnnotationSource` per `data_parameters.md` §1) — these are the spec the test asserts against per `P9`.
|
||||||
|
- No React component state read via hooks or test-only escape hatches; only the DOM + outbound network surface is observable.
|
||||||
|
- No shared memory or filesystem with the SPA.
|
||||||
|
|
||||||
|
## CI/CD Integration
|
||||||
|
|
||||||
|
**When to run**:
|
||||||
|
|
||||||
|
- `fast` profile: on every commit (planned addition to `.woodpecker/build-arm.yml`; currently absent — O14).
|
||||||
|
- `e2e` profile: on PR merge to `dev` / `stage` and pre-release on `main`. Long-running; not gating regular commits.
|
||||||
|
- `static` profile: on every commit (lints + bundle / config checks run as part of the build).
|
||||||
|
|
||||||
|
**Pipeline stage**:
|
||||||
|
|
||||||
|
- `fast` + `static`: between `bun install` and `bun run build`.
|
||||||
|
- `e2e`: after `bun run build`, against the just-built image, in a separate compose job.
|
||||||
|
|
||||||
|
**Gate behavior**:
|
||||||
|
|
||||||
|
- `fast` + `static`: block merge on failure.
|
||||||
|
- `e2e`: block merge on failure for `dev` / `stage`. On `main`, manual approval is allowed for known-quarantined tests (e.g., the Phase B target tests for AC-11 / AC-24 / AC-40 that assert "when implemented").
|
||||||
|
|
||||||
|
**Timeout**: `fast` ≤ 5 min suite total. `e2e` ≤ 30 min suite total. Individual tests timeout per the `Max execution time` field in each scenario.
|
||||||
|
|
||||||
|
## Reporting
|
||||||
|
|
||||||
|
**Format**: CSV (and JUnit XML for CI consumption when the runner produces it).
|
||||||
|
|
||||||
|
**Columns**: `Test ID, Test Name, Profile, Execution Time (ms), Result (PASS|FAIL|SKIP|QUARANTINE), Error Message, Traces to AC, Traces to results_report.md row`.
|
||||||
|
|
||||||
|
**Output path**: `./test-output/report.csv` (mounted from the `playwright-runner` / `vitest-runner` container). For `static` checks, `./test-output/static-report.csv`. Suite-level rollup written to `./test-output/summary.csv`.
|
||||||
|
|
||||||
|
## Test Execution
|
||||||
|
|
||||||
|
**Decision**: **Docker (preferred)** for the `e2e` and `static` profiles; **local Bun** for the `fast` profile and as an option for `e2e` on developer machines that already have Playwright + the suite stack running. The project is **not hardware-dependent** — see "Hardware dependencies found" below.
|
||||||
|
|
||||||
|
### Hardware dependencies found
|
||||||
|
|
||||||
|
| Indicator | Found at | Verdict |
|
||||||
|
|-----------|----------|---------|
|
||||||
|
| GPU / CUDA imports | none in `src/` or `mission-planner/` | absent |
|
||||||
|
| CoreML / MPS imports | none | absent |
|
||||||
|
| Camera / sensor / GPIO / V4L2 | none | absent — the SPA reads `<video>` elements rendered from a server-supplied URL |
|
||||||
|
| OS-specific drivers / kernel modules | none | absent |
|
||||||
|
| Platform-gated source branches | none | absent |
|
||||||
|
| Spec-level constraint | `_docs/00_problem/restrictions.md` H3 ("No GPU expectation in UI image") + H4 ("Chromium / Firefox latest 2") | confirms platform-neutral browser surface |
|
||||||
|
|
||||||
|
Conclusion: classify as **Not hardware-dependent**. Docker headless Chromium reproduces the real production runtime; no real-hardware execution path is required.
|
||||||
|
|
||||||
|
### Execution instructions
|
||||||
|
|
||||||
|
#### Docker mode (preferred; CI default)
|
||||||
|
|
||||||
|
1. **Prerequisites**: Docker Engine 24+ with the `azaion-test-net` network reachable, ARM64 or amd64 host (the UI image is ARM64-only per H1 — CI runners on ARM64; multi-arch builds optional for local dev).
|
||||||
|
2. **Build**: `docker buildx build --platform linux/arm64 -t azaion-ui:test .` (or `--platform linux/amd64` on amd64 dev machines).
|
||||||
|
3. **Compose up**: `docker compose -f e2e/docker-compose.suite-e2e.yml up -d` — brings up `azaion-ui`, `admin`, `flights`, `annotations`, `detect`, the auxiliary services, `owm-stub`, `tile-stub`, `test-db`, and the `playwright-runner`.
|
||||||
|
4. **Run tests**: `docker compose -f e2e/docker-compose.suite-e2e.yml run --rm playwright-runner` — the runner image entrypoint is `bun run test:e2e`. Reports land in `./test-output/`.
|
||||||
|
5. **Tear down**: `docker compose -f e2e/docker-compose.suite-e2e.yml down -v` (volumes wiped between runs).
|
||||||
|
6. **Required environment**: `BASE_URL=http://azaion-ui:80`, `OWM_BASE_URL=http://owm-stub:8081`, `TILE_BASE_URL=http://tile-stub:8082`, `CI_COMMIT_SHA=<sha>` (stamped into `AZAION_REVISION`).
|
||||||
|
|
||||||
|
#### Local mode (for `fast` profile + developer-machine `e2e` runs)
|
||||||
|
|
||||||
|
1. **Prerequisites**: Bun 1.3.11 (matches S4), Chromium + Firefox latest two stable lines installed via Playwright (`bunx playwright install --with-deps chromium firefox`).
|
||||||
|
2. **`fast` profile**: `bun install && bun run test:fast` (alias for `bun test`, runs Vitest under jsdom plus MSW handlers).
|
||||||
|
3. **`e2e` profile (local)**: bring up the suite stack via the parent suite's compose file (`../docker-compose.yml`), point `BASE_URL=http://localhost:80`, then `bun run test:e2e`.
|
||||||
|
4. **Required environment**: same as Docker mode plus `OWM_API_KEY=test-key` (passed through the OWM stub).
|
||||||
|
|
||||||
|
#### CI runner mapping
|
||||||
|
|
||||||
|
| Profile | Runner type | Mode | Gate |
|
||||||
|
|---------|------------|------|------|
|
||||||
|
| `static` | ARM64 build runner | local (no browser) | block merge on failure |
|
||||||
|
| `fast` | ARM64 build runner | local Bun (jsdom + MSW) | block merge on failure |
|
||||||
|
| `e2e` | ARM64 e2e runner with Docker | Docker compose stack | block merge on failure for `dev`/`stage`; manual approval allowed for quarantined tests on `main` (per CI/CD Integration above) |
|
||||||
|
|
||||||
|
The decision is consumed by Phase 4 to choose between `scripts/run-tests.sh` (local Bun for `fast` + `static`) and `e2e/docker-compose.suite-e2e.yml` (Docker for `e2e`).
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
# Performance Tests
|
||||||
|
|
||||||
|
The Azaion UI is a thin SPA; the dominant performance concerns are bundle size, auth-refresh transparency, SSE responsiveness, and UI reflection of server-confirmed state changes. Server-side throughput is OUT of scope here — this file covers the UI's observable timing only.
|
||||||
|
|
||||||
|
### NFT-PERF-01: Initial JS bundle ≤ 2 MB gzipped
|
||||||
|
|
||||||
|
**Summary**: The sum of gzipped initial-route JS chunks in `dist/` stays within the architecture's stated budget.
|
||||||
|
**Traces to**: AC-11, O13
|
||||||
|
**Metric**: gzipped byte total of initial JS entries.
|
||||||
|
**Profile**: static
|
||||||
|
|
||||||
|
**Preconditions**:
|
||||||
|
- `bun run build` has produced `dist/`.
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
|
||||||
|
| Step | Consumer Action | Measurement |
|
||||||
|
|------|----------------|-------------|
|
||||||
|
| 1 | Run `vite build` (or read the build manifest if already built) | `dist/` produced |
|
||||||
|
| 2 | Walk Vite's `manifest.json` to enumerate entry chunks (non-async) | list of initial chunks |
|
||||||
|
| 3 | Gzip-size each chunk (Node `zlib.gzipSync(content, {level:9})` or equivalent) | per-chunk size |
|
||||||
|
| 4 | Sum sizes | total bytes |
|
||||||
|
|
||||||
|
**Pass criteria**: total ≤ 2 097 152 bytes (2 MB). Documentary today — no CI gate (AC-11 status: "target, not currently enforced"). Test exists so the gate flips to blocking the day CI wires it up.
|
||||||
|
**Duration**: ≤ 60 s.
|
||||||
|
**Expected result source**: `results_report.md` row 40.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-PERF-02: Auth refresh — exactly one network round trip per cycle
|
||||||
|
|
||||||
|
**Summary**: A single 401-triggered refresh round consists of exactly one `POST /api/admin/auth/refresh` plus one retry of the original request.
|
||||||
|
**Traces to**: AC-01, AC-23
|
||||||
|
**Metric**: count of `/api/admin/auth/refresh` requests per refresh event.
|
||||||
|
**Profile**: fast
|
||||||
|
|
||||||
|
**Preconditions**:
|
||||||
|
- Authenticated session.
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
|
||||||
|
| Step | Consumer Action | Measurement |
|
||||||
|
|------|----------------|-------------|
|
||||||
|
| 1 | Issue an authenticated request that returns 401 once, then 200 on retry | network log captured |
|
||||||
|
| 2 | Count refresh requests fired in the cycle | exactly 1 |
|
||||||
|
|
||||||
|
**Pass criteria**: refresh count == 1 per cycle (`results_report.md` row 12 — exact).
|
||||||
|
**Duration**: ≤ 5 s.
|
||||||
|
**Expected result source**: `results_report.md` row 12.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-PERF-03: SSE bearer-rotation reconnect ≤ 5 s
|
||||||
|
|
||||||
|
**Summary**: When the bearer rotates while N SSE streams are open, all streams close and reopen with the new token within 5 s.
|
||||||
|
**Traces to**: AC-24
|
||||||
|
**Metric**: per-EventSource time from `close()` observed to next `OPEN` readyState (after reconnect with new token).
|
||||||
|
**Profile**: fast — `quarantined` until SSE refresh-reconnect is implemented (Step 8 hardening)
|
||||||
|
|
||||||
|
**Preconditions**:
|
||||||
|
- Two EventSources open (live-GPS + annotation-status).
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
|
||||||
|
| Step | Consumer Action | Measurement |
|
||||||
|
|------|----------------|-------------|
|
||||||
|
| 1 | Trigger a refresh that rotates the bearer | new bearer in memory |
|
||||||
|
| 2 | For each EventSource: time from old `close` to new `OPEN` | dt_i (ms) |
|
||||||
|
| 3 | Inspect new URLs for new token in query string | new `?token=` value |
|
||||||
|
|
||||||
|
**Pass criteria**: `max(dt_i) ≤ 5 000 ms`; both streams close+open exactly once (`results_report.md` row 13).
|
||||||
|
**Duration**: ≤ 30 s.
|
||||||
|
**Expected result source**: `results_report.md` row 13.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-PERF-04: Live-GPS SSE opens within 5 s of flight select
|
||||||
|
|
||||||
|
**Summary**: After clicking a flight in the Header, the live-GPS EventSource reaches `OPEN` quickly.
|
||||||
|
**Traces to**: AC-08
|
||||||
|
**Metric**: time from select-flight click to EventSource `readyState === 1` (OPEN).
|
||||||
|
**Profile**: e2e (suite live-gps simulator emits events at 1 Hz)
|
||||||
|
|
||||||
|
**Preconditions**:
|
||||||
|
- Authenticated; flight selectable.
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
|
||||||
|
| Step | Consumer Action | Measurement |
|
||||||
|
|------|----------------|-------------|
|
||||||
|
| 1 | Click a flight | one EventSource constructed to `^/api/flights/[0-9]+/live-gps(\?|$)` |
|
||||||
|
| 2 | Wait for `OPEN` | dt (ms) |
|
||||||
|
|
||||||
|
**Pass criteria**: `dt ≤ 5 000 ms` (`results_report.md` row 34).
|
||||||
|
**Duration**: ≤ 10 s.
|
||||||
|
**Expected result source**: `results_report.md` row 34.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-PERF-05: Live-GPS SSE closes within 1 s of deselect
|
||||||
|
|
||||||
|
**Summary**: Deselecting the flight tears down the live-GPS stream promptly.
|
||||||
|
**Traces to**: AC-08
|
||||||
|
**Metric**: time from deselect to `CLOSED`.
|
||||||
|
**Profile**: e2e
|
||||||
|
|
||||||
|
**Preconditions**:
|
||||||
|
- Continuation of NFT-PERF-04.
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
|
||||||
|
| Step | Consumer Action | Measurement |
|
||||||
|
|------|----------------|-------------|
|
||||||
|
| 1 | Deselect the flight | EventSource closes |
|
||||||
|
| 2 | Wait for `CLOSED` | dt (ms) |
|
||||||
|
|
||||||
|
**Pass criteria**: `dt ≤ 1 000 ms` and no remaining open live-GPS sources (`results_report.md` row 35).
|
||||||
|
**Duration**: ≤ 5 s.
|
||||||
|
**Expected result source**: `results_report.md` row 35.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-PERF-06: Annotation-status SSE unsubscribes within 1 s on page unmount
|
||||||
|
|
||||||
|
**Summary**: Navigating away from `/annotations` closes the status-events SSE within 1 s.
|
||||||
|
**Traces to**: AC-09
|
||||||
|
**Metric**: time from unmount to `CLOSED`.
|
||||||
|
**Profile**: fast
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
|
||||||
|
| Step | Consumer Action | Measurement |
|
||||||
|
|------|----------------|-------------|
|
||||||
|
| 1 | Mount `/annotations` then unmount | EventSource transitions |
|
||||||
|
| 2 | Measure dt | ms |
|
||||||
|
|
||||||
|
**Pass criteria**: `dt ≤ 1 000 ms` (`results_report.md` row 25).
|
||||||
|
**Duration**: ≤ 5 s.
|
||||||
|
**Expected result source**: `results_report.md` row 25.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-PERF-07: Bulk-validate UI reflects new status within 2 s
|
||||||
|
|
||||||
|
**Summary**: After a successful bulk-validate, every selected row shows `Validated` quickly.
|
||||||
|
**Traces to**: AC-07
|
||||||
|
**Metric**: time from server 200 to last DOM row update.
|
||||||
|
**Profile**: fast
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
|
||||||
|
| Step | Consumer Action | Measurement |
|
||||||
|
|------|----------------|-------------|
|
||||||
|
| 1 | Select N items, click Validate | request issued |
|
||||||
|
| 2 | Stub responds 200 | UI updates begin |
|
||||||
|
| 3 | Wait for all N rows to show `Validated` | dt (ms) |
|
||||||
|
|
||||||
|
**Pass criteria**: `dt ≤ 2 000 ms` (`results_report.md` row 37).
|
||||||
|
**Duration**: ≤ 5 s.
|
||||||
|
**Expected result source**: `results_report.md` row 37.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-PERF-08: Panel-width persistence debounce ≤ 1 s after resize-end
|
||||||
|
|
||||||
|
**Summary**: A drag-end on a resizable panel triggers a single PUT within 1 s (debounced).
|
||||||
|
**Traces to**: AC-21
|
||||||
|
**Metric**: time from `mouseup` (drag-end) to outbound PUT; PUT count per drag.
|
||||||
|
**Profile**: fast — `quarantined` until Step 4 writer is added
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
|
||||||
|
| Step | Consumer Action | Measurement |
|
||||||
|
|------|----------------|-------------|
|
||||||
|
| 1 | Drag and release a divider | event captured |
|
||||||
|
| 2 | Wait for PUT or 1 s timeout | dt (ms); count |
|
||||||
|
|
||||||
|
**Pass criteria**: exactly 1 PUT within ≤ 1 000 ms; URL = `/api/annotations/settings/user`; body contains `panelWidths` (`results_report.md` row 64).
|
||||||
|
**Duration**: ≤ 5 s.
|
||||||
|
**Expected result source**: `results_report.md` row 64.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-PERF-09: Settings save error surfaces within 2 s
|
||||||
|
|
||||||
|
**Summary**: A 500 on settings save produces an error toast and resets the `saving` flag within 2 s.
|
||||||
|
**Traces to**: AC-27
|
||||||
|
**Metric**: time from server 500 to error visibility + state reset.
|
||||||
|
**Profile**: fast — `quarantined` until Step 4 try/finally fix
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
|
||||||
|
| Step | Consumer Action | Measurement |
|
||||||
|
|------|----------------|-------------|
|
||||||
|
| 1 | Trigger save, stub responds 500 after T ms | failure delivered |
|
||||||
|
| 2 | Wait for error toast and `saving === false` | dt (ms) |
|
||||||
|
|
||||||
|
**Pass criteria**: `dt ≤ 2 000 ms` (`results_report.md` row 68).
|
||||||
|
**Duration**: ≤ 5 s.
|
||||||
|
**Expected result source**: `results_report.md` row 68.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-PERF-10: First Contentful Paint on `/flights` ≤ 3 s on mid-range edge hardware
|
||||||
|
|
||||||
|
**Summary**: A warm-cache load of the default authenticated route renders the main pane within 3 s.
|
||||||
|
**Traces to**: AC-11 (target), H2 (edge deploys)
|
||||||
|
**Metric**: `performance.getEntriesByName('first-contentful-paint')[0].startTime`.
|
||||||
|
**Profile**: e2e — documentary (no enforcement today; no AC binds a hard FCP budget)
|
||||||
|
|
||||||
|
**Preconditions**:
|
||||||
|
- Warm browser cache (second visit).
|
||||||
|
- Edge-profile container: 2 vCPU, 4 GB RAM (the hardware-assessment phase confirms the figure).
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
|
||||||
|
| Step | Consumer Action | Measurement |
|
||||||
|
|------|----------------|-------------|
|
||||||
|
| 1 | Navigate to `/flights` post-login | navigation completes |
|
||||||
|
| 2 | Read FCP entry | ms |
|
||||||
|
|
||||||
|
**Pass criteria**: FCP ≤ 3 000 ms (row 98 — threshold_max).
|
||||||
|
**Duration**: ≤ 30 s.
|
||||||
|
**Expected result source**: `results_report.md` row 98.
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
# Resilience Tests
|
||||||
|
|
||||||
|
Failure / recovery scenarios at the SPA's observable boundary: bearer expiry, refresh cookie loss, upstream 5xx, network partition, oversized uploads, SSE drop. Every fault injection is at the network / browser layer; the UI is observed for graceful behavior and recovery.
|
||||||
|
|
||||||
|
### NFT-RES-01: 401 → refresh → retry recovery is transparent
|
||||||
|
|
||||||
|
**Summary**: An authenticated request that returns 401 mid-session is refreshed and retried without unmounting the routed view.
|
||||||
|
**Traces to**: AC-01, AC-23
|
||||||
|
|
||||||
|
**Preconditions**:
|
||||||
|
- Active session on `/flights`.
|
||||||
|
|
||||||
|
**Fault injection**:
|
||||||
|
- Force a 401 on the next outbound authenticated request.
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
|
||||||
|
| Step | Action | Expected Behavior |
|
||||||
|
|------|--------|------------------|
|
||||||
|
| 1 | Stub the next request to return 401 once | first response 401 |
|
||||||
|
| 2 | Observe the SPA's reaction | `POST /api/admin/auth/refresh` with `credentials:'include'`; on 200, original request retried with new bearer |
|
||||||
|
| 3 | Inspect `<ProtectedRoute>` children | not unmounted; re-render delta ≤ 1 |
|
||||||
|
|
||||||
|
**Pass criteria**: row 03 sequence; row 11 re-render bound (≤ 1); row 12 refresh count == 1 — all hold simultaneously.
|
||||||
|
**Expected result source**: `results_report.md` rows 03, 11, 12.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-RES-02: SSE bearer-rotation — both streams reconnect within 5 s
|
||||||
|
|
||||||
|
**Summary**: A bearer rotation during open SSE streams (live-GPS + annotation-status) tears them down and reopens them with the new token.
|
||||||
|
**Traces to**: AC-24
|
||||||
|
|
||||||
|
**Preconditions**:
|
||||||
|
- Two EventSources open.
|
||||||
|
|
||||||
|
**Fault injection**:
|
||||||
|
- Trigger a server-driven rotation of the bearer (force a refresh).
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
|
||||||
|
| Step | Action | Expected Behavior |
|
||||||
|
|------|--------|------------------|
|
||||||
|
| 1 | Rotate bearer | new token in memory |
|
||||||
|
| 2 | Observe each EventSource | closes, then reopens with new `?token=` |
|
||||||
|
| 3 | Measure max reconnect time | ≤ 5 000 ms |
|
||||||
|
|
||||||
|
**Pass criteria**: both streams close+open exactly once; max reconnect ≤ 5 000 ms (`results_report.md` row 13).
|
||||||
|
**Status**: `quarantined` until SSE reconnect-on-rotation ships (Step 8 hardening).
|
||||||
|
**Expected result source**: `results_report.md` row 13.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-RES-03: Network offline at boot — error state, no offline mode
|
||||||
|
|
||||||
|
**Summary**: With network disabled, app boot results in a user-visible error state — NOT a service worker-served cached UI.
|
||||||
|
**Traces to**: AC-N3
|
||||||
|
|
||||||
|
**Preconditions**:
|
||||||
|
- Browser network disabled (or all `/api/*` stubs respond with offline error).
|
||||||
|
|
||||||
|
**Fault injection**:
|
||||||
|
- All outbound requests fail with `net::ERR_INTERNET_DISCONNECTED` (or equivalent).
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
|
||||||
|
| Step | Action | Expected Behavior |
|
||||||
|
|------|--------|------------------|
|
||||||
|
| 1 | Load the SPA | static assets served by nginx OK; API calls fail |
|
||||||
|
| 2 | Observe DOM | login or general error surface present |
|
||||||
|
| 3 | Inspect `navigator.serviceWorker.controller` | `null` |
|
||||||
|
|
||||||
|
**Pass criteria**: row 93 — error/login-failed state present; no service worker controller.
|
||||||
|
**Expected result source**: `results_report.md` row 93.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-RES-04: ProtectedRoute loading timeout fallback after 10 s
|
||||||
|
|
||||||
|
**Summary**: The `<ProtectedRoute>` spinner has a bounded loading window; a stalled auth bootstrap surfaces a retry CTA / error.
|
||||||
|
**Traces to**: AC-17
|
||||||
|
|
||||||
|
**Preconditions**:
|
||||||
|
- Bootstrap refresh stubbed to never resolve.
|
||||||
|
|
||||||
|
**Fault injection**:
|
||||||
|
- `POST /api/admin/auth/refresh` hangs (no response).
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
|
||||||
|
| Step | Action | Expected Behavior |
|
||||||
|
|------|--------|------------------|
|
||||||
|
| 1 | Mount `<ProtectedRoute>` | spinner rendered |
|
||||||
|
| 2 | Advance fake time to 10 s | timeout fires |
|
||||||
|
| 3 | Inspect DOM | retry CTA or error message present; spinner unmounted |
|
||||||
|
|
||||||
|
**Pass criteria**: row 59 — fallback present, spinner absent.
|
||||||
|
**Status**: `quarantined` until timeout fix lands (Step 4).
|
||||||
|
**Expected result source**: `results_report.md` row 59.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-RES-05: Settings save with upstream 500 — UI state recovers
|
||||||
|
|
||||||
|
**Summary**: A 500 on settings save surfaces an error and resets the `saving` flag.
|
||||||
|
**Traces to**: AC-27
|
||||||
|
|
||||||
|
**Preconditions**:
|
||||||
|
- Form filled in valid state on `/settings`.
|
||||||
|
|
||||||
|
**Fault injection**:
|
||||||
|
- Upstream `PUT /api/annotations/settings/system` returns 500 after T ms.
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
|
||||||
|
| Step | Action | Expected Behavior |
|
||||||
|
|------|--------|------------------|
|
||||||
|
| 1 | Click Save | PUT issued |
|
||||||
|
| 2 | Stub responds 500 within 2 s | failure |
|
||||||
|
| 3 | Inspect within 2 s | toast / inline error; `saving === false`; no route navigation |
|
||||||
|
|
||||||
|
**Pass criteria**: row 68.
|
||||||
|
**Status**: `quarantined` until Step 4 try/finally fix.
|
||||||
|
**Expected result source**: `results_report.md` row 68.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-RES-06: Settings save with network drop — try/finally state reset
|
||||||
|
|
||||||
|
**Summary**: When the underlying fetch throws (network drop), `saving` resets and the user sees an error.
|
||||||
|
**Traces to**: AC-27
|
||||||
|
|
||||||
|
**Preconditions**:
|
||||||
|
- Form filled in valid state.
|
||||||
|
|
||||||
|
**Fault injection**:
|
||||||
|
- Network drop mid-PUT (`fetch` rejects).
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
|
||||||
|
| Step | Action | Expected Behavior |
|
||||||
|
|------|--------|------------------|
|
||||||
|
| 1 | Click Save | PUT issued |
|
||||||
|
| 2 | Stub throws | rejection delivered |
|
||||||
|
| 3 | Inspect | `saving === false`; error surfaced |
|
||||||
|
|
||||||
|
**Pass criteria**: row 69.
|
||||||
|
**Status**: `quarantined`.
|
||||||
|
**Expected result source**: `results_report.md` row 69.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-RES-07: nginx 413 on oversized upload surfaces user-visible error
|
||||||
|
|
||||||
|
**Summary**: An upload that exceeds `client_max_body_size 500M` returns 413; the UI presents a user-facing message (no silent failure, no `alert()`).
|
||||||
|
**Traces to**: AC-10
|
||||||
|
|
||||||
|
**Preconditions**:
|
||||||
|
- Authenticated; `<MediaList>` open.
|
||||||
|
|
||||||
|
**Fault injection**:
|
||||||
|
- Drop a 501 MB synthetic file.
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
|
||||||
|
| Step | Action | Expected Behavior |
|
||||||
|
|------|--------|------------------|
|
||||||
|
| 1 | Upload 501 MB | upload starts |
|
||||||
|
| 2 | nginx rejects | 413 delivered |
|
||||||
|
| 3 | Inspect UI | error containing the i18n "file too large" string; no `alert()` invoked |
|
||||||
|
|
||||||
|
**Pass criteria**: row 39.
|
||||||
|
**Expected result source**: `results_report.md` row 39.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-RES-08: Refresh cookie expired — redirect to /login
|
||||||
|
|
||||||
|
**Summary**: When the refresh cookie is gone (or expired) and a 401 occurs, the SPA redirects the user to `/login` rather than silently looping refresh.
|
||||||
|
**Traces to**: AC-01, AC-22
|
||||||
|
|
||||||
|
**Preconditions**:
|
||||||
|
- Cookie cleared from the browser jar.
|
||||||
|
|
||||||
|
**Fault injection**:
|
||||||
|
- 401 on any authenticated call; subsequent `POST /api/admin/auth/refresh` returns 401.
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
|
||||||
|
| Step | Action | Expected Behavior |
|
||||||
|
|------|--------|------------------|
|
||||||
|
| 1 | Issue authenticated call | 401 |
|
||||||
|
| 2 | Refresh attempted | 401 (no cookie) |
|
||||||
|
| 3 | Observe routing | redirect to `/login` |
|
||||||
|
|
||||||
|
**Pass criteria**: final URL `/login`; no infinite refresh loop (single refresh attempt). Derived from AC-01 + AC-22; no specific results_report row binds the loop bound — Phase 3 flags this and the loop bound is added to row 03 if accepted.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-RES-09: Annotation download tainted-canvas fallback
|
||||||
|
|
||||||
|
**Summary**: When `<canvas>.toBlob()` raises a tainted-canvas exception (cross-origin video frame), the user sees an error rather than a silent no-op.
|
||||||
|
**Traces to**: NFR (`04_verification_log.md` finding on `handleDownload` tainted-canvas risk)
|
||||||
|
|
||||||
|
**Preconditions**:
|
||||||
|
- An annotation is loaded from a video sourced with CORS that taints the canvas.
|
||||||
|
|
||||||
|
**Fault injection**:
|
||||||
|
- Cross-origin video source taints the canvas; `toBlob` throws.
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
|
||||||
|
| Step | Action | Expected Behavior |
|
||||||
|
|------|--------|------------------|
|
||||||
|
| 1 | Click Download | export attempted |
|
||||||
|
| 2 | toBlob throws | error handled |
|
||||||
|
| 3 | Inspect UI | user-visible error; no `alert()`; no silent swallow |
|
||||||
|
|
||||||
|
**Pass criteria**: row 96 — error surfaced; no silent swallow; no fabricated blob; no `alert()`.
|
||||||
|
**Expected result source**: `results_report.md` row 96.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-RES-10: SSE server disconnect — UI surfaces a connection-lost indicator
|
||||||
|
|
||||||
|
**Summary**: When the suite server closes a live-GPS or status-events SSE without rotation, the UI does NOT show stale data and DOES indicate the connection lost.
|
||||||
|
**Traces to**: AC-08, AC-09, AC-24
|
||||||
|
|
||||||
|
**Preconditions**:
|
||||||
|
- One SSE stream open.
|
||||||
|
|
||||||
|
**Fault injection**:
|
||||||
|
- Server force-closes the stream (no rotation).
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
|
||||||
|
| Step | Action | Expected Behavior |
|
||||||
|
|------|--------|------------------|
|
||||||
|
| 1 | Drop the stream from the server | `error` fires on EventSource |
|
||||||
|
| 2 | Observe DOM | a connection-lost indicator is rendered (or stale-data badge) |
|
||||||
|
| 3 | Observe reconnect behavior | `EventSource` auto-retries per browser default; if the SPA re-creates it, exactly one new instance |
|
||||||
|
|
||||||
|
**Pass criteria**: row 97 — connection-lost indicator OR reconnect attempt within 10 s; stale data NOT rendered as live; reconnect attempts ≤ 1 in the 10 s window.
|
||||||
|
**Expected result source**: `results_report.md` row 97.
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
# Resource Limit Tests
|
||||||
|
|
||||||
|
The SPA's resource constraints are bundle size (initial JS), upload size cap (server), runtime image footprint (`nginx:alpine` only), and exclusion of the unbundled `mission-planner/` from production output. Long-session memory / CPU behavior is also covered here at a documentary level — no AC binds a hard runtime memory budget today.
|
||||||
|
|
||||||
|
### NFT-RES-LIM-01: Initial JS bundle ≤ 2 MB gzipped
|
||||||
|
|
||||||
|
**Traces to**: AC-11, O13
|
||||||
|
|
||||||
|
**Preconditions**:
|
||||||
|
- `bun run build` has produced `dist/`.
|
||||||
|
|
||||||
|
**Monitoring**:
|
||||||
|
- Sum of gzipped initial-route JS chunk sizes (computed from Vite's `manifest.json`).
|
||||||
|
|
||||||
|
**Duration**: ≤ 60 s (build + measurement).
|
||||||
|
**Pass criteria**: total gzipped initial JS ≤ 2 097 152 bytes (`results_report.md` row 40). Documentary today; CI gate target.
|
||||||
|
**Expected result source**: `results_report.md` row 40.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-RES-LIM-02: nginx `client_max_body_size 500M`
|
||||||
|
|
||||||
|
**Traces to**: AC-10, E9
|
||||||
|
|
||||||
|
**Preconditions**:
|
||||||
|
- `nginx.conf` present in the repo (and in the built image's `/etc/nginx/`).
|
||||||
|
|
||||||
|
**Monitoring**:
|
||||||
|
- Read the value of `client_max_body_size` from `nginx.conf`.
|
||||||
|
|
||||||
|
**Duration**: ≤ 5 s.
|
||||||
|
**Pass criteria**: value equals `500M` (`results_report.md` row 38).
|
||||||
|
**Expected result source**: `results_report.md` row 38.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-RES-LIM-03: Production image is `nginx:alpine` and carries no Node.js
|
||||||
|
|
||||||
|
**Traces to**: AC-33, S5, O11
|
||||||
|
|
||||||
|
**Preconditions**:
|
||||||
|
- Built image `azaion/ui:<tag>` available locally.
|
||||||
|
|
||||||
|
**Monitoring**:
|
||||||
|
- `docker inspect azaion/ui:<tag>` for the final stage's base image.
|
||||||
|
- Filesystem scan inside the image for a `node` binary.
|
||||||
|
|
||||||
|
**Duration**: ≤ 30 s.
|
||||||
|
**Pass criteria**: base image is `nginx:alpine`; no `node` binary present (`results_report.md` row 42).
|
||||||
|
**Expected result source**: `results_report.md` row 42.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-RES-LIM-04: `mission-planner/` is excluded from production bundle
|
||||||
|
|
||||||
|
**Traces to**: AC-31, O12, ADR-009
|
||||||
|
|
||||||
|
**Preconditions**:
|
||||||
|
- `bun run build` has produced `dist/`.
|
||||||
|
|
||||||
|
**Monitoring**:
|
||||||
|
- Vite's build report / manifest for chunk origins.
|
||||||
|
- Static-import graph analysis starting from `src/main.tsx` — verify no edges into `mission-planner/`.
|
||||||
|
|
||||||
|
**Duration**: ≤ 30 s.
|
||||||
|
**Pass criteria**: no `dist/**` file originates from `mission-planner/**`; the import graph from `src/main.tsx` does NOT reach `mission-planner/` (`results_report.md` row 41).
|
||||||
|
**Expected result source**: `results_report.md` row 41.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-RES-LIM-05: SPA memory stable across a 30-minute annotation session
|
||||||
|
|
||||||
|
**Traces to**: H2 (edge deploy), AC-09 (SSE)
|
||||||
|
**Status**: documentary — no AC binds a hard runtime memory budget today
|
||||||
|
|
||||||
|
**Preconditions**:
|
||||||
|
- E2E profile; user logged in on `/annotations`; annotation-status SSE open.
|
||||||
|
|
||||||
|
**Monitoring**:
|
||||||
|
- Headless Chromium `performance.memory.usedJSHeapSize` sampled every 60 s for 30 min.
|
||||||
|
|
||||||
|
**Duration**: 30 min.
|
||||||
|
**Pass criteria**: `usedJSHeapSize` does not grow by more than 50 % over the session under steady-state interaction (open/close media, page through dataset). A documentary baseline; if Phase 3 deems it un-anchored to an AC, it is downgraded to a metric-only run.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-RES-LIM-06: Live-GPS SSE 1-hour soak — no listener leak, no memory creep
|
||||||
|
|
||||||
|
**Traces to**: AC-08
|
||||||
|
**Status**: documentary
|
||||||
|
|
||||||
|
**Preconditions**:
|
||||||
|
- E2E profile; flight selected; live-GPS simulator emits at 1 Hz.
|
||||||
|
|
||||||
|
**Monitoring**:
|
||||||
|
- `EventSource` instance count sampled every 60 s — must stay at exactly 1.
|
||||||
|
- `usedJSHeapSize` sampled every 60 s.
|
||||||
|
|
||||||
|
**Duration**: 60 min.
|
||||||
|
**Pass criteria**: EventSource count stays at 1 throughout; heap grows by ≤ 30 % (documentary).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-RES-LIM-07: 100 sequential flight selections — no leaked SSEs, no leaked Contexts
|
||||||
|
|
||||||
|
**Traces to**: AC-08, P4
|
||||||
|
**Status**: documentary
|
||||||
|
|
||||||
|
**Preconditions**:
|
||||||
|
- E2E profile; ≥ 5 flights in seed.
|
||||||
|
|
||||||
|
**Monitoring**:
|
||||||
|
- Total EventSource instances created over the loop.
|
||||||
|
- Final open EventSource count (after deselect-then-reselect cycles end at "deselected").
|
||||||
|
|
||||||
|
**Duration**: ≤ 5 min.
|
||||||
|
**Pass criteria**: after the loop, open EventSource count is ≤ 1 (only the currently-selected stream if any). No more than `100 + 1 ` EventSources were created in total (one extra for any pre-test state). Documentary; Phase 3 to confirm or downgrade.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-RES-LIM-08: Edge-host RAM profile of the UI image at steady state
|
||||||
|
|
||||||
|
**Traces to**: H2
|
||||||
|
**Status**: documentary; hardware-assessment phase will pin the exact numbers
|
||||||
|
|
||||||
|
**Preconditions**:
|
||||||
|
- Production image running on the target edge profile (2 vCPU, 4 GB RAM).
|
||||||
|
|
||||||
|
**Monitoring**:
|
||||||
|
- `docker stats azaion-ui` sampled every 10 s for 5 min while a user is actively on `/annotations` with one open SSE.
|
||||||
|
|
||||||
|
**Duration**: 5 min.
|
||||||
|
**Pass criteria**: RSS of the nginx process under sustained traffic stays under 200 MB (documentary baseline; will be tightened or relaxed at hardware-assessment time).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-RES-LIM-09: nginx routes — exactly 9 location blocks for the suite services
|
||||||
|
|
||||||
|
**Traces to**: AC-34, E2
|
||||||
|
|
||||||
|
**Preconditions**:
|
||||||
|
- `nginx.conf` present.
|
||||||
|
|
||||||
|
**Monitoring**:
|
||||||
|
- Parse `nginx.conf` for `location` blocks under the main `server`.
|
||||||
|
|
||||||
|
**Duration**: ≤ 5 s.
|
||||||
|
**Pass criteria**: `location` block set equals `{/api/admin/, /api/flights/, /api/annotations/, /api/detect/, /api/loader/, /api/gps-denied-desktop/, /api/gps-denied-onboard/, /api/autopilot/, /api/resource/}` (`results_report.md` row 43).
|
||||||
|
**Expected result source**: `results_report.md` row 43.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-RES-LIM-10: nginx — each route strips its `/api/<service>/` prefix
|
||||||
|
|
||||||
|
**Traces to**: AC-34, E2
|
||||||
|
|
||||||
|
**Preconditions**:
|
||||||
|
- `nginx.conf` present.
|
||||||
|
|
||||||
|
**Monitoring**:
|
||||||
|
- Per `location` block, inspect the `proxy_pass` / `rewrite` directive shape — verify the prefix is stripped before forwarding upstream.
|
||||||
|
|
||||||
|
**Duration**: ≤ 5 s.
|
||||||
|
**Pass criteria**: every block satisfies the strip-prefix regex (per-block check, `results_report.md` row 44).
|
||||||
|
**Expected result source**: `results_report.md` row 44.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-RES-LIM-11: CI image tag scheme is `${branch}-arm`
|
||||||
|
|
||||||
|
**Traces to**: AC-32, E7
|
||||||
|
|
||||||
|
**Preconditions**:
|
||||||
|
- `.woodpecker/build-arm.yml` present.
|
||||||
|
|
||||||
|
**Monitoring**:
|
||||||
|
- Parse the push step's `tag` field for branches `dev`, `stage`, `main`.
|
||||||
|
|
||||||
|
**Duration**: ≤ 5 s.
|
||||||
|
**Pass criteria**: pushed tag for branch `main` matches `^main-arm$` (`results_report.md` row 70). Same regex shape for `dev` and `stage` (derived).
|
||||||
|
**Expected result source**: `results_report.md` row 70.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-RES-LIM-12: OCI labels present on the pushed image
|
||||||
|
|
||||||
|
**Traces to**: AC-32, E6
|
||||||
|
|
||||||
|
**Preconditions**:
|
||||||
|
- `.woodpecker/build-arm.yml` present.
|
||||||
|
|
||||||
|
**Monitoring**:
|
||||||
|
- Parse the push step's label declarations — count and presence.
|
||||||
|
|
||||||
|
**Duration**: ≤ 5 s.
|
||||||
|
**Pass criteria**: labels `org.opencontainers.image.revision`, `org.opencontainers.image.created`, `org.opencontainers.image.source` are all declared and non-empty; total label count == 3 (`results_report.md` row 71).
|
||||||
|
**Expected result source**: `results_report.md` row 71.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-RES-LIM-13: Revision label equals `$CI_COMMIT_SHA`
|
||||||
|
|
||||||
|
**Traces to**: AC-32, E5
|
||||||
|
|
||||||
|
**Preconditions**:
|
||||||
|
- `.woodpecker/build-arm.yml` present.
|
||||||
|
|
||||||
|
**Monitoring**:
|
||||||
|
- Parse the label value template for `org.opencontainers.image.revision`.
|
||||||
|
|
||||||
|
**Duration**: ≤ 5 s.
|
||||||
|
**Pass criteria**: label value template equals `$CI_COMMIT_SHA` (or the pipeline's documented equivalent) (`results_report.md` row 72).
|
||||||
|
**Expected result source**: `results_report.md` row 72.
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
# Security Tests
|
||||||
|
|
||||||
|
Blackbox security assertions against the SPA's observable surface: token storage discipline, refresh cookie attributes, RBAC route gating, credentials flag, secrets-in-source checks, destructive-action policy, dependency hygiene. These complement the server's RBAC and the suite's security_approach (`_docs/00_problem/security_approach.md`); they do NOT replace server-side enforcement (O4).
|
||||||
|
|
||||||
|
### NFT-SEC-01: Bearer is never written to `localStorage` or `sessionStorage`
|
||||||
|
|
||||||
|
**Traces to**: AC-02, O2
|
||||||
|
**Profile**: static + e2e
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
|
||||||
|
| Step | Consumer Action | Expected Response |
|
||||||
|
|------|----------------|------------------|
|
||||||
|
| 1 | Static code-search on `src/` and `mission-planner/src/` for `localStorage.|sessionStorage.` near `bearer|token|accessToken` | `match_count == 0` |
|
||||||
|
| 2 | E2E: complete a login; inspect `localStorage` and `sessionStorage` keys | neither contains the bearer value |
|
||||||
|
|
||||||
|
**Pass criteria**: row 04 — `match_count == 0`; runtime storage does not contain the bearer.
|
||||||
|
**Expected result source**: `results_report.md` row 04.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-SEC-02: `document.cookie` does not expose the refresh token
|
||||||
|
|
||||||
|
**Traces to**: AC-03
|
||||||
|
**Profile**: e2e + static
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
|
||||||
|
| Step | Consumer Action | Expected Response |
|
||||||
|
|------|----------------|------------------|
|
||||||
|
| 1 | Static code-search for `document.cookie` reads against `refreshToken|refresh-cookie` | `match_count == 0` (row 05) |
|
||||||
|
| 2 | E2E: complete login; read `document.cookie` from page context | returned string does NOT contain the refresh-token value (row 06) |
|
||||||
|
|
||||||
|
**Pass criteria**: rows 05 + 06.
|
||||||
|
**Expected result source**: `results_report.md` rows 05, 06.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-SEC-03: Refresh cookie attributes — `Secure`, `HttpOnly`, `SameSite=Strict`
|
||||||
|
|
||||||
|
**Traces to**: AC-03, E3, O5
|
||||||
|
**Profile**: e2e
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
|
||||||
|
| Step | Consumer Action | Expected Response |
|
||||||
|
|------|----------------|------------------|
|
||||||
|
| 1 | Login via `POST /api/admin/auth/login` against the suite stack | `Set-Cookie` header returned |
|
||||||
|
| 2 | Inspect header value | matches regex `Secure;.*HttpOnly;.*SameSite=Strict` (case-insensitive, attribute-order-tolerant) |
|
||||||
|
|
||||||
|
**Pass criteria**: row 07 — regex match.
|
||||||
|
**Notes**: this is a server-contract assertion; the UI test exists as defence-in-depth so a suite regression is caught before it lands in production.
|
||||||
|
**Expected result source**: `results_report.md` row 07.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-SEC-04: `credentials: 'include'` is set on every authenticated fetch
|
||||||
|
|
||||||
|
**Traces to**: AC-01, O3
|
||||||
|
**Profile**: fast (apiClient wrapper) + e2e (live capture)
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
|
||||||
|
| Step | Consumer Action | Expected Response |
|
||||||
|
|------|----------------|------------------|
|
||||||
|
| 1 | Issue an authenticated request via `apiClient` | RequestInit captured |
|
||||||
|
| 2 | Inspect | `credentials === 'include'` (row 01) |
|
||||||
|
| 3 | Repeat for the bootstrap refresh | same (row 02 — `quarantined` until Step 4 bootstrap fix) |
|
||||||
|
|
||||||
|
**Pass criteria**: rows 01 + 02.
|
||||||
|
**Expected result source**: `results_report.md` rows 01, 02.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-SEC-05: `/admin` route blocks non-admins client-side (defence in depth)
|
||||||
|
|
||||||
|
**Traces to**: AC-22
|
||||||
|
**Profile**: e2e — `quarantined` until role-gate is added (Step 4 / Step 8)
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
|
||||||
|
| Step | Consumer Action | Expected Response |
|
||||||
|
|------|----------------|------------------|
|
||||||
|
| 1 | Log in as `op_alice` (Operator, no admin role) | session active |
|
||||||
|
| 2 | Navigate to `/admin` | URL changes |
|
||||||
|
| 3 | Inspect final URL + DOM | URL is `/flights`; `<AdminPage>` NOT mounted |
|
||||||
|
|
||||||
|
**Pass criteria**: row 08.
|
||||||
|
**Notes**: server-side RBAC is authoritative; the UI gate is a usability + leakage layer.
|
||||||
|
**Expected result source**: `results_report.md` row 08.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-SEC-06: `/settings` route gate is applied per RBAC
|
||||||
|
|
||||||
|
**Traces to**: AC-22
|
||||||
|
**Profile**: e2e — `quarantined`
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
|
||||||
|
| Step | Consumer Action | Expected Response |
|
||||||
|
|------|----------------|------------------|
|
||||||
|
| 1 | Log in as user without SETTINGS permission | session active |
|
||||||
|
| 2 | Navigate to `/settings` | URL changes |
|
||||||
|
| 3 | Inspect | URL is `/flights`; `<SettingsPage>` NOT mounted |
|
||||||
|
|
||||||
|
**Pass criteria**: row 10.
|
||||||
|
**Expected result source**: `results_report.md` row 10.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-SEC-07: `alert()` is forbidden anywhere in the SPA
|
||||||
|
|
||||||
|
**Traces to**: AC-14, O10
|
||||||
|
**Profile**: static
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
|
||||||
|
| Step | Consumer Action | Expected Response |
|
||||||
|
|------|----------------|------------------|
|
||||||
|
| 1 | Regex sweep `src/` and `mission-planner/src/` for `\balert\(` | `match_count == 0` |
|
||||||
|
|
||||||
|
**Pass criteria**: row 50.
|
||||||
|
**Expected result source**: `results_report.md` row 50.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-SEC-08: ConfirmDialog gates every destructive action
|
||||||
|
|
||||||
|
**Traces to**: AC-14, AC-30, O10
|
||||||
|
**Profile**: fast
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
|
||||||
|
| Step | Consumer Action | Expected Response |
|
||||||
|
|------|----------------|------------------|
|
||||||
|
| 1 | For each destructive surface in `_docs/ui_design/` (class delete, user deactivate, dataset bulk-overwrite, etc.) | sequence checked |
|
||||||
|
| 2 | Confirm sequence on click → before any HTTP fires | dialog present (row 51) |
|
||||||
|
| 3 | On Confirm in class-delete flow → exactly one DELETE to `^/api/admin/classes/[0-9]+$` | (row 49) |
|
||||||
|
|
||||||
|
**Pass criteria**: rows 49 + 51.
|
||||||
|
**Expected result source**: `results_report.md` rows 49, 51.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-SEC-09: OpenWeatherMap API key is not shipped in source or bundle
|
||||||
|
|
||||||
|
**Traces to**: AC-20, P10
|
||||||
|
**Profile**: static (source) + static (bundle)
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
|
||||||
|
| Step | Consumer Action | Expected Response |
|
||||||
|
|------|----------------|------------------|
|
||||||
|
| 1 | Regex sweep `src/` and `mission-planner/src/` for the literal current OWM key value | `match_count == 0` (row 63) |
|
||||||
|
| 2 | Regex sweep for `appid=` and `api_key=` literal occurrences in source URLs | `match_count == 0` (row 63) |
|
||||||
|
| 3 | Scan `dist/**/*.js` post-build for the literal key | `match_count == 0` (Phase 3 may downgrade to "until Step 4 fix") |
|
||||||
|
|
||||||
|
**Pass criteria**: row 63.
|
||||||
|
**Status**: `quarantined` for source check until Step 4 fix; the bundle-scan check passes immediately for `src/` (mission-planner not bundled, AC-31).
|
||||||
|
**Expected result source**: `results_report.md` row 63.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-SEC-10: No in-browser ML libs
|
||||||
|
|
||||||
|
**Traces to**: AC-N2
|
||||||
|
**Profile**: static
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
|
||||||
|
| Step | Consumer Action | Expected Response |
|
||||||
|
|------|----------------|------------------|
|
||||||
|
| 1 | Parse `package.json` and `mission-planner/package.json` dependencies | dependency lists |
|
||||||
|
| 2 | Match against `^(onnxruntime|@?tensorflow(?:js)?(?:/.*)?|tflite|coreml|tfjs|@huggingface/.*|transformers\.js)$` | zero matches |
|
||||||
|
|
||||||
|
**Pass criteria**: row 92.
|
||||||
|
**Expected result source**: `results_report.md` row 92.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-SEC-11: No response-signature / JOSE libs on the request path
|
||||||
|
|
||||||
|
**Traces to**: AC-N4
|
||||||
|
**Profile**: static
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
|
||||||
|
| Step | Consumer Action | Expected Response |
|
||||||
|
|------|----------------|------------------|
|
||||||
|
| 1 | Parse `package.json` dependencies | list |
|
||||||
|
| 2 | Match against `^(jsrsasign|tweetnacl|@noble/.*|jose)$` | zero matches |
|
||||||
|
|
||||||
|
**Pass criteria**: row 94.
|
||||||
|
**Expected result source**: `results_report.md` row 94.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-SEC-12: No service worker — offline mode is explicitly absent
|
||||||
|
|
||||||
|
**Traces to**: AC-N3
|
||||||
|
**Profile**: e2e
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
|
||||||
|
| Step | Consumer Action | Expected Response |
|
||||||
|
|------|----------------|------------------|
|
||||||
|
| 1 | Load the SPA in a fresh browser context | app boots |
|
||||||
|
| 2 | Read `navigator.serviceWorker.getRegistrations()` | empty array |
|
||||||
|
|
||||||
|
**Pass criteria**: row 93 — no service worker registered.
|
||||||
|
**Expected result source**: `results_report.md` row 93.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-SEC-13: Dropped legacy features are not present in source
|
||||||
|
|
||||||
|
**Traces to**: AC-N5
|
||||||
|
**Profile**: static
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
|
||||||
|
| Step | Consumer Action | Expected Response |
|
||||||
|
|------|----------------|------------------|
|
||||||
|
| 1 | Regex sweep `src/` and `mission-planner/src/` for `SoundDetections|DroneMaintenance` | `match_count == 0` |
|
||||||
|
|
||||||
|
**Pass criteria**: row 95.
|
||||||
|
**Expected result source**: `results_report.md` row 95.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NFT-SEC-14: Anti-criterion AC-N1 — no concurrent-edit reconciliation surfaces
|
||||||
|
|
||||||
|
**Traces to**: AC-N1
|
||||||
|
**Profile**: e2e + static
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
|
||||||
|
| Step | Consumer Action | Expected Response |
|
||||||
|
|------|----------------|------------------|
|
||||||
|
| 1 | Open the same annotation in two browser sessions; edit both | both save individually |
|
||||||
|
| 2 | Inspect each session's DOM | no merge UI; no presence indicator |
|
||||||
|
|
||||||
|
**Pass criteria**: row 91.
|
||||||
|
**Notes**: this is an anti-criterion — the test enforces that the feature is NOT silently added.
|
||||||
|
**Expected result source**: `results_report.md` row 91.
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
# Test Data Management
|
||||||
|
|
||||||
|
The Azaion UI is a thin client over a typed REST + SSE contract — it carries no database of its own (P3 / P4 / `data_parameters.md`). "Test data" therefore means three things:
|
||||||
|
|
||||||
|
1. **Suite-side seed fixtures** loaded into the `e2e` profile's docker-compose stack (users, flights, aircraft, classes, sample media).
|
||||||
|
2. **Stub responses** mounted on the `fast` profile's request-interception layer (canned `/api/<service>/*` payloads, canned OpenWeatherMap and OSM tile responses).
|
||||||
|
3. **Source-level fixtures** committed in this repo: i18n bundles, enum spec snapshots, `nginx.conf`, `vite.config.ts`, `package.json`, and the `dist/` build output.
|
||||||
|
|
||||||
|
There are NO image / video / `.tlog` input files under `_docs/00_problem/input_data/` for this project — the cell `data_parameters.md` and `results_report.md` already make this explicit. Every observable assertion lives in `_docs/00_problem/input_data/expected_results/results_report.md` (95 rows) which the tests reference verbatim.
|
||||||
|
|
||||||
|
## Seed Data Sets
|
||||||
|
|
||||||
|
| Data Set | Description | Used by Tests | How Loaded | Cleanup |
|
||||||
|
|----------|-------------|---------------|-----------|---------|
|
||||||
|
| `seed_users` | 4 users: `op_alice` (Operator), `op_bob` (Operator, no SETTINGS permission), `admin_carol` (Admin), `integrator_dave` (System Integrator). All with known passwords for test login. | All `e2e` tests that authenticate | `admin/` service init script reads `seed-fixtures/users.json` at container start | `docker compose down -v` wipes `test-db-data` between e2e suite runs |
|
||||||
|
| `seed_aircraft` | 3 aircraft with one marked `isDefault: true` | E2E tests touching `/flights`, `/admin` aircraft tab | `flights/` service init script | as above |
|
||||||
|
| `seed_flights` | 5 flights spanning the 4 users; some with waypoints, one with `LiveGpsEvent` simulator wired | E2E tests touching `/flights` and the live-GPS stream | `flights/` service init script | as above |
|
||||||
|
| `seed_classes` | The contract's `[0..N-1, 20..20+N-1, 40..40+N-1]` ordering (per AC-37 / `data_model.md:158`) — N≥9 so number-key hotkeys 1..9 are all hot. | `<DetectionClasses>` component + `<AdminPage>` class CRUD tests | `annotations/` (read path) + `admin/` (write path) init scripts | as above |
|
||||||
|
| `seed_media` | 6 media items (3 images, 3 videos) attached to `seed_flights`, with mediaStatus values exercising the full enum after AC-04 fix lands (`None`, `New`, `AiProcessing`, `AiProcessed`, `ManualCreated`, `Confirmed`, `Error`) | E2E tests touching `/annotations` and `/dataset` | `annotations/` init script | as above |
|
||||||
|
| `seed_annotations` | Annotations for `seed_media`, including: some with `Source: AI`, some `Manual`; one with `isSplit: true` and a valid `splitTile` "3 0.5 0.5 0.2 0.2" (AC-39); one with malformed `splitTile` ("garbage", AC-39 sad path); status values spanning the full `AnnotationStatus` enum after AC-04 fix (`None=0`, `Created=10`, `Edited=20`, `Validated=30`, `Deleted=40`) | E2E tests for `<AnnotationsPage>`, `<CanvasEditor>`, `<DatasetPage>` | `annotations/` init script | as above |
|
||||||
|
| `seed_user_settings` | Known `selectedFlightId` and `panelWidths` for `op_alice` so the rehydration tests assert against a deterministic state | AC-21, AC-06 tests | `annotations/` init script | as above |
|
||||||
|
| `enum_spec_snapshot` | A committed JSON file `_docs/00_problem/input_data/enum_spec_snapshot.json` that pins the contract values for `AnnotationStatus`, `MediaStatus`, `Affiliation`, `CombatReadiness`, `MediaType`, `AnnotationSource`, `WaypointSource`, `WaypointObjective`. Populated from `../_docs/00_database_schema.md` (authoritative) with the UI's current drift documented in `ui_drift_summary`. `CombatReadiness` + `MediaType` are flagged `verification_pending: true` because the schema does not pin numeric values; Step 4 .NET-service inspection finalizes them. | Static checks for AC-04 / AC-29 (Group 2 rows 14-19 of `results_report.md`) | Read directly from the repo at test time. | n/a (committed at Phase 3). |
|
||||||
|
| `bundle_artifact` | The `dist/` produced by `vite build`. | AC-11, AC-31, AC-33 (Group 7 rows 40-44) | Built by the CI step before tests run. | Ephemeral per CI run. |
|
||||||
|
|
||||||
|
## Data Isolation Strategy
|
||||||
|
|
||||||
|
- **`fast` profile**: each test creates its own MSW handlers and tears them down in the test runner's `afterEach`. No process-wide state. React tree is unmounted between tests. Test files MAY share an enum spec snapshot (read-only) since it is a contract pin.
|
||||||
|
- **`e2e` profile**: each `e2e` run gets a fresh suite docker-compose stack (`docker compose down -v` then `up -d`) before the suite executes. Inside a run, tests group into **isolation buckets** by data set; a bucket runs sequentially against a known seed state, then the seeds are reset between buckets via `admin/` service's `POST /test-only/reset` (a test-only endpoint, gated behind a non-production build flag). Buckets across different machines run on independent compose stacks.
|
||||||
|
- **Per-user isolation**: tests that mutate user-scoped state (e.g., `selectedFlightId`, panel widths) use distinct seed users so concurrent tests within a bucket cannot race on the same `UserSettings` row.
|
||||||
|
- **No cross-test order dependencies** — any test must be re-runnable in isolation by booting the bucket's seed snapshot.
|
||||||
|
|
||||||
|
## Input Data Mapping
|
||||||
|
|
||||||
|
The UI has no consumer-side input files (images, videos, `.tlog`s). Every test "input" is a **trigger** classified per `test-spec/SKILL.md` "Behavioral shape" — a user action, a request the SPA initiates, an SSE event, or a static check against the repo / `dist/`. The `Input` column of each test in `blackbox-tests.md` / etc. references a `results_report.md` row (which already defines the trigger and the quantifiable observable).
|
||||||
|
|
||||||
|
| Input Reference | Source Location | Description | Covers Scenarios |
|
||||||
|
|-----------------|----------------|-------------|-----------------|
|
||||||
|
| `results_report.md` rows 1-13 | `_docs/00_problem/input_data/expected_results/results_report.md` | Auth & token-handling triggers | Group 1 tests (FT-P-01..FT-P-13 / FT-N-* per blackbox-tests.md) |
|
||||||
|
| `results_report.md` rows 14-21 | as above | Wire-contract / enum compliance triggers | Group 2 tests |
|
||||||
|
| `results_report.md` rows 22-31 | as above | Annotations endpoint / payload / SSE / overlay-window triggers | Group 3 tests |
|
||||||
|
| `results_report.md` rows 32-35 | as above | Flight selection persistence + live-GPS SSE triggers | Group 4 tests |
|
||||||
|
| `results_report.md` rows 36-37 | as above | Dataset bulk-validate triggers | Group 5 tests |
|
||||||
|
| `results_report.md` rows 38-39 | as above | Upload size cap triggers | Group 6 tests |
|
||||||
|
| `results_report.md` rows 40-44 | as above | Build / bundle / routing triggers | Group 7 tests |
|
||||||
|
| `results_report.md` rows 45-48 | as above | i18n triggers | Group 8 tests |
|
||||||
|
| `results_report.md` rows 49-51 | as above | Destructive-UX triggers | Group 9 tests |
|
||||||
|
| `results_report.md` rows 52-59 | as above | A11y triggers | Group 10 tests |
|
||||||
|
| `results_report.md` rows 60-62 | as above | Browser-support + responsive triggers | Group 11 tests |
|
||||||
|
| `results_report.md` row 63 | as above | OWM secrets check | Group 12 test |
|
||||||
|
| `results_report.md` rows 64-65 | as above | User-settings persistence triggers | Group 13 tests |
|
||||||
|
| `results_report.md` rows 66-69 | as above | Form hygiene triggers | Group 14 tests |
|
||||||
|
| `results_report.md` rows 70-72 | as above | CI / image / labels triggers | Group 15 tests |
|
||||||
|
| `results_report.md` rows 73-84 | as above | Canvas + DetectionClasses + PhotoMode triggers | Group 16 tests |
|
||||||
|
| `results_report.md` rows 85-90 | as above | Tile splitting + tile-zoom triggers | Group 17 tests |
|
||||||
|
| `results_report.md` rows 91-95 | as above | Anti-criteria triggers | Group 18 tests |
|
||||||
|
|
||||||
|
## Expected Results Mapping
|
||||||
|
|
||||||
|
Every test in `blackbox-tests.md`, `performance-tests.md`, `resilience-tests.md`, `security-tests.md`, and `resource-limit-tests.md` carries an `Expected result source: results_report.md row <N>` line in its body. The comparison method, tolerance, and reference file (if any) are inherited from that row — tests do not re-state them. The `traceability-matrix.md` aggregates: AC → results_report row(s) → test ID(s).
|
||||||
|
|
||||||
|
| Test Scenario ID | Input Data | Expected Result | Comparison Method | Tolerance | Expected Result Source |
|
||||||
|
|-----------------|------------|-----------------|-------------------|-----------|----------------------|
|
||||||
|
| (see `blackbox-tests.md` etc.) | (row reference) | (row's Expected Result column) | (row's Comparison column) | (row's Tolerance column) | `results_report.md` row N |
|
||||||
|
|
||||||
|
No reference files (e.g. JSON / CSV) are required at this stage — every observable in `results_report.md` is small enough to fit inline. If a downstream test needs a multi-row expected payload (e.g. the suite's full class-distribution response shape), the file will be added in `_docs/00_problem/input_data/expected_results/` per the naming convention `<input_name>_expected.<ext>` and the row updated to reference it. The Phase 3 Data Validation Gate will surface this if it happens.
|
||||||
|
|
||||||
|
## External Dependency Mocks
|
||||||
|
|
||||||
|
| External Service | Mock/Stub | How Provided | Behavior |
|
||||||
|
|-----------------|-----------|-------------|----------|
|
||||||
|
| OpenWeatherMap (E10) | `owm-stub` HTTP service (e2e) / MSW handler (fast) | Docker service `owm-stub:8081` (e2e) / per-test MSW handler (fast) | Returns canned `/data/2.5/onecall` JSON with deterministic wind/precip values. Tests that exercise wind compute (`flightPlanUtils.ts` once moved into `src/`) assert against these canned values. |
|
||||||
|
| OSM tile servers | `tile-stub` HTTP service (e2e) / never hit in fast | Docker service `tile-stub:8082` returning a fixed 256x256 PNG | Replaces `a.tile.openstreetmap.org` etc.; map tile loads do not depend on internet. |
|
||||||
|
| Suite microservices (`admin/`, `flights/`, `annotations/`, `detect/`, ...) | Real services (e2e) / MSW handlers (fast) | docker-compose (e2e) / per-test handler (fast) | Production-shape responses per `data_parameters.md` §1. Errors injected per resilience tests (5xx, 413, network drop). |
|
||||||
|
| GPS-Denied services (target) | `gps-denied-desktop` + `gps-denied-onboard` real services (e2e); no fast coverage today | docker-compose | F12 Test Mode is target-only — tests for it are quarantined until the feature lands (see Open Items in `results_report.md`). |
|
||||||
|
| LiveGPS simulator | Embedded in `flights/` service test mode | docker-compose | Emits deterministic `LiveGpsEvent` payloads at 1 Hz on `/api/flights/<id>/live-gps` so AC-08 timing assertions are stable. |
|
||||||
|
| Annotation-status events generator | Embedded in `annotations/` service test mode | docker-compose | Allows tests to trigger `AnnotationStatusEvent` SSE deliveries on demand. |
|
||||||
|
|
||||||
|
All mocks are deterministic: same input always yields the same output. Non-determinism (timestamps, IDs assigned by the suite) is bounded to suite-managed fields that tests do not compare directly — tests use field presence + shape, not value equality, for those fields.
|
||||||
|
|
||||||
|
## Data Validation Rules
|
||||||
|
|
||||||
|
| Data Type | Validation | Invalid Examples | Expected System Behavior |
|
||||||
|
|-----------|-----------|-----------------|------------------------|
|
||||||
|
| Bearer (Authorization header) | Non-empty string starting `Bearer ` | empty header, missing header | `01_api-transport` adds it automatically for authenticated requests; absence triggers a refresh attempt via 401-retry. |
|
||||||
|
| Refresh cookie (response Set-Cookie) | `Secure; HttpOnly; SameSite=Strict` per AC-03 | Missing `HttpOnly` | Test rejects the response (regression — server contract violation). Documented in the suite, asserted by the UI test for defence-in-depth. |
|
||||||
|
| Annotation save body | `{Source, WaypointId, videoTime, mediaId, detections, status}` keys all present (AC-05 / row 23) | missing `Source`, contains `time` instead of `videoTime` | Test FAILS — finding #32 fix regression. |
|
||||||
|
| Waypoint POST body | `{Geopoint:{Lat,Lon,MGRS}, Source, Objective, OrderNum, Height}` shape per `data_parameters.md` (Step 4 fix candidate) | UI's current `{name, latitude, longitude, order}` | Test FAILS once the Step 4 fix lands; pre-fix the test is quarantined (documents the contract drift). |
|
||||||
|
| Enum numeric value on the wire | Member of the spec value set (AC-04 / rows 14-19) | `status: 1` for `Edited` (UI today) instead of `20` (spec) | Test FAILS — exactly the regression that motivated Step 4 AC-04. |
|
||||||
|
| `splitTile` YOLO label | 5 space-separated numeric tokens (AC-39 / row 86) | `"garbage"` (row 87) | Parser surfaces a user-visible error; does NOT silently swallow. Test asserts the error path. |
|
||||||
|
| Numeric form input | Non-empty, parseable, in range (AC-26 / rows 66-67) | empty string, non-numeric, out-of-range | Validation error shown; no PUT fires. |
|
||||||
|
| File upload | ≤ 500 MB (E9 / AC-10 / row 39) | 501 MB | HTTP 413; UI surfaces a user-visible i18n error; no `alert()`. |
|
||||||
|
| i18n key set | `keys(en.json) == keys(ua.json)` (AC-12 / row 45) | UA missing a key present in EN | Static check FAILS. |
|
||||||
|
|
||||||
|
## Open Items For Phase 3 Validation
|
||||||
|
|
||||||
|
- **Enum spec numeric values** (AC-04): **resolved at Phase 3.** Snapshot committed at `_docs/00_problem/input_data/enum_spec_snapshot.json` per `../_docs/00_database_schema.md` (authoritative). `AnnotationStatus`, `MediaStatus`, `Affiliation`, `AnnotationSource` have pinned numerics from the schema. `CombatReadiness` + `MediaType` carry a `verification_pending: true` flag because the schema doesn't pin numerics on those — Step 4 .NET-service inspection lifts the flag (or reorders if the inferred sequential mapping is wrong). UI drift (5 enums) is pinned in the snapshot's `ui_drift_summary` for Step 4 to consume.
|
||||||
|
- **Phase B / target ACs** (AC-11 bundle gate, AC-18 browser-list, AC-24 SSE refresh, AC-25 async-video path, AC-40 tile-zoom UX): the rows are written but the consumer-side behavior does not exist today. Phase 3 will surface these to the user for: (a) downgrade to documentary, (b) quarantine, or (c) accept as gating the day the feature lands.
|
||||||
|
- **Waypoint POST shape** (`data_parameters.md` finding #20): the contract pin is the suite spec, but the UI's current shape is wrong. Tests assert against the contract. Pre-fix the test is quarantined; the Step 4 fix lands together with un-quarantining.
|
||||||
|
- **AC-37 backend ordering**: the class-hotkey contract depends on the `annotations/` service returning classes in `[0..N-1, 20..20+N-1, 40..40+N-1]`. If the seed reveals a different shape, AC-37 row 79 will fail; the fix may need to land server-side or the UI may need a client-side resort. Phase 3 will surface this gap.
|
||||||
|
- **No `Reference File` rows are needed today** — every `Reference File` cell in `results_report.md` is `N/A`. If Phase 2 reveals a need (e.g. for a complex SSE payload sequence), the reference file lands in `_docs/00_problem/input_data/expected_results/` and the row is updated.
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
# Traceability Matrix
|
||||||
|
|
||||||
|
Maps every acceptance criterion and every restriction in `_docs/00_problem/` to the test scenarios that verify it (this directory) and the expected-result row in `_docs/00_problem/input_data/expected_results/results_report.md` that provides the quantifiable observable. Quarantined tests are marked `[Q]` — they assert against a Phase B target feature or a Step 4 fix that has not landed; they activate the day the implementation ships.
|
||||||
|
|
||||||
|
## Acceptance Criteria Coverage
|
||||||
|
|
||||||
|
| AC ID | Acceptance Criterion (short) | Tests | results_report rows | Coverage |
|
||||||
|
|-------|------------------------------|-------|---------------------|----------|
|
||||||
|
| AC-01 | `credentials:'include'` on every authenticated fetch | FT-P-01 [Q for bootstrap], FT-P-02, NFT-PERF-02, NFT-SEC-04, NFT-RES-01, NFT-RES-08 | 01, 02, 03 | Covered |
|
||||||
|
| AC-02 | Bearer never written to client storage | NFT-SEC-01 | 04 | Covered |
|
||||||
|
| AC-03 | Refresh cookie `Secure HttpOnly SameSite=Strict` | NFT-SEC-02, NFT-SEC-03 | 05, 06, 07 | Covered |
|
||||||
|
| AC-04 | Numeric enums match suite spec | FT-P-04, FT-P-05, FT-P-06 | 14, 15, 16, 17, 18, 19 | Covered (`enum_spec_snapshot.json` committed — Phase 3 gate resolved) |
|
||||||
|
| AC-05 | Annotation save endpoint + required body fields | FT-P-07, FT-P-08 | 22, 23 | Covered |
|
||||||
|
| AC-06 | Selected-flight persistence path | FT-P-16, FT-P-17 | 32, 33 | Covered |
|
||||||
|
| AC-07 | Bulk-validate works | FT-P-20, FT-P-21, NFT-PERF-07 | 36, 37 | Covered |
|
||||||
|
| AC-08 | Live-GPS SSE per flight | FT-P-18, FT-P-19, NFT-PERF-04, NFT-PERF-05, NFT-RES-10, NFT-RES-LIM-06, NFT-RES-LIM-07 | 34, 35, 97 | Covered |
|
||||||
|
| AC-09 | Annotation-status SSE during page lifetime | FT-P-09, FT-P-10, NFT-PERF-06, NFT-RES-LIM-05 | 24, 25, 97 | Covered |
|
||||||
|
| AC-10 | Upload size cap 500 MB + UI error path | FT-N-06, NFT-RES-07, NFT-RES-LIM-02 | 38, 39 | Covered |
|
||||||
|
| AC-11 | Initial JS bundle ≤ 2 MB | NFT-PERF-01, NFT-RES-LIM-01 | 40 | Covered (documentary — no CI gate today) |
|
||||||
|
| AC-12 | i18n key parity en ↔ ua | FT-P-22, FT-P-23 | 45, 46 | Covered |
|
||||||
|
| AC-13 | i18n detector + persistence | FT-P-24 [Q], FT-P-25 [Q] | 47, 48 | Covered (quarantined — Step 4 fix) |
|
||||||
|
| AC-14 | Destructive actions require ConfirmDialog + alert() forbidden | FT-P-26, FT-P-27, FT-N-07, NFT-SEC-07, NFT-SEC-08 | 49, 50, 51 | Covered |
|
||||||
|
| AC-15 | ConfirmDialog a11y | FT-P-28, FT-P-29, FT-N-08 | 52, 53, 54 | Covered |
|
||||||
|
| AC-16 | Header flight dropdown a11y | FT-P-30, FT-P-31, FT-N-09 | 55, 56, 57 | Covered |
|
||||||
|
| AC-17 | ProtectedRoute spinner a11y + timeout | FT-P-32, FT-P-33 [Q], NFT-RES-04 [Q] | 58, 59 | Covered (quarantined for timeout) |
|
||||||
|
| AC-18 | Browser support — Chromium + Firefox latest 2 | FT-P-34, NFT-PERF-10 | 60, 98 | Covered (manual smoke, no automated gate today) |
|
||||||
|
| AC-19 | Mobile / desktop breakpoint variants | FT-P-35, FT-P-36 | 61, 62 | Covered |
|
||||||
|
| AC-20 | OpenWeatherMap key not in source | NFT-SEC-09 [Q for source until Step 4] | 63 | Covered (quarantined for source check) |
|
||||||
|
| AC-21 | UserSettings panel-width persistence | FT-P-37 [Q], FT-P-38 [Q], NFT-PERF-08 [Q] | 64, 65 | Covered (quarantined) |
|
||||||
|
| AC-22 | RBAC client-side route gates | FT-N-03 [Q], FT-N-04, FT-N-05 [Q], NFT-SEC-05 [Q], NFT-SEC-06 [Q], NFT-RES-08 | 08, 09, 10 | Covered (quarantined for `/admin` + `/settings` gates) |
|
||||||
|
| AC-23 | Auth refresh transparency | FT-P-02, FT-P-03, NFT-PERF-02, NFT-RES-01 | 11, 12 | Covered |
|
||||||
|
| AC-24 | SSE bearer-rotation reconnect | NFT-PERF-03 [Q], NFT-RES-02 [Q], NFT-RES-10 | 13, 97 | Covered (quarantined — Step 8 hardening) |
|
||||||
|
| AC-25 | Detect endpoint correctness (sync + async) | FT-P-11, FT-P-12 [Q], FT-P-13 [Q] | 26, 27, 28 | Covered (async path quarantined — F7 target) |
|
||||||
|
| AC-26 | Numeric input hygiene | FT-N-11 [Q], FT-N-12 [Q] | 66, 67 | Covered (quarantined — Step 4 fix) |
|
||||||
|
| AC-27 | Save error surfacing in settings | FT-N-13 [Q], FT-N-14 [Q], NFT-PERF-09 [Q], NFT-RES-05 [Q], NFT-RES-06 [Q] | 68, 69 | Covered (quarantined — Step 4 fix) |
|
||||||
|
| AC-28 | Annotation overlay time window `[-50, +150] ms` | FT-P-14, FT-P-15, FT-N-01, FT-N-02 | 29, 30, 31 | Covered |
|
||||||
|
| AC-29 | `mediaType` is typed (no magic literals) | FT-N-15 | 20, 21 | Covered |
|
||||||
|
| AC-30 | Class delete confirmation | FT-P-26, FT-N-07, NFT-SEC-08 | 49 | Covered (overlaps AC-14 row 49) |
|
||||||
|
| AC-31 | `mission-planner/` not in production bundle | NFT-RES-LIM-04 | 41 | Covered |
|
||||||
|
| AC-32 | CI image tag + OCI labels | NFT-RES-LIM-11, NFT-RES-LIM-12, NFT-RES-LIM-13 | 70, 71, 72 | Covered |
|
||||||
|
| AC-33 | Production runtime `nginx:alpine` only | NFT-RES-LIM-03 | 42 | Covered |
|
||||||
|
| AC-34 | nginx routes 9 services with prefix stripping | NFT-RES-LIM-09, NFT-RES-LIM-10 | 43, 44 | Covered |
|
||||||
|
| AC-35 | Manual bbox draw on CanvasEditor | FT-P-39 | 73 | Covered |
|
||||||
|
| AC-36 | 8-handle resize + Ctrl-multi-select + Ctrl-wheel zoom + Ctrl-drag pan | FT-P-40, FT-P-41, FT-P-42, FT-P-43 | 74, 75, 76, 77 | Covered |
|
||||||
|
| AC-37 | Class picker — load + hotkey + click + fallback | FT-P-44, FT-P-45, FT-P-46, FT-P-47 | 78, 79, 80, 81 | Covered (pending backend ordering check — Phase 3 gate) |
|
||||||
|
| AC-38 | PhotoMode switcher | FT-P-48, FT-P-49, FT-P-50 | 82, 83, 84 | Covered |
|
||||||
|
| AC-39 | Tile-splitting endpoint + parser | FT-P-51 [Q], FT-P-52, FT-P-53, FT-N-10 | 85, 86, 87, 88 | Covered (split surface quarantined) |
|
||||||
|
| AC-40 | Tile-zoom auto-zoom + indicator | FT-P-54 [Q], FT-P-55 [Q] | 89, 90 | Covered (quarantined — UX missing today) |
|
||||||
|
| AC-N1 | No collaborative-edit semantics | NFT-SEC-14 | 91 | Covered |
|
||||||
|
| AC-N2 | No in-browser ML | NFT-SEC-10 | 92 | Covered |
|
||||||
|
| AC-N3 | No offline mode | NFT-RES-03, NFT-SEC-12 | 93 | Covered |
|
||||||
|
| AC-N4 | No response-signature library | NFT-SEC-11 | 94 | Covered |
|
||||||
|
| AC-N5 | Dropped legacy features (Sound Detections, Drone Maintenance) | NFT-SEC-13 | 95 | Covered |
|
||||||
|
|
||||||
|
## Restrictions Coverage
|
||||||
|
|
||||||
|
| RID | Restriction (short) | Tests / Mechanism | Coverage |
|
||||||
|
|-----|---------------------|-------------------|----------|
|
||||||
|
| H1 | ARM64-only production image | NFT-RES-LIM-03 (image base check); the build pipeline produces ARM64 only — meta-config | Covered |
|
||||||
|
| H2 | Edge-device deployment target | NFT-PERF-10, NFT-RES-LIM-08 | Covered (documentary) |
|
||||||
|
| H3 | No GPU expectation in UI image | environment.md (no GPU in test rig); NFT-SEC-10 (no ML libs) — together they verify the constraint | Covered (by composition) |
|
||||||
|
| H4 | HTML5 video + canvas + EventSource on Chromium / Firefox latest 2 | FT-P-34 | Covered |
|
||||||
|
| S1 | TypeScript strict mode | STC-S1: static read of `tsconfig.json` for `"strict": true` (planned static check — added by Phase 3 if accepted) | NOT COVERED — Phase 3 to add |
|
||||||
|
| S2 | React 19 | STC-S2: `package.json` dep version pin | NOT COVERED — Phase 3 to add |
|
||||||
|
| S3 | Vite 6 | STC-S3: `package.json` dep version pin | NOT COVERED — Phase 3 to add |
|
||||||
|
| S4 | Bun 1.3.11 | STC-S4: `package.json` `packageManager` field + Dockerfile base image pin | NOT COVERED — Phase 3 to add |
|
||||||
|
| S5 | Static-bundle output only (no Node in prod image) | NFT-RES-LIM-03 | Covered |
|
||||||
|
| S6 | REST + SSE only (no WebSocket / GraphQL / gRPC-Web) | STC-S6: dep scan for `ws`, `socket.io`, `graphql`, `apollo`, `grpc-web` (planned) | NOT COVERED — Phase 3 to add |
|
||||||
|
| S7 | Two React Contexts only (no Redux / Zustand / TanStack Query) | STC-S7: dep scan for `redux`, `zustand`, `@reduxjs/.*`, `@tanstack/.*` | NOT COVERED — Phase 3 to add |
|
||||||
|
| S8 | Tailwind 4 + `az-*` design tokens | STC-S8: dep version pin + presence of token CSS vars | NOT COVERED — Phase 3 to add |
|
||||||
|
| S9 | `leaflet@1.9.4` + `react-leaflet@5` + `leaflet-draw` + `leaflet-polylinedecorator` | STC-S9: `package.json` version pin set | NOT COVERED — Phase 3 to add |
|
||||||
|
| S10 | `chart.js@4` + `react-chartjs-2@4` | STC-S10 | NOT COVERED — Phase 3 to add |
|
||||||
|
| S11 | `@hello-pangea/dnd@18` | STC-S11 | NOT COVERED — Phase 3 to add |
|
||||||
|
| S12 | `i18next` + `react-i18next` with EN + UA bundles only | FT-P-22, FT-P-23 + STC-S12 (no other locale bundle files) | Partially Covered |
|
||||||
|
| S13 | No client-side persistence library | NFT-SEC-01 + STC-S13 (dep scan for `localforage`, `idb`, `dexie`) | Partially Covered |
|
||||||
|
| S14 | No test framework configured today | META — these tests' very existence supersedes this restriction; resolved at Step 5 (Decompose Tests) per `acceptance_criteria.md` AC-14 / S14 note | N/A — meta |
|
||||||
|
| E1 | Air-gap-friendly bundle | NFT-RES-03 (offline boot) + external-dep stubs in environment.md | Partially Covered |
|
||||||
|
| E2 | nginx strips `/api/<service>/` per service | NFT-RES-LIM-09, NFT-RES-LIM-10 | Covered |
|
||||||
|
| E3 | `Secure HttpOnly SameSite=Strict` refresh cookie | NFT-SEC-03 | Covered |
|
||||||
|
| E4 | Vite dev proxy at `/api → http://localhost:8080` | dev-only; not testable in production runtime | NOT COVERED — meta-config (Phase 3 to confirm) |
|
||||||
|
| E5 | `AZAION_REVISION` stamped at build | NFT-RES-LIM-13 | Covered |
|
||||||
|
| E6 | OCI image labels | NFT-RES-LIM-12 | Covered |
|
||||||
|
| E7 | Image registry + tag scheme | NFT-RES-LIM-11 | Covered |
|
||||||
|
| E8 | Branch triggers (`dev` / `stage` / `main`) | STC-E8: parse `.woodpecker/build-arm.yml` triggers (planned) | NOT COVERED — Phase 3 to add |
|
||||||
|
| E9 | `client_max_body_size 500M` | NFT-RES-LIM-02 | Covered |
|
||||||
|
| E10 | OpenWeatherMap direct-from-browser today | NFT-SEC-09 (key check) + environment.md `owm-stub` (E2E isolation) | Covered |
|
||||||
|
| O1 | Bilingual UI mandatory | FT-P-22, FT-P-23 | Covered |
|
||||||
|
| O2 | Bearer never in localStorage / sessionStorage | NFT-SEC-01 | Covered |
|
||||||
|
| O3 | `credentials:'include'` on every authenticated fetch | NFT-SEC-04 | Covered |
|
||||||
|
| O4 | RBAC is server-enforced (UI must NOT trust `AuthUser.role`) | meta-design + FT-N-04 (unauth → /login regardless of any client claim); STC-O4 (no `if (user.role === 'admin')` gating sensitive data, only UI hide/show) | Partially Covered — Phase 3 may add the static gate-pattern lint |
|
||||||
|
| O5 | Refresh cookie attributes | NFT-SEC-03 | Covered |
|
||||||
|
| O6 | No hardcoded credentials | NFT-SEC-09 | Covered |
|
||||||
|
| O7 | Spec is source of truth for numeric enums | FT-P-04, FT-P-05, FT-P-06 | Covered |
|
||||||
|
| O8 | Persist what you type (panel widths) | FT-P-37 [Q], FT-P-38 [Q] | Covered (quarantined) |
|
||||||
|
| O9 | Admin can edit existing detection classes (P12) | NOT COVERED — feature missing today (`acceptance_criteria.md` notes P12 violation; PATCH endpoint to re-introduce in Phase B) | NOT COVERED — Phase B target |
|
||||||
|
| O10 | Destructive actions require ConfirmDialog | NFT-SEC-08, FT-P-26, FT-P-27, FT-N-07 | Covered |
|
||||||
|
| O11 | No SSR / RSC | NFT-RES-LIM-03 (no Node in image) + STC-O11 (no `react-dom/server` import) | Partially Covered |
|
||||||
|
| O12 | `mission-planner/` not compiled by production Vite build | NFT-RES-LIM-04 | Covered |
|
||||||
|
| O13 | Bundle size budget ≤ ~2 MB gzipped initial JS | NFT-PERF-01, NFT-RES-LIM-01 | Covered (target — no CI gate today) |
|
||||||
|
| O14 | No CI test step today | META — resolved at Step 5 (Decompose Tests) | N/A — meta |
|
||||||
|
| O15 | No vuln scan / SBOM / image signing | NOT COVERED — Step 6 / security_approach surface; Phase B addition | NOT COVERED |
|
||||||
|
|
||||||
|
## Coverage Summary
|
||||||
|
|
||||||
|
| Category | Total Items | Covered | Partially Covered | Not Covered | N/A (meta) | Coverage % (Covered+Partial) |
|
||||||
|
|----------|-------------|---------|-------------------|-------------|-----------|--------------------|
|
||||||
|
| Acceptance Criteria | 40 | 40 | 0 | 0 | 0 | 100% (24 fully ungated, 16 with Phase 3 quarantine markers) |
|
||||||
|
| Anti-Criteria | 5 | 5 | 0 | 0 | 0 | 100% |
|
||||||
|
| Restrictions | 41 | 17 | 8 | 13 | 3 | 61% |
|
||||||
|
| **Total** | **86** | **62** | **8** | **13** | **3** | **81%** |
|
||||||
|
|
||||||
|
Acceptance criterion coverage exceeds the 75 % template threshold. Restriction coverage is short of 75 % because most of the un-covered restrictions are dependency-version pins (S1-S11) for which a single static check pass (planned `STC-S*` family) would lift them to Covered without changing the SPA's observable behavior.
|
||||||
|
|
||||||
|
## Uncovered Items Analysis
|
||||||
|
|
||||||
|
| Item | Reason Not Covered | Risk | Mitigation |
|
||||||
|
|------|-------------------|------|-----------|
|
||||||
|
| S1-S11 (TS strict, React/Vite/Bun version pins, Tailwind, Leaflet, Chart.js, DnD) | Dependency-version restrictions need a static `package.json` / `tsconfig.json` parser pass; not authored in this phase | Drift between pinned and installed versions (e.g., transitive resolution); accidental upgrade in a refactor breaking ADRs (ADR-001 …) | Phase 3 to confirm adding the `STC-S*` family; otherwise Step 4 / Step 8 may add them as part of the testability fix |
|
||||||
|
| S12, S13 partials | `STC-S12` (no other locale bundles) and `STC-S13` (no IndexedDB / localForage / Dexie deps) need explicit dep scans | Low — currently aligned per `package.json`; risk only on dep additions | Phase 3 promotion to a single static dep-scan job (run alongside the dep-license lint) |
|
||||||
|
| E4, E8 | Dev-only proxy and pipeline branch triggers are not consumer-observable from a production build | None today; future risk if the proxy diverges from the prod nginx | Document-only; Phase 3 confirms |
|
||||||
|
| O4 partial (RBAC trust pattern) | No lint rule today; the design intent is captured but not asserted in code | UI accidentally gates sensitive data on a client claim | Phase 3 to add an `STC-O4` lint that bans `if (user.role === 'admin') { /* reveal data */ }` patterns; FT-N-04 already locks the unauth path |
|
||||||
|
| O9 (admin can edit classes — P12) | The feature does not exist today (only add + delete); cannot test absence of a regression | Functionality missing relative to spec | Phase B feature cycle will add `PATCH /api/admin/classes/{id}` plus FT-P-XX in the cycle that ships it; AC-37 / AC-30 already enforce the surrounding contract |
|
||||||
|
| O11 partial | NFT-RES-LIM-03 confirms no Node binary in the image; an explicit "no `react-dom/server` import" lint is missing | Could regress to SSR by accident on a refactor; the image base check would still pass | Phase 3 confirms `STC-O11` |
|
||||||
|
| O15 (vuln scan / SBOM / signing) | Pipeline does not emit any of these today | Supply-chain risk | Addressed at Step 6 / `security_approach.md`; outside the test-spec scope |
|
||||||
|
| AC-11, AC-18, AC-24, AC-40, AC-25 async path | Phase B / target features with quarantined tests | Tests do not gate today — risk = the feature ships without the assertion being un-quarantined | Phase 3 chooses: keep quarantined (gates the day the feature ships) OR downgrade to documentary; recommendation: keep quarantined |
|
||||||
|
| AC-04 enum spec numeric values for `MediaStatus` / `Affiliation` / `CombatReadiness` | The exact spec values are not pinned in `enum_spec_snapshot.json` yet | Tests use symbolic comparison and may silently match a wrong UI state | Phase 3 to require population of the snapshot before AC-04 tests gate CI; recommendation: BLOCK Step 4 until the snapshot is committed |
|
||||||
|
| AC-37 backend ordering | The class-hotkey contract requires `[0..N-1, 20..20+N-1, 40..40+N-1]` ordering from the suite; not yet verified | Hotkey test fails at integration; ambiguous responsibility | Phase 3 to surface; fix can land server-side or via a client-side resort, depending on the verification result |
|
||||||
|
|
||||||
|
## Quarantine List (running)
|
||||||
|
|
||||||
|
The following 18 tests assert against a Phase B target or a Step 4 fix and are quarantined until the implementation lands. Phase 3 will decide their disposition.
|
||||||
|
|
||||||
|
| Test | Reason | Activates when |
|
||||||
|
|------|--------|---------------|
|
||||||
|
| FT-P-01 (bootstrap part) | Bootstrap refresh missing `credentials:'include'` per finding | Step 4 fix |
|
||||||
|
| FT-P-12, FT-P-13 | Async video detect (F7) not wired | Phase B feature cycle |
|
||||||
|
| FT-P-24, FT-P-25 | i18n detector + persistence missing | Step 4 fix |
|
||||||
|
| FT-P-33 (timeout) | ProtectedRoute timeout missing | Step 4 fix |
|
||||||
|
| FT-P-37, FT-P-38 | Panel-width writer missing (`useResizablePanel`) | Step 4 fix |
|
||||||
|
| FT-P-51 | Split surface not on dataset page today | Phase B |
|
||||||
|
| FT-P-54, FT-P-55 | Tile-zoom UX missing (finding #24) | Phase B |
|
||||||
|
| FT-N-03, FT-N-05 | `/admin` and `/settings` role-gates missing | Step 4 / Step 8 |
|
||||||
|
| FT-N-11, FT-N-12, FT-N-13, FT-N-14 | Settings form hygiene fixes missing | Step 4 |
|
||||||
|
| NFT-PERF-03 / NFT-RES-02 | SSE refresh-rotation reconnect missing | Step 8 hardening |
|
||||||
|
| NFT-PERF-08 / NFT-PERF-09 | Tied to FT-P-37 / FT-N-13 quarantines | per above |
|
||||||
|
| NFT-SEC-05, NFT-SEC-06 | Tied to FT-N-03, FT-N-05 | per above |
|
||||||
|
| NFT-SEC-09 (source check) | OpenWeatherMap key still in source today | Step 4 fix |
|
||||||
|
| NFT-RES-04 | Tied to FT-P-33 | per above |
|
||||||
|
|
||||||
|
## Phase 3 (Data Validation Gate) — Open Items to Resolve
|
||||||
|
|
||||||
|
### Resolved in Phase 3
|
||||||
|
|
||||||
|
1. ~~**`enum_spec_snapshot.json`** — populate from the suite spec before AC-04 tests gate CI.~~ → **Resolved:** snapshot committed at `_docs/00_problem/input_data/enum_spec_snapshot.json`. `verification_pending: true` markers remain for `CombatReadiness` and `MediaType` (numeric ordering inferred from schema member-listing); Step 4 .NET-service inspection lifts those.
|
||||||
|
2. ~~**NFT-RES-09** — no `results_report.md` row binding.~~ → **Resolved:** row 96 added (tainted-canvas fallback observable).
|
||||||
|
3. ~~**NFT-RES-10** — no `results_report.md` row binding.~~ → **Resolved:** row 97 added (SSE server-disconnect observable; ≤ 10 s indicator-or-reconnect; reconnect attempts ≤ 1 in window).
|
||||||
|
4. ~~**NFT-PERF-10** — FCP baseline has no `results_report.md` row.~~ → **Resolved:** row 98 added (`FCP ≤ 3 000 ms` on warm-cache navigation to `/flights`, headless Chromium, 2 vCPU / 4 GB edge profile).
|
||||||
|
|
||||||
|
### Still open (carry forward to Step 4 / runner)
|
||||||
|
|
||||||
|
5. **AC-37 backend ordering** — verify the `annotations/` service response shape and either confirm matches or schedule a fix on the appropriate side.
|
||||||
|
6. **STC family** — confirm adding the `STC-S*` / `STC-O4` / `STC-O11` static-check IDs to lift the restriction coverage above 75 %.
|
||||||
|
7. **Quarantine disposition** — accept the quarantine list above; decide whether quarantined tests gate CI today (recommended: no, but they are picked up automatically the day the feature lands).
|
||||||
|
8. **AC-04 UI enum drift in `src/types/index.ts`** — tests will FAIL until the Step 4 fix lands per `acceptance_criteria.md`; quarantine until Step 4 OR run them and use the failure as the gate to schedule Step 4. The drift list is pinned in `enum_spec_snapshot.json` → `ui_drift_summary` (5 enums).
|
||||||
|
9. **Parent-suite doc fixes (leftover)** — `../_docs/01_annotations.md` line 208 and `../_docs/09_dataset_explorer.md` line 165 show stale `affiliation: 2 // Hostile` examples; should be 20 per `00_database_schema.md`. Record as a leftover in `_docs/_process_leftovers/` when raised against the parent suite.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Task Dependencies
|
||||||
|
|
||||||
|
| Task | Name | Epic | Complexity | Depends on |
|
||||||
|
|------|------|------|-----------|------------|
|
||||||
|
| AZ-448 | C01 — Externalize OWM API key | AZ-447 | 2 | None |
|
||||||
|
| AZ-449 | C02 — Externalize OWM base URL | AZ-447 | 2 | AZ-448 (same file) |
|
||||||
|
| AZ-450 | C03 — Externalize map tile URLs | AZ-447 | 2 | None |
|
||||||
|
| AZ-451 | C04 — Bundle Leaflet marker icon locally | AZ-447 | 2 | None |
|
||||||
|
| AZ-452 | C05 — `getApiBase()` accessor | AZ-447 | 3 | None |
|
||||||
|
| AZ-453 | C06 — `navigateToLoginImpl()` accessor | AZ-447 | 2 | None |
|
||||||
|
| AZ-454 | C07 — Document `setToken/getToken` | AZ-447 | 1 | None |
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Epic AZ-447 is the umbrella for the autodev existing-code Step 4 testability run (`01-testability-refactoring`).
|
||||||
|
- AZ-448 and AZ-449 share `src/features/flights/flightPlanUtils.ts` and should land in one commit to avoid a mid-state where the URL still hardcodes a base while the key is externalized.
|
||||||
|
- Total: 14 complexity points across 7 tasks.
|
||||||
|
- Every task fits the existing-code flow Step 4 allowed-change list (externalize hardcoded URLs/credentials, wrap globals in thin accessors, comment-only documentation). Deferred items are in `_docs/04_refactoring/01-testability-refactoring/deferred_to_refactor.md`.
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
# Externalize OpenWeatherMap API key
|
||||||
|
|
||||||
|
**Task**: AZ-448_refactor_owm_api_key
|
||||||
|
**Name**: Externalize OWM API key to VITE_OWM_API_KEY
|
||||||
|
**Description**: Move the hardcoded OpenWeatherMap API key from `flightPlanUtils.ts` into a Vite-driven env var so it can be redacted in source and overridden in the test profiles.
|
||||||
|
**Complexity**: 2 points
|
||||||
|
**Dependencies**: None
|
||||||
|
**Component**: `src/features/flights/flightPlanUtils.ts`, `.env.example`, `src/vite-env.d.ts`
|
||||||
|
**Tracker**: AZ-448
|
||||||
|
**Epic**: AZ-447
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
`src/features/flights/flightPlanUtils.ts:60` inlines `const apiKey = '335799082893fad97fa36118b131f919'`. The literal:
|
||||||
|
|
||||||
|
- Violates restriction O6 (no hardcoded credentials) and AC-O6 (`acceptance_criteria.md`).
|
||||||
|
- Blocks the security static check NFT-SEC-09 (results_report.md row 63 — quarantined until this fix lands).
|
||||||
|
- Prevents the e2e profile's `owm-stub` from intercepting OWM calls deterministically.
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
- Source tree has no literal OWM key string after the change.
|
||||||
|
- The fast and e2e test profiles can supply test-time keys via env vars without touching source.
|
||||||
|
- NFT-SEC-09 source-string check (`scripts/run-tests.sh --static-only`) passes.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
### Included
|
||||||
|
- Reading `import.meta.env.VITE_OWM_API_KEY` at call time inside `getWeatherData`.
|
||||||
|
- Adding `.env.example` (workspace-root) documenting the variable.
|
||||||
|
- Extending the `ImportMetaEnv` type in `src/vite-env.d.ts`.
|
||||||
|
|
||||||
|
### Excluded
|
||||||
|
- Changing the `getWeatherData(lat, lon)` signature or callers.
|
||||||
|
- Reworking the weather-fetch error handling — only the key sourcing changes.
|
||||||
|
- Touching the OWM base URL (that is C02 / AZ-449).
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
**AC-1: Key sourcing**
|
||||||
|
Given the SPA is built with `VITE_OWM_API_KEY=<key>` in the environment,
|
||||||
|
When `getWeatherData(lat, lon)` is invoked,
|
||||||
|
Then the resulting HTTP request URL contains `appid=<key>`.
|
||||||
|
|
||||||
|
**AC-2: No literal in source**
|
||||||
|
Given a `grep -r "335799082893fad97fa36118b131f919" src/` after the change,
|
||||||
|
When the grep runs,
|
||||||
|
Then the literal key produces no matches.
|
||||||
|
|
||||||
|
**AC-3: Missing-env tolerance**
|
||||||
|
Given the SPA is built with `VITE_OWM_API_KEY` unset,
|
||||||
|
When `getWeatherData(lat, lon)` is invoked,
|
||||||
|
Then it returns `null` (matches the existing try/catch fallback) and does NOT throw.
|
||||||
|
|
||||||
|
**AC-4: Type declaration**
|
||||||
|
Given a TypeScript build (`bun run build`),
|
||||||
|
When the env interface is resolved,
|
||||||
|
Then `ImportMetaEnv` in `src/vite-env.d.ts` declares `readonly VITE_OWM_API_KEY: string | undefined`.
|
||||||
|
|
||||||
|
## Non-Functional Requirements
|
||||||
|
|
||||||
|
**Compatibility**
|
||||||
|
- Build tooling pinned to Vite 6 (S3) — `import.meta.env` is built-in; no new dependency.
|
||||||
|
|
||||||
|
**Security**
|
||||||
|
- `.env.example` MUST use a placeholder (`<your-key>`) — never check in the production key.
|
||||||
|
|
||||||
|
## Blackbox Tests
|
||||||
|
|
||||||
|
| AC Ref | Initial Data / Conditions | What to Test | Expected Behavior | NFR References |
|
||||||
|
|--------|--------------------------|--------------|-------------------|----------------|
|
||||||
|
| AC-1 | `VITE_OWM_API_KEY=test-key-123` set at build; `getWeatherData(0, 0)` called | Outbound fetch URL contains `appid=test-key-123` | Pass | — |
|
||||||
|
| AC-2 | Repo state after task lands | `grep -r "335799082893fad97fa36118b131f919" src/` | No matches | Security |
|
||||||
|
| AC-3 | `VITE_OWM_API_KEY` undefined at build; `getWeatherData(0, 0)` called | Function returns null without throwing | Pass | — |
|
||||||
|
| AC-4 | Repo state after task lands | `src/vite-env.d.ts` extends `ImportMetaEnv` with the field | Declaration present | Compat |
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
- Adhere to S3 (Vite 6) and the existing project tooling — no new dependency, no `process.env` polyfill.
|
||||||
|
- Adhere to AC-N4 (no signature library) — env-driven config only.
|
||||||
|
|
||||||
|
## Risks & Mitigation
|
||||||
|
|
||||||
|
**Risk 1: Build-time leakage**
|
||||||
|
- *Risk*: Vite inlines `import.meta.env.VITE_*` at build time, so a build run with the production key embedded into `dist/` leaks the key into the bundle.
|
||||||
|
- *Mitigation*: This is a known Vite behavior, identical to today's hardcoded key. Long-term fix is to proxy OWM through the suite's `loader/` (per E10) — out of scope for this testability task; flagged for Phase B.
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# Externalize OpenWeatherMap base URL
|
||||||
|
|
||||||
|
**Task**: AZ-449_refactor_owm_base_url
|
||||||
|
**Name**: Externalize OWM base URL to VITE_OWM_BASE_URL
|
||||||
|
**Description**: Move the hardcoded `https://api.openweathermap.org/data/2.5/weather` URL into a Vite env var so the e2e profile's `owm-stub` can intercept OWM calls without code edits.
|
||||||
|
**Complexity**: 2 points
|
||||||
|
**Dependencies**: AZ-448_refactor_owm_api_key (same file)
|
||||||
|
**Component**: `src/features/flights/flightPlanUtils.ts`, `.env.example`, `src/vite-env.d.ts`
|
||||||
|
**Tracker**: AZ-449
|
||||||
|
**Epic**: AZ-447
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
The OWM endpoint is hardcoded at `flightPlanUtils.ts:60`. The e2e test profile spins up an `owm-stub:8081` service to keep tests deterministic and avoid rate-limiting, but the stub cannot intercept calls when the base URL is baked into source.
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
- The base URL is resolvable from `import.meta.env.VITE_OWM_BASE_URL` at call time.
|
||||||
|
- Default behavior (unset env var) preserves today's production endpoint.
|
||||||
|
- The e2e profile sets `VITE_OWM_BASE_URL=http://owm-stub:8081/data/2.5` at build time and weather calls hit the stub.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
### Included
|
||||||
|
- Reading `VITE_OWM_BASE_URL` (default `https://api.openweathermap.org/data/2.5`) inside `getWeatherData`.
|
||||||
|
- Adding the entry to `.env.example` and `src/vite-env.d.ts`.
|
||||||
|
|
||||||
|
### Excluded
|
||||||
|
- Any change to error handling, caching, or response shape.
|
||||||
|
- Touching the API key (AZ-448 covers it).
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
**AC-1: Default endpoint preserved**
|
||||||
|
Given `VITE_OWM_BASE_URL` is unset at build time,
|
||||||
|
When `getWeatherData(lat, lon)` is invoked,
|
||||||
|
Then the fetch target hostname is `api.openweathermap.org`.
|
||||||
|
|
||||||
|
**AC-2: Override honored**
|
||||||
|
Given `VITE_OWM_BASE_URL=http://owm-stub:8081/data/2.5` at build time,
|
||||||
|
When `getWeatherData(lat, lon)` is invoked,
|
||||||
|
Then the fetch target URL begins with `http://owm-stub:8081/data/2.5/weather?lat=...`.
|
||||||
|
|
||||||
|
**AC-3: Type declaration**
|
||||||
|
Given a TypeScript build,
|
||||||
|
When the env interface resolves,
|
||||||
|
Then `ImportMetaEnv` declares `readonly VITE_OWM_BASE_URL: string | undefined`.
|
||||||
|
|
||||||
|
## Blackbox Tests
|
||||||
|
|
||||||
|
| AC Ref | Initial Data / Conditions | What to Test | Expected Behavior | NFR References |
|
||||||
|
|--------|--------------------------|--------------|-------------------|----------------|
|
||||||
|
| AC-1 | env unset; `getWeatherData(0, 0)` called | Outbound URL host | `api.openweathermap.org` | Compat |
|
||||||
|
| AC-2 | env set to stub; `getWeatherData(0, 0)` called | Outbound URL prefix | Matches stub URL | E2E determinism |
|
||||||
|
| AC-3 | Build run | `vite-env.d.ts` declaration present | Pass | Compat |
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
- Land in one commit with AZ-448 to keep the file's mid-state coherent.
|
||||||
|
- Must not break the (unset-env) production behavior.
|
||||||
|
|
||||||
|
## Risks & Mitigation
|
||||||
|
|
||||||
|
**Risk 1: Trailing-slash drift**
|
||||||
|
- *Risk*: A consumer might set the env to `https://host/` and the path concatenation produces `//weather`.
|
||||||
|
- *Mitigation*: Normalize by trimming a trailing slash before concatenation; document the rule in `.env.example`.
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# Externalize map tile URLs
|
||||||
|
|
||||||
|
**Task**: AZ-450_refactor_tile_urls
|
||||||
|
**Name**: Externalize OSM + Esri tile URLs
|
||||||
|
**Description**: Move the hardcoded OSM + Esri-satellite tile URLs into Vite env vars so the e2e profile's `tile-stub` can serve tiles deterministically and the air-gap test (AC-N3 / NFT-RES-03) does not trigger external network calls.
|
||||||
|
**Complexity**: 2 points
|
||||||
|
**Dependencies**: None
|
||||||
|
**Component**: `src/features/flights/types.ts`, `.env.example`, `src/vite-env.d.ts`
|
||||||
|
**Tracker**: AZ-450
|
||||||
|
**Epic**: AZ-447
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
`src/features/flights/types.ts:56-57` inlines:
|
||||||
|
|
||||||
|
- `classic: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'`
|
||||||
|
- `satellite: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'`
|
||||||
|
|
||||||
|
These URLs:
|
||||||
|
|
||||||
|
- Break the e2e profile's `tile-stub:8082` intercept (Leaflet bypasses any local override path).
|
||||||
|
- Make AC-N3 / NFT-RES-03 (offline boot) flap because Leaflet attempts external tile fetches on the map mount.
|
||||||
|
- Violate restriction E1 (air-gap-friendly bundle).
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
- Both tile URLs are resolvable from `import.meta.env.VITE_OSM_TILE_URL` and `VITE_ESRI_TILE_URL`.
|
||||||
|
- Defaults preserve today's production URLs.
|
||||||
|
- The e2e profile redirects tile traffic to `tile-stub` without source edits.
|
||||||
|
- Call sites in `FlightMap.tsx` (and any other consumer of `TILE_URLS`) are unchanged.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
### Included
|
||||||
|
- `TILE_URLS` re-exported from `types.ts` as a const computed from `import.meta.env`.
|
||||||
|
- `.env.example` and `src/vite-env.d.ts` updated.
|
||||||
|
|
||||||
|
### Excluded
|
||||||
|
- Any Leaflet config change in `FlightMap.tsx`.
|
||||||
|
- Adding new tile providers.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
**AC-1: Default tiles preserved**
|
||||||
|
Given env vars unset,
|
||||||
|
When `FlightMap.tsx` mounts with `mapType='classic'`,
|
||||||
|
Then the Leaflet `TileLayer.url` resolves to `https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png`.
|
||||||
|
|
||||||
|
**AC-2: Override honored — classic**
|
||||||
|
Given `VITE_OSM_TILE_URL=http://tile-stub:8082/{z}/{x}/{y}.png` at build time,
|
||||||
|
When `FlightMap.tsx` mounts,
|
||||||
|
Then the Leaflet `TileLayer.url` resolves to the stub URL.
|
||||||
|
|
||||||
|
**AC-3: Override honored — satellite**
|
||||||
|
Given `VITE_ESRI_TILE_URL=http://tile-stub:8082/sat/{z}/{y}/{x}` at build time,
|
||||||
|
When `mapType='satellite'`,
|
||||||
|
Then the Leaflet `TileLayer.url` resolves to the stub URL.
|
||||||
|
|
||||||
|
**AC-4: Type declarations**
|
||||||
|
Given a TypeScript build,
|
||||||
|
Then `ImportMetaEnv` declares both fields as `string | undefined`.
|
||||||
|
|
||||||
|
## Blackbox Tests
|
||||||
|
|
||||||
|
| AC Ref | Initial Data / Conditions | What to Test | Expected Behavior | NFR References |
|
||||||
|
|--------|--------------------------|--------------|-------------------|----------------|
|
||||||
|
| AC-1 | env unset; FlightMap mounted | TileLayer URL (DOM `src` of the first tile request) | OSM URL | Compat |
|
||||||
|
| AC-2/3 | env set; FlightMap mounted | TileLayer URL captured at network boundary | Stub URL | E2E determinism |
|
||||||
|
| AC-4 | Build run | `vite-env.d.ts` declarations present | Pass | Compat |
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
- Leaflet's TileLayer API surface MUST NOT change.
|
||||||
|
- `TILE_URLS.classic | TILE_URLS.satellite` accessor pattern preserved.
|
||||||
|
|
||||||
|
## Risks & Mitigation
|
||||||
|
|
||||||
|
**Risk 1: Computed-at-module-load timing**
|
||||||
|
- *Risk*: `import.meta.env` is statically replaced at build time; module-load timing in tests using Vite's dev server should match.
|
||||||
|
- *Mitigation*: Standard Vite pattern — no special handling needed. Smoke-test by toggling the env var across two builds.
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# Bundle the Leaflet marker icon locally
|
||||||
|
|
||||||
|
**Task**: AZ-451_refactor_leaflet_marker_icon
|
||||||
|
**Name**: Replace `unpkg.com` marker icon URL with a Vite-bundled asset
|
||||||
|
**Description**: Stop loading the Leaflet marker icon from `unpkg.com` at runtime. Import the PNG from the already-installed `leaflet` package as a Vite asset so the bundle is air-gap-friendly and the version matches the pinned dependency.
|
||||||
|
**Complexity**: 2 points
|
||||||
|
**Dependencies**: None
|
||||||
|
**Component**: `src/features/flights/mapIcons.ts`, bundled asset under `dist/assets/`
|
||||||
|
**Tracker**: AZ-451
|
||||||
|
**Epic**: AZ-447
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
`src/features/flights/mapIcons.ts:18` sets `iconUrl: 'https://unpkg.com/leaflet@1.7.1/dist/images/marker-icon.png'`. This:
|
||||||
|
|
||||||
|
- Breaks AC-N3 / NFT-RES-03 / restriction E1 (air-gap-friendly bundle) — the app fetches from `unpkg.com` on every map mount.
|
||||||
|
- Pins `leaflet@1.7.1` in a URL while `package.json` pins `leaflet@^1.9.4` — a silent version mismatch.
|
||||||
|
- Cannot be intercepted by the e2e `tile-stub` (different host).
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
- No external CDN URL remains in `src/`.
|
||||||
|
- The marker icon renders from a bundled asset path produced by Vite (e.g., `/assets/marker-icon-<hash>.png`).
|
||||||
|
- The icon version matches the pinned `leaflet` package version.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
### Included
|
||||||
|
- `import markerIcon from 'leaflet/dist/images/marker-icon.png'` in `mapIcons.ts`.
|
||||||
|
- Updating `iconUrl` to use the imported asset.
|
||||||
|
|
||||||
|
### Excluded
|
||||||
|
- Any change to icon size, anchor, or color logic in `mapIcons.ts`.
|
||||||
|
- Adding new marker variants.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
**AC-1: No external CDN**
|
||||||
|
Given the repo after the change,
|
||||||
|
When `grep -r "unpkg.com" src/` runs,
|
||||||
|
Then it produces no matches.
|
||||||
|
|
||||||
|
**AC-2: Asset bundled by Vite**
|
||||||
|
Given a `bun run build`,
|
||||||
|
When the `dist/` directory is inspected,
|
||||||
|
Then a file matching `dist/assets/marker-icon-*.png` exists and `dist/index.html` or a chunk references it.
|
||||||
|
|
||||||
|
**AC-3: Marker still renders**
|
||||||
|
Given a manual smoke against the running dev server,
|
||||||
|
When the `FlightMap` displays a waypoint,
|
||||||
|
Then the marker icon is visible at the expected position (no broken-image placeholder).
|
||||||
|
|
||||||
|
## Blackbox Tests
|
||||||
|
|
||||||
|
| AC Ref | Initial Data / Conditions | What to Test | Expected Behavior | NFR References |
|
||||||
|
|--------|--------------------------|--------------|-------------------|----------------|
|
||||||
|
| AC-1 | Repo state after task lands | `grep -r unpkg.com src/` | No matches | Security / air-gap |
|
||||||
|
| AC-2 | `dist/` after build | `find dist/assets -name 'marker-icon-*.png'` | Exactly one match | Compat |
|
||||||
|
| AC-3 | Dev server running | Visual smoke | Marker visible | UX |
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
- Adhere to S9 (Leaflet 1.9.4) — use the pinned package's assets.
|
||||||
|
- The `customIcon` export shape MUST NOT change (callers depend on the `L.divIcon | L.icon` instance).
|
||||||
|
|
||||||
|
## Risks & Mitigation
|
||||||
|
|
||||||
|
**Risk 1: Type for the PNG import**
|
||||||
|
- *Risk*: TypeScript may complain about `import markerIcon from '...png'` without a module declaration.
|
||||||
|
- *Mitigation*: Vite's default `client.d.ts` (already referenced via `tsconfig.json` "types") provides the declaration. If TS still complains, extend `src/vite-env.d.ts` with the asset declaration.
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# Add a getApiBase() accessor
|
||||||
|
|
||||||
|
**Task**: AZ-452_refactor_api_base_accessor
|
||||||
|
**Name**: Extract `getApiBase()` for `/api/...` prefix
|
||||||
|
**Description**: Add a thin accessor that prepends an optional base URL to every API request. Default empty string preserves today's behavior. Tests and alternative deployments can supply `VITE_API_BASE_URL`.
|
||||||
|
**Complexity**: 3 points
|
||||||
|
**Dependencies**: None
|
||||||
|
**Component**: `src/api/client.ts`, `src/api/sse.ts`, `src/vite-env.d.ts`
|
||||||
|
**Tracker**: AZ-452
|
||||||
|
**Epic**: AZ-447
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
`src/api/client.ts:44` and every `api.get('/api/...')` call site assume the SPA and the suite share the same nginx (production layout). There is no override hook for test scenarios that need to redirect API traffic to a different host (e.g., MSW handlers serving from a separate origin, or e2e runs targeting a specific suite revision).
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
- A `getApiBase()` function is exported from `src/api/client.ts`.
|
||||||
|
- `request()`, `refreshToken()`, and `createSSE()` prepend its result to the URL.
|
||||||
|
- Default `''` preserves every existing call site unchanged.
|
||||||
|
- Tests can override per build via `VITE_API_BASE_URL`.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
### Included
|
||||||
|
- New `getApiBase()` function in `src/api/client.ts`.
|
||||||
|
- Threading the value through `request()` + `refreshToken()` in `client.ts` and `createSSE()` in `sse.ts`.
|
||||||
|
- `.env.example` and `src/vite-env.d.ts` updated.
|
||||||
|
|
||||||
|
### Excluded
|
||||||
|
- Changing the `api.get` / `api.post` / `api.put` / `api.patch` / `api.delete` / `api.upload` signatures.
|
||||||
|
- Touching the auth header or 401-retry logic.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
**AC-1: Default preserves today's behavior**
|
||||||
|
Given `VITE_API_BASE_URL` is unset,
|
||||||
|
When `api.get('/api/admin/auth/me')` is invoked,
|
||||||
|
Then the fetch URL is `/api/admin/auth/me` (no prefix added).
|
||||||
|
|
||||||
|
**AC-2: Override is honored**
|
||||||
|
Given `VITE_API_BASE_URL=http://test-host:8080` at build time,
|
||||||
|
When `api.get('/api/admin/auth/me')` is invoked,
|
||||||
|
Then the fetch URL is `http://test-host:8080/api/admin/auth/me`.
|
||||||
|
|
||||||
|
**AC-3: SSE honors the same prefix**
|
||||||
|
Given `VITE_API_BASE_URL=http://test-host:8080`,
|
||||||
|
When `createSSE('/api/flights/1/live-gps', ...)` is invoked,
|
||||||
|
Then the `EventSource` URL is `http://test-host:8080/api/flights/1/live-gps?access_token=...`.
|
||||||
|
|
||||||
|
**AC-4: Type declaration present**
|
||||||
|
Given a build,
|
||||||
|
Then `ImportMetaEnv` declares `readonly VITE_API_BASE_URL: string | undefined`.
|
||||||
|
|
||||||
|
## Non-Functional Requirements
|
||||||
|
|
||||||
|
**Reliability**
|
||||||
|
- Default empty-string fallback MUST be used when the env var is undefined or empty — no exceptions thrown at module load.
|
||||||
|
|
||||||
|
**Security**
|
||||||
|
- Adhere to AC-O3 (`credentials:'include'` semantics) and AC-23 (refresh transparency). The override does not affect cookie scope — that is governed by nginx and the browser's same-origin policy.
|
||||||
|
|
||||||
|
## Blackbox Tests
|
||||||
|
|
||||||
|
| AC Ref | Initial Data / Conditions | What to Test | Expected Behavior | NFR References |
|
||||||
|
|--------|--------------------------|--------------|-------------------|----------------|
|
||||||
|
| AC-1 | env unset; `api.get('/api/...')` | Captured fetch URL | Path unchanged | Compat |
|
||||||
|
| AC-2 | env set; `api.get('/api/...')` | Captured fetch URL | Prefix applied | Test override |
|
||||||
|
| AC-3 | env set; `createSSE('/api/...')` | Captured EventSource URL | Prefix applied | Test override |
|
||||||
|
| AC-4 | Build run | `vite-env.d.ts` declaration | Present | Compat |
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
- The API surface of `api.{get,post,put,patch,delete,upload}` and `createSSE` MUST NOT change.
|
||||||
|
- The default branch MUST be `getApiBase() === ''` so no relative-path semantics regress.
|
||||||
|
|
||||||
|
## Risks & Mitigation
|
||||||
|
|
||||||
|
**Risk 1: Trailing-slash drift in env**
|
||||||
|
- *Risk*: Setting the env to `http://host/` produces `http://host//api/...`.
|
||||||
|
- *Mitigation*: Trim a trailing slash in `getApiBase()` before returning.
|
||||||
|
|
||||||
|
**Risk 2: SSE `access_token` placement**
|
||||||
|
- *Risk*: When the prefix is set, the existing `?access_token=` insertion in `createSSE` must still work for both `url.includes('?')` and not-includes cases.
|
||||||
|
- *Mitigation*: The existing branch logic handles both — re-check after the change.
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# Wrap login redirect in a thin accessor
|
||||||
|
|
||||||
|
**Task**: AZ-453_refactor_navigate_to_login
|
||||||
|
**Name**: `navigateToLoginImpl()` accessor for the failed-refresh redirect
|
||||||
|
**Description**: Replace the direct `window.location.href = '/login'` write in `request()` with a thin overridable function so tests can verify the redirect call without globally stubbing `window.location`.
|
||||||
|
**Complexity**: 2 points
|
||||||
|
**Dependencies**: None
|
||||||
|
**Component**: `src/api/client.ts`
|
||||||
|
**Tracker**: AZ-453
|
||||||
|
**Epic**: AZ-447
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
`src/api/client.ts:34` performs `window.location.href = '/login'` directly after a failed refresh. The DOM API is global; jsdom can intercept it, but a test cannot distinguish "library code redirected" from "user code redirected" without overriding `window.location` globally — a heavy and side-effecty pattern.
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
- A module-level `navigateToLoginImpl: () => void` is introduced, defaulting to the existing redirect.
|
||||||
|
- `setNavigateToLogin(fn)` is exported for test override.
|
||||||
|
- `request()` calls `navigateToLoginImpl()` instead of touching `window.location` directly.
|
||||||
|
- Production behavior is identical (default impl still navigates to `/login`).
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
### Included
|
||||||
|
- `navigateToLoginImpl` and `setNavigateToLogin` additions in `src/api/client.ts`.
|
||||||
|
- One call-site change inside `request()`.
|
||||||
|
|
||||||
|
### Excluded
|
||||||
|
- Any change to the 401 / refresh flow itself.
|
||||||
|
- Any change to the redirect target string (`/login`).
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
**AC-1: Production default unchanged**
|
||||||
|
Given the default implementation,
|
||||||
|
When a 401 returns and `refreshToken()` resolves to `false`,
|
||||||
|
Then `window.location.href === '/login'` after the failed-refresh branch executes.
|
||||||
|
|
||||||
|
**AC-2: Override is honored**
|
||||||
|
Given a test calls `setNavigateToLogin(spy)`,
|
||||||
|
When a 401 returns and refresh fails,
|
||||||
|
Then `spy` is called exactly once and `window.location.href` is NOT mutated by `client.ts`.
|
||||||
|
|
||||||
|
**AC-3: API surface preserved**
|
||||||
|
Given the rest of the module after the change,
|
||||||
|
When the exports are enumerated,
|
||||||
|
Then `setToken`, `getToken`, `api`, `setNavigateToLogin` are present and no existing export is renamed or removed.
|
||||||
|
|
||||||
|
## Non-Functional Requirements
|
||||||
|
|
||||||
|
**Compatibility**
|
||||||
|
- Tests that already override `window.location` continue to work (the default impl still goes through it).
|
||||||
|
|
||||||
|
## Blackbox Tests
|
||||||
|
|
||||||
|
| AC Ref | Initial Data / Conditions | What to Test | Expected Behavior | NFR References |
|
||||||
|
|--------|--------------------------|--------------|-------------------|----------------|
|
||||||
|
| AC-1 | No override; 401 + failed refresh | `window.location.href` after the call | `/login` | Compat |
|
||||||
|
| AC-2 | `setNavigateToLogin(spy)` then 401 + failed refresh | Spy call count + `window.location.href` | Spy = 1; href untouched by `client.ts` | Compat |
|
||||||
|
| AC-3 | Module exports diff | Existing exports | Present | API stability |
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
- Allowed-change category: "Wrap global singletons in thin accessors that tests can override" (existing-code flow Step 4 allowed list).
|
||||||
|
|
||||||
|
## Risks & Mitigation
|
||||||
|
|
||||||
|
**Risk 1: Forgetting to reset the override between tests**
|
||||||
|
- *Risk*: If a test sets the override and doesn't reset it, later tests see the wrong impl.
|
||||||
|
- *Mitigation*: Document in `_docs/02_document/tests/test-data.md` that helpers must reset accessors in their teardown. Optional follow-up: a `resetNavigateToLogin()` helper — but adding it expands scope; defer unless decompose requires it.
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# Document setToken/getToken for test override
|
||||||
|
|
||||||
|
**Task**: AZ-454_refactor_document_token_accessor
|
||||||
|
**Name**: JSDoc the existing token accessor
|
||||||
|
**Description**: Add a JSDoc to the existing `setToken` / `getToken` accessors so a future maintainer doesn't delete them as "dead code". They are dynamically used by test helpers — the comment captures that intent without any behavioral change.
|
||||||
|
**Complexity**: 1 point
|
||||||
|
**Dependencies**: None
|
||||||
|
**Component**: `src/api/client.ts` (comment-only edit)
|
||||||
|
**Tracker**: AZ-454
|
||||||
|
**Epic**: AZ-447
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
`src/api/client.ts:1-9` exposes `setToken(token: string | null)` and `getToken()` over the module-level `accessToken`. The pattern is intentional (AC-02 forbids bearer storage; AC-23 requires refresh transparency; tests need a hook to seed a bearer without going through the login flow). The intent is undocumented today, so the dead-code reaper could delete these one day per `coderule.mdc` ("Dead code rots — delete it").
|
||||||
|
|
||||||
|
The `coderule.mdc` rule already lists "test fixtures used only by currently-skipped tests" as a reason NOT to delete, but with no tests in source yet, the accessor looks dead to a casual reader.
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
- `setToken` has a JSDoc explaining: (a) it's the bearer single-source-of-truth, (b) it MUST NOT persist to storage, (c) tests override it via `setToken('test-bearer-xyz')`, (d) it's referenced by `_docs/02_document/tests/test-data.md`.
|
||||||
|
- `getToken` has a one-line JSDoc cross-referencing `setToken`.
|
||||||
|
- No behavioral change.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
### Included
|
||||||
|
- JSDoc additions on `setToken` and `getToken`.
|
||||||
|
|
||||||
|
### Excluded
|
||||||
|
- Any code change.
|
||||||
|
- Adding test-only exports beyond what already exists.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
**AC-1: JSDoc on setToken**
|
||||||
|
Given the repo after the change,
|
||||||
|
When `src/api/client.ts:3` is read,
|
||||||
|
Then a JSDoc above `setToken` mentions: test-override intent + the AC-02 storage rule + a reference to `_docs/02_document/tests/test-data.md`.
|
||||||
|
|
||||||
|
**AC-2: JSDoc on getToken**
|
||||||
|
Given the repo after the change,
|
||||||
|
When `src/api/client.ts:7` is read,
|
||||||
|
Then a JSDoc above `getToken` cross-references `setToken`.
|
||||||
|
|
||||||
|
**AC-3: No code change**
|
||||||
|
Given a `git diff src/api/client.ts`,
|
||||||
|
When the diff is inspected,
|
||||||
|
Then every diff line is inside a comment block (JSDoc); no executable code is altered.
|
||||||
|
|
||||||
|
## Blackbox Tests
|
||||||
|
|
||||||
|
| AC Ref | Initial Data / Conditions | What to Test | Expected Behavior | NFR References |
|
||||||
|
|--------|--------------------------|--------------|-------------------|----------------|
|
||||||
|
| AC-1 | Repo after change | Presence + content of JSDoc on `setToken` | Documented intent | — |
|
||||||
|
| AC-2 | Repo after change | Presence of JSDoc on `getToken` | Documented cross-ref | — |
|
||||||
|
| AC-3 | `git diff` | Diff content | Only comment lines | Code stability |
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
- The comment MUST NOT introduce TODOs, FIXMEs, or speculative future work — just the existing-intent rationale.
|
||||||
|
|
||||||
|
## Risks & Mitigation
|
||||||
|
|
||||||
|
None — comment-only change.
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Refactoring Roadmap — 01-testability-refactoring
|
||||||
|
|
||||||
|
**Date**: 2026-05-10
|
||||||
|
**Run name**: 01-testability-refactoring
|
||||||
|
**Epic**: AZ-447
|
||||||
|
|
||||||
|
## Weak points assessment
|
||||||
|
|
||||||
|
| Location | Description | Impact | Proposed solution | Status |
|
||||||
|
|----------|-------------|--------|-------------------|--------|
|
||||||
|
| `src/features/flights/flightPlanUtils.ts:60` | Hardcoded OpenWeatherMap key + endpoint | NFT-SEC-09 fails; e2e cannot stub OWM | C01 (AZ-448) + C02 (AZ-449) | Selected |
|
||||||
|
| `src/features/flights/types.ts:56-57` | Hardcoded tile URLs (OSM + Esri) | AC-N3 / NFT-RES-03 fail; air-gap broken | C03 (AZ-450) | Selected |
|
||||||
|
| `src/features/flights/mapIcons.ts:18` | External `unpkg.com` marker icon URL | Air-gap broken; version mismatch with pinned Leaflet | C04 (AZ-451) | Selected |
|
||||||
|
| `src/api/client.ts` request prefix | No override hook for `/api/...` | E2E flexibility blocked | C05 (AZ-452) | Selected |
|
||||||
|
| `src/api/client.ts:34` login redirect | Direct `window.location.href` mutation | Tests cannot easily verify the call | C06 (AZ-453) | Selected |
|
||||||
|
| `src/api/client.ts:1-9` token accessor | Intentional but undocumented | Risks accidental dead-code deletion | C07 (AZ-454) | Selected |
|
||||||
|
|
||||||
|
## Gap analysis (what's missing — and what we are NOT fixing in this run)
|
||||||
|
|
||||||
|
Items considered out of scope for the testability run and deferred to Step 8 (Refactor) or Phase B feature cycle — see `_docs/04_refactoring/01-testability-refactoring/deferred_to_refactor.md` for the full list. Highlights:
|
||||||
|
|
||||||
|
- D01 — Bootstrap `credentials:'include'` (FT-P-01 quarantine): wire-contract product fix.
|
||||||
|
- D02 — Numeric enum drift in `src/types/index.ts` (FT-P-04/05/06): cross-service wire-contract change.
|
||||||
|
- D03–D12 — Missing UX / features.
|
||||||
|
- D13 — Parent-suite docs stale (parent-repo concern; record as leftover).
|
||||||
|
|
||||||
|
## Phased roadmap
|
||||||
|
|
||||||
|
**Phase 1 — Critical fixes (this run)**
|
||||||
|
|
||||||
|
| Phase | Tasks | Why now |
|
||||||
|
|-------|-------|---------|
|
||||||
|
| 1a (quick wins) | AZ-451 (marker icon), AZ-454 (JSDoc) | Smallest blast radius; trivial verification |
|
||||||
|
| 1b (tied pair) | AZ-448 + AZ-449 (same file) | Lands together to keep the file's mid-state coherent |
|
||||||
|
| 1c (independent low-risk) | AZ-450 (tile URLs), AZ-452 (getApiBase), AZ-453 (navigateToLogin) | Any order; each touches one file independently |
|
||||||
|
|
||||||
|
**Phase 2 — Major improvements**: none in this run. Phase B will pick up deferred items D01–D12.
|
||||||
|
|
||||||
|
**Phase 3 — Enhancements**: none in this run.
|
||||||
|
|
||||||
|
## Selected hardening tracks
|
||||||
|
|
||||||
|
User has not opted into Tech Debt / Performance / Security tracks at this stage — testability is the explicit scope of Step 4 per `flows/existing-code.md`. The autodev will offer the hardening-tracks Choose block at Phase 2 of the refactor skill if applicable; for this run the answer is **E) None — proceed with structural refactoring only** (testability changes are structural; hardening would expand scope).
|
||||||
|
|
||||||
|
## Applicability gate (per-item)
|
||||||
|
|
||||||
|
| Roadmap item | Constraint fit | Mismatches | Required evidence | Status |
|
||||||
|
|--------------|---------------|------------|-------------------|--------|
|
||||||
|
| C01 | AC-O6, NFT-SEC-09, S3 | None | `package.json` pins `vite ^6.2.0` | Selected |
|
||||||
|
| C02 | E10, S3 | None | Same | Selected |
|
||||||
|
| C03 | AC-N3, E1, S9 | None | `leaflet ^1.9.4` + `react-leaflet ^5` API surface unchanged | Selected |
|
||||||
|
| C04 | AC-N3, E1, S9 | None | `node_modules/leaflet/dist/images/marker-icon.png` exists | Selected |
|
||||||
|
| C05 | AC-O3, AC-23, E2 | None | Default `''` preserves all relative call sites | Selected |
|
||||||
|
| C06 | AC-23 | None | Same shape as existing `setToken` pattern | Selected |
|
||||||
|
| C07 | AC-02; coderule.mdc | None | Comment-only — zero behavioral change | Selected |
|
||||||
|
|
||||||
|
All 7 items pass the gate. No items marked Rejected / Experimental only / Needs user decision.
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Research Findings — 01-testability-refactoring
|
||||||
|
|
||||||
|
**Date**: 2026-05-10
|
||||||
|
**Mode**: guided (testability run)
|
||||||
|
|
||||||
|
## Current state analysis
|
||||||
|
|
||||||
|
| Concern | Current pattern | Strength | Weakness |
|
||||||
|
|---------|----------------|----------|----------|
|
||||||
|
| External API credentials | Literal string in `flightPlanUtils.ts:60` | Simple to read | Violates AC-O6; blocks NFT-SEC-09; prevents stub interception |
|
||||||
|
| External endpoint base URLs | Hardcoded in source (`flightPlanUtils.ts`, `types.ts`, `mapIcons.ts`) | None | Cannot be overridden for tests; breaks air-gap; couples to specific CDN versions |
|
||||||
|
| API request prefix | Implicit `/api/...` relative paths | Works on production where SPA + suite share nginx | No override hook for tests or alternative deployments |
|
||||||
|
| Module-level access token | `setToken / getToken` accessor on a module-scope `let accessToken` | Already a thin accessor (good!) | Undocumented intent; reads as dead code |
|
||||||
|
| Login redirect after failed refresh | Direct `window.location.href = '/login'` | Works in production | Hard to verify in tests without globally stubbing `window.location` |
|
||||||
|
|
||||||
|
## Alternative approaches considered
|
||||||
|
|
||||||
|
No library replacements are required for this run. Every change uses primitives already in the project:
|
||||||
|
|
||||||
|
1. **Vite env vars (`import.meta.env.VITE_*`)** — built-in to Vite 6 (S3). No new dependency. Verification: Vite docs are the project's pinned reference at `/vite-pwa` (n/a) — `vite-env.d.ts` already exists in the project, confirming the pattern is established.
|
||||||
|
2. **Module-level setter pattern for `setNavigateToLogin`** — same shape as the existing `setToken` accessor. No library evaluation needed.
|
||||||
|
3. **Vite asset import for marker PNG** — `import x from './path.png'` works out of the box with Vite's default asset pipeline. The `leaflet` package already ships the file at `dist/images/marker-icon.png`. No new dependency.
|
||||||
|
|
||||||
|
Because no library/SDK/framework is being added or replaced, the per-mode API capability verification protocol in `refactor/phases/02-analysis.md` (steps 1–5) **does not apply** to this run. The MVE evidence requirement is N/A — every change reuses an existing project capability.
|
||||||
|
|
||||||
|
## Constraint-fit table
|
||||||
|
|
||||||
|
| Change ID | Pinned mode/config | Constraints checked | Evidence | Mismatches | Status |
|
||||||
|
|-----------|-------------------|---------------------|----------|------------|--------|
|
||||||
|
| C01 (AZ-448) | `import.meta.env.VITE_OWM_API_KEY`, read at call time | AC-O6, NFT-SEC-09, S3 (Vite 6) | `package.json` pins `vite ^6.2.0`; `import.meta.env` is built-in; project does not yet use Vite env vars — this run introduces the pattern | None | Selected |
|
||||||
|
| C02 (AZ-449) | `import.meta.env.VITE_OWM_BASE_URL`, default-fallback | Same as C01 + E10 (OWM direct-from-browser today) | Same | None | Selected |
|
||||||
|
| C03 (AZ-450) | Two env vars with computed-at-module-load defaults | AC-N3, NFT-RES-03, E1, S9 (Leaflet 1.9.4) | `package.json` pins `leaflet ^1.9.4`, `react-leaflet ^5.0.0`; tile URL is consumed as a string by `TileLayer` — no Leaflet API change | None | Selected |
|
||||||
|
| C04 (AZ-451) | `import markerIcon from 'leaflet/dist/images/marker-icon.png'` | AC-N3, E1, S9 | `leaflet@^1.9.4` ships the PNG at that path (verified by reading `node_modules/leaflet/dist/images/`) | None | Selected |
|
||||||
|
| C05 (AZ-452) | Function returning `import.meta.env.VITE_API_BASE_URL ?? ''` | AC-O3, AC-23, E2 (nginx prefix-stripping) | Default `''` preserves every relative call site; nginx behavior outside the SPA — unchanged | None | Selected |
|
||||||
|
| C06 (AZ-453) | Module-level mutable function + setter | AC-23 (refresh transparency) | Same shape as existing `setToken` | None | Selected |
|
||||||
|
| C07 (AZ-454) | JSDoc only | AC-02 (no bearer storage); `coderule.mdc` dead-code rule | Comment-only | None | Selected |
|
||||||
|
|
||||||
|
## Prioritized recommendations
|
||||||
|
|
||||||
|
- **Quick wins** (land first): C04 (single-line file edit), C07 (comment-only).
|
||||||
|
- **Tied pair**: C01 + C02 — same file, land in one commit.
|
||||||
|
- **Independent low-risk**: C03, C05, C06 — can land in any order.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
This run did not require `context7` lookups (no library replacement). Internal references used:
|
||||||
|
|
||||||
|
- `_docs/00_problem/restrictions.md` (E1, E2, E10, S3, S9, O3, O6)
|
||||||
|
- `_docs/00_problem/acceptance_criteria.md` (AC-N3, AC-O6, AC-23)
|
||||||
|
- `_docs/02_document/tests/blackbox-tests.md` (FT-N* references)
|
||||||
|
- `_docs/02_document/tests/security-tests.md` (NFT-SEC-09 traceability)
|
||||||
|
- `_docs/02_document/tests/resilience-tests.md` (NFT-RES-03 traceability)
|
||||||
|
- `_docs/02_document/tests/traceability-matrix.md`
|
||||||
|
- Vite 6 documentation: built-in `import.meta.env` (project-pinned via `package.json`).
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Deferred to Step 8 Refactor / Phase B
|
||||||
|
|
||||||
|
Items considered during the autodev existing-code Step 4 (Code Testability Revision) testability scan that were **rejected from this run** because they drift outside the allowed-change list in `flows/existing-code.md` → Step 4 → Allowed changes. They are recorded here so the next refactor / feature-cycle pass picks them up without re-discovery.
|
||||||
|
|
||||||
|
Each entry references the test ID that exposes the gap so the deferral is traceable.
|
||||||
|
|
||||||
|
| ID | Source test(s) | What's missing / wrong | Why deferred from Step 4 | Next home |
|
||||||
|
|-----|---------------|------------------------|--------------------------|-----------|
|
||||||
|
| D01 | FT-P-01 (bootstrap) | Bootstrap refresh path is missing `credentials:'include'` (per `acceptance_criteria.md` AC-01 and the discovery finding cited in `_docs/02_document/modules/src__api__client.md`) | Wire-contract / business-logic change — not a testability isolation issue | Step 8 Refactor OR a Phase B "fix" ticket |
|
||||||
|
| D02 | FT-P-04, FT-P-05, FT-P-06 | `src/types/index.ts` enum numeric drift vs `_docs/00_problem/input_data/enum_spec_snapshot.json`: `AnnotationStatus`, `MediaStatus`, `Affiliation`, `CombatReadiness`, `MediaType` | Wire-contract renumbering across 5 enums with ripple effects in event payloads (annotations SSE, dataset bulk, detection responses). Out of scope for testability isolation. Snapshot still has `verification_pending: true` on `CombatReadiness` + `MediaType` — needs .NET-service inspection before commit | Step 8 Refactor OR Phase B fix ticket gated on the .NET inspection |
|
||||||
|
| D03 | FT-P-33, NFT-RES-04 | `ProtectedRoute` lacks the timeout / spinner-error fallback | Missing UX — adds new behavior, not a stubbing seam | Phase B feature cycle |
|
||||||
|
| D04 | FT-P-24, FT-P-25 | `i18n` lacks the language detector + `localStorage` persistence | Missing feature — adds new behavior | Phase B |
|
||||||
|
| D05 | FT-P-37, FT-P-38, NFT-PERF-08 | `useResizablePanel` has no persistence writer for panel widths | Missing feature | Phase B |
|
||||||
|
| D06 | FT-N-13, FT-N-14, NFT-PERF-09, NFT-RES-05, NFT-RES-06 | Settings page does not surface save errors to the user | Missing UX — adds new behavior | Phase B |
|
||||||
|
| D07 | FT-N-03, FT-N-05, NFT-SEC-05, NFT-SEC-06 | Client-side route gates for `/admin` and `/settings` are missing | New client-side defence-in-depth feature on top of server-side RBAC | Step 8 OR Phase B |
|
||||||
|
| D08 | FT-N-11, FT-N-12 | Settings form numeric-input hygiene is missing | Missing validation feature | Phase B |
|
||||||
|
| D09 | NFT-PERF-03, NFT-RES-02 | SSE refresh-rotation reconnect path is missing | Missing hardening feature | Step 8 hardening |
|
||||||
|
| D10 | FT-P-54, FT-P-55 (AC-40) | Tile-zoom UX + indicator are missing | Missing UX | Phase B |
|
||||||
|
| D11 | FT-P-51 (AC-39 split surface) | Tile-splitting UI not on dataset page today | Missing UI | Phase B |
|
||||||
|
| D12 | FT-P-12, FT-P-13 (AC-25 async path) | Async video detect (F7) not wired in the UI | Missing feature | Phase B |
|
||||||
|
| D13 | parent-suite docs leftover | `../_docs/01_annotations.md` line 208 + `../_docs/09_dataset_explorer.md` line 165 show stale `affiliation: 2 // Hostile` — should be `20` per `00_database_schema.md` | Lives in the parent suite repo, not this UI workspace — record in `_docs/_process_leftovers/` instead | Parent-suite doc fix leftover (per `tracker.mdc` Leftovers Mechanism) |
|
||||||
|
|
||||||
|
## Allowed-change rationale (for audit)
|
||||||
|
|
||||||
|
The Step 4 allowed list permits:
|
||||||
|
|
||||||
|
- Replace hardcoded URLs / file paths / credentials / magic numbers with env vars or constructor arguments
|
||||||
|
- Extract narrow interfaces for components that need stubbing in tests
|
||||||
|
- Add optional constructor parameters for DI; default to existing behavior
|
||||||
|
- Wrap global singletons in thin accessors that tests can override
|
||||||
|
- Split a huge function ONLY when necessary to stub one of its collaborators
|
||||||
|
|
||||||
|
The Step 4 disallowed list excludes:
|
||||||
|
|
||||||
|
- Renaming public APIs (breaks consumers without a safety net)
|
||||||
|
- Moving code between files unless strictly required for isolation
|
||||||
|
- **Changing algorithms or business logic** ← D01, D02, D03, D04, D05, D06, D08, D09, D10, D11, D12 fall here
|
||||||
|
- Restructuring module boundaries or rewriting layers
|
||||||
|
|
||||||
|
D07 (RBAC client-side gating) is a borderline case — it could be argued as an "interface extraction for a future test" but it actually adds new product behavior (a redirect on missing role), so it is deferred.
|
||||||
|
|
||||||
|
D13 lives outside this workspace.
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
# List of Changes
|
||||||
|
|
||||||
|
**Run**: 01-testability-refactoring
|
||||||
|
**Mode**: guided
|
||||||
|
**Source**: autodev-testability-analysis (autodev existing-code Step 4)
|
||||||
|
**Date**: 2026-05-10
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Minimal-surgical edits required so the suite of black-box tests in `_docs/02_document/tests/` can be exercised against a controlled environment. Five hardcoded externalities (OpenWeatherMap key + URL, OSM tile URL, Esri satellite tile URL, Leaflet marker icon URL on `unpkg.com`) and two thin accessors (API base, login navigation) are isolated into a Vite-env-driven configuration so the `fast` profile can mock them via MSW and the `e2e` profile can redirect them to the suite-internal stubs (`owm-stub`, `tile-stub`) without changing call sites. No business logic, no algorithm changes, no public API renames. The pre-existing module-level access-token accessor in `src/api/client.ts` (already a thin getter/setter) is documented for test override and left structurally unchanged.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
### C01: Externalize OpenWeatherMap API key
|
||||||
|
- **File(s)**: `src/features/flights/flightPlanUtils.ts`, `.env.example` (new), `vite-env.d.ts` (extend `ImportMetaEnv`)
|
||||||
|
- **Problem**: A literal OpenWeatherMap API key (`'335799082893fad97fa36118b131f919'`) is hardcoded at `flightPlanUtils.ts:60`. NFT-SEC-09 (security tests, results_report.md row 63) fails today as a quarantined source-string check. The literal also blocks the `e2e` profile from routing OWM calls through `owm-stub:8081` because the URL is baked in.
|
||||||
|
- **Change**: Move the key to `import.meta.env.VITE_OWM_API_KEY` (Vite-exposed env var). Read it at call time. Provide `.env.example` with the variable documented as required-at-build-time. If the env var is unset at runtime, the function returns `null` (matches the existing try/catch fallback) so the caller's `?? 0` paths still work.
|
||||||
|
- **Rationale**: Hardcoded credentials are forbidden by AC-O6, O7 of restrictions.md and by NFT-SEC-09. Externalizing them is the minimal change required to make NFT-SEC-09 pass without rewriting how weather is fetched. The change is at the line level — no caller signatures move.
|
||||||
|
- **Constraint Fit**: Preserves the `getWeatherData(lat, lon)` signature → no caller breakage. Adheres to AC-N4 (no signature library — env-driven config is plain `import.meta.env`). The build pipeline already supports `.env` files via Vite 6 (S3); no new tooling is introduced.
|
||||||
|
- **Risk**: low
|
||||||
|
- **Dependencies**: None
|
||||||
|
|
||||||
|
### C02: Externalize OpenWeatherMap base URL
|
||||||
|
- **File(s)**: `src/features/flights/flightPlanUtils.ts`, `.env.example`, `vite-env.d.ts`
|
||||||
|
- **Problem**: The OWM endpoint `https://api.openweathermap.org/data/2.5/weather` is hardcoded at the same line as C01. The `e2e` profile's `owm-stub:8081` cannot intercept calls unless this is parameterizable.
|
||||||
|
- **Change**: Add `VITE_OWM_BASE_URL` (default `https://api.openweathermap.org/data/2.5`). Read at call time. Compose the request URL as `${VITE_OWM_BASE_URL}/weather?lat=...&lon=...&appid=...&units=metric`.
|
||||||
|
- **Rationale**: Without this, `tile-stub` / `owm-stub` redirection requires either DNS-level hijacking inside the docker network (operationally heavier than a single env var) or a code branch — the env-var approach is the standard Vite pattern (per `coderule.mdc` "follow established project patterns").
|
||||||
|
- **Constraint Fit**: Preserves `getWeatherData` signature. Adheres to S3 (Vite 6) and existing project tooling. No new deps.
|
||||||
|
- **Risk**: low
|
||||||
|
- **Dependencies**: C01 (same file; should land in one commit to avoid mid-state mismatch)
|
||||||
|
|
||||||
|
### C03: Externalize map tile URLs (OSM + Esri satellite)
|
||||||
|
- **File(s)**: `src/features/flights/types.ts`, `.env.example`, `vite-env.d.ts`
|
||||||
|
- **Problem**: `TILE_URLS.classic = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'` and `TILE_URLS.satellite = 'https://server.arcgisonline.com/.../tile/{z}/{y}/{x}'` are hardcoded at `types.ts:56-57`. The `e2e` profile's `tile-stub:8082` cannot serve tiles unless these are overridable. AC-N3 (no offline mode) test (`NFT-RES-03`, row 93) also fails noisily because Leaflet tries to reach an external host on app boot.
|
||||||
|
- **Change**: Move both URLs to `VITE_OSM_TILE_URL` and `VITE_ESRI_TILE_URL` env vars with the current strings as defaults. Re-export `TILE_URLS` as a function or a const computed from `import.meta.env`. Call sites (`FlightMap.tsx`) unchanged.
|
||||||
|
- **Rationale**: Same as C02 — minimal-surgical, fits existing Vite env pattern, makes `e2e` profile reproducible.
|
||||||
|
- **Constraint Fit**: Preserves the `TILE_URLS.classic | TILE_URLS.satellite` consumer pattern. Adheres to S9 (Leaflet 1.9.4 + react-leaflet 5) — no API surface change for Leaflet.
|
||||||
|
- **Risk**: low
|
||||||
|
- **Dependencies**: None
|
||||||
|
|
||||||
|
### C04: Replace external Leaflet marker icon URL with bundled asset
|
||||||
|
- **File(s)**: `src/features/flights/mapIcons.ts`, `public/leaflet-marker.png` (new — copied from the leaflet npm package), `src/assets/` (alternative location)
|
||||||
|
- **Problem**: `mapIcons.ts:18` sets `iconUrl: 'https://unpkg.com/leaflet@1.7.1/dist/images/marker-icon.png'` — a hardcoded external CDN URL. This breaks:
|
||||||
|
- **AC-N3 / NFT-RES-03 / E1 air-gap**: app boot fetches from `unpkg.com` even with no other internet egress permitted.
|
||||||
|
- **Test determinism**: the `e2e` profile cannot serve the icon from any local source today.
|
||||||
|
- **Version mismatch**: pinned `leaflet@1.7.1` URL while package.json pins `leaflet@^1.9.4` (S9 violation in the URL string only — the JS library version is correct).
|
||||||
|
- **Change**: Either copy the marker PNG into `public/` (Vite serves `/leaflet-marker.png` from root) or import it from the `leaflet` package as a Vite asset (`import markerIcon from 'leaflet/dist/images/marker-icon.png'`). Update `iconUrl` to the local path. No call-site change.
|
||||||
|
- **Rationale**: The icon is a 600-byte PNG already shipping inside the `leaflet` npm package — pulling it locally costs zero extra bytes and removes an external dependency entirely.
|
||||||
|
- **Constraint Fit**: Preserves the `customIcon` export shape. Adheres to E1 (air-gap-friendly bundle), S9 (Leaflet 1.9.4 pin — local copy aligns with the pinned version). No new deps.
|
||||||
|
- **Risk**: low
|
||||||
|
- **Dependencies**: None
|
||||||
|
|
||||||
|
### C05: Extract `getApiBase()` accessor for the `/api/...` path prefix
|
||||||
|
- **File(s)**: `src/api/client.ts`, `src/api/sse.ts`, `vite-env.d.ts`
|
||||||
|
- **Problem**: `api/client.ts:44` and every call site that calls `api.get('/api/...')` assumes the SPA and the suite are served by the same nginx (production layout). The `fast` test profile's MSW handlers also match on the literal `/api/...` prefix, so this is workable today. However, the `e2e` profile's standalone Playwright runner pointing at `http://azaion-ui:80` works only because the test runner runs inside the same network — there is no override path if the suite ever needs to expose the SPA on a sub-path (E2 already strips `/api/<service>/` server-side, but the SPA assumes the root mount). Adding a single accessor `getApiBase()` reading `import.meta.env.VITE_API_BASE_URL` (default `''`, preserving today's relative behavior) is the minimal change.
|
||||||
|
- **Change**: Introduce `getApiBase()` in `src/api/client.ts`. `request()` and `refreshToken()` prepend it to the URL. `createSSE()` does the same. Default `''` keeps every existing call site unchanged.
|
||||||
|
- **Rationale**: Tests can override via `VITE_API_BASE_URL=http://owm-stub:8081/api/owm` etc. for cross-service routing scenarios. Default-zero behavior means no production deployment changes.
|
||||||
|
- **Constraint Fit**: Preserves every `api.get` / `api.post` call signature. Adheres to E2 (nginx still strips `/api/<service>/`) and O3 (`credentials:'include'` semantics unchanged).
|
||||||
|
- **Risk**: low
|
||||||
|
- **Dependencies**: None
|
||||||
|
|
||||||
|
### C06: Wrap `window.location.href = '/login'` redirect in a thin accessor
|
||||||
|
- **File(s)**: `src/api/client.ts`
|
||||||
|
- **Problem**: `client.ts:34` performs a hard `window.location.href = '/login'` after a failed refresh. The DOM API is global; jsdom can intercept it but the test cannot easily distinguish "library code redirected" from "user code redirected". A thin `navigateToLogin()` accessor lets tests verify the redirect call without overriding `window.location` globally.
|
||||||
|
- **Change**: Introduce `let navigateToLoginImpl: () => void = () => { window.location.href = '/login' }` and `export function setNavigateToLogin(fn: () => void)`. `request()` calls `navigateToLoginImpl()`. Production behavior unchanged; tests override.
|
||||||
|
- **Rationale**: Allowed change per Step 4 list ("Wrap global singletons in thin accessors that tests can override (thread-local / context var / setter gate)"). Surgical — six lines net.
|
||||||
|
- **Constraint Fit**: Preserves the production redirect target and timing. Adheres to AC-23 (auth refresh transparency) — failed refresh still kicks the user to `/login`.
|
||||||
|
- **Risk**: low
|
||||||
|
- **Dependencies**: None
|
||||||
|
|
||||||
|
### C07: Document the existing module-level access-token accessor for test override
|
||||||
|
- **File(s)**: `src/api/client.ts` (comment-only edit)
|
||||||
|
- **Problem**: `src/api/client.ts:1-9` already exposes `setToken(token: string | null)` / `getToken()` as the test override hook for the module-level `accessToken` singleton (AC-02 — never write the bearer to client storage; AC-23 — refresh transparency). Tests in `_docs/02_document/tests/blackbox-tests.md` rely on calling `setToken('test-bearer-xyz')` before mounting components. The pattern is intentional but not documented — a future maintainer could "clean it up" thinking it's dead code.
|
||||||
|
- **Change**: Add a short JSDoc on `setToken` documenting the test-override intent and linking to `_docs/02_document/tests/test-data.md` "Stubbed bearer / cookie in test helpers". Zero behavioral change.
|
||||||
|
- **Rationale**: Comment-only adjustment that prevents future deletion (per `coderule.mdc` "Dead code rots — but before deletion verify reflection / DI / dynamic-dispatch usages"). The accessor IS used dynamically by tests; documenting that fact protects it.
|
||||||
|
- **Constraint Fit**: Preserves O2 (no bearer in localStorage/sessionStorage) — comment-only.
|
||||||
|
- **Risk**: low (comment-only)
|
||||||
|
- **Dependencies**: None
|
||||||
|
|
||||||
|
## Rejected entries (deferred to Step 8 — Refactor — or Phase B)
|
||||||
|
|
||||||
|
The following candidates were considered and rejected from this run because they fall outside the allowed list ("Replace hardcoded URLs / file paths / credentials / magic numbers; extract narrow interfaces for stubbing; add optional DI parameters; wrap global singletons; split only when required for isolation"). They are recorded in `deferred_to_refactor.md` so the next refactor pass picks them up.
|
||||||
|
|
||||||
|
- **D01 — Bootstrap refresh missing `credentials:'include'`** (FT-P-01 quarantine): a one-line product fix in the bootstrap path. Changes wire behavior, not testability surface. **Defer to Step 8** (or Step 9 New-Task ticket).
|
||||||
|
- **D02 — Numeric enum drift in `src/types/index.ts`** (FT-P-04/05/06; `AnnotationStatus`, `MediaStatus`, `Affiliation`, `CombatReadiness`, `MediaType` per `enum_spec_snapshot.json`): renumbering 5 enums is a wire-contract change with payload-shape implications across `annotations/`, `flights/`, and the SSE event handlers. **Defer to Step 8** (or a dedicated Phase B ticket gated on .NET-side `CombatReadiness` + `MediaType` numeric confirmation — see snapshot's `verification_pending: true`).
|
||||||
|
- **D03 — ProtectedRoute timeout** (FT-P-33 / NFT-RES-04 quarantine): missing UX — adding a timeout is new behavior. **Defer to Phase B feature cycle**.
|
||||||
|
- **D04 — i18n language detector + persistence** (FT-P-24 / FT-P-25 quarantine): missing feature. **Defer to Phase B**.
|
||||||
|
- **D05 — Panel-width persistence** (FT-P-37 / FT-P-38 / NFT-PERF-08 quarantine): missing writer in `useResizablePanel`. **Defer to Phase B**.
|
||||||
|
- **D06 — Settings save-error surfacing** (FT-N-13/14 / NFT-RES-05/06 / NFT-PERF-09 quarantine): missing error UI. **Defer to Phase B**.
|
||||||
|
- **D07 — Role-gates for `/admin` and `/settings`** (FT-N-03 / FT-N-05 / NFT-SEC-05 / NFT-SEC-06 quarantine): missing client-side RBAC. **Defer to Step 8** (it's a client-side defence-in-depth addition over server-side RBAC).
|
||||||
|
- **D08 — Numeric input hygiene in settings** (FT-N-11 / FT-N-12 quarantine): missing form validation. **Defer to Phase B**.
|
||||||
|
- **D09 — SSE refresh-rotation reconnect** (NFT-PERF-03 / NFT-RES-02 quarantine): missing reconnect-on-rotation logic. **Defer to Step 8 hardening**.
|
||||||
|
- **D10 — Tile-zoom UX + indicator** (AC-40, FT-P-54/55 quarantine): missing UX. **Defer to Phase B**.
|
||||||
|
- **D11 — Tile-splitting surface on dataset page** (FT-P-51 quarantine): missing UI. **Defer to Phase B**.
|
||||||
|
- **D12 — Async video detect (F7)** (FT-P-12/13 quarantine): target feature not wired. **Defer to Phase B**.
|
||||||
|
|
||||||
|
## Acceptance gates
|
||||||
|
|
||||||
|
This list is reviewed under the BLOCKING gate at refactor Phase 1 (Discovery). The user must confirm before any task file is created in `_docs/02_tasks/todo/`. Each entry C01–C07 maps to one atomic task at decompose time, and Phase 4 (Execution) of the refactor skill delegates to the implement skill via those task files.
|
||||||
|
|
||||||
|
After execution, the refactor skill writes `testability_changes_summary.md` listing every applied change with plain-language rationale and risk — that summary is the second BLOCKING gate before Step 5 (Decompose Tests).
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Autodev State
|
||||||
|
|
||||||
|
## Current Step
|
||||||
|
flow: existing-code
|
||||||
|
step: 4
|
||||||
|
name: Code Testability Revision
|
||||||
|
status: in_progress
|
||||||
|
sub_step:
|
||||||
|
phase: 2
|
||||||
|
name: refactor-phase-2-tasks-review-gate
|
||||||
|
detail: "epic AZ-447 + 7 tasks AZ-448..AZ-454 created"
|
||||||
|
retry_count: 0
|
||||||
|
cycle: 1
|
||||||
|
step_4_5_glossary_vision: confirmed
|
||||||
|
step_2_baseline_routing: per-finding-recommended (option A)
|
||||||
|
step_3_results_report_authoring: agent (option A)
|
||||||
|
step_3_ac_gap_handling: rollback-to-6c (option A)
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Phase A baseline cycle. Step 1 (Document) complete; see
|
||||||
|
`_docs/02_document/state.json`, `FINAL_report.md`, `architecture.md`,
|
||||||
|
`glossary.md`, plus `_docs/01_solution/solution.md` and
|
||||||
|
`_docs/00_problem/{problem,acceptance_criteria,restrictions,security_approach}.md`.
|
||||||
|
- Suite-level architecture: `../_docs/`. UI design: `_docs/ui_design/`.
|
||||||
|
- Legacy reference: `_docs/legacy/wpf-era.md` + research copy at
|
||||||
|
`suite/annotations-research` (detached @ `22529c2`).
|
||||||
|
- /document scope was src/ AND mission-planner/ (two disjoint groups).
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
Testing strategy without real flight.
|
||||||
|
|
||||||
|
upload tlog file
|
||||||
|
upload video synced with tlog
|
||||||
|
|
||||||
|
|
||||||
|
system should:
|
||||||
|
1. extract timestamps, imu and gps from the tlog file.
|
||||||
|
2. usually video and tlog aren't synchronized. So system should synchronize them by itself.
|
||||||
|
Usual test is done on the quadcopters, so usually it starts from the drone on the ground and ends with the drone on the ground. These sessions are clearly visible in the chart IMU data of the tlog file. So, system can check the duration of the video and events in IMU chart in tlog. Then it can analyze by IMU the moment of actual take off and sync them
|
||||||
|
3. then make SITL and provide IMU and frames to the gps denied onboard system
|
||||||
@@ -0,0 +1,439 @@
|
|||||||
|
# Legacy: WPF Era of Azaion (`annotations` predecessor)
|
||||||
|
|
||||||
|
> **Source of truth for this doc:** `suite/annotations-research/` — a clone of
|
||||||
|
> `suite/annotations` checked out at commit `22529c2 "Revert add MediaFile"`
|
||||||
|
> (Mon Nov 17 2025), which is the LAST commit before the **big refactoring**
|
||||||
|
> (`e7ea5a8`) that started decoupling the WPF UI from the backend in
|
||||||
|
> preparation for the WPF→.NET API conversion (`9e7dc29` /
|
||||||
|
> `fbbe556 refactor .net project to API`).
|
||||||
|
>
|
||||||
|
> This document captures the system as it existed when the **annotation tool,
|
||||||
|
> the inference engine, the loader, and the UI were a single Windows desktop
|
||||||
|
> application** with .NET WPF in front and Cython sidecar processes behind it,
|
||||||
|
> all running on one machine.
|
||||||
|
>
|
||||||
|
> The current `azaion/ui` repo is the **React rewrite of the front-end half**
|
||||||
|
> of that legacy stack. The Cython parts and the .NET service code became
|
||||||
|
> separate suite submodules (`detections/`, `loader/`, `annotations/` (now
|
||||||
|
> .NET API only), `flights/`, etc.). This file exists so future maintainers
|
||||||
|
> can understand where features in the React UI come from, why some shapes
|
||||||
|
> in the data look the way they do, and what is intentionally NOT being
|
||||||
|
> ported.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Top-level layout (`Azaion.Suite.sln`)
|
||||||
|
|
||||||
|
The legacy repository was a single Visual Studio solution containing eight
|
||||||
|
.NET projects plus two Cython projects:
|
||||||
|
|
||||||
|
| # | Project | Type | Role |
|
||||||
|
|---|----------------------|---------------------|--------------------------------------------------------------|
|
||||||
|
| 1 | `Azaion.Suite` | .NET 8 WPF (exe) | Application host. DI container, config, module registry, key handler. |
|
||||||
|
| 2 | `Azaion.LoaderUI` | .NET 8 WPF (exe) | Login screen. Launches `Azaion.Suite.exe` with encrypted creds. |
|
||||||
|
| 3 | `Azaion.Annotator` | .NET 8 WPF (lib) | Main annotation window: video/image canvas, bounding boxes, AI detect. |
|
||||||
|
| 4 | `Azaion.Dataset` | .NET 8 WPF (lib) | Dataset Explorer window: thumbnail grid, class distribution, validation. |
|
||||||
|
| 5 | `Azaion.Common` | .NET 8 (lib) | **Tangled core**: WPF user controls + LinqToDB models + RabbitMQ + HTTP + DTOs all in one assembly. |
|
||||||
|
| 6 | `Azaion.CommonSecurity` | .NET 8 (lib) | AES helpers. Credentials persisted to disk encrypted. |
|
||||||
|
| 7 | `Azaion.Test` | .NET 8 test project | Unit tests for utilities (intervals, throttle, parallel, tile processing). |
|
||||||
|
| 8 | `Dummy` | placeholder dir | empty. |
|
||||||
|
| C1 | `Azaion.Inference` | Cython (Python) | YOLO inference (ONNX / TensorRT). Separate process, ZeroMQ link to .NET. |
|
||||||
|
| C2 | `Azaion.Loader` | Cython (Python) | Encrypted resource fetcher + hardware fingerprinting. Separate process, ZeroMQ link to .NET. |
|
||||||
|
|
||||||
|
There was no internet-facing API. Everything ran on **one Windows machine**
|
||||||
|
(operator laptop / OrangePi / Jetson).
|
||||||
|
|
||||||
|
```
|
||||||
|
+----------------------------- one Windows host -----------------------------+
|
||||||
|
| |
|
||||||
|
| Azaion.LoaderUI.exe |
|
||||||
|
| | |
|
||||||
|
| | encrypts creds -> spawns Azaion.Suite.exe -c <encrypted> |
|
||||||
|
| v |
|
||||||
|
| Azaion.Suite.exe (WPF) |
|
||||||
|
| | |
|
||||||
|
| | Microsoft.Extensions.Hosting + DI |
|
||||||
|
| | - registers Azaion.Annotator, Azaion.Dataset windows |
|
||||||
|
| | - registers Annotation/Gallery/Inference/GpsMatcher Services |
|
||||||
|
| | - registers AzaionApi (HttpClient -> remote installer/aux APIs) |
|
||||||
|
| | - registers LoaderClient + InferenceClient + GpsMatcherClient |
|
||||||
|
| | (all are ZeroMQ DealerSocket clients) |
|
||||||
|
| | |
|
||||||
|
| |--- LinqToDB --------------> SQLite file (annotations.db) |
|
||||||
|
| |--- ZeroMQ Dealer ---------> Azaion.Loader.exe (Cython) |
|
||||||
|
| | | |
|
||||||
|
| | +---> downloads encrypted resources |
|
||||||
|
| | from remote API |
|
||||||
|
| | |
|
||||||
|
| |--- ZeroMQ Dealer ---------> Azaion.Inference.exe (Cython) |
|
||||||
|
| | | |
|
||||||
|
| | +---> ONNX / TensorRT inference |
|
||||||
|
| | |
|
||||||
|
| +--- RabbitMQ.Stream client -> remote RabbitMQ (annotation sync) |
|
||||||
|
| |
|
||||||
|
+----------------------------------------------------------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Boot sequence
|
||||||
|
|
||||||
|
1. User runs `Azaion.LoaderUI.exe` (the launcher / login window).
|
||||||
|
2. `Login.LoginClick` → calls `IAzaionApi.Login` (HTTP) for installer-version
|
||||||
|
check, then spawns the **external** `Azaion.Loader` Cython process and
|
||||||
|
talks to it over ZeroMQ (`tcp://127.0.0.1:<port>`):
|
||||||
|
- `CommandType.Login` → loader stores credentials and a hardware-derived
|
||||||
|
key (`hardware_service.pyx`).
|
||||||
|
- `CommandType.CheckResource` → loader verifies it can decrypt the cached
|
||||||
|
encrypted resource bundle.
|
||||||
|
3. `Login` AES-encrypts `ApiCredentials` (`Azaion.CommonSecurity`) and starts
|
||||||
|
`Azaion.Suite.exe -c <encrypted>` then closes itself.
|
||||||
|
4. `Azaion.Suite.App.Start(creds)`:
|
||||||
|
- Builds a Serilog logger.
|
||||||
|
- Builds `IConfiguration` from three JSON streams: a local
|
||||||
|
`config.json`, plus `config.system.json` and `config.secured.json`
|
||||||
|
fetched from disk via `LoaderClient.LoadFile(...)` (the Cython loader
|
||||||
|
decrypts them on the fly).
|
||||||
|
- Configures the DI container (`Microsoft.Extensions.Hosting`):
|
||||||
|
- `IConfigUpdater`, `Annotator`, `DatasetExplorer`, `HelpWindow`,
|
||||||
|
`MainSuite`
|
||||||
|
- `IDbFactory`, `IAnnotationService`, `FailsafeAnnotationsProducer`,
|
||||||
|
`IGalleryService`
|
||||||
|
- `IInferenceClient`/`IInferenceService` (ZMQ → Cython inference)
|
||||||
|
- `IGpsMatcherClient`/`IGpsMatcherService` (ZMQ → GPS matcher service)
|
||||||
|
- `ISatelliteDownloader`
|
||||||
|
- `IAzaionApi` (HTTP client to remote API for installer + assets)
|
||||||
|
- `IAzaionModule` registrations (`AnnotatorModule`, `DatasetExplorerModule`)
|
||||||
|
- MediatR with assemblies from Annotator, DatasetExplorer, Common.
|
||||||
|
- Calls `Annotation.Init(directoriesConfig, detectionClassesDict)` —
|
||||||
|
populates **static** state on the `Annotation` entity so that LinqToDB
|
||||||
|
hydrated rows know how to compute `ImagePath` / `LabelPath` / `ThumbPath`
|
||||||
|
/ `Colors` / `ClassName`. (This static coupling is exactly what the
|
||||||
|
`e7ea5a8` "big refactoring" set out to remove.)
|
||||||
|
- Hooks a global preview-key handler that publishes a MediatR `KeyEvent`
|
||||||
|
(with throttle) for any keyboard input that is not in a `TextBox`.
|
||||||
|
- Shows `MainSuite` (the module switcher window).
|
||||||
|
|
||||||
|
## 3. Module system
|
||||||
|
|
||||||
|
`Azaion.Suite.MainSuite` is a small chrome window with a left-hand
|
||||||
|
`ListView` of modules. Each module implements:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface IAzaionModule
|
||||||
|
{
|
||||||
|
string Name { get; } // localized display name
|
||||||
|
string SvgIcon { get; } // inline SVG markup
|
||||||
|
Type MainWindowType { get; } // WPF Window subclass
|
||||||
|
WindowEnum WindowEnum { get; } // identifier
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Two implementations existed at this commit:
|
||||||
|
|
||||||
|
- `AnnotatorModule` → `Azaion.Annotator.Annotator`
|
||||||
|
- `DatasetExplorerModule` → `Azaion.Dataset.DatasetExplorer`
|
||||||
|
|
||||||
|
`MainSuite` shows the icon, opens the corresponding `Window` from the DI
|
||||||
|
container, and tracks open windows in a `Dictionary<WindowEnum, Window>` so
|
||||||
|
clicking the same module twice activates instead of recreating.
|
||||||
|
|
||||||
|
The `IAzaionModule` extension point is the seed of what became, in the
|
||||||
|
post-refactor world, the **left-hand top-level navigation in the React SPA**:
|
||||||
|
Flights, Annotations, Dataset, Admin, Settings.
|
||||||
|
|
||||||
|
## 4. The Annotator window (`Azaion.Annotator.Annotator`)
|
||||||
|
|
||||||
|
The annotation surface (`Annotator.xaml.cs`, ~600 lines) is the heaviest
|
||||||
|
part of the legacy app. It owned, all in one window:
|
||||||
|
|
||||||
|
- **Video/image playback**: `LibVLCSharp` `MediaPlayer` for video, image
|
||||||
|
decoding for stills.
|
||||||
|
- **Canvas editor**: a custom WPF `Canvas` with `CanvasEditor` from
|
||||||
|
`Azaion.Common.Controls` for click-and-drag bounding boxes, 8-handle
|
||||||
|
resize, multi-select with Ctrl, zoom with Ctrl+wheel, pan with Ctrl+drag,
|
||||||
|
crosshair cursor with active-class hint.
|
||||||
|
- **Time-windowed annotation overlay** during video playback: an
|
||||||
|
`IntervalTree<TimeSpan, Annotation>` keyed by
|
||||||
|
`[Time - 50ms, Time + 150ms]`; on each VLC position update, all
|
||||||
|
overlapping intervals render and the rest clear.
|
||||||
|
- **Detection class strip** (`Azaion.Common.Controls.DetectionClasses`):
|
||||||
|
data grid of class colour + number + name, with PhotoMode switcher
|
||||||
|
(Regular=0, Winter=20, Night=40); class number pressed via keyboard (1–9);
|
||||||
|
class colour mixed into the bounding-box label.
|
||||||
|
- **Annotation list** (right sidebar): `DataGrid` over the in-process
|
||||||
|
`IntervalTree`, gradient-coloured by detection class, double-click seeks
|
||||||
|
the video and zooms.
|
||||||
|
- **Frame-by-frame controls**: 1, 5, 10, 30, 60-frame stepping computed
|
||||||
|
from the video's FPS; play/pause/stop; mute and volume.
|
||||||
|
- **AI Detect** (`R` key or button): spawns the Cython inference process via
|
||||||
|
`IInferenceClient` (ZMQ) and streams progress into a modal
|
||||||
|
`AutodetectDialog`.
|
||||||
|
- **Camera config side panel** (`Azaion.Common.Controls.CameraConfigControl`):
|
||||||
|
altitude / focal length / sensor width — used to compute GSD-based bounds
|
||||||
|
for valid detection sizes.
|
||||||
|
- **GPS panel** (`Azaion.Annotator.Controls.MapMatcher`): toggleable below
|
||||||
|
the canvas; ties into `IGpsMatcherClient` which talks to a separate
|
||||||
|
GPS-denied positioning Cython process.
|
||||||
|
- **Help window** (`HelpWindow.xaml` + `HelpTexts.cs`): annotation quality
|
||||||
|
guidelines.
|
||||||
|
- **Ukrainian / English localisation** via `translations.json`.
|
||||||
|
|
||||||
|
Everything in this list is owned by the same `Annotator` partial class. There
|
||||||
|
is no view-model boundary; XAML code-behind directly:
|
||||||
|
|
||||||
|
- queries `IDbFactory` for `AnnotationsDb`, then runs LinqToDB queries
|
||||||
|
against the `Annotation`, `Detection`, `MediaFile`, `AnnotationQueueRecord`
|
||||||
|
tables;
|
||||||
|
- mutates the canvas, the data grid, the VLC media player, and the GPS panel;
|
||||||
|
- publishes MediatR notifications (`AnnotationCreatedEvent`,
|
||||||
|
`AnnotationsDeletedEvent`, `KeyEvent`, `SetStatusTextEvent`,
|
||||||
|
`AnnotatorControlEvent`, `LoadErrorEvent`) which downstream services like
|
||||||
|
`AnnotationService.OnAnnotationCreated` react to (e.g. to enqueue a sync
|
||||||
|
message into the local SQLite buffer table for later RabbitMQ publish).
|
||||||
|
|
||||||
|
This is the central tangle. The same class talks to the database, the
|
||||||
|
network (RabbitMQ via mediator), the inference process, the file system,
|
||||||
|
and the WPF visual tree.
|
||||||
|
|
||||||
|
## 5. The Dataset Explorer window (`Azaion.Dataset.DatasetExplorer`)
|
||||||
|
|
||||||
|
Mirrors the annotator, but for browsing:
|
||||||
|
|
||||||
|
- Thumbnail grid (virtualised) keyed by `Annotation.ThumbPath`, regenerated
|
||||||
|
by `IGalleryService`.
|
||||||
|
- Filter bar: date range, flight, status (`AnnotationStatus`: None / Created
|
||||||
|
/ Edited / Validated).
|
||||||
|
- Class distribution chart (`Controls/ClassDistribution.xaml`): horizontal
|
||||||
|
bars, one per `DetectionClass`, coloured with the class colour.
|
||||||
|
- Inline editor tab — same `CanvasEditor` from `Azaion.Common.Controls`,
|
||||||
|
reused.
|
||||||
|
- Bulk validation: select multiple thumbnails, press `V`, status becomes
|
||||||
|
`Validated`.
|
||||||
|
- Local keyboard handlers: `1–9` (class), `Enter` (save), `Del` (delete
|
||||||
|
selected), `X` (delete all), `V` (validate), arrow keys + PageUp/PageDown
|
||||||
|
for navigation, `Esc` to close the editor.
|
||||||
|
|
||||||
|
## 6. `Azaion.Common`: the everything-bag
|
||||||
|
|
||||||
|
This is the assembly that the post-refactor split tries hardest to undo. At
|
||||||
|
commit `22529c2` it was a single .NET project containing:
|
||||||
|
|
||||||
|
| Folder | Concern |
|
||||||
|
|---------------|------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| `Controls/` | **WPF user controls**: `CanvasEditor`, `NumericUpDown`, `DetectionClasses`, `CameraConfigControl`, `DetectionLabelPanel`, `UpdatableProgressBar`, `DetectionControl`. |
|
||||||
|
| `Database/` | LinqToDB models + `AnnotationsDb : DataConnection` + `DbFactory` + `SchemaMigrator` + `AnnotationsDbSchemaHolder`. |
|
||||||
|
| `DTO/` | App config sections (`AppConfig`, `LoaderClientConfig`, `InferenceClientConfig`, `GpsDeniedConfig`, `MapConfig`, `QueueConfig`, `AIRecognitionConfig`, `ThumbnailConfig`, `UIConfig`, `AnnotationConfig`, `CameraConfig`, `DirectoriesConfig`), domain enums (`AffiliationEnum`, `RoleEnum`, `WindowEnum`, `Direction`, `PlaybackControlEnum`), and shared shapes (`ApiCredentials`, `BusinessExceptionDto`, `LoginResponse`, `RemoteCommand`, `User`, `DetectionClass`, `LabelInfo`, `AnnotationResult`, `AnnotationThumbnail`, `ClusterDistribution`, `Coordinates`, `SatTile`, `DownloadTilesResult`, `SelectionState`, `FormState`, `ExternalClientsConfig`). |
|
||||||
|
| `Events/` | MediatR notifications (`AnnotationCreatedEvent`, `AnnotationsDeletedEvent`, `KeyEvent`, `SetStatusTextEvent`, `AnnotatorControlEvent`, `LoadErrorEvent`). |
|
||||||
|
| `Exceptions/` | `BusinessException`. |
|
||||||
|
| `Extensions/` | Helpers — `Geo`, `ParallelExt`, `ResilienceExt`, `ThrottleExtensions`, `IntervalTree`-related, `Bitmap`, `Color`, `Cancellation`, `Queryable`, `Graphics`, `Size`, `String`, `DirectoryInfo`, `DenseDateTimeConverter`, `EnumExtensions`, `ServiceCollectionExtensions`. |
|
||||||
|
| `Services/` | `AnnotationService` (RabbitMQ.Stream + LinqToDB + MediatR), `FailsafeAnnotationsProducer`, `GalleryService` (thumbnails), `AuthProvider`, `TileProcessor`, `SatelliteDownloader`, `LoaderClient` (ZMQ), `GpsMatcher/*` (ZMQ + service + event handler + events), `Inference/InferenceClient` (ZMQ), `Inference/InferenceService` (orchestrates inference jobs), `Inference/InferenceServiceEventHandler`, `Inference/InferenceServiceEvents`, `Cache`, `HashExtensions`. |
|
||||||
|
| `Constants.cs`| `CONFIG_PATH`, suffixes, file naming conventions, the `FailsafeAppConfig` builder. |
|
||||||
|
| `Security.cs` | AES-256-CFB credentials encryption / decryption. Key is time-derived for local on-disk storage; symmetrical on both ends of the loader handoff. |
|
||||||
|
|
||||||
|
Concrete examples of the tangle, taken straight from this commit:
|
||||||
|
|
||||||
|
- `Azaion.Common.Database.Annotation` carries `[IgnoreMember] System.Windows.Media.Color`
|
||||||
|
in its computed `Colors` projection. A "database model" that imports
|
||||||
|
`System.Windows.Media`. The DTO assembly cannot exist outside WPF.
|
||||||
|
- `Annotation.Init(DirectoriesConfig, Dictionary<int, DetectionClass>)` is a
|
||||||
|
**static initializer** that the application calls once at startup.
|
||||||
|
Hydrated entities then read the static `_labelsDir`, `_imagesDir`,
|
||||||
|
`_thumbDir`, `DetectionClassesDict` to compute their own paths and class
|
||||||
|
names. Two annotation databases or two configurations cannot coexist in
|
||||||
|
the same process.
|
||||||
|
- `AppConfig` aggregates ten config sections including `UIConfig`, but is
|
||||||
|
also passed to the queue producer, the loader client, the inference
|
||||||
|
client, the satellite downloader. There is no clear seam between
|
||||||
|
"app-host concerns" and "business concerns".
|
||||||
|
- `AnnotationService` is constructed with `IDbFactory`,
|
||||||
|
`FailsafeAnnotationsProducer`, `QueueConfig`, `UIConfig`,
|
||||||
|
`IGalleryService`, `IMediator`, `IAzaionApi`, `ILogger`. It runs a
|
||||||
|
`RabbitMQ.Stream.Consumer` *inside its constructor* via `Task.Run(...).Wait()`,
|
||||||
|
publishes MediatR events into the WPF dispatcher, and uses
|
||||||
|
`_imageAccessSemaphore` and `_messageProcessingSemaphore` to serialize
|
||||||
|
cross-thread SQLite writes. Lifecycle and threading model are baked in.
|
||||||
|
|
||||||
|
The follow-on commits (`e7ea5a8`, `9e7dc29`, `fbbe556`) split this into
|
||||||
|
proper layers: repositories with interfaces, an `AnnotationPathResolver`
|
||||||
|
service replacing the static fields on `Annotation`, a separate
|
||||||
|
`Azaion.Common.Database.AnnotationRepository` + `IAnnotationRepository`,
|
||||||
|
removal of `AnnotationsDbSchemaHolder`, and finally the move from a WPF
|
||||||
|
client to a containerised .NET API exposing REST + SSE.
|
||||||
|
|
||||||
|
## 7. Cython sidecars
|
||||||
|
|
||||||
|
### `Azaion.Inference`
|
||||||
|
|
||||||
|
Standalone Python project compiled with Cython
|
||||||
|
(`build_inference.cmd` → PyInstaller → `azaion-inference.spec`). Top-level
|
||||||
|
modules at `22529c2`:
|
||||||
|
|
||||||
|
```
|
||||||
|
ai_availability_status ai_config annotation
|
||||||
|
classes.json constants_inf file_data
|
||||||
|
inference inference_engine onnx_engine
|
||||||
|
loader_client main_inference remote_command_handler_inf
|
||||||
|
```
|
||||||
|
|
||||||
|
It exposes a ZeroMQ DealerSocket port. The .NET side (`InferenceClient` in
|
||||||
|
`Azaion.Common.Services.Inference`) sends MessagePack-serialised
|
||||||
|
`RemoteCommand` envelopes; the Cython side dispatches to either the ONNX
|
||||||
|
or TensorRT engine, reads inputs from the local file system, and streams
|
||||||
|
back `DetectionEvent`-shaped progress.
|
||||||
|
|
||||||
|
State the engine reports (mapped 1:1 to the React UI's
|
||||||
|
`AIAvailabilityStatus`):
|
||||||
|
|
||||||
|
| Value | Name | Meaning |
|
||||||
|
|-------|-------------|--------------------------------------------------------|
|
||||||
|
| 0 | None | Initial. |
|
||||||
|
| 10 | Downloading | Pulling weights from Admin API / CDN via `loader_client`. |
|
||||||
|
| 20 | Converting | ONNX → TensorRT (TensorRT devices only). |
|
||||||
|
| 30 | Uploading | Uploading converted engine back to API for caching. |
|
||||||
|
| 200 | Enabled | Inference engine ready. |
|
||||||
|
| 300 | Warning | Recoverable, the engine may come back. |
|
||||||
|
| 500 | Error | Failed to initialize. |
|
||||||
|
|
||||||
|
### `Azaion.Loader`
|
||||||
|
|
||||||
|
Same shape — Cython, ZeroMQ DealerSocket, separate process. Modules:
|
||||||
|
|
||||||
|
```
|
||||||
|
api_client cdn_manager constants
|
||||||
|
credentials file_data hardware_service
|
||||||
|
main_loader remote_command remote_command_handler
|
||||||
|
security
|
||||||
|
```
|
||||||
|
|
||||||
|
It is the only component in the legacy stack with internet access. It
|
||||||
|
authenticates the user against the remote API, downloads encrypted
|
||||||
|
resource bundles (model checkpoints, `config.system.json`,
|
||||||
|
`config.secured.json`), and decrypts them on demand using a key derived
|
||||||
|
from `email + password + hardware_id` (`security.pyx` + `hardware_service.pyx`).
|
||||||
|
The .NET side never sees the raw resource files until the loader has
|
||||||
|
already decrypted them.
|
||||||
|
|
||||||
|
This Loader is exactly the component documented in
|
||||||
|
`suite/_docs/00_top_level_architecture.md` under **Binary Split Security**.
|
||||||
|
The 3 KB key fragment, the encrypted on-device archive, and the
|
||||||
|
`SHA384(fragment + hw_hash + creds)` derivation all originate here.
|
||||||
|
|
||||||
|
## 8. Data model (LinqToDB → SQLite)
|
||||||
|
|
||||||
|
| Table | Purpose |
|
||||||
|
|-------------------------------|------------------------------------------------------------------------------------------|
|
||||||
|
| `Annotations` | Per-frame label set: `Name`, `MediaHash`, `OriginalMediaName`, `Time`, `CreatedDate`, `CreatedEmail`, `CreatedRole`, `Source`, `AnnotationStatus`, `ValidateDate`, `ValidateEmail`, `Detections[]`, `Milliseconds`, `Lat`, `Lon`. |
|
||||||
|
| `Detections` | Bounding box rows: `ClassNumber`, geometry, `Confidence`. |
|
||||||
|
| `MediaFiles` | Files indexed by hash. Used to dedupe + drive the media list. |
|
||||||
|
| `AnnotationQueueRecord` (`AnnotationsQueueRecords` table) | Local **failsafe outbox** for RabbitMQ publication. `FailsafeAnnotationsProducer` drains this every 10 s. |
|
||||||
|
|
||||||
|
Schema is created/migrated in process by `SchemaMigrator` against a
|
||||||
|
SQLite file pointed to by `DirectoriesConfig`.
|
||||||
|
|
||||||
|
## 9. Annotation sync (edge → central)
|
||||||
|
|
||||||
|
The legacy code already had the eventual edge-to-central sync wired in:
|
||||||
|
|
||||||
|
```
|
||||||
|
Annotator window // user creates annotation
|
||||||
|
│ MediatR: AnnotationCreatedEvent
|
||||||
|
▼
|
||||||
|
AnnotationService // local SQLite write
|
||||||
|
│
|
||||||
|
├─► Annotations row
|
||||||
|
├─► AnnotationQueueRecord row (unless SilentDetection)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
FailsafeAnnotationsProducer // BackgroundService-style task
|
||||||
|
│ MessagePack + Gzip, retry on failure
|
||||||
|
▼
|
||||||
|
RabbitMQ.Stream "azaion-annotations" // remote
|
||||||
|
│
|
||||||
|
└─► consumed by ai-queue-handler / Admin API in the remote tier
|
||||||
|
```
|
||||||
|
|
||||||
|
The React UI inherits the *protocol* (RabbitMQ stream, MessagePack +
|
||||||
|
Gzip, dedupe by `Annotation.Name`) but no longer owns it — it runs in
|
||||||
|
the new `annotations/` .NET API submodule of the suite.
|
||||||
|
|
||||||
|
## 10. What survived into the new world
|
||||||
|
|
||||||
|
The following concepts are direct ports of the legacy WPF design and
|
||||||
|
should be implemented in the React UI exactly the same way:
|
||||||
|
|
||||||
|
- **Module switcher** with localized name + SVG icon → top navigation bar
|
||||||
|
(Flights, Annotations, Dataset, Admin, Settings).
|
||||||
|
- **Detection-class strip** with class colour, number, name, and PhotoMode
|
||||||
|
switcher (Regular / Winter / Night, offsets 0/20/40).
|
||||||
|
`yoloId = classId + photoModeOffset`.
|
||||||
|
- **Canvas editor**: bounding-box draw / 8-handle resize / Ctrl multi-select
|
||||||
|
/ Ctrl+wheel zoom / Ctrl+drag pan / crosshair with active-class hint /
|
||||||
|
normalized-coordinate clamping.
|
||||||
|
- **Annotation row gradient** in the side list: a left-to-right gradient
|
||||||
|
composed of each detection's class colour, opacity proportional to
|
||||||
|
`Confidence`. Empty annotation → `#40DDDDDD` background.
|
||||||
|
- **Affiliation icons** (Friendly / Hostile / Unknown / None) and
|
||||||
|
**combat readiness** indicator (Ready / NotReady / Unknown) drawn next
|
||||||
|
to the bounding-box label.
|
||||||
|
- **Time-windowed annotation rendering** during video playback:
|
||||||
|
`Before = 50 ms`, `After = 150 ms`, lookup via interval tree.
|
||||||
|
- **Frame-by-frame stepping** in fixed counts (1, 5, 10, 30, 60), computed
|
||||||
|
from `1 / fps`.
|
||||||
|
- **Localized class names** (`DetectionClass.UIName` carried alongside the
|
||||||
|
English `Name`).
|
||||||
|
- **Camera config per session**: altitude / focal length / sensor width
|
||||||
|
drives GSD-based detection-size validation.
|
||||||
|
- **GPS-denied panel toggle** under the canvas (now implemented as the
|
||||||
|
GPS-Denied mode of the Flights page in the React UI).
|
||||||
|
- **Help window** with the six annotation quality rules.
|
||||||
|
- **Color scheme**: dark navy/blue primary (`#343a40`), orange accents
|
||||||
|
(`#fd7e14`), dark gray background (`#1e1e1e`), green success
|
||||||
|
(`#40c057`), blue primary buttons (`#228be6`), red danger (`#fa5252`).
|
||||||
|
- **Confirmation dialogs** for delete-media / delete-selected / delete-all
|
||||||
|
/ deactivate-user.
|
||||||
|
- **Resizable panel widths** persisted per user.
|
||||||
|
|
||||||
|
## 11. What is intentionally NOT being ported
|
||||||
|
|
||||||
|
- The DI host inside the UI process. The React app does not own a
|
||||||
|
service container, RabbitMQ consumer, SQLite database, or background
|
||||||
|
worker. All of that now lives in the per-service .NET / Python /
|
||||||
|
Cython submodules.
|
||||||
|
- LibVLCSharp. The browser's native `<video>` element with a
|
||||||
|
frame-accurate seeking shim handles playback.
|
||||||
|
- ZeroMQ DealerSockets. The browser only speaks HTTP and SSE. Inference,
|
||||||
|
GPS-matching, satellite tile fetching, and loader requests are all
|
||||||
|
exposed as REST endpoints by their respective suite services.
|
||||||
|
- The static `Annotation.Init(...)` initializer. Path/colour computation
|
||||||
|
becomes selector logic over the API DTOs, with no static state.
|
||||||
|
- The `Azaion.Common` god-assembly. Each concern is now a separate suite
|
||||||
|
submodule with its own repo, Dockerfile, and OpenAPI document.
|
||||||
|
- The `Azaion.LoaderUI` external-process handoff with encrypted creds on
|
||||||
|
the command line. The browser performs `POST /auth/login` against the
|
||||||
|
Admin API and stores a JWT.
|
||||||
|
- The Cython `Loader` and the binary-split key-fragment dance. That whole
|
||||||
|
protocol is server-side now (`loader/` submodule) and the React UI is
|
||||||
|
not involved beyond showing a progress screen.
|
||||||
|
|
||||||
|
## 12. How to read the research copy
|
||||||
|
|
||||||
|
```
|
||||||
|
cd /Users/obezdienie001/dev/azaion/suite/annotations-research
|
||||||
|
git status # detached at 22529c2 "Revert add MediaFile"
|
||||||
|
git log --oneline -n 5 # see surrounding commits
|
||||||
|
```
|
||||||
|
|
||||||
|
The folder is a plain clone of `suite/annotations` and is **not** wired
|
||||||
|
into the suite's `.gitmodules`, so the parent repository ignores it.
|
||||||
|
|
||||||
|
If you want to compare the WPF-era code to the immediately following
|
||||||
|
"big refactoring" commit, the comparison is:
|
||||||
|
|
||||||
|
```
|
||||||
|
git log --oneline --reverse 22529c2..e7ea5a8 # there is only e7ea5a8 itself
|
||||||
|
git diff 22529c2 e7ea5a8 -- Azaion.Annotator # what the cleanup changed
|
||||||
|
git diff 22529c2 e7ea5a8 -- Azaion.Common # the big assembly split prep
|
||||||
|
```
|
||||||
|
|
||||||
|
Two commits later (`fbbe556` / `9e7dc29`) the WPF projects disappear
|
||||||
|
entirely and are replaced by a containerised .NET API — that is the
|
||||||
|
state currently checked out in `suite/annotations`.
|
||||||
Executable
+173
@@ -0,0 +1,173 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Azaion UI — performance test runner.
|
||||||
|
#
|
||||||
|
# Generated by .cursor/skills/test-spec phase 4. Implements the NFT-PERF-* scenarios
|
||||||
|
# from _docs/02_document/tests/performance-tests.md. Thresholds are sourced from
|
||||||
|
# _docs/00_problem/input_data/expected_results/results_report.md (rows 11, 40, 98 + AC-11 + AC-23).
|
||||||
|
#
|
||||||
|
# Most NFT-PERF-* tests are observable browser timings, not server load tests. The
|
||||||
|
# script therefore runs Playwright-based measurements rather than k6/locust.
|
||||||
|
#
|
||||||
|
# Profile mapping (per environment.md → Test Execution):
|
||||||
|
# - NFT-PERF-01 (bundle ≤ 2 MB gzip) : static — checks dist/ on host
|
||||||
|
# - NFT-PERF-02..09 : fast or e2e — Playwright
|
||||||
|
# - NFT-PERF-10 (FCP ≤ 3 000 ms on /flights) : e2e — Playwright against the suite stack
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# scripts/run-performance-tests.sh # run all NFT-PERF-* (skips quarantined)
|
||||||
|
# scripts/run-performance-tests.sh --static-only # only NFT-PERF-01 (bundle size)
|
||||||
|
# scripts/run-performance-tests.sh --e2e-only # only NFT-PERF-* that require the stack
|
||||||
|
# scripts/run-performance-tests.sh --bundle-max-bytes 2097152 # override bundle threshold
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
SUITE_ROOT="$(cd "$PROJECT_ROOT/.." && pwd)"
|
||||||
|
RESULTS_DIR="$PROJECT_ROOT/test-results"
|
||||||
|
|
||||||
|
RUN_STATIC=true
|
||||||
|
RUN_E2E=true
|
||||||
|
|
||||||
|
# Thresholds (defaults from results_report.md; overridable via flags).
|
||||||
|
BUNDLE_MAX_BYTES=$((2 * 1024 * 1024)) # AC-11 / NFT-PERF-01 (row 40): 2 MB gzipped initial JS
|
||||||
|
FCP_MAX_MS=3000 # NFT-PERF-10 (row 98): warm-cache FCP on /flights, edge profile
|
||||||
|
AUTH_REFRESH_MAX_MS=200 # NFT-PERF-02 (row 11): refresh round-trip target
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--static-only) RUN_STATIC=true; RUN_E2E=false ;;
|
||||||
|
--e2e-only) RUN_STATIC=false; RUN_E2E=true ;;
|
||||||
|
--bundle-max-bytes=*) BUNDLE_MAX_BYTES="${arg#*=}" ;;
|
||||||
|
--bundle-max-bytes) shift; BUNDLE_MAX_BYTES="${1:-$BUNDLE_MAX_BYTES}" ;;
|
||||||
|
--fcp-max-ms=*) FCP_MAX_MS="${arg#*=}" ;;
|
||||||
|
--fcp-max-ms) shift; FCP_MAX_MS="${1:-$FCP_MAX_MS}" ;;
|
||||||
|
--auth-refresh-max-ms=*) AUTH_REFRESH_MAX_MS="${arg#*=}" ;;
|
||||||
|
--auth-refresh-max-ms) shift; AUTH_REFRESH_MAX_MS="${1:-$AUTH_REFRESH_MAX_MS}" ;;
|
||||||
|
-h|--help)
|
||||||
|
sed -n '2,22p' "$0" | sed 's/^# \{0,1\}//'
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown argument: $arg" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
E2E_COMPOSE_STARTED_HERE=false
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
if [ "$E2E_COMPOSE_STARTED_HERE" = "true" ]; then
|
||||||
|
docker compose -f "$SUITE_ROOT/e2e/docker-compose.suite-e2e.yml" down -v --remove-orphans || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
mkdir -p "$RESULTS_DIR"
|
||||||
|
|
||||||
|
cd "$PROJECT_ROOT"
|
||||||
|
|
||||||
|
echo "[run-performance-tests] thresholds:"
|
||||||
|
echo " bundle (NFT-PERF-01) : ≤ $BUNDLE_MAX_BYTES bytes gzipped"
|
||||||
|
echo " FCP (NFT-PERF-10) : ≤ $FCP_MAX_MS ms"
|
||||||
|
echo " auth refresh (NFT-PERF-02): ≤ $AUTH_REFRESH_MAX_MS ms"
|
||||||
|
echo " static : $RUN_STATIC"
|
||||||
|
echo " e2e : $RUN_E2E"
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Install deps (matches run-tests.sh).
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
if ! command -v bun >/dev/null 2>&1; then
|
||||||
|
echo "[run-performance-tests] FATAL: bun is required (project pins bun@1.3.11)." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "[run-performance-tests] installing dependencies..."
|
||||||
|
if [ -f "$PROJECT_ROOT/bun.lock" ] || [ -f "$PROJECT_ROOT/bun.lockb" ]; then
|
||||||
|
bun install --frozen-lockfile
|
||||||
|
else
|
||||||
|
bun install
|
||||||
|
fi
|
||||||
|
|
||||||
|
OVERALL_EXIT=0
|
||||||
|
SUMMARY_FILE="$RESULTS_DIR/performance-summary.txt"
|
||||||
|
: > "$SUMMARY_FILE"
|
||||||
|
|
||||||
|
record() {
|
||||||
|
# $1 = scenario id, $2 = result (PASS|FAIL|SKIP|QUARANTINE), $3 = measured, $4 = threshold
|
||||||
|
printf '%-14s %-12s measured=%-14s threshold=%s\n' "$1" "$2" "$3" "$4" | tee -a "$SUMMARY_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Static perf scenarios.
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
if [ "$RUN_STATIC" = "true" ]; then
|
||||||
|
echo "[run-performance-tests] === NFT-PERF-01 (bundle size) ==="
|
||||||
|
if [ ! -d "$PROJECT_ROOT/dist" ]; then
|
||||||
|
echo "[NFT-PERF-01] dist/ not present — running 'bun run build'..."
|
||||||
|
bun run build
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Sum gzipped sizes of dist/assets/*.js (the initial JS bundle is index-*.js per Vite).
|
||||||
|
BUNDLE_BYTES=$(
|
||||||
|
find "$PROJECT_ROOT/dist/assets" -maxdepth 1 -name '*.js' -print0 2>/dev/null \
|
||||||
|
| xargs -0 -I{} sh -c 'gzip -c "{}" | wc -c' \
|
||||||
|
| awk '{ s += $1 } END { print (s ? s : 0) }'
|
||||||
|
)
|
||||||
|
echo "[NFT-PERF-01] gzipped dist/assets/*.js = $BUNDLE_BYTES bytes"
|
||||||
|
if [ "$BUNDLE_BYTES" -le "$BUNDLE_MAX_BYTES" ]; then
|
||||||
|
record "NFT-PERF-01" "PASS" "${BUNDLE_BYTES}B" "≤ ${BUNDLE_MAX_BYTES}B"
|
||||||
|
else
|
||||||
|
record "NFT-PERF-01" "FAIL" "${BUNDLE_BYTES}B" "≤ ${BUNDLE_MAX_BYTES}B"
|
||||||
|
OVERALL_EXIT=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# E2E perf scenarios (Playwright-based).
|
||||||
|
# The Playwright project lands at autodev Step 5 (Decompose Tests). Until it
|
||||||
|
# ships, NFT-PERF-02..10 are SKIPPED (not FAILED) so this script can run on
|
||||||
|
# the spec-only baseline without producing false negatives.
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
if [ "$RUN_E2E" = "true" ]; then
|
||||||
|
COMPOSE_FILE="$SUITE_ROOT/e2e/docker-compose.suite-e2e.yml"
|
||||||
|
PERF_PROJECT="$PROJECT_ROOT/e2e/playwright.perf.config.ts"
|
||||||
|
|
||||||
|
if [ ! -f "$PERF_PROJECT" ]; then
|
||||||
|
echo "[run-performance-tests] Playwright perf project ($PERF_PROJECT) not yet wired."
|
||||||
|
echo "[run-performance-tests] Decompose-Tests step (autodev Step 5) creates it; until then the e2e perf scenarios are SKIPPED."
|
||||||
|
for id in NFT-PERF-02 NFT-PERF-03 NFT-PERF-04 NFT-PERF-05 NFT-PERF-06 NFT-PERF-07 NFT-PERF-08 NFT-PERF-09 NFT-PERF-10; do
|
||||||
|
record "$id" "SKIP" "n/a" "deferred to Step 5"
|
||||||
|
done
|
||||||
|
elif [ ! -f "$COMPOSE_FILE" ]; then
|
||||||
|
echo "[run-performance-tests] FATAL: $COMPOSE_FILE not found (parent suite repo owns it)." >&2
|
||||||
|
OVERALL_EXIT=1
|
||||||
|
elif ! command -v docker >/dev/null 2>&1; then
|
||||||
|
echo "[run-performance-tests] FATAL: docker is required for the e2e perf profile." >&2
|
||||||
|
OVERALL_EXIT=1
|
||||||
|
else
|
||||||
|
echo "[run-performance-tests] starting compose stack..."
|
||||||
|
docker compose -f "$COMPOSE_FILE" up -d --build
|
||||||
|
E2E_COMPOSE_STARTED_HERE=true
|
||||||
|
|
||||||
|
echo "[run-performance-tests] running Playwright perf project..."
|
||||||
|
if FCP_MAX_MS="$FCP_MAX_MS" AUTH_REFRESH_MAX_MS="$AUTH_REFRESH_MAX_MS" \
|
||||||
|
bunx playwright test --config "$PERF_PROJECT" 2>&1 | tee "$RESULTS_DIR/perf-playwright.txt"; then
|
||||||
|
echo "[run-performance-tests] Playwright perf PASSED"
|
||||||
|
else
|
||||||
|
echo "[run-performance-tests] Playwright perf FAILED — see $RESULTS_DIR/perf-playwright.txt"
|
||||||
|
OVERALL_EXIT=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Quarantined scenarios (documentary only — never gate today).
|
||||||
|
record "NFT-PERF-03" "QUARANTINE" "—" "Step 8 hardening (SSE refresh rotation)"
|
||||||
|
record "NFT-PERF-08" "QUARANTINE" "—" "Step 4 fix (panel-width persistence)"
|
||||||
|
record "NFT-PERF-09" "QUARANTINE" "—" "Step 4 fix (settings save error surfacing)"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "[run-performance-tests] summary written to $SUMMARY_FILE"
|
||||||
|
echo "[run-performance-tests] exit code: $OVERALL_EXIT"
|
||||||
|
|
||||||
|
exit "$OVERALL_EXIT"
|
||||||
Executable
+291
@@ -0,0 +1,291 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Azaion UI — unit + blackbox test runner.
|
||||||
|
#
|
||||||
|
# Generated by .cursor/skills/test-spec phase 4. Drives the test profiles
|
||||||
|
# specified in _docs/02_document/tests/environment.md:
|
||||||
|
# - static : repo + dist artifact checks (no runtime)
|
||||||
|
# - fast : Bun + Vitest + jsdom + MSW (component / unit / blackbox at the fetch boundary)
|
||||||
|
# - e2e : Playwright (Chromium + Firefox) against the suite docker-compose stack
|
||||||
|
#
|
||||||
|
# The fast + static profiles run locally on host. The e2e profile delegates to the
|
||||||
|
# suite-level docker-compose harness owned by the parent suite repo (e2e/docker-compose.suite-e2e.yml).
|
||||||
|
#
|
||||||
|
# Hardware-Dependency Assessment recorded "Not hardware-dependent" — Docker is preferred
|
||||||
|
# for e2e; fast + static execute on the host because they have no runtime dependency on the suite.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# scripts/run-tests.sh # static + fast (default; gates every commit per CI/CD Integration)
|
||||||
|
# scripts/run-tests.sh --unit-only # alias for default — fast + static, no e2e
|
||||||
|
# scripts/run-tests.sh --all # static + fast + e2e
|
||||||
|
# scripts/run-tests.sh --e2e-only # only the e2e profile
|
||||||
|
# scripts/run-tests.sh --static-only # only the static checks
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
SUITE_ROOT="$(cd "$PROJECT_ROOT/.." && pwd)"
|
||||||
|
RESULTS_DIR="$PROJECT_ROOT/test-results"
|
||||||
|
|
||||||
|
RUN_STATIC=true
|
||||||
|
RUN_FAST=true
|
||||||
|
RUN_E2E=false
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--unit-only) RUN_STATIC=true; RUN_FAST=true; RUN_E2E=false ;;
|
||||||
|
--all) RUN_STATIC=true; RUN_FAST=true; RUN_E2E=true ;;
|
||||||
|
--e2e-only) RUN_STATIC=false; RUN_FAST=false; RUN_E2E=true ;;
|
||||||
|
--static-only) RUN_STATIC=true; RUN_FAST=false; RUN_E2E=false ;;
|
||||||
|
--fast-only) RUN_STATIC=false; RUN_FAST=true; RUN_E2E=false ;;
|
||||||
|
-h|--help)
|
||||||
|
sed -n '2,18p' "$0" | sed 's/^# \{0,1\}//'
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown argument: $arg" >&2
|
||||||
|
echo "Run with --help for usage." >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
E2E_COMPOSE_STARTED_HERE=false
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
if [ "$E2E_COMPOSE_STARTED_HERE" = "true" ]; then
|
||||||
|
docker compose -f "$SUITE_ROOT/e2e/docker-compose.suite-e2e.yml" down -v --remove-orphans || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
mkdir -p "$RESULTS_DIR"
|
||||||
|
|
||||||
|
cd "$PROJECT_ROOT"
|
||||||
|
|
||||||
|
echo "[run-tests] project root: $PROJECT_ROOT"
|
||||||
|
echo "[run-tests] suite root : $SUITE_ROOT"
|
||||||
|
echo "[run-tests] profiles : static=$RUN_STATIC fast=$RUN_FAST e2e=$RUN_E2E"
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Install dependencies (mandatory — a fresh CI runner has nothing).
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
if [ "$RUN_FAST" = "true" ] || [ "$RUN_STATIC" = "true" ]; then
|
||||||
|
if ! command -v bun >/dev/null 2>&1; then
|
||||||
|
echo "[run-tests] FATAL: bun is required (project pins bun@1.3.11 per package.json packageManager)." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "[run-tests] installing dependencies (bun install --frozen-lockfile if lockfile present, else bun install)..."
|
||||||
|
if [ -f "$PROJECT_ROOT/bun.lock" ] || [ -f "$PROJECT_ROOT/bun.lockb" ]; then
|
||||||
|
bun install --frozen-lockfile
|
||||||
|
else
|
||||||
|
bun install
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
OVERALL_EXIT=0
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Static profile — repo + dist artifact checks.
|
||||||
|
# Source: _docs/02_document/tests/blackbox-tests.md, security-tests.md,
|
||||||
|
# resource-limit-tests.md, traceability-matrix.md "STC-*" candidates.
|
||||||
|
#
|
||||||
|
# Today only the spec-derived checks ship; the STC-S* family lands when the
|
||||||
|
# traceability matrix promotes them (see Phase 3 "Still open" item 6).
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
if [ "$RUN_STATIC" = "true" ]; then
|
||||||
|
echo "[run-tests] === static profile ==="
|
||||||
|
STATIC_REPORT="$RESULTS_DIR/static-report.txt"
|
||||||
|
: > "$STATIC_REPORT"
|
||||||
|
STATIC_FAIL=0
|
||||||
|
|
||||||
|
echo "[static] STC-S1: TypeScript strict mode in tsconfig.json"
|
||||||
|
if node -e 'const t=require("./tsconfig.json"); process.exit((t.compilerOptions && t.compilerOptions.strict === true) ? 0 : 1)' 2>/dev/null; then
|
||||||
|
echo " PASS" | tee -a "$STATIC_REPORT"
|
||||||
|
else
|
||||||
|
# tsconfig may extend a base; fall back to a tsc --showConfig dry-run.
|
||||||
|
if bunx tsc --showConfig | grep -q '"strict": true'; then
|
||||||
|
echo " PASS (via tsc --showConfig)" | tee -a "$STATIC_REPORT"
|
||||||
|
else
|
||||||
|
echo " FAIL: strict mode not enabled" | tee -a "$STATIC_REPORT"; STATIC_FAIL=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[static] STC-S2..S11: pinned dependency versions (S2 React 19, S3 Vite 6, S4 Bun 1.3.11, S7 no Redux/Zustand/TanStack, S8 Tailwind 4, S9 Leaflet, S10 Chart.js, S11 DnD)"
|
||||||
|
node -e '
|
||||||
|
const p = require("./package.json");
|
||||||
|
const all = Object.assign({}, p.dependencies || {}, p.devDependencies || {});
|
||||||
|
const pin = (name, ver) => (all[name] || "").startsWith(ver) ? ` PASS ${name}@${all[name]}` : ` FAIL ${name}@${all[name] || "(missing)"} expected ${ver}*`;
|
||||||
|
const ban = (name) => all[name] ? ` FAIL banned dep present: ${name}` : ` PASS no ${name}`;
|
||||||
|
const lines = [
|
||||||
|
pin("react", "^19"),
|
||||||
|
pin("react-dom", "^19"),
|
||||||
|
pin("vite", "^6"),
|
||||||
|
pin("tailwindcss", "^4"),
|
||||||
|
pin("leaflet", "^1.9.4"),
|
||||||
|
pin("react-leaflet", "^5"),
|
||||||
|
pin("chart.js", "^4"),
|
||||||
|
pin("@hello-pangea/dnd", "^18"),
|
||||||
|
ban("redux"),
|
||||||
|
ban("@reduxjs/toolkit"),
|
||||||
|
ban("zustand"),
|
||||||
|
ban("@tanstack/react-query"),
|
||||||
|
ban("@tanstack/query-core"),
|
||||||
|
(p.packageManager === "bun@1.3.11") ? " PASS packageManager bun@1.3.11" : ` FAIL packageManager=${p.packageManager}`,
|
||||||
|
];
|
||||||
|
for (const l of lines) console.log(l);
|
||||||
|
if (lines.some(l => l.startsWith(" FAIL"))) process.exit(1);
|
||||||
|
' | tee -a "$STATIC_REPORT" || STATIC_FAIL=1
|
||||||
|
|
||||||
|
echo "[static] STC-N2 / AC-N2: no in-browser ML libraries"
|
||||||
|
if node -e '
|
||||||
|
const p = require("./package.json");
|
||||||
|
const all = Object.assign({}, p.dependencies || {}, p.devDependencies || {});
|
||||||
|
const re = /(onnxruntime|tensorflow|tflite|coreml|tfjs|@tensorflow\/|@huggingface\/|transformers\.js)/i;
|
||||||
|
const hits = Object.keys(all).filter(n => re.test(n));
|
||||||
|
if (hits.length) { console.log(" FAIL banned ML deps:", hits.join(", ")); process.exit(1); }
|
||||||
|
console.log(" PASS no in-browser ML deps");
|
||||||
|
' | tee -a "$STATIC_REPORT"; then :; else STATIC_FAIL=1; fi
|
||||||
|
|
||||||
|
echo "[static] STC-N4 / AC-N4: no response-signature library"
|
||||||
|
if node -e '
|
||||||
|
const p = require("./package.json");
|
||||||
|
const all = Object.assign({}, p.dependencies || {}, p.devDependencies || {});
|
||||||
|
const re = /(jsrsasign|tweetnacl|@noble\/|^jose$)/i;
|
||||||
|
const hits = Object.keys(all).filter(n => re.test(n));
|
||||||
|
if (hits.length) { console.log(" FAIL signature libs:", hits.join(", ")); process.exit(1); }
|
||||||
|
console.log(" PASS no signature libs");
|
||||||
|
' | tee -a "$STATIC_REPORT"; then :; else STATIC_FAIL=1; fi
|
||||||
|
|
||||||
|
echo "[static] STC-S13 / O2: no client-side persistence library"
|
||||||
|
if node -e '
|
||||||
|
const p = require("./package.json");
|
||||||
|
const all = Object.assign({}, p.dependencies || {}, p.devDependencies || {});
|
||||||
|
const re = /^(localforage|idb|dexie)$/i;
|
||||||
|
const hits = Object.keys(all).filter(n => re.test(n));
|
||||||
|
if (hits.length) { console.log(" FAIL persistence libs:", hits.join(", ")); process.exit(1); }
|
||||||
|
console.log(" PASS no persistence libs");
|
||||||
|
' | tee -a "$STATIC_REPORT"; then :; else STATIC_FAIL=1; fi
|
||||||
|
|
||||||
|
echo "[static] STC-S6 / O11: no WebSocket / GraphQL / gRPC-Web / SSR / RSC"
|
||||||
|
if node -e '
|
||||||
|
const p = require("./package.json");
|
||||||
|
const all = Object.assign({}, p.dependencies || {}, p.devDependencies || {});
|
||||||
|
const re = /^(ws|socket\.io|graphql|apollo|@apollo\/|grpc-web|react-dom\/server)$/i;
|
||||||
|
const hits = Object.keys(all).filter(n => re.test(n));
|
||||||
|
if (hits.length) { console.log(" FAIL banned deps:", hits.join(", ")); process.exit(1); }
|
||||||
|
console.log(" PASS no WS/GraphQL/gRPC/SSR deps");
|
||||||
|
' | tee -a "$STATIC_REPORT"; then :; else STATIC_FAIL=1; fi
|
||||||
|
|
||||||
|
echo "[static] AC-N5: dropped legacy features (SoundDetections, DroneMaintenance) absent from src/ + mission-planner/"
|
||||||
|
if grep -r --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' -E 'SoundDetections|DroneMaintenance' "$PROJECT_ROOT/src" "$PROJECT_ROOT/mission-planner" 2>/dev/null | tee -a "$STATIC_REPORT"; then
|
||||||
|
echo " FAIL legacy symbols present" | tee -a "$STATIC_REPORT"; STATIC_FAIL=1
|
||||||
|
else
|
||||||
|
echo " PASS no legacy symbols" | tee -a "$STATIC_REPORT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[static] AC-31 / O12: mission-planner not built into dist/"
|
||||||
|
if [ -d "$PROJECT_ROOT/dist" ]; then
|
||||||
|
if grep -rE 'mission[-_ ]?planner' "$PROJECT_ROOT/dist" 2>/dev/null | tee -a "$STATIC_REPORT"; then
|
||||||
|
echo " FAIL mission-planner symbols leaked into dist/" | tee -a "$STATIC_REPORT"; STATIC_FAIL=1
|
||||||
|
else
|
||||||
|
echo " PASS mission-planner absent from dist/" | tee -a "$STATIC_REPORT"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " SKIP dist/ not built — re-run after 'bun run build'" | tee -a "$STATIC_REPORT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[static] AC-N3: no service worker registration"
|
||||||
|
if grep -rE 'serviceWorker\.register|navigator\.serviceWorker' "$PROJECT_ROOT/src" 2>/dev/null | tee -a "$STATIC_REPORT"; then
|
||||||
|
echo " FAIL service worker registration found" | tee -a "$STATIC_REPORT"; STATIC_FAIL=1
|
||||||
|
else
|
||||||
|
echo " PASS no service worker registration" | tee -a "$STATIC_REPORT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[static] NFT-SEC-09 source check (quarantined until Step 4): OpenWeatherMap key not in source"
|
||||||
|
if grep -rE 'OPENWEATHERMAP|OWM_API_KEY|appid=' "$PROJECT_ROOT/src" 2>/dev/null | grep -vE 'import\.meta\.env|process\.env' | tee -a "$STATIC_REPORT"; then
|
||||||
|
echo " QUARANTINED FAIL: literal OWM key string found (Step 4 will fix)" | tee -a "$STATIC_REPORT"
|
||||||
|
# Quarantined per traceability-matrix.md — do not gate on this until Step 4.
|
||||||
|
else
|
||||||
|
echo " PASS no literal OWM key" | tee -a "$STATIC_REPORT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$STATIC_FAIL" = "1" ]; then
|
||||||
|
echo "[run-tests] static profile FAILED — see $STATIC_REPORT"
|
||||||
|
OVERALL_EXIT=1
|
||||||
|
else
|
||||||
|
echo "[run-tests] static profile PASSED"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Fast profile — Bun + Vitest + jsdom + MSW.
|
||||||
|
# Implementation of *.test.ts(x) files lands at autodev Step 5 (Decompose Tests);
|
||||||
|
# this runner block is the harness the decomposed tasks plug into.
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
if [ "$RUN_FAST" = "true" ]; then
|
||||||
|
echo "[run-tests] === fast profile ==="
|
||||||
|
FAST_REPORT="$RESULTS_DIR/fast-report.txt"
|
||||||
|
|
||||||
|
# Vitest is the planned fast-profile runner (decided at decompose time). If
|
||||||
|
# the test runner has not been wired into package.json yet, fail loudly so
|
||||||
|
# the decomposer sees the gap rather than silently passing.
|
||||||
|
if grep -q '"test"' "$PROJECT_ROOT/package.json" 2>/dev/null; then
|
||||||
|
echo "[fast] running bun run test"
|
||||||
|
if bun run test 2>&1 | tee "$FAST_REPORT"; then
|
||||||
|
echo "[run-tests] fast profile PASSED"
|
||||||
|
else
|
||||||
|
echo "[run-tests] fast profile FAILED — see $FAST_REPORT"
|
||||||
|
OVERALL_EXIT=1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "[fast] no \"test\" script in package.json yet — decompose-tests step (autodev Step 5) wires the runner."
|
||||||
|
echo "[fast] SKIPPED (no runner)" | tee "$FAST_REPORT"
|
||||||
|
# Do not gate; this is the expected state before Step 5 ships.
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# E2E profile — Playwright (Chromium + Firefox) against the suite docker stack.
|
||||||
|
# The compose file is owned by the parent suite repo; this script only invokes it.
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
if [ "$RUN_E2E" = "true" ]; then
|
||||||
|
echo "[run-tests] === e2e profile ==="
|
||||||
|
COMPOSE_FILE="$SUITE_ROOT/e2e/docker-compose.suite-e2e.yml"
|
||||||
|
E2E_REPORT="$RESULTS_DIR/e2e-report.txt"
|
||||||
|
|
||||||
|
if [ ! -f "$COMPOSE_FILE" ]; then
|
||||||
|
echo "[e2e] FATAL: $COMPOSE_FILE not found." >&2
|
||||||
|
echo "[e2e] The suite-level docker-compose harness is owned by the parent suite repo (..)." >&2
|
||||||
|
echo "[e2e] See _docs/02_document/tests/environment.md → Test Execution → Docker mode." >&2
|
||||||
|
OVERALL_EXIT=1
|
||||||
|
elif ! command -v docker >/dev/null 2>&1; then
|
||||||
|
echo "[e2e] FATAL: docker is required for the e2e profile." >&2
|
||||||
|
OVERALL_EXIT=1
|
||||||
|
else
|
||||||
|
echo "[e2e] starting compose stack at $COMPOSE_FILE..."
|
||||||
|
docker compose -f "$COMPOSE_FILE" up -d --build
|
||||||
|
E2E_COMPOSE_STARTED_HERE=true
|
||||||
|
|
||||||
|
echo "[e2e] running playwright-runner..."
|
||||||
|
if docker compose -f "$COMPOSE_FILE" run --rm playwright-runner 2>&1 | tee "$E2E_REPORT"; then
|
||||||
|
echo "[run-tests] e2e profile PASSED"
|
||||||
|
else
|
||||||
|
echo "[run-tests] e2e profile FAILED — see $E2E_REPORT"
|
||||||
|
OVERALL_EXIT=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Summary
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
echo ""
|
||||||
|
echo "[run-tests] summary"
|
||||||
|
echo "[run-tests] static profile : $([ "$RUN_STATIC" = "true" ] && echo "ran" || echo "skipped")"
|
||||||
|
echo "[run-tests] fast profile : $([ "$RUN_FAST" = "true" ] && echo "ran" || echo "skipped")"
|
||||||
|
echo "[run-tests] e2e profile : $([ "$RUN_E2E" = "true" ] && echo "ran" || echo "skipped")"
|
||||||
|
echo "[run-tests] results dir : $RESULTS_DIR"
|
||||||
|
echo "[run-tests] exit code : $OVERALL_EXIT"
|
||||||
|
|
||||||
|
exit "$OVERALL_EXIT"
|
||||||
Reference in New Issue
Block a user