# 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: 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 `#` | 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 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,
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.