mirror of
https://github.com/azaion/ui.git
synced 2026-06-21 17:51:10 +00:00
f7dd6c98d8
ci/woodpecker/push/build-arm Pipeline failed
Security audit (5 phases) → reports under _docs/05_security/. AZ-501 (F-SAST-1, HIGH): Externalize hardcoded Google Geocode key from mission-planner/src/config.ts to VITE_GOOGLE_GEOCODE_KEY via new GeocodeService.ts; fail-soft warn when unset; STC-SEC1D static deny-list gate; +5 unit tests in tests/mission_planner_geocode.test.ts. AZ-502 (F-DEP-1, HIGH): Force vite>=6.4.2 and postcss>=8.5.10 via package.json overrides in both roots; clean reinstall clears all bun audit advisories. Test-spec sync (Step 12) + Update Docs (Step 13) deltas: AC-43, AC-44, NFT-SEC-09b, FT-P-61, FT-N-17, ripple log, batch_12 report. Pending user actions: revoke Google + OWM keys (AC-6 / AZ-499 AC-7). 229 PASS / 13 SKIP / 0 FAIL on static + fast suites. Co-authored-by: Cursor <cursoragent@cursor.com>
22 KiB
22 KiB
Acceptance Criteria — Azaion UI
Output of
/documentStep 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_OWM_API_KEY (and VITE_OWM_BASE_URL); zero hardcoded keys in any src/ or mission-planner/ module |
Static check (regex against the previously-committed literal — STC-SEC1, STC-SEC1B, STC-SEC1C); CI step |
P10; closed cycle 2 / 2026-05-12 by AZ-448 (main SPA), AZ-499 (mission-planner); see also AC-42 |
| 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 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) |
| AC-41 | Map tiles served by self-hosted satellite-provider via cookie auth |
(a) <TileLayer> url prop equals import.meta.env.VITE_SATELLITE_TILE_URL (or the dev default http://localhost:5100/tiles/{z}/{x}/{y} when unset). (b) Every <TileLayer> the SPA renders carries crossOrigin="use-credentials" so the browser attaches the satellite-provider auth cookie on same-origin requests. (c) The classic/satellite map-type toggle, the mapType state, and the MiniMap.Props.mapType prop are absent. (d) A 401 / 503 from the tile endpoint MUST NOT crash the map; broken-tile placeholder is rendered for the failing cell. |
Fast component tests (src/features/flights/__tests__/satellite_tile.test.tsx) + e2e infrastructure check (e2e/tests/infrastructure.e2e.ts AC-2) + STC-T1 typecheck + STC-FP22 i18n parity (post-key removal). Cycle-2 spec rows: FT-P-56, FT-P-57, FT-P-58, FT-P-59, NFT-RES-11. |
Closed cycle 2 / 2026-05-12 by AZ-498 (epic AZ-497). _docs/02_document/contracts/satellite-provider/tiles.md v1.0.0 owns the wire shape. Cross-workspace prereq for production deploy: satellite-provider cookie-auth on GET /tiles/{z}/{x}/{y} (gated at autodev Step 16). |
| AC-42 | mission-planner OpenWeatherMap config externalized; fail-soft on missing key | (a) mission-planner/src/services/WeatherService.ts::getWeatherData(lat, lon) builds the outbound URL from import.meta.env.VITE_OWM_API_KEY + VITE_OWM_BASE_URL (falls back to https://api.openweathermap.org/data/2.5 when the base URL is unset; trailing slash on the base URL is stripped). (b) When VITE_OWM_API_KEY is unset/empty, getWeatherData returns null and issues NO outbound fetch. (c) Static check STC-SEC1C (scripts/check-banned-deps.mjs --kind=owm_key_in_source) FAILS on any future re-introduction of the previously-committed literal under src/ or mission-planner/. (d) The previously-committed key MUST be revoked at the OpenWeatherMap dashboard (manual deliverable — defense-in-depth). |
Fast tests (tests/mission_planner_weather.test.ts) + STC-SEC1C static check + STC-T1 typecheck. Cycle-2 spec rows: FT-P-60, FT-N-16, NFT-SEC-09 step 3. |
Closed cycle 2 / 2026-05-12 by AZ-499 (epic AZ-497). Closes the AZ-482 source-scan gap (which previously only checked src/ for the regex shape and dist/ for the literal — mission-planner/ stays out of dist/ per AC-31, so the dist scan alone could not catch it). |
| AC-43 | mission-planner Google Geocode config externalized; fail-soft on missing key | (a) The previously-hardcoded Google Geocode API key has been EXTRACTED from mission-planner/src/config.ts to a new mission-planner/src/services/GeocodeService.ts module that builds the outbound URL from import.meta.env.VITE_GOOGLE_GEOCODE_KEY. (b) When the env var is unset/empty, geocodeAddress(address) returns null, issues NO outbound fetch, and emits exactly one console.warn mentioning VITE_GOOGLE_GEOCODE_KEY. (c) Static check STC-SEC1D (scripts/check-banned-deps.mjs --kind=google_key_in_source) FAILS on any future re-introduction of the previously-committed literal under src/ or mission-planner/. (d) The previously-committed key MUST be revoked at the Google Cloud Console (manual deliverable — defense-in-depth). (e) LeftBoard.tsx imports geocodeAddress from the service module; the inline geocode function and the GOOGLE_GEOCODE_KEY import are removed. |
Fast tests (tests/mission_planner_geocode.test.ts) + STC-SEC1D static check + STC-T1 typecheck. Cycle-2 spec rows: FT-P-61, FT-N-17, NFT-SEC-09b. |
Closed cycle 2 / 2026-05-12 by AZ-501 (filed during the security audit, _docs/05_security/). Mirrors the AZ-499 pattern (env var + fail-soft + literal-scan static gate + manual revocation). Manual deliverable AZ-501 AC-6 (key revocation at Google Cloud Console) PENDING USER. |
| AC-44 | Vite + PostCSS supply chain past published CVEs | bun audit in BOTH ui/ and mission-planner/ reports zero advisories. Achieved by bun update vite plus package.json overrides flooring vite >= 6.4.2 and postcss >= 8.5.10 in both roots — required because vitest@3.2.4 nests its own vite copy that the direct upgrade alone does not lift past the <= 6.4.1 advisory range. |
bun audit exit code 0 in both roots after bun install from a clean node_modules. CI gate (bun audit --severity high in .woodpecker/build-arm.yml) is a Phase B follow-up tracked at _docs/05_security/infrastructure_review.md F-INF-1. |
Closed cycle 2 / 2026-05-12 by AZ-502 (filed during the security audit). Affected advisories: GHSA-p9ff-h696-f583 (HIGH — Vite WebSocket file-read), GHSA-4w7w-66w2-5vf9 (MODERATE — Vite path traversal), GHSA-qx2v-qp2m-jg93 (MODERATE — PostCSS XSS). Production-bundle exposure was NONE before the upgrade (Vite is dev-server-only); the upgrade closes the developer-machine exposure and the audit-tool noise. |
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-20 (OWM key — closed cycle 2 by AZ-448 + AZ-499; STC-SEC1/SEC1B/SEC1C all green), AC-25 (sync path; async path is target-only), AC-31, AC-33, AC-34, AC-41 (self-hosted satellite tiles + cookie auth — closed cycle 2 by AZ-498; production deploy still gated on cross-workspace satellite-provider cookie-auth ticket), AC-42 (mission-planner OWM env-var hardening — closed cycle 2 by AZ-499; manual key revocation pending), AC-43 (mission-planner Google Geocode env-var hardening — closed cycle 2 by AZ-501; manual key revocation pending), AC-44 (Vite + PostCSS supply chain — closed cycle 2 by AZ-502; CI audit gate is a Phase B follow-up).
- 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-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 ofsplitTiletoday). - Phase B targets (not currently in scope of
/documentStep 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 forisSplit; the YOLO-label parser hardening lands when the splitTile consumer is wired in Phase B).