[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:
Oleksandr Bezdieniezhnykh
2026-05-11 00:38:49 +03:00
parent da0a5aa187
commit 510df68bcf
84 changed files with 13065 additions and 0 deletions
+656
View File
@@ -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 | F4F11 (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 19 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.