This commit captures everything produced during autodev existing-code Steps 1 (Document), 2 (Architecture Baseline Scan), and 3 (Test Spec), together with the targeted auth + CORS re-sync triggered on 2026-05-14 when codebase drift was detected at Step 4 entry. None of this work was previously committed. Step 1 (Document) — 50+ _docs/02_document/ files: problem, solution, architecture, system flows, glossary, module-layout, per-component specs (01..06), modules, deployment, diagrams, data model, FINAL report, verification log, discovery. Step 2 (Architecture Baseline) — architecture_compliance_baseline.md. Verdict PASS_WITH_WARNINGS (0 Critical, 0 High, 1 Medium, 2 Low). No High/Critical findings; auto-chained to Step 3 per existing-code flow. Step 3 (Test Spec) — _docs/02_document/tests/* (67 scenarios across blackbox, security, resilience, resource-limit, performance), plus e2e/docker-compose.test.yml, e2e/seed/run.sh, scripts/run-tests.sh, scripts/run-performance-tests.sh. Coverage 88% over the active scope (40 of 45 items covered, 6 RB-deferred, 5 documented-as-uncovered). Targeted auth + CORS re-sync — replaces the deleted in-house token issuer with a JWKS-verifier model. AuthController and TokenService removed; JwtExtensions switched from HS256 symmetric to ES256 over admin's JWKS. ConfigurationResolver and CorsConfigurationValidator added under src/Infrastructure/. ADR-002 and ADR-006 retired; SEC-01, SEC-02, SEC-03 marked Closed. One new testability risk recorded in architecture.md Open Risks Section 6 (JWKS HTTPS gating). Source changes: - src/Auth/JwtExtensions.cs (modified) — ES256, JWKS, alg pinning - src/Program.cs (modified) — DI wiring for ConfigurationResolver and CorsConfigurationValidator - src/Controllers/AuthController.cs (deleted) — no in-service issuance - src/Services/TokenService.cs (deleted) — same - src/Infrastructure/ConfigurationResolver.cs (new) - src/Infrastructure/CorsConfigurationValidator.cs (new) - .env.example (new) — required env var documentation - .gitignore (updated) Cross-repo coordination: _docs/cross-repo/flights_h1_h2_h3_change_spec captures the change-spec for downstream services that consumed the now deleted /auth endpoints. Co-authored-by: Cursor <cursoragent@cursor.com>
20 KiB
Azaion.Annotations — System Flows
Bottom-up: traces in this document are derived from
components/*/description.md,modules/*.md, and the source undersrc/. Mermaid diagrams per flow are linked underdiagrams/flows/.
Flow Inventory
| # | Flow Name | Trigger | Primary Components | Criticality |
|---|---|---|---|---|
| F1 | Annotation Create (with image bytes) | POST /annotations from detections service or UI |
01 + 02 + 06 + 03 | High |
| F2 | Annotation Listing / Read | GET /annotations, `GET /annotations/{id}/{thumbnail |
image}` | 01 + 06 + 03 |
| F3 | Real-time SSE Subscription | GET /annotations/events from UI |
01 + 02 + 06 | High |
| F4 | Failsafe Outbox Drain → RabbitMQ Stream | FailsafeProducer background loop |
02 + 06 | High |
| F5 | Media Upload (single + batch) | POST /media, POST /media/batch |
03 + 06 | High |
| F6 | Auth Refresh (out-of-process) | Long-running callers refresh against admin's POST /token/refresh; annotations only verifies the resulting access token |
06 (verifier) + admin (issuer, out-of-scope) | Medium |
| F7 | Directory Settings Change → Path Cache Reset | PUT /settings/directories |
05 + 06 | Medium |
| F8 | Dataset Bulk Status | PATCH /dataset/.../status, bulk variant |
04 + 06 | Medium |
Flow Dependencies
| Flow | Depends on | Shares data with |
|---|---|---|
| F1 | F5 (media must exist for create-with-MediaId) |
F2 (read-after-write), F3 (Create-only event publish), F4 (Create-only queue insert, gated by silent_detection) |
| F2 | F1 (writes data being read), F5 | F3 (consistency window) |
| F3 | F1 (SSE stream is fed by F1 Create publishes only) | — |
| F4 | F1 (reads outbox written by F1 Create only) | downstream consumers (admin sync, AI training) |
| F5 | — | F1 |
| F6 | — | all [Authorize] flows (refreshes the token they use) |
| F7 | — | F1, F2, F4, F5 (all paths via PathResolver) |
| F8 | F1 | none today — F8 does not feed F3 or F4 (open question) |
Flow F1: Annotation Create (with image bytes)
Description
Detections service or UI POSTs an annotation payload with image bytes (or a MediaId for an existing media row). The service hashes the bytes, derives the annotation id, writes the image to disk, ensures a media row exists, persists annotation + detection rows, writes the YOLO label file, publishes an in-process SSE event, and — unless system_settings.silent_detection is true — enqueues an outbox row for downstream RabbitMQ stream export. Thumbnails are not generated in this flow (they are read-only via PhysicalFile from a separately populated path).
Preconditions
- Caller holds a JWT with
permissions: ANN. directory_settingsrow exists (seeded by migrator with/data/...defaults).- Postgres reachable (errors otherwise surfaced as 500 by
ErrorHandlingMiddleware).
Sequence Diagram
See diagrams/flows/flow_annotation_create.md for the full sequence + flowchart.
sequenceDiagram
autonumber
participant Caller as Detections / UI
participant Ctrl as AnnotationsController (01)
participant Svc as AnnotationService (01)
participant Path as PathResolver (06)
participant DB as PostgreSQL (06)
participant FS as Filesystem
participant Evt as AnnotationEventService (02)
participant Q as annotations_queue_records (DB / 02)
Caller->>Ctrl: POST /annotations (CreateAnnotationRequest, JWT)
Ctrl->>Svc: CreateAnnotation(request, userId from JWT)
alt request.Image bytes provided
Svc->>Svc: ComputeHash (XxHash64 over sampled bytes) -> id
Svc->>Path: GetImagePath(id)
Svc->>FS: write {id}.jpg
Svc->>DB: SELECT media WHERE id=id
opt media row missing
Svc->>DB: INSERT media (Image, MediaStatus.New, ...)
end
else request.MediaId provided
Svc->>DB: SELECT media WHERE id=MediaId (404 if missing)
Svc->>Path: GetImagePath(id)
opt source media file exists & target image missing
Svc->>FS: copy media.Path -> {id}.jpg
end
end
Svc->>DB: INSERT annotations
Svc->>DB: BulkCopy detection rows
Svc->>Path: GetLabelPath(id)
Svc->>FS: write {id}.txt (YOLO)
Svc->>Evt: PublishAsync(AnnotationEventDto)
Svc->>DB: SELECT system_settings (FirstOrDefault)
alt SilentDetection != true
Svc->>Q: FailsafeProducer.EnqueueAsync(db, id, QueueOperation.Created)
end
Svc-->>Ctrl: Annotation
Ctrl-->>Caller: 201 Created (Location: /annotations/{id})
Data Flow
| Step | From | To | Data | Format |
|---|---|---|---|---|
| 1 | Caller | AnnotationsController |
CreateAnnotationRequest + JWT |
JSON / Bearer |
| 2 | AnnotationService |
Filesystem | image bytes | {id}.jpg under images_dir |
| 3 | AnnotationService |
DB | media row (insert if absent) |
SQL via Linq2DB |
| 4 | AnnotationService |
DB | annotations row |
SQL |
| 5 | AnnotationService |
DB | detection rows |
BulkCopyAsync |
| 6 | AnnotationService |
Filesystem | YOLO label {id}.txt |
text lines class cx cy w h |
| 7 | AnnotationService |
AnnotationEventService |
AnnotationEventDto |
in-memory Channel<> |
| 8 | AnnotationService |
DB outbox | annotations_queue_records (operation=Created) |
row, only if SilentDetection != true |
Error Scenarios
| Error | Where | Detection | Recovery |
|---|---|---|---|
| Neither bytes nor MediaId provided | request validation | ArgumentException in service |
mapped to 400 by middleware |
Referenced MediaId not found |
media lookup | KeyNotFoundException |
404 |
| Filesystem write fails (no perms / disk full) | step 2 / 6 | IOException | 500 via middleware; NOT transactional with DB — risk of orphan files on partial failure |
| DB write fails after FS success | steps 3–5 | Linq2DB exception | 500; orphan image / label may remain (open risk) |
| SSE publish fails | step 7 | unbounded channel — failure unlikely | logged via default ASP.NET Core logger |
| Outbox insert fails after SSE publish | step 8 | exception | 500; UI saw the event but downstream stream consumers will not — observable inconsistency |
| RabbitMQ unavailable | n/a here | — | F4 handles drain offline — F1 itself is unaffected |
Performance Expectations
| Metric | Target | Notes |
|---|---|---|
| End-to-end latency | not specified in code | dominant cost: hashing + 3 disk writes; flag for 00_problem extraction |
| Throughput | not specified | single instance bounded by DB + disk FS |
Flow F2: Annotation Listing / Read
Description
UIs and dataset consumers list annotations with filters (e.g., FlightId, status) and fetch image / thumbnail bytes. Read path is read-only against Postgres + PhysicalFile from the configured directories.
Preconditions
- Caller holds JWT with
ANN(orDATASETfor the dataset variant in F8).
Sequence Diagram
sequenceDiagram
autonumber
participant UI
participant Ctrl as AnnotationsController (01)
participant Svc as AnnotationService (01)
participant DB
participant Path as PathResolver (06)
participant FS as Filesystem
UI->>Ctrl: GET /annotations?filters
Ctrl->>Svc: GetAnnotations(query)
Svc->>DB: SELECT annotations × detection × media
DB-->>Svc: rows
Svc-->>Ctrl: PaginatedResponse<AnnotationListItem>
Ctrl-->>UI: 200 OK (JSON)
UI->>Ctrl: GET /annotations/{id}/thumbnail
Ctrl->>Path: GetThumbnailPath(id)
Path-->>Ctrl: /data/thumbnails/{id}.jpg
Ctrl->>FS: File.Exists?
alt exists
Ctrl-->>UI: 200 OK (image/jpeg, PhysicalFile)
else missing
Ctrl-->>UI: 404 NotFound
end
Data Flow
| Step | From | To | Data | Format |
|---|---|---|---|---|
| 1 | UI | controller | GetAnnotationsQuery |
query string |
| 2 | service | DB | filtered join | SQL |
| 3 | service | UI | list + paging metadata | PaginatedResponse<AnnotationListItem> |
| 4 | controller | UI | image / thumbnail bytes | image/jpeg |
Error Scenarios
| Error | Where | Detection | Recovery |
|---|---|---|---|
| Missing image file | thumbnail / image route | File.Exists false |
404 |
| Auth failure | model binding | JWT pipeline | 401 / 403 |
| DB error | listing | Linq2DB | 500 via middleware |
Flow F3: Real-time SSE Subscription
Description
UI opens a long-lived text/event-stream connection and receives JSON-serialized AnnotationEventDto payloads as they are published by F1, F8, and any other annotation mutation.
Sequence Diagram
sequenceDiagram
autonumber
participant UI
participant Ctrl as AnnotationsController.Events (02 doc-ownership)
participant Evt as AnnotationEventService (02)
participant Producer as Other flows (F1/F8)
UI->>Ctrl: GET /annotations/events (Accept: text/event-stream, JWT ANN)
Ctrl->>Evt: subscribe(Reader)
loop until cancelled
Producer->>Evt: PublishAsync(AnnotationEventDto)
Evt-->>Ctrl: ReadAllAsync yields event
Ctrl-->>UI: data: {json}\n\n
end
UI--xCtrl: client disconnect / cancel
Data Flow
| Step | From | To | Data | Format |
|---|---|---|---|---|
| 1 | UI | controller | upgrade to SSE | HTTP/1.1 |
| 2 | producer | service | AnnotationEventDto |
in-memory message |
| 3 | controller | UI | data: {json}\n\n |
SSE frame |
Error Scenarios
| Error | Where | Detection | Recovery |
|---|---|---|---|
| Auth failure | request | JWT pipeline | 401 |
| Client disconnect | streaming | CancellationToken |
controller exits cleanly |
| Process restart | streaming | n/a | UI must reconnect; buffered events between disconnect and restart are lost (intentional — durability handled by F4) |
Performance Expectations
In-process channel; latency is bounded by Channel<> + write-flush — sub-millisecond locally.
Flow F4: Failsafe Outbox Drain → RabbitMQ Stream
Description
FailsafeProducer is a singleton BackgroundService that polls annotations_queue_records, re-reads image bytes for Created operations, packs AnnotationQueueMessage / AnnotationBulkQueueMessage (MessagePack), and publishes to the azaion-annotations RabbitMQ stream. After a successful publish, the row is deleted.
Sequence Diagram
sequenceDiagram
autonumber
participant FP as FailsafeProducer (02)
participant DB
participant Path as PathResolver (06)
participant FS as Filesystem
participant RMQ as RabbitMQ Stream
loop while host running
FP->>DB: SELECT annotations_queue_records
DB-->>FP: pending rows
loop per row
alt operation = Created
FP->>Path: GetImagePath(annotationId)
FP->>FS: read bytes
end
FP->>FP: serialize MessagePack (Annotation* QueueMessage)
FP->>RMQ: publish stream entry
alt publish ok
FP->>DB: DELETE annotations_queue_records WHERE id = ...
else stream unavailable
FP->>FP: backoff + retry next loop
end
end
end
Data Flow
| Step | From | To | Data | Format |
|---|---|---|---|---|
| 1 | DB | producer | outbox rows | SQL |
| 2 | filesystem | producer | image bytes | binary |
| 3 | producer | RabbitMQ stream | AnnotationQueueMessage / AnnotationBulkQueueMessage |
MessagePack (gzip per impl) |
| 4 | producer | DB | DELETE | SQL |
Error Scenarios
| Error | Where | Detection | Recovery |
|---|---|---|---|
| RabbitMQ unreachable | publish | client exception | row stays in outbox; retried next tick |
Image file missing for Created |
step 2 | FS read fails | open question — current behavior should be confirmed in code-review (skip vs retry) |
| Concurrent drainers (multiple instances) | step 4 | no leasing | rows may be picked up twice → duplicate stream entries; consumers must dedupe |
Performance Expectations
Bounded by RabbitMQ stream throughput + disk read for Created; durability is the priority (see ADR-003).
Flow F5: Media Upload (single + batch)
Description
UI uploads media files. MediaController accepts a single JSON-described upload (POST /media) or a multipart batch (POST /media/batch with waypointId + IFormFileCollection). MediaService writes the file under the configured media directory and persists a media row.
Sequence Diagram
sequenceDiagram
autonumber
participant UI
participant Ctrl as MediaController (03)
participant Svc as MediaService (03)
participant Path as PathResolver (06)
participant DB
participant FS as Filesystem
UI->>Ctrl: POST /media[/batch] (multipart or JSON, JWT ANN)
Ctrl->>Svc: CreateMedia / CreateBatch
Svc->>Path: GetMediaDir(...)
Svc->>FS: write file(s) under media dir
Svc->>DB: INSERT media row(s)
Svc-->>Ctrl: created media id(s)
Ctrl-->>UI: 201 Created
Error Scenarios
| Error | Where | Detection | Recovery |
|---|---|---|---|
| Filesystem write fails | service | IOException | 500 |
| Unsupported format | service | format check | 400 (per service validation; confirm during Step 4 verification) |
Flow F6: Auth Refresh — REMOVED
Annotations no longer mints tokens. The legacy POST /auth/refresh endpoint and its backing TokenService were removed; admin (POST /token/refresh) is now the sole refresh issuer for the suite. Detections and any other long-running caller must refresh against admin and pass the resulting access token to annotations.
This service is a verifier only: it validates the Authorization: Bearer … header against admin's JWKS (JWT_JWKS_URL) on every [Authorize] route — see JwtExtensions in _docs/02_document/modules/auth-identity.md.
Flow F7: Directory Settings Change → Path Cache Reset
Description
Admin updates filesystem roots (videos_dir, images_dir, labels_dir, thumbnails_dir, results_dir, gps_*) via PUT /settings/directories. SettingsService persists the row and must call PathResolver.Reset() so subsequent reads see the new roots.
Sequence Diagram
sequenceDiagram
autonumber
participant Admin
participant Ctrl as SettingsController (05)
participant Svc as SettingsService (05)
participant DB
participant Path as PathResolver (06)
Admin->>Ctrl: PUT /settings/directories (UpdateDirectoriesRequest, JWT ADM)
Ctrl->>Svc: UpdateDirectories(request)
Svc->>DB: UPDATE directory_settings
Svc->>Path: Reset()
Svc-->>Ctrl: ok
Ctrl-->>Admin: 204 NoContent
Verified
SettingsService calls pathResolver.Reset() on directory updates (lines 71 and 85 of Services/SettingsService.cs). The invariant holds today.
Error Scenarios
| Error | Where | Detection | Recovery |
|---|---|---|---|
| Multi-instance deployments | n/a | each instance caches independently in its own PathResolver singleton |
each pod re-loads on next miss; no cross-pod fan-out — flagged for horizontal scale planning |
Flow F8: Dataset Bulk Status
Description
Dataset Explorer changes annotation status one at a time or in bulk. DatasetService.UpdateStatus / BulkUpdateStatus issue a direct UPDATE annotations SET status = ... via AppDataConnection. Today this flow does NOT publish SSE and does NOT enqueue the failsafe outbox — the Annotator UI will not see dataset-driven status changes in real time, and downstream stream consumers will not see the lifecycle event. Open behavioral question (see Open Items below).
Routes
PATCH /dataset/{annotationId}/status(single)POST /dataset/bulk-statuswithBulkStatusRequest { AnnotationIds, Status }(bulk)
Both require [Authorize(Policy = "DATASET")].
Sequence Diagram
sequenceDiagram
autonumber
participant UI as Dataset Explorer
participant Ctrl as DatasetController (04)
participant Svc as DatasetService (04)
participant DB
UI->>Ctrl: PATCH /dataset/{id}/status OR POST /dataset/bulk-status (JWT DATASET)
Ctrl->>Svc: UpdateStatus(id, status) OR BulkUpdateStatus(request)
alt single
Svc->>DB: UPDATE annotations SET status WHERE id = :id
DB-->>Svc: rowcount
opt rowcount = 0
Svc-->>Ctrl: KeyNotFoundException
Ctrl-->>UI: 404
end
else bulk
Svc->>Svc: validate ids list non-empty (else 400)
Svc->>DB: UPDATE annotations SET status WHERE id IN (:ids)
end
Svc-->>Ctrl: ok
Ctrl-->>UI: 200 / 204
Error Scenarios
| Error | Where | Detection | Recovery |
|---|---|---|---|
| Empty bulk list | BulkUpdateStatus |
ArgumentException |
400 via middleware |
| Annotation not found (single) | UpdateStatus |
updated == 0 |
404 |
| Partial bulk failure under DB error | service | exception mid-update | UPDATE is a single SQL statement (Set + UpdateAsync) — atomic at the statement level; either all listed rows update or none |
Open behavioral questions
- Should this flow publish SSE so the Annotator UI updates live?
- Should this flow enqueue the outbox so AI training / admin sync reflect dataset status decisions?
- Today the answer to both is "no" — confirm with stakeholders.
Stakeholder Resolutions (Step 4 outcome)
These were the open behavioral questions raised by the verification pass; resolved with the maintainer on 2026-05-14. The architecture doc carries the full ADRs (ADR-008..ADR-011) and the Refactor Backlog (RB-01..RB-06). Summary here:
- Silent Update / Delete / dataset-status changes — confirmed real gap, not intent. World B is the design (drainer is already plumbed for
ValidatedandDeletedperFailsafeProducer.cs:108–123; the producer side was simply never wired in the new HTTP backend after the WPF split). Tracked: ADR-009 / RB-01. system_settings.silent_detection— debug-time switch superseded by the suite e2e harness. Remove the flag and gating logic. Tracked: ADR-010 / RB-02.- F1 atomicity — adopt a business-transaction wrapper (transactional outbox): DB rows + outbox commit first, FS writes execute post-commit. Tracked: ADR-008 / RB-03.
- Annotation id collision risk — switch to
XxHash3.Hash128over the same sampled buffer to keep the hash file-size-independent (videos can be 3–5 GB) while moving from 64-bit to 128-bit collision space. Tracked: ADR-004 / RB-04. FailsafeProducer.EnqueueAsyncstatic method doing DB I/O — accepted as-is despite thecoderule.mdcdeviation; documented exception, no refactor.detection_classesstatic catalog — promote to admin-managed (POST/PUT/DELETE /classesunder[ADM]) with a read-through cache modeled onPathResolver.Reset(). Tracked: ADR-011 / RB-06.
Sub-questions deferred to RB-01 implementation
UpdateAnnotation(replaces detections, setsStatus=Edited) → re-enqueue asCreated(rich payload) or addQueueOperation.Updatedand a new drainer branch?- Status transitions other than
→ Validated/→ Deleted— should they enqueue at all? DeleteAnnotationis hard-delete today even thoughAnnotationStatus.Deleted = 40exists. Confirm hard- vs soft-delete semantics.
Verified during Step 4
- F7 (
PathResolver.Reseton directory change) — invariant holds;SettingsServicecallsReseton lines 71 + 85. - All endpoint routes / policies match controller attributes.
AnnotationService.CreateAnnotationexact sequence (image file → media row → annotation → detections → label file → SSE → outbox).BulkUpdateStatusempty-list rejection (ArgumentException).- Whole
src/tree has exactly two producer call sites:AnnotationService.cs:90(PublishAsync) and:102(EnqueueAsync). All other paths are silent today.
Open at flow level (residual)
- F4 missing-file behavior for
Createdoperations:FailsafeProducer.cs:138swallowsIOExceptionsilently and emits a stream message withimage = null. Tracked as RB-05 (architecture doc). - F4 multi-drainer dedupe: still required — outbox uses no leasing. Suite consumer contract should dedupe by
(annotationId, operation).
Mermaid renderings of each flow are kept simple (no styling) per the template convention.