# 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 (bootstrap + 401-retry)

> **Cycle 3 / 2026-05-13 — AZ-510 consolidated the two refresh paths.** The historical "two divergent paths" wording below has been rewritten. The previous bug (finding B3 / Vision P3 violation) is now CLOSED.

### Description

There are two refresh trigger points in the source, but they now share a single wire shape:

1. **Bootstrap path** — `AuthContext.tsx` (`runBootstrap()` helper, guarded by a module-scoped `bootstrapInflight` promise to deduplicate React 18+ StrictMode dev double-mounts). On `<AuthProvider>` mount it calls `fetch(getApiBase() + endpoints.admin.authRefresh(), { method: 'POST', credentials: 'include' })`. On success it sets the bearer and **chains** `api.get<AuthUser>(endpoints.admin.usersMe())` (= `GET /api/admin/users/me`) to fetch the user record (the POST refresh response is `{ token }` only). On any failure path the bearer is cleared first, then `user: null` + `loading: false`.
2. **401-retry path** — `api/client.ts:73` automatically calls `fetch('/api/admin/auth/refresh', { method: 'POST', credentials: 'include' })` and replays the original request when any authenticated fetch returns 401.

Both paths now POST with `credentials:'include'` and rely on the HttpOnly refresh cookie set on `/login`.

### 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 — POST refresh + chained `/users/me`, AZ-510)

```mermaid
sequenceDiagram
    participant App
    participant AuthCtx as 02_auth/AuthContext
    participant AdminApi as admin/ service

    App->>AuthCtx: <AuthProvider> mounts
    AuthCtx->>AuthCtx: bootstrapInflight guard (StrictMode dedupe)
    AuthCtx->>AdminApi: POST /admin/auth/refresh (credentials:'include')
    AdminApi-->>AuthCtx: 200 {token} + Set-Cookie: refresh=...
    AuthCtx->>AuthCtx: setToken(token)
    AuthCtx->>AdminApi: GET /admin/users/me (Authorization: Bearer <token>)
    AdminApi-->>AuthCtx: 200 {id, email, permissions}
    AuthCtx->>AuthCtx: setUser(...), setLoading(false)
    AuthCtx-->>App: ProtectedRoute sees user → renders gated route
```

### Error Scenarios

| Error | Where | Detection | Recovery |
|-------|-------|-----------|----------|
| ~~Bootstrap GET refresh missing `credentials:'include'`~~ | — | — | **CLOSED 2026-05-13 by AZ-510.** Bootstrap now POSTs with `credentials:'include'`. Finding B3 / Vision P3 violation resolved. |
| Refresh 401 on bootstrap | `AuthContext.tsx` `runBootstrap()` | non-OK response from POST refresh | `setUser(null)` + `setLoading(false)` → `ProtectedRoute` redirects to `/login`. No console.error (expected on first visit / signed-out user). |
| Refresh network error on bootstrap | `AuthContext.tsx` `runBootstrap()` | outer `.catch` on the POST refresh fetch | `setToken(null)` + `setUser(null)` + `setLoading(false)` + `console.error('[AuthContext] Bootstrap failed:', err)`. UI redirects to `/login`. |
| Refresh 200 → `/users/me` failure (401, network, etc.) | `AuthContext.tsx` `runBootstrap()` | inner `try/catch` around `api.get(usersMe())` | `setToken(null)` first (Constraint #4 — bearer cleared before user state) + `console.error('[AuthContext] Refresh succeeded but /users/me failed:', err)` + return null → top-level then-handler sets `user: null` + `loading: false`. Covered by `AC-4 (AZ-510)` regression test. |
| 401-retry path inside `api/client.ts` | `api/client.ts:73` | 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.
