mirror of
https://github.com/azaion/ui.git
synced 2026-06-21 18: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>
77 lines
17 KiB
Markdown
77 lines
17 KiB
Markdown
# 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) || 0` patterns (currently a finding) | finding B4 |
|
||
| 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 1–9** (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-15–AC-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).
|