Phase B cycle 1 was a structural refactor only: F4 (barrel imports + STC-ARCH-01) and F7 (endpoint builders + STC-ARCH-02). This commit brings docs in line with source after the cycle, no code changes. Module docs (12 consumers): swap every /api/<service>/... literal in code snippets and integration tables for the matching endpoints.* builder; note the barrel import migration in Dependencies. New module doc: src__api__endpoints.md (public surface, F4 barrel re-export note, STC-ARCH-02 enforcement, contract-test reference). Architecture compliance baseline: mark F4 + F7 CLOSED with commit hashes (23746ec,8a461a2). 01_api-transport component description: add endpoints.ts + barrel to Internal Interfaces, close the F7 caveat, extend Module Inventory. ripple_log_cycle1.md: Task Step 0.5 reverse-dep analysis records the import-graph closure (no extra docs needed beyond the direct set). Carry-over reports landed alongside the docs: - test_run_report_phase_b_cycle1.md (Step 11 outcome) - implementation_report_refactor_phase_b_cycle1.md (cycle summary) State file: trimmed to the autodev <30-line target; Steps 14 + 15 recorded as SKIPPED with rationale (no security or perf surface changed in this cycle); pointer moved to Step 16 (Deploy). Co-authored-by: Cursor <cursoragent@cursor.com>
19 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 endpoints.annotations.annotations()(=/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 endpoints.annotations.annotationEvents()(=/api/annotations/annotations/events) SSE feed; show each row with the gradient defined in_docs/ui_design/README.md. - Trigger AI detection via
POST endpoints.detect.media(mediaId)(=/api/detect/{mediaId}) — modal log overlay. - Download an annotation as YOLO
.txt+ a PNG of the frame with rectangles burned in.
All path strings produced by
endpoints.*builders fromsrc/api/endpoints.ts(since AZ-486 / F7).
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 endpoints.annotations.media(qs), endpoints.annotations.mediaItem(id) (DELETE), endpoints.annotations.mediaBatch() (POST). |
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. Image / video bytes via endpoints.annotations.mediaFile(id). |
AnnotationsSidebar.tsx |
sub-component | Right panel: SSE-driven annotation list (endpoints.annotations.annotationEvents() filtered by mediaId), AI detect button (endpoints.detect.media(mediaId)), 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:
endpoints.detect.media(mediaId)→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
| Builder → Path | Where | Direction | Notes |
|---|---|---|---|
endpoints.annotations.media(qs) → GET /api/annotations/media?flightId&name&pageSize=1000 |
MediaList.fetchMedia |
egress | Hardcoded ceiling 1000 (in caller). |
endpoints.annotations.mediaFile(id) → GET /api/annotations/media/{id}/file |
CanvasEditor, VideoPlayer, AnnotationsPage.handleDownload |
egress | Image / video bytes. |
endpoints.annotations.mediaBatch() → POST /api/annotations/media/batch |
MediaList.uploadFiles |
egress | multipart/form-data. |
endpoints.annotations.mediaItem(id) → DELETE /api/annotations/media/{id} |
MediaList.handleDelete |
egress | Skipped for local blob: entries. |
endpoints.annotations.annotationsByMedia(mediaId, 1000) → GET /api/annotations/annotations?mediaId&pageSize=1000 |
MediaList.handleSelect, AnnotationsSidebar, AnnotationsPage.handleSave |
egress | Refresh after save / SSE event. |
endpoints.annotations.annotations() → POST /api/annotations/annotations |
AnnotationsPage.handleSave |
egress | Body: { mediaId, time, detections }. Falls back to in-memory on 4xx/5xx. |
endpoints.annotations.annotationImage(id) → GET /api/annotations/annotations/{id}/image |
CanvasEditor.loadImage |
egress | Per-annotation hashed image; preferred over the raw media for image annotations. |
endpoints.annotations.annotationEvents() → GET /api/annotations/annotations/events (SSE) |
AnnotationsSidebar |
egress | Listens for { annotationId, mediaId, status } events; filters client-side by media.id. |
endpoints.detect.media(mediaId) → 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. |
Path builders live in src/api/endpoints.ts (since AZ-486 / F7); STC-ARCH-02 forbids re-introducing literal /api/... strings in src/.
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).