mirror of
https://github.com/azaion/ui.git
synced 2026-06-21 09:21:10 +00:00
510df68bcf
Captures the full output of autodev existing-code Phase A through Step 4 (Code Testability Revision) for the Azaion UI workspace: - Step 1 Document: _docs/02_document/ (FINAL_report, architecture, glossary, components/, modules/, diagrams/, system-flows, module-layout) plus _docs/00_problem/ + _docs/01_solution/ + _docs/legacy/ + _docs/how_to_test + README. - Step 2 Architecture Baseline: architecture_compliance_baseline.md. - Step 3 Test Spec: _docs/02_document/tests/ (environment, test-data, blackbox/performance/resilience/security/ resource-limit tests, traceability-matrix), enum_spec_snapshot, expected_results/results_report.md (98 rows), plus the run-tests.sh + run-performance-tests.sh runners. - Step 4 Code Testability Revision: 01-testability-refactoring/ run dir (list-of-changes C01-C07, deferred_to_refactor, analysis/research_findings + refactoring_roadmap) and the 7 child task specs AZ-448..AZ-454 under _docs/02_tasks/todo/ plus _dependencies_table.md. - _docs/_autodev_state.md pins the cursor at Step 4 / refactor Phase 4 entry so /autodev resumes cleanly. Epic AZ-447 (UI testability gates) tracks the 7 child tasks that will land in subsequent commits. Co-authored-by: Cursor <cursoragent@cursor.com>
657 lines
32 KiB
Markdown
657 lines
32 KiB
Markdown
# Azaion UI — System Flows
|
||
|
||
> Synthesis output of `/document` Step 3b. Each flow is grounded in component
|
||
> specs (`components/*/description.md`) and module docs (`modules/*.md`). When a
|
||
> step references a finding (e.g., #14), the source is one of:
|
||
> `_docs/02_document/modules/src__features__annotations.md`,
|
||
> `..__flights.md`, `..__dataset__DatasetPage.md`, or `..__App-and-main.md`.
|
||
|
||
## Flow Inventory
|
||
|
||
| # | Flow Name | Trigger | Primary Components | Criticality |
|
||
|---|-----------|---------|--------------------|-------------|
|
||
| F1 | Login | User submits `/login` form | `04_login`, `02_auth`, `admin/` | High |
|
||
| F2 | Bearer auto-refresh on 401 | Any authenticated fetch returns 401 | `02_auth`, `01_api-transport`, `admin/` | High |
|
||
| F3 | Select active flight | User picks a flight in Header dropdown | `03_shared-ui` (FlightContext + Header), `flights/` + `annotations/settings/user` | High |
|
||
| F4 | Create / save flight + waypoints | User clicks Save in `FlightsPage` | `05_flights`, `flights/` | High |
|
||
| F5 | Annotate media (manual bbox) | User drags on canvas | `06_annotations`, `annotations/` | High |
|
||
| F6 | AI Detect — image (sync) | User clicks AI Detect with image selected | `06_annotations`, `detect/` | Medium |
|
||
| F7 | AI Detect — video (async) — **NOT WIRED** | User clicks AI Detect with video selected | (target) `06_annotations`, `detect/`, `01_api-transport/sse.ts` | High |
|
||
| F8 | Dataset browse + filter | User opens `/dataset` | `07_dataset`, `annotations/` | Medium |
|
||
| F9 | Dataset bulk-validate | User selects thumbnails + Validate (planned, missing today) | `07_dataset`, `annotations/` | Medium |
|
||
| F10 | Admin: detection class CRUD | User edits in `/admin` | `08_admin`, `admin/` (write) + `annotations/` (read) | High |
|
||
| F11 | Settings: persist user prefs | User saves panel widths / system / camera | `09_settings`, `annotations/` (system/dirs/user) + `flights/` (aircraft) | Medium |
|
||
| F12 | GPS-Denied Test Mode (planned) | User uploads `.tlog` + video | `05_flights` (Test Mode tab), `gps-denied-desktop/`, `gps-denied-onboard/` | Medium (planned) |
|
||
| F13 | Live-GPS aircraft telemetry (SSE) | User opens FlightsPage with selected flight | `05_flights/FlightsPage`, `flights/` (`/api/flights/${id}/live-gps` SSE) | Medium |
|
||
| F14 | Annotation-status events (SSE) | User opens AnnotationsPage with selected media | `06_annotations/AnnotationsSidebar`, `annotations/` (`/api/annotations/annotations/events` SSE) | Medium |
|
||
|
||
## Flow Dependencies
|
||
|
||
| Flow | Depends On | Shares Data With |
|
||
|------|-----------|------------------|
|
||
| F1 | — (entry) | F2 (auth state cycles forever afterwards) |
|
||
| F2 | F1 (initial bearer must exist) | every authenticated flow |
|
||
| F3 | F1 | F4–F11 (selected flight scopes most lists) |
|
||
| F4 | F3 | F5 (annotations are scoped per media → per flight) |
|
||
| F5 | F3, media exists | F6, F7 (manual edits coexist with AI runs) |
|
||
| F6 | F3, image media exists | F5 |
|
||
| F7 | F3, video media exists | F5 |
|
||
| F8 | F3, F5/F6/F7 produced data | F9 |
|
||
| F9 | F8 | F10 (validation status feeds class metrics) |
|
||
| F10 | F1 (admin role) | F5 (class definitions are inputs to drawing) |
|
||
| F11 | F1 | F5 (panel widths are read by `useResizablePanel`, but persistence is missing today — finding #11) |
|
||
| F12 | F4 (a flight context for the test) | F7 (output detections may be cross-checked) |
|
||
| F13 | F3 (selected flight) | F4 (live-GPS overlays the planned route) |
|
||
| F14 | F5 (an annotation must exist for an event to fire) | F8 (dataset reflects status changes pushed via this stream) |
|
||
|
||
---
|
||
|
||
## Flow F1: Login
|
||
|
||
### Description
|
||
|
||
Operator submits credentials at `/login`. On success, the Admin service issues a JWT bearer (response body) and a `Secure; HttpOnly; SameSite=Strict` refresh-token cookie. `AuthContext` stores the bearer in memory; `ProtectedRoute` lets the user enter the app.
|
||
|
||
### Preconditions
|
||
|
||
- The Admin service is reachable through the nginx `/api/admin/*` proxy (or Vite dev proxy).
|
||
- The user has not yet authenticated (or their bearer + refresh cookie are both expired).
|
||
|
||
### Sequence Diagram
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
actor User
|
||
participant LoginPage as 04_login/LoginPage
|
||
participant AuthCtx as 02_auth/AuthContext
|
||
participant ApiClient as 01_api-transport/client
|
||
participant AdminApi as admin/ service
|
||
|
||
User->>LoginPage: Enter email + password, click Submit
|
||
LoginPage->>AuthCtx: login(email, password)
|
||
AuthCtx->>ApiClient: api.post('/api/admin/auth/login', {email, password})
|
||
ApiClient->>AdminApi: POST /admin/auth/login (credentials:'include')
|
||
AdminApi-->>ApiClient: 200 {bearer, user} + Set-Cookie: refresh=...
|
||
ApiClient-->>AuthCtx: {bearer, user}
|
||
AuthCtx-->>LoginPage: ok
|
||
LoginPage-->>User: Redirect to '/' (= '/flights' default)
|
||
```
|
||
|
||
### Flowchart
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
Start([User submits login form]) --> ValidateForm{Both fields filled?}
|
||
ValidateForm -->|No| ShowFormError[Show inline error]
|
||
ValidateForm -->|Yes| Theatrical[runUnlockSequence: 4×600 ms theatrical animation — finding B4]
|
||
Theatrical --> CallApi[POST /api/admin/auth/login]
|
||
CallApi --> Resp{200 OK?}
|
||
Resp -->|Yes| StoreBearer[AuthContext stores bearer in memory]
|
||
StoreBearer --> Redirect([Navigate to '/'])
|
||
Resp -->|401| ShowAuthError[Show 'Invalid credentials']
|
||
Resp -->|5xx / network| ShowGenericError[Show 'Try again']
|
||
ShowAuthError --> Start
|
||
ShowGenericError --> Start
|
||
ShowFormError --> Start
|
||
```
|
||
|
||
### Data Flow
|
||
|
||
| Step | From | To | Data | Format |
|
||
|------|------|----|------|--------|
|
||
| 1 | User | LoginPage | email + password | form input |
|
||
| 2 | LoginPage | AuthContext | `{ email, password }` | function args |
|
||
| 3 | AuthContext | admin/ via api/client | `POST /api/admin/auth/login` | JSON body |
|
||
| 4 | admin/ | AuthContext | `{ bearer, user: AuthUser }` + Set-Cookie | JSON + cookie |
|
||
| 5 | AuthContext | LoginPage | success | promise resolution |
|
||
| 6 | LoginPage | router | `navigate('/')` | client-side redirect |
|
||
|
||
### Error Scenarios
|
||
|
||
| Error | Where | Detection | Recovery |
|
||
|-------|-------|-----------|----------|
|
||
| Invalid credentials | step 4 | 401 from admin/ | Show inline message; let user retry |
|
||
| Network failure | step 3 | fetch reject | Show generic error; let user retry |
|
||
| Theatrical animation hides server error | step 2 (theatrical waits 2.4 s) | none | UX cost — finding (B4) suggests removing the animation entirely |
|
||
| Refresh cookie blocked by browser | step 4 | next F2 immediately fails | User must log in again — accepted today |
|
||
|
||
### Performance Expectations
|
||
|
||
| Metric | Target | Notes |
|
||
|--------|--------|-------|
|
||
| End-to-end latency | < 1 s server-side + 2.4 s artificial delay | Artificial delay is the dominant cost — Step 4 fix |
|
||
|
||
---
|
||
|
||
## Flow F2: Bearer auto-refresh on 401 (TWO refresh paths exist in code)
|
||
|
||
### Description
|
||
|
||
There are **two distinct refresh code paths** in the source — the verification pass (Step 4) caught both:
|
||
|
||
1. **Bootstrap path** — `AuthContext.tsx:24` calls `api.get('/api/admin/auth/refresh')` on app mount. This **does NOT have `credentials:'include'`** because `api/client.ts` doesn't add it on GET. Result: the cookie is not sent, the bootstrap silently fails, the user starts unauthenticated even when they have a valid refresh cookie.
|
||
2. **401-retry path** — `api/client.ts:44` calls `fetch('/api/admin/auth/refresh', { method: 'POST', credentials: 'include' })` automatically when any authenticated fetch returns 401. This path IS correct.
|
||
|
||
The bootstrap path is the bug surfaced as finding B3 PRIORITY. The 401-retry path is the silent fallback that does work but only after the user has already hit a 401.
|
||
|
||
### Preconditions
|
||
|
||
- A valid refresh-cookie exists on the browser.
|
||
- The 401 came from a request that should be retryable (idempotent or annotated as retry-safe).
|
||
|
||
### Sequence Diagram (401-retry path inside `api/client.ts` — works correctly)
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant Page
|
||
participant ApiClient as 01_api-transport/client
|
||
participant AdminApi as admin/ service
|
||
|
||
Page->>ApiClient: api.get('/api/...')
|
||
ApiClient->>AdminApi: GET /... (with bearer)
|
||
AdminApi-->>ApiClient: 401 (bearer expired)
|
||
ApiClient->>AdminApi: POST /admin/auth/refresh (credentials:'include')
|
||
AdminApi-->>ApiClient: 200 {bearer} + Set-Cookie: refresh=...
|
||
ApiClient->>AdminApi: GET /... (retry with new bearer)
|
||
AdminApi-->>ApiClient: 200 OK
|
||
ApiClient-->>Page: response
|
||
```
|
||
|
||
### Sequence Diagram (Bootstrap path on app mount — broken)
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant App
|
||
participant AuthCtx as 02_auth/AuthContext
|
||
participant AdminApi as admin/ service
|
||
|
||
App->>AuthCtx: <AuthProvider> mounts
|
||
AuthCtx->>AdminApi: GET /admin/auth/refresh (NO credentials:'include' — finding B3)
|
||
AdminApi-->>AuthCtx: 401 (no cookie sent)
|
||
AuthCtx->>AuthCtx: setLoading(false), user stays null
|
||
AuthCtx-->>App: ProtectedRoute sees null user → redirects to /login
|
||
```
|
||
|
||
### Error Scenarios
|
||
|
||
| Error | Where | Detection | Recovery |
|
||
|-------|-------|-----------|----------|
|
||
| Bootstrap GET refresh missing `credentials:'include'` | `AuthContext.tsx:24` | server returns 401 because cookie was not sent | **Bug today** — finding B3 PRIORITY. Symptom: a user with a valid refresh cookie still gets bounced to `/login` on every fresh tab. Step 4 fix: change to POST with `credentials:'include'` (matching the 401-retry path), or just delete the bootstrap GET and let the first authenticated fetch's 401 trigger the retry path. |
|
||
| 401-retry path | `api/client.ts:44` | works | (no fix needed) |
|
||
| Refresh cookie expired or revoked | refresh call | 401 | UI redirects to `/login`. |
|
||
| SSE subscription holds a stale bearer | active EventSource | server closes the SSE stream | No reconnect logic today (Step 8 hardening). |
|
||
|
||
---
|
||
|
||
## Flow F3: Select active flight (CORRECTED Step 4)
|
||
|
||
### Description
|
||
|
||
Most pages are scoped to a "currently selected flight". `FlightContext` loads the flight list AND the user's stored selection on mount, then lets the user switch via the Header dropdown. The selection is **persisted as part of `UserSettings`** via the `annotations/` service — there is **no `/api/flights/select` endpoint**; this was a Step 3 documentation error caught at Step 4 verification.
|
||
|
||
### Sequence Diagram
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
actor User
|
||
participant Header as 03_shared-ui/Header
|
||
participant FlightCtx as 03_shared-ui/FlightContext
|
||
participant ApiClient
|
||
participant FlightsApi as flights/ service
|
||
participant AnnotationsApi as annotations/ service
|
||
|
||
Note over FlightCtx: On mount
|
||
FlightCtx->>FlightsApi: GET /api/flights?pageSize=1000
|
||
FlightsApi-->>FlightCtx: { items: Flight[] }
|
||
Note over FlightCtx: Flights with index > 1000 are silently dropped (finding B3)
|
||
FlightCtx->>AnnotationsApi: GET /api/annotations/settings/user
|
||
AnnotationsApi-->>FlightCtx: UserSettings { selectedFlightId }
|
||
alt selectedFlightId is set
|
||
FlightCtx->>FlightsApi: GET /api/flights/{selectedFlightId}
|
||
FlightsApi-->>FlightCtx: Flight
|
||
end
|
||
|
||
User->>Header: Click flight in dropdown
|
||
Header->>FlightCtx: selectFlight(flight)
|
||
FlightCtx->>FlightCtx: setSelectedFlight(flight) immediately (optimistic)
|
||
FlightCtx->>AnnotationsApi: PUT /api/annotations/settings/user { selectedFlightId }
|
||
Note over FlightCtx,AnnotationsApi: Fire-and-forget — error swallowed (finding B3)
|
||
```
|
||
|
||
### Error Scenarios
|
||
|
||
| Error | Where | Detection | Recovery |
|
||
|-------|-------|-----------|----------|
|
||
| Flights list > 1000 | initial load | not detected | **Silent ceiling** — finding B3. Step 4 fix: paginate or stream. |
|
||
| `selectFlight` PUT fails | step | none — fire-and-forget | UI keeps the local selection but the server does not — next reload reverts it. Step 4 fix. |
|
||
|
||
---
|
||
|
||
## Flow F4: Create / save flight + waypoints
|
||
|
||
### Description
|
||
|
||
User edits a flight in `FlightsPage`: drags waypoints on the map, edits altitudes, types parameters. On Save, the UI sends all changes to the `flights/` service.
|
||
|
||
### Sequence Diagram (current implementation — lossy)
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
actor User
|
||
participant FlightsPage as 05_flights/FlightsPage
|
||
participant ApiClient
|
||
participant FlightsApi as flights/ service
|
||
|
||
User->>FlightsPage: Drag waypoints, edit fields, click Save
|
||
FlightsPage->>ApiClient: api.put('/api/flights/{id}', {name, aircraftId, ...})
|
||
ApiClient->>FlightsApi: PUT /flights/{id}
|
||
FlightsApi-->>FlightsPage: 200
|
||
Note over FlightsPage,FlightsApi: Then a delete-then-recreate cycle for waypoints (finding #19)
|
||
FlightsPage->>FlightsApi: DELETE /flights/{id}/waypoints (loop or bulk)
|
||
loop for each new waypoint
|
||
FlightsPage->>FlightsApi: POST /flights/{id}/waypoints {name, lat, lng, order}
|
||
end
|
||
Note over FlightsPage,FlightsApi: But the suite spec wants {Geopoint:{Lat,Lon,MGRS}, Source, Objective, OrderNum, Height} — finding #20 (server may return 400)
|
||
FlightsApi-->>FlightsPage: per-waypoint result
|
||
FlightsPage-->>User: Saved (or partial-failure UI not built today)
|
||
```
|
||
|
||
### Error Scenarios
|
||
|
||
| Error | Where | Detection | Recovery |
|
||
|-------|-------|-----------|----------|
|
||
| Waypoint shape mismatch (UI vs spec, finding #20) | waypoint POST | server 400 | UI does not surface server errors well today — Step 4 priority |
|
||
| Partial failure (some waypoints created, some failed) | step (per-waypoint loop) | server returns mixed status | UI does not detect — Step 4 priority |
|
||
| Network drop mid-save | any | timeout | UI does not retry — Step 4 priority |
|
||
|
||
### Performance Expectations
|
||
|
||
| Metric | Target | Notes |
|
||
|--------|--------|-------|
|
||
| Save latency | < 1 s for ≤ 50 waypoints | Currently degrades with the N+M round-trip pattern (1 PUT + 1 DELETE + N POSTs). Bulk endpoint is a Step 5 solution candidate. |
|
||
|
||
---
|
||
|
||
## Flow F5: Annotate media (manual bbox)
|
||
|
||
### Description
|
||
|
||
User drags on the canvas to draw a bounding box, picks a class via 1–9 keyboard or DetectionClasses strip, and saves. The annotation persists to the `annotations/` service.
|
||
|
||
### Sequence Diagram
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
actor User
|
||
participant CanvasEditor as 06_annotations/CanvasEditor
|
||
participant Sidebar as 06_annotations/AnnotationsSidebar
|
||
participant Page as 06_annotations/AnnotationsPage
|
||
participant ApiClient
|
||
participant AnnotationsApi as annotations/ service
|
||
|
||
User->>CanvasEditor: Mouse drag (draw bbox)
|
||
User->>CanvasEditor: Press [1]–[9] (pick class)
|
||
CanvasEditor-->>Sidebar: New detection added (in-memory)
|
||
User->>Page: Click Save
|
||
Page->>ApiClient: api.post('/api/annotations/annotations', body)
|
||
Note over Page,ApiClient: Endpoint is doubly-prefixed (suite-service + resource path)
|
||
Note over Page,ApiClient: handleSave body today: {classNum, x, y, w, h, time} — finding #32
|
||
Note over Page,ApiClient: Spec wants: {classNum, x, y, w, h, videoTime, Source, WaypointId} — Step 4
|
||
ApiClient->>AnnotationsApi: POST /annotations/annotations
|
||
AnnotationsApi-->>Page: 200 {id}
|
||
Page-->>User: Saved
|
||
```
|
||
|
||
### Error Scenarios
|
||
|
||
| Error | Where | Detection | Recovery |
|
||
|-------|-------|-----------|----------|
|
||
| Save body shape wrong (finding #32) | save call | server 400 | Today the UI logs `console.error` and the user sees nothing — Step 4 priority |
|
||
| Time-window math (50/150 ms vs implementation 200/200, finding #6) | overlay render | wrong annotations show during playback | Step 4 fix |
|
||
| Gradient cap (16 % vs 25 % spec, finding #9) | sidebar render | visual drift only | Step 4 fix |
|
||
| Cross-origin video tainted canvas (finding #12) | export image | browser CSP error | Step 4 fix or Step 5 design (server-side render) |
|
||
|
||
---
|
||
|
||
## Flow F6: AI Detect — image (sync)
|
||
|
||
### Description
|
||
|
||
User clicks AI Detect with an image selected. The detect service runs YOLO inference synchronously and returns detections inline.
|
||
|
||
### Sequence Diagram
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
actor User
|
||
participant Sidebar as 06_annotations/AnnotationsSidebar
|
||
participant ApiClient
|
||
participant DetectApi as detect/ service
|
||
participant AnnotationsApi as annotations/ service
|
||
|
||
User->>Sidebar: Click AI Detect (image selected)
|
||
Sidebar->>ApiClient: api.post('/api/detect/{mediaId}')
|
||
ApiClient->>DetectApi: POST /detect/{mediaId}
|
||
DetectApi-->>ApiClient: 200 {detections[]}
|
||
ApiClient-->>Sidebar: detections
|
||
Note over Sidebar,AnnotationsApi: Detections may auto-persist (server-side) or require client save — verify in Step 4
|
||
Sidebar-->>User: Render detections on canvas
|
||
```
|
||
|
||
### Error Scenarios
|
||
|
||
| Error | Where | Detection | Recovery |
|
||
|-------|-------|-----------|----------|
|
||
| Inference timeout | step | fetch timeout | None today — finding #21 (errors silently `console.error`'d) |
|
||
| Class not in admin list | render | `getClassNameFallback` falls back to `#<n>` | Acceptable; covered by `11_class-colors` |
|
||
|
||
---
|
||
|
||
## Flow F7: AI Detect — video (async, with SSE) — NOT WIRED TODAY
|
||
|
||
### Description (target — what should exist)
|
||
|
||
User clicks AI Detect with a video. The detect service kicks off an async job and returns a job ID. The UI subscribes to `GET /api/detect/stream/{jobId}` (SSE) for progress + final results.
|
||
|
||
### What's actually in code (Step 4 verification)
|
||
|
||
The async-video flow is **completely absent** from the codebase. `AnnotationsSidebar.tsx:39` only does `POST /api/detect/${media.id}` — the same endpoint, regardless of whether the media is an image or a video. There are NO calls to `/api/detect/video/...` and NO EventSource subscriptions to `/api/detect/stream/...`. The only annotation-related SSE in the codebase is `createSSE('/api/annotations/annotations/events', ...)` in the same file (line 25), which streams annotation-**status** events, not detect progress.
|
||
|
||
The previously cited finding #21 ("AI-detect doesn't stream progress") is therefore stronger than originally documented: the async path does not exist at all. The fix in Step 4 must build:
|
||
- `POST /api/detect/video/${mediaId}` request (with `X-Refresh-Token` header, finding #30)
|
||
- `createSSE('/api/detect/stream/${jobId}', ...)` subscription
|
||
- progress UI in AnnotationsSidebar.
|
||
|
||
### Sequence Diagram (target — what should happen)
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
actor User
|
||
participant Sidebar as 06_annotations/AnnotationsSidebar
|
||
participant Page as 06_annotations/AnnotationsPage
|
||
participant ApiClient
|
||
participant SseClient as 01_api-transport/sse
|
||
participant DetectApi as detect/ service
|
||
|
||
User->>Sidebar: Click AI Detect (video selected)
|
||
Sidebar->>ApiClient: api.post('/api/detect/video/{mediaId}', {}, headers={X-Refresh-Token})
|
||
ApiClient->>DetectApi: POST /detect/video/{mediaId}
|
||
DetectApi-->>ApiClient: 202 {jobId}
|
||
Page->>SseClient: subscribeSSE(`/api/detect/stream/${jobId}?token=...`)
|
||
SseClient-->>DetectApi: GET /detect/stream/{jobId}?token=... (EventSource)
|
||
loop progress events
|
||
DetectApi-->>SseClient: data: {progress: 0..100}
|
||
SseClient-->>Page: onMessage(progress)
|
||
Page-->>User: Update progress bar
|
||
end
|
||
DetectApi-->>SseClient: data: {detections[], status: 'done'}
|
||
SseClient-->>Page: onMessage(final)
|
||
Page-->>User: Render detections; close stream
|
||
```
|
||
|
||
### Error Scenarios
|
||
|
||
| Error | Where | Detection | Recovery |
|
||
|-------|-------|-----------|----------|
|
||
| No SSE subscription today (finding #10) | step | user sees "AI Detect" go silent forever | Build the subscription — Step 4 PRIORITY |
|
||
| Bearer expires mid-job (no `X-Refresh-Token`, finding #30) | server side | job aborts | Send `X-Refresh-Token` so server can rotate transparently — Step 4 |
|
||
| EventSource holds stale token (finding cross-link) | refresh-rotation occurs while subscribed | server closes stream | Reconnect with new token — Step 8 hardening |
|
||
| Network blip | mid-stream | `onerror` | Reconnect with backoff — Step 8 |
|
||
|
||
---
|
||
|
||
## Flow F8: Dataset browse + filter
|
||
|
||
### Description
|
||
|
||
User opens `/dataset`, filters by class / status / search; thumbnails appear in a grid.
|
||
|
||
### Sequence Diagram
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
actor User
|
||
participant DatasetPage as 07_dataset/DatasetPage
|
||
participant ApiClient
|
||
participant AnnotationsApi as annotations/ service
|
||
|
||
User->>DatasetPage: Type in filter
|
||
Note over DatasetPage: 300 ms debounce via useDebounce
|
||
DatasetPage->>ApiClient: api.get('/api/annotations/dataset?status=...&classNum=...&q=...')
|
||
ApiClient->>AnnotationsApi: GET ...
|
||
AnnotationsApi-->>DatasetPage: PaginatedResponse<DatasetItem>
|
||
DatasetPage-->>User: Render thumbnails (NOT virtualised — finding #3)
|
||
```
|
||
|
||
### Error Scenarios
|
||
|
||
| Error | Where | Detection | Recovery |
|
||
|-------|-------|-----------|----------|
|
||
| Status filter sentinels collide (finding #8) | client query build | wrong items returned | Step 4 fix after enum-drift resolution |
|
||
| `classNum=0` collides with real class 0 (finding #9) | client query build | class 0 selectable but matches "All" | Step 4 fix |
|
||
| Long lists render all thumbnails (finding #3) | render | UI lag | Add virtualisation — Step 4 / Step 8 |
|
||
|
||
---
|
||
|
||
## Flow F9: Dataset bulk-validate (Step 4 CORRECTED — partially wired)
|
||
|
||
### Description
|
||
|
||
User multi-selects thumbnails (Ctrl+click) and clicks the **Validate** button — `DatasetPage.tsx:142-146` only renders this button when `selectedIds.size > 0`, so it's discoverable. On click, the page POSTs `/api/annotations/dataset/bulk-status` with `{annotationIds, status: AnnotationStatus.Validated}`. The selection is then cleared and the items re-fetched.
|
||
|
||
**The earlier doc draft claimed this was missing**. It's not; only the **`[V]` keyboard shortcut** is missing (finding #1 — keyboard shortcuts as a category).
|
||
|
||
### Sequence Diagram (actual)
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
actor User
|
||
participant DatasetPage as 07_dataset/DatasetPage
|
||
participant ApiClient
|
||
participant AnnotationsApi as annotations/ service
|
||
|
||
User->>DatasetPage: Ctrl+click thumbnails (build selectedIds)
|
||
User->>DatasetPage: Click Validate button (visible when selectedIds.size > 0)
|
||
DatasetPage->>ApiClient: api.post('/api/annotations/dataset/bulk-status', {annotationIds, status: AnnotationStatus.Validated})
|
||
ApiClient->>AnnotationsApi: POST /dataset/bulk-status
|
||
AnnotationsApi-->>DatasetPage: 200
|
||
DatasetPage->>DatasetPage: setSelectedIds(new Set()); fetchItems()
|
||
DatasetPage-->>User: Refresh visible thumbnails (status badge updated)
|
||
```
|
||
|
||
### Error Scenarios
|
||
|
||
| Error | Where | Detection | Recovery |
|
||
|-------|-------|-----------|----------|
|
||
| `[V]` keyboard shortcut missing | finding #1 (Dataset) | only mouse click works | Step 4 — add `useEffect` keydown listener on the page when `tab === 'annotations'` |
|
||
| `bulk-status` request body uses string instead of numeric (finding #11) | server payload | server may reject | Step 4 — verify the suite spec then fix the client side to match |
|
||
| Bulk action fails partway | server | `handleValidate` has no try/catch | Step 4 — surface a toast, keep selection so the user can retry |
|
||
|
||
---
|
||
|
||
## Flow F10: Admin — detection class CRUD
|
||
|
||
### Description
|
||
|
||
Admin user opens `/admin`, edits detection classes (color, name, photoMode, maxSizeM). Deletes are destructive but currently lack a confirm dialog (finding B4). Read uses `annotations/classes`; write uses `admin/classes` (finding B4 — verify with suite ADRs).
|
||
|
||
### Sequence Diagram (Step 4 corrected — no PUT/edit endpoint exists)
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
actor Admin
|
||
participant AdminPage as 08_admin/AdminPage
|
||
participant ApiClient
|
||
participant AnnotationsApi as annotations/ service
|
||
participant AdminApi as admin/ service
|
||
|
||
AdminPage->>ApiClient: api.get('/api/annotations/classes')
|
||
ApiClient->>AnnotationsApi: GET /classes
|
||
AnnotationsApi-->>AdminPage: DetectionClass[]
|
||
Admin->>AdminPage: Click "Add Class"
|
||
AdminPage->>ApiClient: api.post('/api/admin/classes', newClass)
|
||
ApiClient->>AdminApi: POST /classes
|
||
AdminApi-->>AdminPage: 200
|
||
AdminPage->>ApiClient: api.get('/api/annotations/classes') (re-read)
|
||
Admin->>AdminPage: Click Delete
|
||
Note over AdminPage: NO ConfirmDialog today (finding B4) — destructive action goes through immediately
|
||
AdminPage->>ApiClient: api.delete('/api/admin/classes/{id}')
|
||
ApiClient->>AdminApi: DELETE /classes/{id}
|
||
AdminApi-->>AdminPage: 200
|
||
```
|
||
|
||
> **Step 4 verification finding**: there is no edit-class endpoint in the codebase today. Admins can only **add** new classes and **delete** existing ones — they cannot modify an existing class's color / name / maxSizeM / photoMode. This is a real gap, not just a documentation drift; the WPF era supported in-place edits via the same DataGrid. Surface for Step 4 (decide: add a `PATCH /api/admin/classes/{id}` and a UI form, or accept add-and-delete as the only mutation path).
|
||
|
||
### Error Scenarios
|
||
|
||
| Error | Where | Detection | Recovery |
|
||
|-------|-------|-----------|----------|
|
||
| `/admin` route lacks RBAC gate | route entry | non-admin can browse | **Security PRIORITY** (App+main finding #1) — Step 4 |
|
||
| AI Settings + GPS Settings forms don't save (finding B4 PRIORITY) | save click | nothing happens | Step 4 PRIORITY |
|
||
| Hardcoded GPS device defaults (`192.168.1.100:5535`) shipped to prod (finding B4) | bundle | leaks internal network shape | Step 4 |
|
||
|
||
---
|
||
|
||
## Flow F11: Settings — persist user prefs (Step 4 CORRECTED)
|
||
|
||
### Description
|
||
|
||
User edits System / Directory / Camera / User settings, clicks Save. **PUTs go to `annotations/` (`/api/annotations/settings/{system,directories,user}`) — NOT `admin/` as initially drafted.** The aircraft default-toggle hits `flights/` (`PATCH /api/flights/aircrafts/${id}`). The "settings" name is a misnomer — what the UI calls "settings" is split across two services.
|
||
|
||
Panel widths (annotations / dataset, left / right) are typed fields in `UserSettings` (`00_foundation/types/index.ts`) but **`useResizablePanel` does not write them back today** — finding #11. The wire endpoint exists; the client wiring is the gap.
|
||
|
||
### Error Scenarios
|
||
|
||
| Error | Where | Detection | Recovery |
|
||
|-------|-------|-----------|----------|
|
||
| `saveSystem` / `saveDirs` lack try/finally (finding B4) | PUT failure | `saving:true` stays forever | Step 4 fix |
|
||
| Numeric inputs `parseInt(v) ‖ 0` (finding B4) | input clear | silent zero write | Step 4 fix |
|
||
| No optimistic concurrency (finding B4) | concurrent admin edits | last-write-wins, no conflict UI | Step 6 problem-extraction surface |
|
||
|
||
---
|
||
|
||
## Flow F12: GPS-Denied Test Mode (planned, not implemented)
|
||
|
||
### Description (target — per `_docs/how_to_test.md`)
|
||
|
||
The operator wants to validate the GPS-Denied onboard system without a real flight. They upload a `.tlog` file + a synced video (or one that the system will auto-sync via IMU analysis). The system feeds the simulated frames + IMU/GPS into the SITL pipeline and the onboard service consumes them as if they were live.
|
||
|
||
### Sequence Diagram (target)
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
actor Operator
|
||
participant TestMode as 05_flights/GPS-Denied/Test Mode (planned)
|
||
participant ApiClient
|
||
participant GpsDeskApi as gps-denied-desktop/ service
|
||
participant GpsBoardApi as gps-denied-onboard/ service
|
||
|
||
Operator->>TestMode: Upload tlog + video (drag-drop)
|
||
TestMode->>ApiClient: api.post('/api/gps-denied-desktop/test/sync', files)
|
||
ApiClient->>GpsDeskApi: POST /test/sync (multipart)
|
||
GpsDeskApi-->>TestMode: {sessionId, syncOffset, imuChart}
|
||
Note over GpsDeskApi: Server-side: extract timestamps + IMU + GPS from tlog,<br/>auto-sync video by detecting takeoff in IMU + video duration
|
||
|
||
Operator->>TestMode: Confirm sync, click Start SITL
|
||
TestMode->>ApiClient: api.post('/api/gps-denied-desktop/test/run', {sessionId})
|
||
ApiClient->>GpsDeskApi: POST /test/run
|
||
GpsDeskApi->>GpsBoardApi: stream IMU + frames (in-cluster)
|
||
GpsBoardApi-->>GpsDeskApi: positioning estimates
|
||
GpsDeskApi-->>TestMode: SSE stream of positioning estimates
|
||
TestMode-->>Operator: Render trajectory overlay + comparison chart
|
||
```
|
||
|
||
### Preconditions
|
||
|
||
- A flight context exists (Test Mode is scoped within a flight).
|
||
- The `.tlog` and video are valid; the IMU chart in the tlog is "drone-on-ground → take-off → land" so auto-sync can find the take-off event.
|
||
|
||
### Open architectural questions for Test Mode
|
||
|
||
1. Where does the IMU-based sync analysis live — in `gps-denied-desktop/`, in a new Cython worker, or client-side?
|
||
2. Is SITL state persisted across page reloads (= server-side session), or is it in-memory client-side only?
|
||
3. Output rendering — overlay on the GPS-Denied tab's existing map, or a dedicated comparison chart view?
|
||
|
||
These belong in Step 4.5 (Architecture Vision); Test Mode is in this flow inventory only as a **planned** flow so downstream consumers don't lose it.
|
||
|
||
---
|
||
|
||
## Flow F13: Live-GPS aircraft telemetry (SSE) — added at Step 4
|
||
|
||
### Description
|
||
|
||
When the user has a flight selected and FlightsPage is open, the page subscribes to a per-flight live-GPS event stream so the map can render the aircraft's current position in real time. Discovered at Step 4 verification (`FlightsPage.tsx:67`).
|
||
|
||
### Sequence Diagram
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
actor User
|
||
participant FlightsPage as 05_flights/FlightsPage
|
||
participant SseClient as 01_api-transport/sse
|
||
participant FlightsApi as flights/ service
|
||
|
||
User->>FlightsPage: Select a flight (Header dropdown)
|
||
FlightsPage->>SseClient: createSSE(`/api/flights/${flightId}/live-gps`, onEvent)
|
||
SseClient-->>FlightsApi: GET /api/flights/{flightId}/live-gps?token=... (EventSource)
|
||
loop while flight is selected
|
||
FlightsApi-->>SseClient: data: { lat, lon, satellites, status }
|
||
SseClient-->>FlightsPage: onEvent(payload)
|
||
FlightsPage-->>User: Update aircraft marker on map
|
||
end
|
||
User->>FlightsPage: Switch flight or navigate away
|
||
FlightsPage->>SseClient: source.close()
|
||
```
|
||
|
||
### Error Scenarios
|
||
|
||
| Error | Where | Detection | Recovery |
|
||
|-------|-------|-----------|----------|
|
||
| Bearer expires mid-stream | server | server closes the stream | EventSource auto-reconnects with **stale token** — same Step 8 hardening as F7 |
|
||
| Aircraft offline | data shape | `status: 'offline'` payload | UI should grey out the marker — verify in Step 4 |
|
||
| User switches flights rapidly | client | new `createSSE` before old `.close()` | Memory leak risk — verify cleanup in Step 4 |
|
||
|
||
---
|
||
|
||
## Flow F14: Annotation-status events (SSE) — added at Step 4
|
||
|
||
### Description
|
||
|
||
When the user opens AnnotationsPage with media selected, `AnnotationsSidebar.tsx:25` subscribes to `/api/annotations/annotations/events` to receive **annotation-status events** (created / edited / validated). This is the SSE that exists today; it is **NOT** the detect-progress SSE (F7), which doesn't exist yet.
|
||
|
||
### Sequence Diagram
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
actor User
|
||
participant Sidebar as 06_annotations/AnnotationsSidebar
|
||
participant SseClient as 01_api-transport/sse
|
||
participant AnnotationsApi as annotations/ service
|
||
|
||
User->>Sidebar: Open AnnotationsPage (media selected)
|
||
Sidebar->>SseClient: createSSE('/api/annotations/annotations/events', onEvent)
|
||
SseClient-->>AnnotationsApi: GET /api/annotations/annotations/events?token=... (EventSource)
|
||
loop while page mounted
|
||
AnnotationsApi-->>SseClient: data: { annotationId, mediaId, status }
|
||
SseClient-->>Sidebar: onEvent(payload)
|
||
Sidebar->>Sidebar: refetch annotations for current media
|
||
end
|
||
User->>Sidebar: Navigate away
|
||
Sidebar->>SseClient: source.close()
|
||
```
|
||
|
||
### Notes
|
||
|
||
- The stream is **not scoped per media** — every connected user receives all annotation-status events. Filtering happens client-side (`event.mediaId === selectedMedia.id`). Acceptable for low-volume admin use; flag as a Step 6 problem-extraction surface for scale.
|
||
- Bearer-rotation issue applies here too (Step 8 hardening).
|
||
|
||
---
|
||
|
||
## Mermaid Diagram Conventions
|
||
|
||
- **Participants**: matched to component IDs `NN_name` (e.g., `04_login`, `02_auth`, `01_api-transport`).
|
||
- **External services**: named after their suite-service folder (`admin/`, `flights/`, `annotations/`, `detect/`, `gps-denied-desktop/`, `gps-denied-onboard/`).
|
||
- **Decision nodes**: `{Question?}`.
|
||
- **Start/End**: `([label])` stadium shape.
|
||
- **No styling** — let the renderer theme handle it.
|