Files
ui/_docs/02_document/system-flows.md
Oleksandr Bezdieniezhnykh 09449bda2c
ci/woodpecker/push/build-arm Pipeline failed
[AZ-510][AZ-511][AZ-512][AZ-513] Cycle 3 Steps 12-15 + admin prereq
Wrap up cycle 3 across the autodev existing-code Phase B steps that
follow Implement (Steps 12-15), plus the cross-workspace prerequisite
ticket filed for AZ-512.

Step 12 - Test-Spec Sync:
- Un-quarantine FT-P-01 in traceability-matrix (closed by AZ-510)
- Add AZ-510 chained /users/me failure-path test reference under AC-23
- Note AZ-512 deferral status under O9 (P12 Phase B target)

Step 13 - Update Docs (task mode):
- Refresh src__auth__AuthContext module doc with AZ-510 wire shape
  (POST refresh + chained /users/me + bootstrapInflight guard)
- Add usersMe() to src__api__endpoints module doc + consumer note
- Rename src__features__annotations__classColors module doc to
  src__class-colors__classColors (matches AZ-511 git mv); refresh header
- Refresh src__components__DetectionClasses + src__features__annotations
  module group doc for the new class-colors barrel import path
- Update components/11_class-colors Module Inventory to point at the
  renamed module doc filename
- Rewrite system-flows.md Flow F2 (Bearer auto-refresh) with the AZ-510
  POST + chained /users/me sequence; close Finding B3 references
- Generate ripple_log_cycle3 documenting all changed source files,
  their reverse-dependency search results, and the docs touched

Step 14 - Security Audit (cycle-3 delta):
- Resume mode against cycle-2 baseline; cycle-2 artifacts untouched
- Re-run bun audit on both roots: clean (cycle-2 inline fix held)
- Re-rate OWASP A06: FAIL -> PASS; A07: PASS_WITH_KNOWN -> PASS (B3
  closed by AZ-510)
- New finding F-SAST-CY3-1 (LOW): __resetBootstrapInflightForTests
  exposed via src/auth public barrel; defer to hygiene cycle
- Verdict: FAIL -> PASS_WITH_WARNINGS; one HIGH (F-SAST-1
  mission-planner git-history key, unchanged) remains
- Add amendment banner to cycle-2 security_report.md

Step 15 - Performance Test:
- Static profile NFT-PERF-01 PASS (290 575 B gzipped vs 2 MB budget;
  ~14% of budget; no regression from AZ-510 surface additions)
- E2E profile SKIP (Playwright perf project still pending AZ-457..AZ-482);
  legitimate skip per test-run skill, gap acknowledged in report
- AZ-510 200ms p95 chain NFR verified at spec level only - no CI gate
  yet (covered by future AZ-457..AZ-482 work)

Cross-workspace prerequisite (AZ-513 just filed):
- Updated _docs/_process_leftovers/2026-05-13_az-512-admin-classes-prereq.md
  to reflect AZ-513 filing on admin/ workspace (parent epic AZ-509,
  Blocks link to AZ-512). Companion task spec added in admin/ repo
  (separate commit there, owned by admin/ workspace).

State file: advanced to Step 16 (Deploy) per autodev existing-code flow.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-13 03:58:21 +03:00

666 lines
33 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 (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 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.