Files
ui/_docs/00_problem/acceptance_criteria.md
T
Oleksandr Bezdieniezhnykh 510df68bcf [AZ-447] autodev Steps 1-4 baseline: docs, tests, refactor specs
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>
2026-05-11 00:38:49 +03:00

17 KiB
Raw Blame History

Acceptance Criteria — Azaion UI

Output of /document Step 6c. Criteria derived from measurable values already evidenced in code or config: server-side hard caps, validation rules, health checks, perf configs, and architectural non-negotiables. Aspirational targets without a concrete check are explicitly marked.

Status: synthesised-from-verified-docs (Step 6c — /document) Date: 2026-05-10


Format

Every criterion must have a measurable value. Each row carries a unique ID (AC-NN), the criterion, a measurement method, and the source-of-truth.

AC Criterion Measurable value How to measure Source
AC-01 Authenticated requests carry the HttpOnly refresh cookie credentials:'include' on every authenticated fetch and on the refresh call Static check (linter / test) on src/api/client.ts and src/auth/AuthContext.tsx; runtime test that 401 → POST refresh → retry succeeds src/api/client.ts:44; _docs/02_document/04_verification_log.md F2
AC-02 Bearer is never written to client storage Zero localStorage.* / sessionStorage.* calls touching the bearer Code-search regression test (Grep on src/) P3; _docs/02_document/architecture.md § 7
AC-03 Refresh cookie attributes Cookie issued by admin/ MUST carry Secure HttpOnly SameSite=Strict Server-side concern; UI test asserts the cookie is non-readable from JS (document.cookie does not contain the refresh token) _docs/02_document/architecture.md § 7
AC-04 Numeric enums match the suite spec on the wire AnnotationStatus, MediaStatus, Affiliation, CombatReadiness numeric values match the spec verbatim Unit test asserting each enum's values; contract test on every api.*() payload using these enums P9; src/types/index.ts; 04_verification_log.md enum drift
AC-05 Annotation save endpoint Save POSTs to /api/annotations/annotations (doubly-prefixed) Integration test asserting the URL and body shape (must include Source, WaypointId, videoTime) src/features/annotations/AnnotationsPage.tsx:39; 04_verification_log.md F5 + finding #32
AC-06 Selected-flight persistence path Selection persists via PUT /api/annotations/settings/user with {selectedFlightId} (NOT a dedicated /api/flights/select endpoint) Integration test on FlightContext.selectFlight round trip src/components/FlightContext.tsx:24,31,34,44; 04_verification_log.md F3
AC-07 Bulk-validate works POST /api/annotations/dataset/bulk-status transitions selected items to AnnotationStatus.Validated E2E test: select N items → click Validate → assert status update src/features/dataset/DatasetPage.tsx:65-73,142-146; 04_verification_log.md F9
AC-08 Live-GPS SSE per selected flight createSSE('/api/flights/${flightId}/live-gps', ...) is open while a flight is selected; closes on unselect Integration test: select flight, observe EventSource open; deselect, observe close src/features/flights/FlightsPage.tsx:67; F13
AC-09 Annotation-status SSE createSSE('/api/annotations/annotations/events', ...) open during 06_annotations page lifetime Integration test on subscribe / unsubscribe src/features/annotations/AnnotationsSidebar.tsx:25; F14
AC-10 Upload size cap Server-side hard cap is client_max_body_size 500M; UI error path on 413 produces a user-visible message nginx config check; integration test posts 501 MB → asserts 413 + UI surfaces nginx.conf client_max_body_size 500M; architecture.md § 6
AC-11 Bundle size budget Initial JS (gzipped) ≤ ~2 MB target vite build artifact size measured in CI; no gate today — adding the gate is a Phase B task architecture.md § 6 NFR row "Bundle size" — target, not currently enforced
AC-12 i18n coverage Every user-visible string has both an en.json and ua.json entry; no string literals in components beyond proper-noun acronyms Lint rule + assertion test that Object.keys(en) === Object.keys(ua) P6; src/i18n/i18n.ts
AC-13 i18n language detection / persistence i18next lng resolves from a detector (cookie / Accept-Language) and persists across reloads. Currently lng:'en' is hardcoded — Step 4 fix Manual + integration test that toggling language in Header survives reload src/i18n/i18n.ts; finding
AC-14 Destructive actions require ConfirmDialog Class delete (AdminPage.handleDeleteClass) and other destructive flows MUST present ConfirmDialog; alert() is forbidden Static check + integration test for delete flows O10; finding B4; MediaList alert() finding
AC-15 a11y — ConfirmDialog role=dialog + aria-modal=true + focus-trap + Esc-to-cancel Component test using @testing-library/react finding (ConfirmDialog lacks aria-modal/role=dialog)
AC-16 a11y — Header flight dropdown role=combobox, aria-expanded, Esc-to-close, focus-trap, outside-click handler attached only when open Component test finding (Header.tsx outside-click handler always attached; missing combobox roles)
AC-17 a11y — ProtectedRoute spinner role=status + accessible label; loading state has a timeout Component test asserting a11y attributes; integration test asserting timeout fallback finding
AC-18 Browser support Chromium-based + Firefox latest 2 versions render the SPA correctly Manual smoke (no browserslist enforcement today) architecture.md § 6 — manual / aspirational
AC-19 Mobile responsiveness Header bottom-nav variant renders at < 768 px; main pages render at ≥ 768 px Manual smoke at the two breakpoints Header.tsx:113-129; architecture.md § 6
AC-20 OpenWeatherMap key NOT in source import.meta.env.VITE_OPENWEATHERMAP_API_KEY (or proxied via suite); zero hardcoded keys in any src/ or mission-planner/ module Static check (regex against the current literal); CI step P10; mission-planner/src/utils/flightPlanUtils.ts:60 (current violation, Step 4 fix)
AC-21 UserSettings persistence — panel widths Panel-width changes via useResizablePanel write back to PUT /api/annotations/settings/user; reload restores widths Integration test: change width → reload → assert restored P11; src/hooks/useResizablePanel.ts (current violation)
AC-22 RBAC client-side route gates /admin and /settings redirect non-privileged users to /flights (or /login if not authenticated). Server-side 403 is the authoritative gate; UI gate is convenience Integration test: log in as non-admin → navigate to /admin → assert redirect finding (/admin route lacks role-gate — security PRIORITY)
AC-23 Auth refresh transparency One refresh = one network round trip; no UI re-render past <ProtectedRoute> Integration test asserting <ProtectedRoute> does not unmount during refresh architecture.md § 6 NFR row "Auth refresh"; 04_verification_log.md F2
AC-24 SSE bearer-rotation handling When the bearer rotates (refresh), open SSE connections must reconnect with the new bearer Integration test: open SSE → trigger refresh → assert reconnection. Currently NOT implemented (Step 8 hardening) ADR-008; architecture.md § 7
AC-25 Detect endpoint correctness Sync image detect uses POST /api/detect/${mediaId}. Async video detect (F7) — when implemented in Phase B — uses POST /api/detect/video/${mediaId} returning a job ID + SSE on /api/detect/stream/${jobId}. Long-video flows MUST send X-Refresh-Token (per _docs/10_auth.md) Integration tests per path src/features/annotations/AnnotationsSidebar.tsx:39; F6 / F7 / F14
AC-26 Numeric input hygiene Numeric form inputs in 09_settings and 08_admin reject empty input rather than silently writing 0 Component tests on `parseInt(v)
AC-27 Save error surfacing 09_settings save handlers (saveSystem, saveDirs) use try/finally to reset saving:true; failure is surfaced via toast / inline error Integration test that simulates a 500 on PUT and asserts state reset finding B4
AC-28 Annotation overlay time window The on-canvas annotation overlay window is asymmetric [-50 ms, +150 ms] around the current frame (matches WPF source _thresholdBefore=50ms / _thresholdAfter=150ms). Currently symmetric ±200 ms — Step 4 fix Component test asserting overlay membership at currentTime ± 50/150 ms finding #6; 04_verification_log.md §2d
AC-29 mediaType is typed All mediaType references use the MediaType enum (None=0, Image=1, Video=2); zero magic literals Static check (Grep mediaType\s*===\s*[0-9]) finding #5 / #10; P9
AC-30 Class delete confirmation AdminPage.handleDeleteClass shows ConfirmDialog before issuing DELETE /api/admin/classes/${id} Integration test finding B4
AC-31 mission-planner/ is not in the production bundle vite build output does not include any mission-planner/** chunk Bundle inspection; static-import check vite.config.ts; ADR-009; P2
AC-32 CI tags + labels Image is pushed with ${branch}-arm tag and OCI labels (org.opencontainers.image.{revision,created,source}) Pipeline assertion on the push step .woodpecker/build-arm.yml
AC-33 Production runtime is nginx:alpine only Final image stage is nginx:alpine; no Node.js binary in the production image Container inspection (docker inspect) Dockerfile
AC-34 nginx routes 9 services nginx.conf declares /api/admin/, /api/flights/, /api/annotations/, /api/detect/, /api/loader/, /api/gps-denied-desktop/, /api/gps-denied-onboard/, /api/autopilot/, /api/resource/ — each strips its /api/<service>/ prefix Config assertion test nginx.conf; ADR-006
AC-35 Manual bbox draw on CanvasEditor A mousedown → mousemove → mouseup gesture on the canvas creates one new local detection with classNum = selectedClassNum + photoModeOffset (per AC-38) and x,y,w,h (normalised) matching the dragged rectangle within ±1 normalised px-equivalent; the new detection is appended to local state and is rendered immediately Component test on CanvasEditor with synthetic pointer events; verify local-state shape components/06_annotations/description.md; system-flows.md Flow F5; solution.md:165,224
AC-36 8-handle bbox resize + canvas modifier interactions (a) Dragging any of the 8 resize handles (4 corners + 4 edge midpoints) of a selected bbox updates only the corresponding edges; (b) Ctrl+click on a bbox adds it to the selection set (multi-select); (c) Ctrl+wheel over the canvas zooms in/out around the cursor; (d) Ctrl+drag on empty canvas pans the view. Bboxes have a minimum normalised size > 0 so handle-drag past zero clamps instead of inverting. Component tests on CanvasEditor with synthetic events (one per modifier path); assert resulting bbox / selection set / viewport state components/06_annotations/description.md; glossary.md:45 (CanvasEditor); 01_legacy_coverage_gaps.md:29-30; solution.md:224
AC-37 Class picker (DetectionClasses widget) Widget loads class list from GET /api/annotations/classes; number-key 19 (window keydown) selects classes[(num-1) + photoMode] and emits onSelect(class.id); clicking a class entry emits the same; the rendered visible label index i+1 matches the hotkey number for that class within the currently active PhotoMode (per AC-38). Fallback list is used when the API returns empty or errors. Backend class ordering MUST be [0..N-1] (Regular), [20..20+N-1] (Winter), [40..40+N-1] (Night) — when it is not, this AC fails (Step 4 verification candidate). Component test on DetectionClasses with mocked API + simulated keypresses + clicks; contract test asserting backend response ordering on a fixture components/03_shared-ui/description.md:37; modules/src__components__DetectionClasses.md; data_model.md:158; _docs/legacy/wpf-era.md §10
AC-38 PhotoMode switcher (Regular / Winter / Night) PhotoMode buttons emit values from the set {0, 20, 40} (Regular=0, Winter=+20, Night=+40). Switching mode: (a) re-filters the class list to entries whose photoMode equals the new mode; (b) if the previously-selected classNum is not in the new filtered set, auto-selects the first class of the new mode and emits onSelect. On annotation save, the wire Detection.classNum (a.k.a. yoloId) equals classId + photoModeOffset. Component test on the mode-switch effect + integration test on the save payload modules/src__components__DetectionClasses.md §22, §31-43; data_model.md:84; components/11_class-colors/description.md:31-35; ui_design/README.md:127-128; ui_design/annotations.html:84-93
AC-39 Tile-splitting endpoint + wire shape POST /api/annotations/dataset/{id}/split exists and is callable from the dataset surface; success response is JSON with HTTP 200. AnnotationListItem.isSplit: boolean and AnnotationListItem.splitTile: string | null (YOLO label <class> <cx> <cy> <w> <h>) are honored on read. When isSplit === true and splitTile is non-null, the client parses the 5-token YOLO label without throwing; malformed splitTile surfaces a user-visible error (no silent swallow). DatasetItem.isSplit?: boolean is read on the dataset list path (parent-suite-doc fix applied — see _docs/_process_leftovers/2026-05-10_parent-suite-doc-fixes.md). Integration test against a fixture response; unit test on the YOLO-label parser with valid + malformed inputs components/07_dataset/description.md:28; data_model.md:104-105,130,164; modules/src__features__annotations.md:31,75; modules/src__types__index.md:24-28
AC-40 Tile-zoom auto-zoom on split-image annotation open When the user opens a splitTile-bearing annotation (double-click in AnnotationsSidebar or seek via the annotation list), CanvasEditor auto-zooms to the tile region encoded by splitTile (parsed per AC-39). The visible viewport rectangle equals the tile rectangle within ±1 px on each edge. A small visual tile-zoom indicator (icon / badge) is rendered while the tile zoom is active so the operator knows the view is constrained. Currently MISSING — finding #24 in modules/src__features__annotations.md; Step 4 / Phase B fix. Component test on CanvasEditor with a splitTile-bearing annotation; assert viewport rect + presence of the tile-zoom indicator components/06_annotations/description.md:62, 103; modules/src__features__annotations.md:75 finding #24; legacy/wpf-era.md (OpenAnnotationResult seek + ZoomTo)

Anti-criteria — explicit non-goals

AC# Statement Source
AC-N1 The UI does NOT support real-time multi-user collaborative annotation. F14 caveat: server pushes status events, the UI consumes; no concurrent edit semantics
AC-N2 The UI does NOT host any in-browser ML model. All inference is server-side. package.json has no ML libs
AC-N3 The UI does NOT support offline mode. (Tile cache for field deployments is a separate, future concern.) architecture.md § 2
AC-N4 The UI does NOT enforce a server-side response signature / checksum on REST replies. (Server is trusted within the suite network.) absence of any signature library in package.json
AC-N5 The UI does NOT port WPF Sound Detections or Drone Maintenance — both dropped per Step 4.5 decision. 01_legacy_coverage_gaps.md Step 4.5 update

Coverage status

  • Currently met & enforced: AC-02 (no token storage), AC-05 (annotation save URL — body shape pending), AC-06, AC-07, AC-08, AC-09, AC-10 (server cap; UI surface is a finding), AC-25 (sync path; async path is target-only), AC-31, AC-33, AC-34.
  • Currently met but not enforced by CI: AC-04 (enum values), AC-12 (i18n parity), AC-29 (typed mediaType), AC-35 (manual bbox draw), AC-37 (class picker — pending Step 4 backend-ordering verification), AC-38 (PhotoMode switcher).
  • Currently violated — Step 4 fix candidates: AC-01 (bootstrap refresh), AC-13 (i18n detector), AC-14 / AC-30 (class-delete dialog; alert() use), AC-15AC-17 (a11y), AC-20 (OWM key), AC-21 (panel widths), AC-22 (route role-gate), AC-23 (refresh re-render — code-path correct, but bootstrap-refresh fix needed), AC-26 (numeric input hygiene), AC-27 (save error surfacing), AC-28 (overlay window), AC-36 (Ctrl-multi-select / Ctrl-wheel zoom / Ctrl-drag pan flagged "Partially missing"), AC-40 (tile-zoom auto-zoom — finding #24, no consumer of splitTile today).
  • Phase B targets (not currently in scope of /document Step 6): AC-11 (bundle gate), AC-18 (browser-list), AC-19 (mobile floor), AC-24 (SSE refresh re-subscribe), AC-25 async path, AC-32 (CI label assertions), AC-39 (tile-split endpoint — parent-suite-doc fix applied for isSplit; the YOLO-label parser hardening lands when the splitTile consumer is wired in Phase B).