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>
18 KiB
Module group: src/features/annotations/
Compact doc covering all 5 annotations modules (
classColors.tsis a shared leaf — see existingsrc__features__annotations__classColors.md). The annotations feature is the central legacy concern of the codebase per_docs/legacy/wpf-era.md §4(Azaion.Annotatorwindow) — what's documented here is the React port. For the canonical product spec see_docs/ui_design/README.md(Annotations Tab Layout, Annotation Quality Guidelines, Affiliation Icons, Combat Readiness, Annotation Row Gradient, Keyboard Shortcuts, Video Annotation Time-Window Display) and parent suite../../../../_docs/01_annotations.mdfor the API contract.
Scope
Owns the /annotations route. Lets the user:
- Browse media (video / image) for the currently selected flight, with a 300-ms debounced name filter, drag-and-drop / file-picker / folder-picker upload, and right-click delete.
- Play / pause / step a video, scrub the timeline, mute, with frame stepping at 1 / 5 / 10 / 30 / 60 frames in both directions (assumed 30 FPS — see Findings).
- Draw / move / resize / multi-select bounding boxes on a custom canvas overlay, click-and-drag with Ctrl-modifier behaviours, snap to image-normalized coordinates 0–1.
- Pick the active detection class (1–9 hotkeys) and PhotoMode (Regular / Winter / Night) via the shared
DetectionClassescomponent. - Save the per-frame detection set back to
POST /api/annotations/annotations, with fall-back to in-memory storage when the file is a local blob: URL or the backend is unreachable. - Stream the annotations sidebar from the
GET /api/annotations/annotations/eventsSSE feed; show each row with the gradient defined in_docs/ui_design/README.md. - Trigger AI detection via
POST /api/detect/{mediaId}(modal log overlay). - Download an annotation as YOLO
.txt+ a PNG of the frame with rectangles burned in.
Module map
| Module | Layer | Responsibility |
|---|---|---|
classColors.ts |
leaf | (already documented separately) Class-number → colour + photoMode-suffix lookup. |
MediaList.tsx |
sub-component | Left panel media browser. Owns media[] state, debounced filter, dropzone upload, blob: local-mode fallback when backend POST fails. Calls GET /api/annotations/media, DELETE /api/annotations/media/{id}, POST /api/annotations/media/batch. |
VideoPlayer.tsx |
sub-component | Native <video> wrapper. forwardRef exposes seek(seconds) and getVideoElement(). Custom progress slider + frame-step toolbar. Global keydown handler for Space / ←/→ (Ctrl=±150) / M. |
AnnotationsSidebar.tsx |
sub-component | Right panel: SSE-driven annotation list (/api/annotations/annotations/events filtered by mediaId), AI detect button, gradient row background built from per-detection class colour + confidence-modulated alpha, download button (delegates to page). |
CanvasEditor.tsx |
sub-component | Canvas overlay used in both image-only and video-overlay modes. forwardRef exposes deleteSelected / deleteAll / hasSelection. Owns zoom (Ctrl+wheel, 0.1–10×), pan (held in pan state — there is no Ctrl+drag pan code, only an unused pan state — see Findings), 8-handle resize, multi-select, draw-on-Ctrl-mousedown. Renders detections + a "time-window" preview of other annotations during video playback. |
AnnotationsPage.tsx |
page | Orchestrator: 3-panel layout via useResizablePanel (left 250 / right 200, min/max bounds). Owns selectedMedia, annotations, detections, selectedClassNum, photoMode, currentTime. Wires MediaList → CanvasEditor ↔ VideoPlayer ↔ AnnotationsSidebar. Handles the Save POST + local-fallback. Builds the YOLO + PNG download. |
Key contracts
Detection(fromsrc/types/index.ts):{ id, classNum, label, confidence ∈ [0,1], affiliation: 0|1|2, combatReadiness, centerX, centerY, width, height }— all coords normalized 0–1. Matches_docs/legacy/wpf-era.md §10and parent suite_docs/01_annotations.mdDetection DTO.AnnotationListItem:{ id, mediaId, time: 'HH:MM:SS.mmm' | null, createdDate, source: AnnotationSource, status: AnnotationStatus, isSplit, splitTile, detections[] }. MatchesAnnotationstable in parent_docs/00_database_schema.mdmodulo client-sideisSplit / splitTile.- AI detect endpoint:
POST /api/detect/{mediaId}— matches parent../../../../_docs/03_detections.md §2after nginx strips/api/detect/. NOTE: UI does NOT forward theX-Refresh-Tokenheader that the spec requires for long-running video detection (_docs/03_detections.md §2 request headers). Carried as a finding. - Save body:
{ mediaId, time: 'HH:MM:SS.mmm' | null, detections: Detection[] }. .NETTimeSpan.Parseaccepts that format so the round-trip works fortime → VideoTime. Body is missing requiredSourceand optionalWaypointIdrequired by parent specCreateAnnotationRequest— see Findings.
External integrations
| Endpoint / origin | Where | Direction | Notes |
|---|---|---|---|
GET /api/annotations/media?flightId&name&pageSize=1000 |
MediaList.fetchMedia |
egress | Hardcoded ceiling 1000. |
GET /api/annotations/media/{id}/file |
CanvasEditor, VideoPlayer, AnnotationsPage.handleDownload |
egress | Image / video bytes. |
POST /api/annotations/media/batch |
MediaList.uploadFiles |
egress | multipart/form-data. |
DELETE /api/annotations/media/{id} |
MediaList.handleDelete |
egress | Skipped for local blob: entries. |
GET /api/annotations/annotations?mediaId&pageSize=1000 |
MediaList.handleSelect, AnnotationsSidebar, AnnotationsPage.handleSave |
egress | Refresh after save / SSE event. |
POST /api/annotations/annotations |
AnnotationsPage.handleSave |
egress | Body: { mediaId, time, detections }. Falls back to in-memory on 4xx/5xx. |
GET /api/annotations/annotations/{id}/image |
CanvasEditor.loadImage |
egress | Per-annotation hashed image; preferred over the raw media for image annotations. |
GET /api/annotations/annotations/events (SSE) |
AnnotationsSidebar |
egress | Listens for { annotationId, mediaId, status } events; filters client-side by media.id. |
POST /api/detect/{mediaId} |
AnnotationsSidebar.handleDetect |
egress | Triggers AI inference — endpoint shape unverified vs _docs/03_detections.md. |
URL.createObjectURL(File) |
MediaList.uploadFiles, AnnotationsPage.handleDownload |
browser API | Local-mode blob URLs are revoked on delete or unmount. |
Findings carried into Step 4 / 6 / 8
-
VideoPlayer.stepFrameshardcodesfps = 30— frame stepping is wrong for any other fps (most drone footage is 25 / 30 / 60). UI spec says "Frame duration = 1 / video FPS" (_docs/ui_design/README.md). Should read fromvideo.getVideoPlaybackQuality()/ metadata. Step 4. -
VideoPlayerCtrl+Left/Right is documented in_docs/ui_design/README.mdas "skip 5 seconds" but here it does±150 frames(= 5s @ 30fpsonly). Same fps coupling as #1. -
VideoPlayer.errorstate has no setter call beyond initialnull/onLoadedMetadata reset — but thesetErrorcall ononErroronly sets a string when source fails to load; spec says status messages should appear in a status bar (_docs/ui_design/README.md). The status-bar pattern is not implemented. -
CanvasEditorCtrl+drag pan is documented (_docs/ui_design/README.md) but not implemented —panstate exists, onlysetPan(...)calls are inside the (no-op) zoom flow. The canvas always renders atpan = {0,0}. Step 4 / Step 6 problem extraction. -
CanvasEditor.useEffect[draw]has missing deps (isVideo,imgSizedependency tracked indirectly through other deps;currentTime/annotationsare listed butgetTimeWindowDetectionsis recreated each render and is closed-over). Specifically:draw's closure readsgetTimeWindowDetections()and the innergetTimeWindowDetectionsreadsmedia,currentTime,annotations— those are in the dep list, butmediaitself is missing. Step 4 a11y / correctness. -
CanvasEditortime-window threshold is< 2_000_000ticks ingetTimeWindowDetections— at 10 000 000 ticks/second this is ±200 ms (400 ms total window). Spec is asymmetric 50 ms before + 150 ms after (200 ms total) per../../ui_design/README.md. Implementation window is 4× too wide and centred on the wrong offset. Step 4. -
CanvasEditorctx.fillStyle = color; ctx.globalAlpha = 0.1; ctx.fillRect(...)— the box "fill" is class colour at 10% opacity, but the affiliation icon (NATO MIL-STD-2525 shape) and combat-readiness indicator are missing. The current code only renders a tiny green dot forcombatReadiness === 1(Ready). UI spec demands Friendly/Hostile/Unknown/None affiliation icons and Ready/NotReady/Unknown CR. Step 4 vs_docs/ui_design/README.md— significant gap. -
CanvasEditor.AFFILIATION_COLORSconstant is dead code — defined but never used. Likely the seed of the missing affiliation rendering. Step 4. -
Annotation row gradient cap is wrong by ~9 percentage points:
AnnotationsSidebar.getRowGradientusesMath.round(alpha * 40).toString(16)—40is decimal, not hex, so the maximum alpha byte is0x28(decimal 40 ≈ 16% opacity). Spec wireframe../../ui_design/annotations.htmlusesrgba(...,0.25)=0x40hex (25%). Almost certainly a typo: should be* 64(decimal) or* 0x40to match the wireframe. Step 4. -
AnnotationsSidebarAI-detect modal:setDetectLogwrites "Detection complete" immediately afterawait api.post(...)returns — there is no progress streaming despite the spec ("scrolling log of detection progress" viaDetectionEventSSE per_docs/03_detections.md). The Detection SSE feed is not subscribed. Step 6. -
AnnotationsSidebarSSE refresh is fire-and-forget (.catch(() => {})) — silent error suppression, violatescoderule.mdc. Step 4. -
AnnotationsPage.formatTicks(seconds)producesHH:MM:SS.mmm(string). Parent suite_docs/01_annotations.mdcarriesTimeSpan VideoTime— .NETTimeSpan.Parseaccepts that format, so the round-trip works, but the API contract isTimeSpanticks, not a string. Verify in Step 4. -
AnnotationsPage.handleDownloadnever setscrossOriginon the video element (only on the standalone image fallback) — CanvastoBlobwill throw "tainted canvas" if the video served from/api/annotations/media/.../filedoesn't includeAccess-Control-Allow-Origin. Same-origin via the dev proxy and nginx is fine, but cross-origin (CDN / direct) breaks download. Step 4. -
AnnotationsPage.handleSelectannotation flow does NOT exposeValidate(V key per UI spec). The Annotations tab shouldn't validate (that's Dataset Explorer), but UI spec keyboard shortcuts list V on Annotations tab too — confirm scope in Step 6. -
No
Rkey for AI detect in any module here despite UI spec (R = Trigger AI detection). The AI Detect button is only reachable by mouse. Step 4. -
No PageUp / PageDown for prev/next media file despite UI spec.
-
No Camera config side panel (altitude / FocalLength / SensorWidth) per
_docs/ui_design/README.md— completely missing. Documented in Findings ofAdminPage.tsxtoo: aircraft camera defaults are global, but per-session override UI is not built. Step 6. -
MediaListusesalert(...)for "Unsupported file type" — not the project modal/toast pattern. Step 4. -
MediaListblob: local-mode is a graceful degradation that lets the page work without backend — useful for demos but the user has no indication that "you're working offline, save will be lost on reload". Document explicitly in Step 6. -
MediaList.fetchMediaalways merges blob: locals on top of backend results even when filtering — local entries ignore the name filter. Step 4. -
AnnotationsSidebartry { ... } catch (e: any)—anycast bypasses TS strict;e.messagemay beundefined. Step 4. -
AnnotationsPageleft/right panels resize but widths NOT persisted toUserSettings— UI spec says they should be restored per-user across sessions. TheuseResizablePanelhook only owns runtime state. Step 6 / Step 8. -
CanvasEditor.handleMouseDownCtrl-modifier semantics: Ctrl+click on a box should multi-select (per spec). Code does this. Ctrl+click on empty space should NOT start a draw — but the current branch startsdragState = 'draw'either way, then differentiates insidehandleMouseUpby drawRect size. Slight UX cost only. Step 4. -
Tile / split-image annotation rendering: spec mentions "Tile zoom" — auto-zoom to a tile region when opening a split-image detection.
AnnotationListItem.splitTileexists but no consumer code reads it. Step 6 problem extraction. -
AnnotationsPage.handleSave4xx/5xx fallback creates an in-memorylocal-${uuid}annotation that looks identical to a saved one. The user can't distinguish the two — risk of data loss on reload. Step 6 problem extraction. -
AnnotationsPagedoes not subscribe to the inference detect SSE — no AI progress visualization, no update on detect completion, no error if inference returns 5xx. Step 6. -
[STEP 4 — UI FIX, spec is canonical, user 2026-05-10]
AnnotationStatusenum diverges from spec. UI declaresCreated=0, Edited=1, Validated=2(src/types/index.ts:23). Spec (../../../../_docs/09_dataset_explorer.md) isNone=0, Created=10, Edited=20, Validated=30, Deleted=40. Action: changesrc/types/index.tsto the spec values. Cascades through every status filter, every PATCH/POST that sends a status, every render. PRIORITY — without this fix every dataset status filter and every PATCH/dataset/{id}/statusfrom the UI sends a wrong integer. -
[STEP 4 — UI FIX, spec is canonical, user 2026-05-10]
Affiliationenum diverges from spec. UI has 3 valuesUnknown=0, Friendly=1, Hostile=2. Spec (../../ui_design/README.mdAffiliation Icons + parent../../../../_docs/03_detections.md) requires four valuesNone, Friendly, Hostile, Unknown. Action: changesrc/types/index.tsto the four-value set; integer values to be confirmed once with the .NET service before patching (likelyNone=0, Friendly=1, Hostile=2, Unknown=3). Without this fix the UI cannot send/render the None (no-icon) case. -
[STEP 4 — UI FIX, spec is canonical, user 2026-05-10]
CombatReadinessenum diverges from spec. UI hasNotReady=0, Ready=1. Spec addsUnknown(no indicator rendered). Action: addUnknowntosrc/types/index.ts; confirm integer values with the .NET service. -
[STEP 4 — UI FIX, spec is canonical, user 2026-05-10]
MediaStatusenum diverges from spec. UI hasNew=0, AiProcessing=1, AiProcessed=2, ManualCreated=3. Spec (../../../../_docs/01_annotations.mdSSE +_docs/03_detections.md §2) isNone, New, AIProcessing, AIProcessed, ManualCreated, Confirmed, Error. Action: changesrc/types/index.tsto the seven-value set; confirm integer values with the .NET service. Without this fix the UI cannot render the Error SSE event when inference fails. -
[RESOLVED 2026-05-10 — PARENT-DOC FIX APPLIED]
AnnotationSourcenumeric serialization is canonical; UI is correct (AI=0, Manual=1). Parent../../../../_docs/01_annotations.md§5 response example and AnnotationSource enum table updated to integer values;../../../../_docs/09_dataset_explorer.md §1, §2, §4 examples likewise. Both files also got a "wire format is numeric" header note. No UI change needed. -
[STEP 4 — UI FIX, user 2026-05-10]
AnnotationsPage.handleSavemust addSourceandWaypointIdto the request body and renametime→videoTime. Required body shape per parent../../../../_docs/01_annotations.md §1CreateAnnotationRequest:{ "mediaId": "<string>", "waypointId": "<guid | null>", "source": 1, "videoTime": "HH:MM:SS.mmm", "detections": [ ... ] }Notes: (a)
userIdis supplied server-side from JWT — do not send. (b)imageis omitted in the save flow; the server reuses the media's frame atvideoTime. (c)sourceisManual(=1) for hand-edited save; AI inference posts useAI(=0) and are sent server-to-server by the Detections service. (d)waypointIdmust come fromMedia.waypointId(already typed insrc/types/index.ts:16); the UI currently does not thread waypoint association through to save. (e) Field nametimemust becomevideoTime(.NET PascalCaseVideoTime→ camelCase on the wire). -
[RESOLVED 2026-05-10 — PARENT-DOC FIX APPLIED]
DatasetItem.isSplitis correct on the UI side; parent spec response now includes it.../../../../_docs/09_dataset_explorer.md §1items[]shape updated withisSplit: booleanplus a description tying it to_docs/03_detections.md §4Tile-Based Detection. No UI change needed. -
[STEP 4 — UI FIX, user 2026-05-10]
Detectendpoint needs conditionalX-Refresh-Tokenheader. Spec../../../../_docs/03_detections.md §2lists it as required for long-running video detection. Access tokens expire in 3600 s per../../../../_docs/10_auth.md §1, so any video that takes >1 h to process loses auth on the server-to-serverPOST /annotationscall. Action: whenmedia.mediaType === MediaType.Video, attachX-Refresh-Token: <localStorage refreshToken>to thePOST /api/detect/{mediaId}request. Caveat:/auth/refreshrotates the refresh token (per_docs/10_auth.md §2), so the value can go stale if the UI also refreshes its own token mid-flight. Step 4 must decide whether to (i) cache the refresh token at detect-start, (ii) use a long-lived service token, or (iii) accept the failure mode and surface it to the user. For images and short videos the header is unnecessary — but the UI cannot tell upfront, so default to "always send for video".
Tests
None.
Cross-doc references
- Parent suite Annotations API:
../../../../_docs/01_annotations.md - Parent suite Detections (AI inference):
../../../../_docs/03_detections.md - Parent suite Database schema:
../../../../_docs/00_database_schema.md - Legacy WPF reference:
../../legacy/wpf-era.md§4 (Annotator window) and §10 (what survived). - UI spec:
../../ui_design/README.md(Annotations Tab Layout + keyboard shortcuts + time-window + annotation row gradient + affiliation icons + combat readiness). - Shared component used here:
src/components/DetectionClasses.tsx(already documented).