[AZ-485] [AZ-486] Cycle 1 docs refresh (Step 13)
ci/woodpecker/push/build-arm Pipeline was successful

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>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-12 00:01:04 +03:00
parent 8a461a2051
commit 17d5bb45e7
17 changed files with 456 additions and 172 deletions
@@ -21,10 +21,10 @@ Detection approach: TypeScript `import ... from '...'` parsing across all `.ts`
| F1 | Critical | Architecture | `mission-planner/**` vs `src/features/flights/**` | Mission-planner duplicates 13+ modules of the deployed flights tree |
| F2 | High | Architecture | `src/features/dataset/DatasetPage.tsx:9``../annotations/CanvasEditor` | Cross-feature same-layer edge — `07_dataset` reaches into `06_annotations` |
| F3 | High | Architecture | `src/features/annotations/classColors.ts` | Physical / logical owner split — `11_class-colors` file lives inside `06_annotations` |
| F4 | High | Architecture | every component | No Public API barrels — every internal file is de-facto public |
| F4 | High | Architecture | every component | No Public API barrels — every internal file is de-facto public **CLOSED 2026-05-11 by AZ-485 (`23746ec`)** |
| F5 | High | Architecture | `mission-planner/src/flightPlanning/MapView.tsx ↔ MiniMap.tsx` | Pre-existing import cycle inside port-source |
| F6 | Medium | Architecture | (codebase-wide) | No `src/shared/` infrastructure for cross-cutting concerns |
| F7 | Medium | Architecture | every `api.*` / `createSSE` call site | Hardcoded `/api/<service>/...` paths instead of env-driven endpoints |
| F7 | Medium | Architecture | every `api.*` / `createSSE` call site | Hardcoded `/api/<service>/...` paths instead of env-driven endpoints**CLOSED 2026-05-11 by AZ-486 (`8a461a2`)** |
| F8 | Low | Architecture | `_docs/02_document/module-layout.md` | Layering-table inconsistency — Header → useAuth is unannotated |
| F9 | Low | Architecture | `mission-planner/src/{main,App,setupTests,vite-env}.tsx` | Inert second Vite entry tree at port-source root |
@@ -86,12 +86,16 @@ Detection approach: TypeScript `import ... from '...'` parsing across all `.ts`
- **Suggestion**: Move physical file to `src/shared/classColors.ts` (introducing a `src/shared/` layer for true Layer-0 utilities) or to `src/components/detection/classColors.ts` (under `03_shared-ui`). Either move drops the workaround and aligns physical/logical ownership.
- **Task / Epic**: Step 4 testability — minimal, surgical move (rename + import-path update across 4 consumers).
### F4: No Public API barrels — every internal file is de-facto public (High / Architecture)
### F4: No Public API barrels — every internal file is de-facto public (High / Architecture) — **CLOSED 2026-05-11 by AZ-485 (commit `23746ec`)**
- **Location**: every component root (no `src/<component>/index.ts` exists today; only `src/types/index.ts` and `mission-planner/src/types/index.ts` are barrels and they're re-export hubs, not component facades).
- **Description**: Cross-component imports use file-name granularity (`import { api } from '../api/client'`, `import { useFlight } from '../../components/FlightContext'`, etc.). Consequence: there is **no enforceable Public API surface**. Any internal refactor inside a component (split a file, rename an export) is a breaking change to every importer. Phase 7 Check #2 ("Public API respect") cannot meaningfully fail in this codebase because everything is public. Module-layout Verification #3 records the same observation.
- **Suggestion**: Step 4 testability candidate — add `src/<component>/index.ts` for every component, re-exporting only the symbols listed in module-layout's "Public API (de-facto)" line for that component. Then a future Phase 7 invocation can flag deep imports as Architecture findings instead of folding into background noise.
- **Task / Epic**: Step 4 testability (single mechanical change per component; ~11 new files + ~30 import-path edits).
- **Resolution**: 11 component barrels (`src/<component>/index.ts`) added — one per component except `10_app-shell` (top-level file collection, never imported as a unit). Every cross-component import in `src/`, `tests/`, and `e2e/` now goes through the barrel. The `STC-ARCH-01` static gate (`scripts/check-arch-imports.mjs --mode=arch-imports`, wired into `scripts/run-tests.sh --static`) fails the build on any deep-import regression. The architecture test `tests/architecture_imports.test.ts` exercises the gate with synthetic fixtures (AC-4 fail-on-synthetic, AC-5 pass-on-migrated). Module-layout Layout Rule #3 records the convention.
- **Carried-forward exemption**: `src/features/annotations/classColors` — the file is logically owned by `11_class-colors` but physically lives under `06_annotations` (F3). Re-exporting it through the `06_annotations` barrel creates a circular import (`AnnotationsPage → DetectionClasses → 06_annotations barrel → AnnotationsPage`). Consumers import the path directly under an `EXEMPT_RE` in the static check. The exemption disappears when F3 moves the file.
- **Pre-resolution context (preserved for trace)**:
- **Location**: every component root (no `src/<component>/index.ts` existed before AZ-485; only `src/types/index.ts` and `mission-planner/src/types/index.ts` were barrels and those are re-export hubs, not component facades).
- **Description**: Cross-component imports used file-name granularity (`import { api } from '../api/client'`, `import { useFlight } from '../../components/FlightContext'`, etc.). Consequence: there was **no enforceable Public API surface**. Any internal refactor inside a component (split a file, rename an export) was a breaking change to every importer. Phase 7 Check #2 ("Public API respect") could not meaningfully fail in this codebase because everything was public.
- **Suggestion (executed)**: add `src/<component>/index.ts` for every component, re-exporting only the symbols listed in module-layout's "Public API" line.
- **Task / Epic**: Step 4 testability — moved to Phase B cycle 1 batch 9 / AZ-485 / Epic AZ-447.
### F5: Pre-existing import cycle inside port-source (High / Architecture)
@@ -111,12 +115,16 @@ Detection approach: TypeScript `import ... from '...'` parsing across all `.ts`
- `shared/endpoints.ts` — typed endpoint constants (closes F7).
- **Task / Epic**: Phase B candidate (one cycle for shared infrastructure) OR fold into Step 8 refactor if user picks A on the Step 8 gate.
### F7: Hardcoded `/api/<service>/...` paths instead of env-driven endpoints (Medium / Architecture)
### F7: Hardcoded `/api/<service>/...` paths instead of env-driven endpoints (Medium / Architecture) — **CLOSED 2026-05-11 by AZ-486 (commit `8a461a2`)**
- **Location**: every call site of `api.*()` and `createSSE()` across `src/features/**` and `src/auth/`, `src/components/FlightContext.tsx`, `src/components/DetectionClasses.tsx`. Approximately 30 call sites.
- **Description**: Consequence of ADR-006 (nginx prefix-strip). Each call site repeats `/api/<service>/<path>` as a string literal. Testability suffers — every test fixture must duplicate paths; any nginx-route change touches every feature. Architecture intent (ADR-006 Consequences) explicitly flags this: *"The SPA hardcodes /api/<service>/... paths in source instead of an env-driven base URL — testability is poor (finding tracked)."*
- **Suggestion**: Step 4 testability — introduce `src/shared/endpoints.ts` (or per-component `endpoints.ts` if shared/ is deferred) that exposes typed builders: `endpoints.auth.login()`, `endpoints.flights.byId(id)`, `endpoints.annotations.media(query)`, etc. Replace every string-literal path. Allows tests to mock at the endpoints layer rather than at every `fetch` call. Compounds well with F6 if `src/shared/` lands first.
- **Task / Epic**: Step 4 testability (mechanical extract; per-component cohort).
- **Resolution**: `src/api/endpoints.ts` introduced as the single source of truth — 25 typed builders covering every `/api/<service>/<path>` URL the UI talks to today. Re-exported through the F4 barrel `src/api/index.ts`; consumers import `{ endpoints } from '../api'` (or `../../api`). Every production callsite of `api.*` and `createSSE()` migrated to `endpoints.*` — 13 source files (admin, annotations × 5, flights, settings, dataset, auth, client, FlightContext, DetectionClasses). The `STC-ARCH-02` static gate (`scripts/check-arch-imports.mjs --mode=api-literals`, wired into `scripts/run-tests.sh --static`) fails the build on any new `/api/<service>/` literal in `src/` outside the contract owner (`endpoints.ts`) and `*.test.tsx?` files. The colocated `src/api/endpoints.test.ts` (36 assertions, character-identical to pre-refactor URL strings) serves as the wire-contract documentation per `module-layout.md`'s "code-derived documentation" pattern. Module-layout Verification Needed item #3a records the convention.
- **F6 interaction**: `endpoints.ts` lives under `01_api-transport` (not `src/shared/`) — F6 is explicitly deferred. When/if F6 lands and moves the file, only `src/api/index.ts` flips the re-export source; consumers do not change. This is exactly the protection F4 was built to provide.
- **Pre-resolution context (preserved for trace)**:
- **Location**: every call site of `api.*()` and `createSSE()` across `src/features/**` and `src/auth/`, `src/components/FlightContext.tsx`, `src/components/DetectionClasses.tsx`. Approximately 30 call sites.
- **Description**: Consequence of ADR-006 (nginx prefix-strip). Each call site repeated `/api/<service>/<path>` as a string literal. Testability suffered — every test fixture had to duplicate paths; any nginx-route change touched every feature. Architecture intent (ADR-006 Consequences) explicitly flagged this: *"The SPA hardcodes /api/<service>/... paths in source instead of an env-driven base URL — testability is poor (finding tracked)."*
- **Suggestion (executed)**: introduce a typed endpoints module exposing builders like `endpoints.auth.login()`, `endpoints.flights.byId(id)`, `endpoints.annotations.media(query)`, etc.
- **Task / Epic**: Step 4 testability — moved to Phase B cycle 1 batch 10 / AZ-486 / Epic AZ-447.
### F8: Layering-table inconsistency — Header → useAuth is unannotated (Low / Architecture)
@@ -28,6 +28,16 @@
|--------|-----------|-------|
| `subscribe<T>(url, onMessage, onError?): { close }` | factory | Creates `EventSource` with the **bearer token in the query string** (browser `EventSource` can't set headers). Returns a `close()` handle. |
### `src/api/endpoints.ts` (since AZ-486 / F7)
| Export | Signature | Notes |
|--------|-----------|-------|
| `endpoints` | `Readonly<{ admin, annotations, flights, detect }>` of typed builder functions | Single source of truth for every `/api/<service>/...` URL the UI talks to. Each leaf is a function — `() => string` for constant paths, `(id, ...) => string` for parameterised ones. Wire-contract pinned by `src/api/endpoints.test.ts` (36 assertions). |
### `src/api/index.ts` (Public API barrel, since AZ-485 / F4)
Re-exports the component's public surface: `api`, `createSSE`, `setToken`, `getToken`, `getApiBase`, `setNavigateToLogin`, `endpoints`. Consumers OUTSIDE this component MUST import from the barrel; direct imports of `src/api/{client,sse,endpoints}` from other components are blocked by `STC-ARCH-01`.
## 3. External API Specification
This component does not *expose* an API; it consumes the suite's. The set of consumed endpoints (collected from feature module docs):
@@ -40,7 +50,7 @@ This component does not *expose* an API; it consumes the suite's. The set of con
| `detect/` | `/api/detect/...` | `06_annotations` |
| `loader/`, `resource/`, `gps-denied-*`, `autopilot/` | `/api/{loader,resource,gps-denied-desktop,gps-denied-onboard,autopilot}/...` | various features |
**No service-specific client modules exist**. URL strings are inlined at every call site (testability finding from autodev Step 4).
**No service-specific client modules exist**. URL strings are produced by typed builders in `src/api/endpoints.ts` (added by AZ-486 / F7, commit `8a461a2`) — the previous "URL strings inlined at every call site" testability finding (F7) is **CLOSED**. The `STC-ARCH-02` static gate (`scripts/check-arch-imports.mjs --mode=api-literals`, wired into `scripts/run-tests.sh`) forbids re-introducing `/api/<service>/...` literals under `src/`.
## 5. Implementation Details
@@ -60,7 +70,7 @@ This component does not *expose* an API; it consumes the suite's. The set of con
- **No timeout / cancellation**. (Step 4.)
- **Bearer in SSE query string**. Accepted trade-off; document in `security_approach.md` (Step 6).
- **No reconnect-on-token-rotate** for SSE consumers — every feature that uses SSE will silently stop receiving events after the first refresh (Step 8 hardening).
- **No service-specific clients** → URL strings duplicated across features. Risk of typos surfacing as 404s only at runtime (Step 4).
- ~~No service-specific clients~~ → **CLOSED by AZ-486 / F7**: URL strings centralised in `src/api/endpoints.ts`; STC-ARCH-02 enforces it. Typos now surface at build time (TS strict on the builder names) or in `endpoints.test.ts`, never at runtime.
## 8. Dependency Graph
@@ -76,3 +86,5 @@ This component does not *expose* an API; it consumes the suite's. The set of con
|------|------------|
| `src/api/client.ts` | `_docs/02_document/modules/src__api__client.md` |
| `src/api/sse.ts` | `_docs/02_document/modules/src__api__sse.md` |
| `src/api/endpoints.ts` | `_docs/02_document/modules/src__api__endpoints.md` |
| `src/api/index.ts` (barrel) | (no separate doc — re-exports surface listed in §2 above) |
@@ -42,11 +42,11 @@ export const api = {
- `204``undefined as T`.
- `!res.ok` → throw `new Error(\`${status}: ${text || statusText}\`)`. Body text is read defensively (`.catch(() => '')`).
- Otherwise → `res.json()` (no schema validation — caller types the response).
- `refreshToken()``POST /api/admin/auth/refresh` with `credentials: 'include'`. On 200, expects `{ token: string }` and stores it. Returns boolean.
- `refreshToken()``POST endpoints.admin.authRefresh()` (i.e. `/api/admin/auth/refresh`) with `credentials: 'include'`. On 200, expects `{ token: string }` and stores it. Returns boolean. (Path produced by the `endpoints` builder; closes F7.)
## Dependencies
- **Internal**: none.
- **Internal**: `./endpoints``endpoints.admin.authRefresh()` used by the internal `refreshToken()` helper (since AZ-486 / F7).
- **External**: `fetch`, `Headers`, `FormData`, `Response` (browser globals). No npm runtime dependency.
## Consumers (intra-repo)
@@ -71,9 +71,9 @@ None defined here. The generic `T` parameter is supplied by call sites.
## Configuration
URLs are **string literals** at every call site (`/api/admin/...`, `/api/flights?...`, etc.). There is no base-URL constant. The `vite.config.ts` dev proxy and `nginx.conf` production rules forward `/api/*` to per-service backends.
URLs are produced by typed builders in `src/api/endpoints.ts` (see `src__api__endpoints.md`) — the F7 finding from the architecture baseline is now CLOSED. Every consumer (this module included) imports `endpoints` and calls `endpoints.<service>.<method>(...)`; the `STC-ARCH-02` static gate forbids re-introducing literal `/api/<service>/...` strings under `src/`.
A `VITE_API_BASE_URL` env-var fix is the canonical Step 4 testability candidate (workspace `README.md` calls this out).
There is no base-URL constant: the path strings are still relative. The `vite.config.ts` dev proxy and `nginx.conf` production rules forward `/api/*` to per-service backends. `getApiBase()` (exported from this module) supplies the host prefix at runtime where the consumer needs an absolute URL (e.g. the manual `fetch(getApiBase() + endpoints.admin.authRefresh(), ...)` call inside `refreshToken()`).
## External integrations
@@ -0,0 +1,135 @@
# Module: `src/api/endpoints.ts`
> **Source**: `src/api/endpoints.ts` (79 lines)
> **Topo batch**: B2 (leaf — no internal imports)
> **Introduced**: AZ-486 (2026-05-11, commit `8a461a2`), closing architecture baseline finding F7.
## Purpose
Single source of truth for every `/api/<service>/<path>` URL the UI talks to. Replaces the hardcoded string literals that previously lived at each `api.*` / `createSSE` call site (and at every `src={...}` URL for API-served images / videos). The `endpoints` object is the canonical wire-contract documentation: each builder produces a character-identical string to the literal it superseded, so MSW handlers + e2e stubs + the nginx routing table all keep matching.
Together with the `STC-ARCH-02` static gate (see [Configuration](#configuration)), this module enforces "no hardcoded API path literals in `src/`" as a build-time invariant rather than a code-review aspiration.
## Public interface
```ts
export const endpoints = {
admin: {
authRefresh: () => string
authLogin: () => string
authLogout: () => string
users: () => string
user: (id: string) => string
classes: () => string
class: (id: string | number) => string
},
annotations: {
classes: () => string
settingsUser: () => string
settingsSystem: () => string
settingsDirectories: () => string
annotations: () => string
annotationsByMedia: (mediaId: string, pageSize?: number) => string // pageSize default = 1000
annotationImage: (annotationId: string) => string
annotationThumbnail: (annotationId: string) => string
annotationEvents: () => string
media: (queryString: string) => string
mediaFile: (mediaId: string) => string
mediaItem: (mediaId: string) => string
mediaBatch: () => string
dataset: (queryString: string) => string
datasetItem: (annotationId: string) => string
datasetBulkStatus: () => string
datasetClassDistribution: () => string
},
flights: {
collection: (queryString?: string) => string // GET ?pageSize=... lists; POST (no qs) creates
aircrafts: () => string
aircraft: (id: string) => string
flight: (id: string) => string
flightWaypoints: (id: string) => string
flightWaypoint: (flightId: string, waypointId: string) => string
flightLiveGps: (id: string) => string
},
detect: {
media: (mediaId: string) => string // POST → trigger detection for a media item
},
} as const
```
The whole object is `as const`, so each leaf's return type is the narrow string literal where possible (e.g. `'/api/admin/auth/refresh'`) and the parameterised builders carry a `string` return.
## Internal logic
- **Pure data + template strings.** No side effects, no I/O, no caching. Every builder is a one-line `() => '...'` or arrow returning a template literal.
- **Function form (not constants)**, per direction at task-creation time:
- Parameterised paths (e.g. `flight(id)`) need a function anyway. Keeping every entry as a function — even the constant ones — gives a single uniform call shape at every site (`endpoints.x.y()`) so reviewers don't have to remember which entries take parens and which don't.
- Per-builder tree-shaking under Vite's production rollup remains intact.
- **Query strings owned by the caller for variable-shape paths.** Where the query is dynamic (`flights.collection`, `annotations.media`, `annotations.dataset`), the caller builds a `URLSearchParams.toString()` and the builder owns only the path + `?`. This keeps the wire contract identical to pre-refactor literals at every callsite.
## Public API (barrel re-export)
`src/api/index.ts` re-exports `endpoints` alongside `api`, `createSSE`, `setToken`, `getToken`, `getApiBase`, `setNavigateToLogin`. Consumers OUTSIDE the `01_api-transport` component MUST import from the barrel (`import { endpoints } from '@/api'` or `from '../api'`) — direct imports of `src/api/endpoints` from other components are blocked by `STC-ARCH-01` (F4 closure, see `src__api__client.md`).
## Dependencies
- **Internal**: none.
- **External**: none.
## Consumers (intra-repo)
After the AZ-486 migration, `endpoints` is imported by:
- `src/api/client.ts` — internal `refreshToken()` helper uses `endpoints.admin.authRefresh()`.
- `src/auth/AuthContext.tsx``authRefresh`, `authLogin`, `authLogout`.
- `src/components/FlightContext.tsx``flights.collection`, `flights.flight`, `annotations.settingsUser`.
- `src/components/DetectionClasses.tsx``admin.classes`, `admin.class`.
- `src/features/admin/AdminPage.tsx``admin.users`, `admin.user`.
- `src/features/annotations/AnnotationsPage.tsx` — annotation CRUD endpoints, `detect.media`.
- `src/features/annotations/AnnotationsSidebar.tsx``annotations.annotationEvents` (SSE), bulk-status, dataset endpoints.
- `src/features/annotations/CanvasEditor.tsx``annotations.annotationImage`, `annotations.annotationThumbnail`.
- `src/features/annotations/MediaList.tsx``annotations.media`, `annotations.mediaFile`, `annotations.mediaItem`, `annotations.mediaBatch`.
- `src/features/annotations/VideoPlayer.tsx``annotations.mediaFile`.
- `src/features/dataset/DatasetPage.tsx``annotations.dataset*` family, `annotations.classes`, `annotations.annotationImage`.
- `src/features/flights/FlightsPage.tsx` — full `flights.*` surface + `annotations.settingsUser`.
- `src/features/settings/SettingsPage.tsx``annotations.settings*`, `flights.aircrafts`.
This is the full intra-repo consumer list — `STC-ARCH-02` guarantees no production-source caller falls outside it.
## Data models
None defined here. Path-string output only.
## Configuration
The module IS the API-path configuration. The only "config" is the nginx routing table that maps each `/api/<service>/...` prefix to a concrete backend service — see `src__api__client.md` → External integrations for the live table.
**Static enforcement (`STC-ARCH-02`)**:
- **Script**: `scripts/check-arch-imports.mjs --mode=api-literals`.
- **Wired into**: `scripts/run-tests.sh` (functional profile, static group) — runs before any unit test.
- **What it forbids**: any `/api/<service>/...` literal in `[`'"]` quoting under `src/`.
- **Exempt files**: this file (`src/api/endpoints.ts`) and `src/**/*.test.ts(x)` only.
- **Bypass policy**: none. Adding a new exempt path requires updating the exempt regex in the script AND a `module-layout.md` rule revision in the same commit.
## External integrations
This module integrates nothing directly. It documents — as TypeScript values — the wire contract for every external integration the SPA has, as routed by `nginx.conf`. See the routing table in `src__api__client.md` → External integrations for the per-prefix backend mapping.
## Security
- **No bearer plumbing here.** Token injection still happens in `client.ts` (`Authorization` header) and `sse.ts` (`access_token` query parameter). Builders return URLs **without** the token.
- **No URL-encoding** of interpolated `id` / `mediaId` / `queryString` parameters. All current callsites pass already-safe values (UUIDs, ints, pre-built `URLSearchParams.toString()` output). If any future caller passes user-controlled text, the builder must add `encodeURIComponent` (see open question below).
- **No CSRF surface change** — same posture as the pre-refactor literals.
## Tests
- **`src/api/endpoints.test.ts`** (36 Vitest assertions): pins every builder's output to its exact pre-refactor URL string. This is the contract documentation — any wire-contract change MUST update this test in the same commit as the backend / MSW / e2e stub change. Includes one barrel-re-export assertion (`endpoints` is reachable via `import { endpoints } from '../api'`).
- **`tests/architecture_imports.test.ts`** (AZ-486 / STC-ARCH-02 suite, 6 cases): verifies the static gate passes on the migrated codebase AND fails when a synthetic single-quoted / double-quoted / template-literal `/api/<service>/...` literal is introduced in `src/`. Also verifies the `*.test.ts` and `//` comment exemptions.
## Notes / open questions
- **`detect.media` only exposes the single-segment path** that the UI uses today (`POST /api/detect/<mediaId>`). The full `detect/` service has more endpoints (per the nginx table) but no UI callsite consumes them. Add new builders only when a real callsite needs them — don't pre-populate.
- **`flights.collection` overloads its return** on whether `queryString` is provided. Acceptable while the contract is "GET with `?pageSize`, POST without" — but if a third flights-collection verb (DELETE? PUT?) is ever added with its own query shape, split into named builders rather than threading more conditional logic through one.
- **No URL-encoding of interpolated params** (see Security). Add `encodeURIComponent` at the first callsite that needs it, plus a contract-test case in `endpoints.test.ts`. Currently safe across all 36 pinned URLs.
- **Wire-contract test coverage is exact-string, not shape.** This is deliberate: a "looks like a path" matcher would silently accept a hyphen-to-underscore change that breaks the backend. Updating these strings IS a wire-contract change — treat the test as a release-gate.
+1 -1
View File
@@ -49,7 +49,7 @@ None defined here. The generic `T` is supplied by the caller.
## Configuration
URLs are passed in by callers (string-literal at call sites). The same testability remark as `api/client.ts` applies: a `VITE_API_BASE_URL` is the natural Step 4 fix.
URLs are passed in by callers. Since AZ-486 / F7 (commit `8a461a2`), callers obtain those URLs from `endpoints.*` builders in `src/api/endpoints.ts` rather than from inline string literals. The `STC-ARCH-02` static gate enforces this at every callsite under `src/`. `createSSE` itself is path-agnostic and takes any `url` — the `endpoints` discipline is upheld at the call site, not here.
## External integrations
@@ -34,18 +34,18 @@ State:
**Bootstrap effect (mount-only)**:
```ts
api.get<{ user: AuthUser; token: string }>('/api/admin/auth/refresh')
api.get<{ user: AuthUser; token: string }>(endpoints.admin.authRefresh())
.then(data => { setToken(data.token); setUser(data.user) })
.catch(() => {})
.finally(() => setLoading(false))
```
The refresh endpoint is invoked with `credentials: 'include'` only inside `client.ts`'s **internal** `refreshToken()` helper — but here we go through the public `api.get()` path, which does NOT include credentials. **This is a real divergence**: `client.ts`'s internal `refreshToken()` (used in the 401 retry) sends the cookie; the bootstrap call in `AuthContext` does not. The endpoint must therefore accept the refresh either via cookie (then bootstrap fails silently for non-cookie clients — which is everyone after a hard reload) **or** via some other mechanism (a refresh token in `localStorage`, etc.). **Flag for Step 4 verification** against the `admin/` service contract; this is likely a real bug masking by silent `.catch`.
The refresh endpoint is invoked with `credentials: 'include'` only inside `client.ts`'s **internal** `refreshToken()` helper — but here we go through the public `api.get()` path, which does NOT include credentials. **This is a real divergence**: `client.ts`'s internal `refreshToken()` (used in the 401 retry) sends the cookie; the bootstrap call in `AuthContext` does not. The endpoint must therefore accept the refresh either via cookie (then bootstrap fails silently for non-cookie clients — which is everyone after a hard reload) **or** via some other mechanism (a refresh token in `localStorage`, etc.). **Flag for Step 4 verification** against the `admin/` service contract; this is likely a real bug masking by silent `.catch`. The path string itself is unaffected by AZ-486 — `endpoints.admin.authRefresh()` produces `'/api/admin/auth/refresh'` character-identically to the pre-refactor literal, so the divergence is structural, not URL-based.
**`login(email, password)`**:
```ts
const data = await api.post<{ token; user }>('/api/admin/auth/login', { email, password })
const data = await api.post<{ token; user }>(endpoints.admin.authLogin(), { email, password })
setToken(data.token); setUser(data.user)
```
@@ -54,7 +54,7 @@ Throws to caller (LoginPage) on bad credentials.
**`logout()`**:
```ts
try { await api.post('/api/admin/auth/logout') } catch {}
try { await api.post(endpoints.admin.authLogout()) } catch {}
setToken(null); setUser(null)
```
@@ -65,7 +65,7 @@ Network failure on logout is silently swallowed because we want to clear local a
## Dependencies
- **Internal**:
- `../api/client``api`, `setToken`.
- `../api` (barrel)`api`, `endpoints`, `setToken`. (Since AZ-485 / F4 + AZ-486 / F7: barrel import + endpoint builders.)
- `../types``AuthUser` type.
- **External**: `react` (`createContext`, `useContext`, `useState`, `useCallback`, `useEffect`, `ReactNode`).
@@ -86,7 +86,7 @@ From the §7a dependency graph:
## Configuration
Endpoints (string-literal): `/api/admin/auth/refresh`, `/api/admin/auth/login`, `/api/admin/auth/logout`. Routed by `nginx.conf` to the `admin/` service.
Endpoints (typed builders from `../api/endpoints`, since AZ-486 / F7): `endpoints.admin.authRefresh()`, `endpoints.admin.authLogin()`, `endpoints.admin.authLogout()` — producing `/api/admin/auth/refresh`, `.../login`, `.../logout` respectively. Routed by `nginx.conf` to the `admin/` service.
No env vars consumed directly — token storage policy is defined in `client.ts` (in-memory; not persisted to `localStorage`).
@@ -24,7 +24,7 @@ Fully controlled — the parent owns `selectedClassNum` and `photoMode`. The cur
## Internal logic
- **Class catalogue load** (mount-only `useEffect`):
- `api.get<DetectionClass[]>('/api/annotations/classes')`.
- `api.get<DetectionClass[]>(endpoints.annotations.classes())` (= `/api/annotations/classes`, since AZ-486 / F7).
- On a non-empty array → `setClasses(list)`.
- On an empty array OR a thrown error → `setClasses(FALLBACK_CLASSES)`.
- **`FALLBACK_CLASSES`** is a module-private 3 × |`FALLBACK_CLASS_NAMES`| matrix:
@@ -45,8 +45,8 @@ Fully controlled — the parent owns `selectedClassNum` and `photoMode`. The cur
## Dependencies
- **Internal**:
- `../api/client``api.get<T>()`.
- `../features/annotations/classColors``getClassColor(i)`, `FALLBACK_CLASS_NAMES`.
- `../api` (barrel) — `api`, `endpoints`. (Since AZ-485 / F4 + AZ-486 / F7.)
- `../features/annotations/classColors``getClassColor(i)`, `FALLBACK_CLASS_NAMES`. (Cross-component import preserved; flagged in Consumers below.)
- `../types``DetectionClass` type.
- **External**: `react`, `react-i18next`, `react-icons/md`, `react-icons/fa`.
@@ -70,7 +70,7 @@ This is the **canonical example** of the cross-layer import flagged in `_docs/02
## Configuration
Endpoint: `/api/annotations/classes` — string-literal URL (testability fix scheduled for Step 4).
Endpoint: `endpoints.annotations.classes()` `/api/annotations/classes` (typed builder from `../api/endpoints`, since AZ-486 / F7).
Photo-mode value set is `{0, 20, 40}` — hardcoded, mirrored by `FALLBACK_CLASSES`. If the backend grows a fourth mode (e.g. thermal, IR), every consumer of `photoMode` will need a coordinated change.
@@ -78,7 +78,7 @@ Tailwind tokens: `bg-az-orange` (Regular), `bg-az-blue` (Winter), `bg-purple-600
## External integrations
- HTTP `GET /api/annotations/classes``DetectionClass[]`. Backed by the `annotations/` service (`.NET`) per `nginx.conf`.
- HTTP `GET endpoints.annotations.classes()` (= `/api/annotations/classes`)`DetectionClass[]`. Backed by the `annotations/` service (`.NET`) per `nginx.conf`.
## Security
@@ -27,13 +27,13 @@ export function FlightProvider({ children }: { children: ReactNode }): JSX.Eleme
State:
- `flights: Flight[]` — most recent list returned by `GET /api/flights?pageSize=1000`.
- `flights: Flight[]` — most recent list returned by `GET endpoints.flights.collection('pageSize=1000')` (= `/api/flights?pageSize=1000`).
- `selectedFlight: Flight | null` — the active flight, or `null` if none. Survives across pages because the provider is mounted above the route tree.
**`refreshFlights()`** (`useCallback`, no deps):
```ts
const data = await api.get<{ items: Flight[] }>('/api/flights?pageSize=1000')
const data = await api.get<{ items: Flight[] }>(endpoints.flights.collection('pageSize=1000'))
setFlights(data.items ?? [])
```
@@ -42,8 +42,8 @@ Errors are silently swallowed (`try { ... } catch {}`). `pageSize=1000` is a har
**Bootstrap effect** (`useEffect` keyed on `[refreshFlights]`, runs once because `refreshFlights` is `useCallback([])`):
1. `refreshFlights()` (no `await` — runs in parallel with #2).
2. `api.get<UserSettings>('/api/annotations/settings/user')`
- if `settings?.selectedFlightId` is truthy: `api.get<Flight>('/api/flights/${settings.selectedFlightId}')``setSelectedFlight(f)`.
2. `api.get<UserSettings>(endpoints.annotations.settingsUser())`
- if `settings?.selectedFlightId` is truthy: `api.get<Flight>(endpoints.flights.flight(settings.selectedFlightId))``setSelectedFlight(f)`.
- errors at every step silently swallowed.
The selected flight is therefore looked up by **its own GET**, not by indexing into the cached `flights` list. This is intentional — the user might have a `selectedFlightId` that fell off the first 1000 flights.
@@ -52,7 +52,7 @@ The selected flight is therefore looked up by **its own GET**, not by indexing i
```ts
setSelectedFlight(f)
api.put('/api/annotations/settings/user', { selectedFlightId: f?.id ?? null }).catch(() => {})
api.put(endpoints.annotations.settingsUser(), { selectedFlightId: f?.id ?? null }).catch(() => {})
```
Optimistic — local state updates immediately; the persisted setting is fire-and-forget. If the PUT fails, the next reload will fall back to the previously-stored ID and the user's selection silently reverts. Flag for Step 4.
@@ -60,7 +60,7 @@ Optimistic — local state updates immediately; the persisted setting is fire-an
## Dependencies
- **Internal**:
- `../api/client``api`.
- `../api` (barrel) — `api`, `endpoints`. (Since AZ-485 / F4 + AZ-486 / F7: barrel import + endpoint builders.)
- `../types``Flight`, `UserSettings` types.
- **External**: `react` (`createContext`, `useContext`, `useState`, `useEffect`, `useCallback`, `ReactNode`).
@@ -80,9 +80,9 @@ From the §7a dependency graph:
## Configuration
Endpoints (string-literal): `/api/flights?pageSize=1000`, `/api/flights/{id}`, `/api/annotations/settings/user`. Routed by `nginx.conf` to the `flights/` and `annotations/` services.
Endpoints (typed builders from `../api/endpoints`, since AZ-486 / F7): `endpoints.flights.collection('pageSize=1000')`, `endpoints.flights.flight(id)`, `endpoints.annotations.settingsUser()` — producing `/api/flights?pageSize=1000`, `/api/flights/{id}`, `/api/annotations/settings/user` respectively. Routed by `nginx.conf` to the `flights/` and `annotations/` services.
`pageSize=1000` is a **hardcoded ceiling** — if the system ever has >1000 flights, the cache is silently truncated. The `flights/` service contract probably caps at 1000; verify and document in `data_parameters.md` (Step 6). Flag.
`pageSize=1000` is a **hardcoded ceiling** — if the system ever has >1000 flights, the cache is silently truncated. The `flights/` service contract probably caps at 1000; verify and document in `data_parameters.md` (Step 6). Flag. (Note: the literal lives in the caller, not in the `endpoints.flights.collection` builder — moving the ceiling into the builder is a future change.)
## External integrations
@@ -40,9 +40,9 @@ No props. Reads everything via `api/client` and local state.
- **Bootstrap effect** (`useEffect([])` — runs once at mount):
```ts
api.get<DetectionClass[]>('/api/annotations/classes').then(setClasses).catch(() => {})
api.get<Aircraft[]>('/api/flights/aircrafts').then(setAircrafts).catch(() => {})
api.get<User[]>('/api/admin/users').then(setUsers).catch(() => {})
api.get<DetectionClass[]>(endpoints.annotations.classes()).then(setClasses).catch(() => {})
api.get<Aircraft[]>(endpoints.flights.aircrafts()).then(setAircrafts).catch(() => {})
api.get<User[]>(endpoints.admin.users()).then(setUsers).catch(() => {})
```
Three independent calls, all silently swallowed on error. No retry,
@@ -51,10 +51,11 @@ No props. Reads everything via `api/client` and local state.
`_docs/ui_design/README.md`.
- **`handleAddClass()`**:
1. Guard: `if (!newClass.name) return`.
2. `await api.post('/api/admin/classes', newClass)`.
3. Refetch via `api.get('/api/annotations/classes')` — note the
**read** path is the public `annotations/` endpoint, while the
**write** path is the `admin/` endpoint. Architectural caveat:
2. `await api.post(endpoints.admin.classes(), newClass)` (= `/api/admin/classes`).
3. Refetch via `api.get(endpoints.annotations.classes())` — note the
**read** path is the public `annotations/` endpoint
(`/api/annotations/classes`), while the **write** path is the
`admin/` endpoint (`/api/admin/classes`). Architectural caveat:
two different services own the same logical entity. Document in
`architecture.md` §integration-points (Step 3a).
4. Reset `newClass` to its initial values.
@@ -62,25 +63,26 @@ No props. Reads everything via `api/client` and local state.
non-2xx); the throw is uncaught and reaches React's error boundary
(none configured). Flag.
- **`handleDeleteClass(id)`**: optimistic local update —
`await api.delete('/api/admin/classes/${id}')` then
`setClasses(prev => prev.filter(c => c.id !== id))`. **No
`await api.delete(endpoints.admin.class(id))` (= `/api/admin/classes/${id}`)
then `setClasses(prev => prev.filter(c => c.id !== id))`. **No
ConfirmDialog** despite this being destructive. Inconsistent with
the user-deactivation flow which uses ConfirmDialog. Flag for Step 4
against `_docs/ui_design/README.md` confirmation-dialog spec.
- **`handleAddUser()`** — analogous to `handleAddClass` against
`POST /api/admin/users` and `GET /api/admin/users`. Guards on
`email && password`.
`POST endpoints.admin.users()` and `GET endpoints.admin.users()`
(both → `/api/admin/users`). Guards on `email && password`.
- **`handleDeactivate()`** — fired from the ConfirmDialog confirm:
1. `PATCH /api/admin/users/${deactivateId}` with `{ isActive: false }`.
1. `PATCH endpoints.admin.user(deactivateId)` (= `/api/admin/users/${deactivateId}`) with `{ isActive: false }`.
2. Optimistic local update: marks the row inactive.
3. Closes the dialog (`setDeactivateId(null)`).
No "reactivate" path — once `isActive: false`, the row only renders
the badge and no Deactivate button. Verify with `admin/` service:
is reactivation an admin task or out of scope?
- **`handleToggleDefault(a)`** — `PATCH /api/flights/aircrafts/${a.id}`
with `{ isDefault: !a.isDefault }`, then optimistic local flip. Note
this allows multiple `isDefault: true` aircraft to coexist (the
backend should enforce exclusivity; the UI does not).
- **`handleToggleDefault(a)`** — `PATCH endpoints.flights.aircraft(a.id)`
(= `/api/flights/aircrafts/${a.id}`) with `{ isDefault: !a.isDefault }`,
then optimistic local flip. Note this allows multiple `isDefault:
true` aircraft to coexist (the backend should enforce exclusivity;
the UI does not).
- **Layout** (left → center → right, all in one horizontal flex):
- **Left column** (`w-[340px]`): detection-classes table + add row.
- **Center column** (`flex-1 max-w-md`): AI settings form, GPS
@@ -93,7 +95,7 @@ No props. Reads everything via `api/client` and local state.
## Dependencies
- **Internal**:
- `../../api/client` — `api`.
- `../../api` (barrel) — `api`, `endpoints`. (Since AZ-485 / F4 + AZ-486 / F7.)
- `../../components/ConfirmDialog` — for user deactivation.
- `../../types` — `DetectionClass`, `Aircraft`, `User`.
- **External**: `react` (`useState`, `useEffect`),
@@ -137,19 +139,18 @@ backend assigns `id` and other server-managed fields.
## External integrations
| Method | Path | Purpose |
| Method | Builder → Path | Purpose |
|---|---|---|
| `GET` | `/api/annotations/classes` | List detection classes (read path uses annotations service) |
| `POST` | `/api/admin/classes` | Create detection class (write path uses admin service) |
| `DELETE` | `/api/admin/classes/{id}` | Delete detection class |
| `GET` | `/api/flights/aircrafts` | List aircraft |
| `PATCH` | `/api/flights/aircrafts/{id}` | Toggle `isDefault` |
| `GET` | `/api/admin/users` | List users |
| `POST` | `/api/admin/users` | Create user |
| `PATCH` | `/api/admin/users/{id}` | Set `isActive: false` (deactivate) |
| `GET` | `endpoints.annotations.classes()` → `/api/annotations/classes` | List detection classes (read path uses annotations service) |
| `POST` | `endpoints.admin.classes()` → `/api/admin/classes` | Create detection class (write path uses admin service) |
| `DELETE` | `endpoints.admin.class(id)` → `/api/admin/classes/{id}` | Delete detection class |
| `GET` | `endpoints.flights.aircrafts()` → `/api/flights/aircrafts` | List aircraft |
| `PATCH` | `endpoints.flights.aircraft(id)` → `/api/flights/aircrafts/{id}` | Toggle `isDefault` |
| `GET` | `endpoints.admin.users()` → `/api/admin/users` | List users |
| `POST` | `endpoints.admin.users()` → `/api/admin/users` | Create user |
| `PATCH` | `endpoints.admin.user(id)` → `/api/admin/users/{id}` | Set `isActive: false` (deactivate) |
Routed by `nginx.conf` to `admin/`, `annotations/`, `flights/`
backends.
Path builders live in `src/api/endpoints.ts` (since AZ-486 / F7). Routed by `nginx.conf` to `admin/`, `annotations/`, `flights/` backends.
## Security
@@ -9,19 +9,21 @@ Owns the `/annotations` route. Lets the user:
2. 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).
3. Draw / move / resize / multi-select bounding boxes on a custom canvas overlay, click-and-drag with Ctrl-modifier behaviours, snap to image-normalized coordinates 01.
4. Pick the active detection class (19 hotkeys) and PhotoMode (Regular / Winter / Night) via the shared `DetectionClasses` component.
5. 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.
6. Stream the annotations sidebar from the `GET /api/annotations/annotations/events` SSE feed; show each row with the gradient defined in `_docs/ui_design/README.md`.
7. Trigger AI detection via `POST /api/detect/{mediaId}` (modal log overlay).
5. 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.
6. 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`.
7. Trigger AI detection via `POST endpoints.detect.media(mediaId)` (= `/api/detect/{mediaId}`) — modal log overlay.
8. Download an annotation as YOLO `.txt` + a PNG of the frame with rectangles burned in.
> All path strings produced by `endpoints.*` builders from `src/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 `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). |
| `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.110×), 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. |
@@ -29,24 +31,26 @@ Owns the `/annotations` route. Lets the user:
- **`Detection`** (from `src/types/index.ts`): `{ id, classNum, label, confidence ∈ [0,1], affiliation: 0|1|2, combatReadiness, centerX, centerY, width, height }` — all coords normalized 01. Matches `_docs/legacy/wpf-era.md §10` and parent suite `_docs/01_annotations.md` Detection DTO.
- **`AnnotationListItem`**: `{ id, mediaId, time: 'HH:MM:SS.mmm' | null, createdDate, source: AnnotationSource, status: AnnotationStatus, isSplit, splitTile, detections[] }`. Matches `Annotations` table in parent `_docs/00_database_schema.md` modulo client-side `isSplit / splitTile`.
- **AI detect endpoint**: `POST /api/detect/{mediaId}` — matches parent `../../../../_docs/03_detections.md §2` after nginx strips `/api/detect/`. NOTE: UI does NOT forward the `X-Refresh-Token` header that the spec requires for long-running video detection (`_docs/03_detections.md §2 request headers`). Carried as a finding.
- **AI detect endpoint**: `endpoints.detect.media(mediaId)``POST /api/detect/{mediaId}` — matches parent `../../../../_docs/03_detections.md §2` after nginx strips `/api/detect/`. NOTE: UI does NOT forward the `X-Refresh-Token` header 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[] }`. .NET `TimeSpan.Parse` accepts that format so the round-trip works for `time → VideoTime`. **Body is missing required `Source` and optional `WaypointId`** required by parent spec `CreateAnnotationRequest` — see Findings.
## External integrations
| Endpoint / origin | Where | Direction | Notes |
| Builder → Path | 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`. |
| `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
1. **`VideoPlayer.stepFrames` hardcodes `fps = 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 from `video.getVideoPlaybackQuality()` / metadata. Step 4.
@@ -16,25 +16,27 @@ Default-exported page component, no props. Mounts under `/dataset` in `App.tsx`.
- **Filters**: `fromDate`, `toDate`, `statusFilter` (`AnnotationStatus`), `selectedClassNum` (from `DetectionClasses`), `objectsOnly` (boolean), `search` (400 ms debounced via `useDebounce`).
- **Pagination**: client `page` state, server `pageSize` fixed at 20, `totalPages = ceil(totalCount / pageSize)`.
- **Selection**: `Set<annotationId>`. Plain click replaces; Ctrl+click toggles. Validate button appears when set is non-empty.
- **Validate**: `POST /api/annotations/dataset/bulk-status` with `{ annotationIds[], status: Validated }`.
- **Distribution**: lazy-loaded on tab switch via `GET /api/annotations/dataset/class-distribution`.
- **Editor tab** mounts `<CanvasEditor>` with a synthesised `Media` stub built from `editorAnnotation.mediaId` (most fields blank). Used so `CanvasEditor` can fetch the image via `/api/annotations/annotations/{id}/image`.
- **Validate**: `POST endpoints.annotations.datasetBulkStatus()` (= `/api/annotations/dataset/bulk-status`) with `{ annotationIds[], status: Validated }`.
- **Distribution**: lazy-loaded on tab switch via `GET endpoints.annotations.datasetClassDistribution()` (= `/api/annotations/dataset/class-distribution`).
- **Editor tab** mounts `<CanvasEditor>` with a synthesised `Media` stub built from `editorAnnotation.mediaId` (most fields blank). Used so `CanvasEditor` can fetch the image via `endpoints.annotations.annotationImage(id)` (= `/api/annotations/annotations/{id}/image`).
## Dependencies
- Internal: `api/client`, `useDebounce`, `useResizablePanel` (left panel 250 / 200400), `FlightContext`, `DetectionClasses`, `ConfirmDialog` (imported but not used here — see Findings), `features/annotations/CanvasEditor`, `types/index`.
- Internal: `api` (barrel — `api`, `endpoints`, since AZ-485 / F4 + AZ-486 / F7), `useDebounce`, `useResizablePanel` (left panel 250 / 200400), `FlightContext`, `DetectionClasses`, `ConfirmDialog` (imported but not used here — see Findings), `features/annotations/CanvasEditor`, `types/index`.
- External: `react`, `react-i18next`.
## External integrations
| Endpoint | Where | Notes |
| Builder → Path | Where | Notes |
|---|---|---|
| `GET /api/annotations/dataset?...` | `fetchItems` | Filters: `page`, `pageSize`, `fromDate`, `toDate`, `flightId`, `status`, `classNum`, `hasDetections`, `name`. |
| `GET /api/annotations/dataset/{id}` | `handleDoubleClick` | Loads full `AnnotationListItem` for the editor tab. |
| `POST /api/annotations/dataset/bulk-status` | `handleValidate` | Body: `{ annotationIds, status }`. |
| `GET /api/annotations/dataset/class-distribution` | `loadDistribution` | Class histogram. |
| `GET /api/annotations/annotations/{id}/thumbnail` | grid tile `<img src=>` | One HTTP per visible tile. |
| `GET /api/annotations/annotations/{id}/image` | `CanvasEditor` (delegated) | When the editor is open. |
| `endpoints.annotations.dataset(qs)``/api/annotations/dataset?...` | `fetchItems` | Filters: `page`, `pageSize`, `fromDate`, `toDate`, `flightId`, `status`, `classNum`, `hasDetections`, `name` (caller builds `URLSearchParams.toString()`). |
| `endpoints.annotations.datasetItem(id)``/api/annotations/dataset/{id}` | `handleDoubleClick` | Loads full `AnnotationListItem` for the editor tab. |
| `endpoints.annotations.datasetBulkStatus()``/api/annotations/dataset/bulk-status` | `handleValidate` | Body: `{ annotationIds, status }`. |
| `endpoints.annotations.datasetClassDistribution()``/api/annotations/dataset/class-distribution` | `loadDistribution` | Class histogram. |
| `endpoints.annotations.annotationThumbnail(id)``/api/annotations/annotations/{id}/thumbnail` | grid tile `<img src=>` | One HTTP per visible tile. |
| `endpoints.annotations.annotationImage(id)``/api/annotations/annotations/{id}/image` | `CanvasEditor` (delegated) | When the editor is open. |
Path builders live in `src/api/endpoints.ts` (since AZ-486 / F7).
Spec contract is in parent suite `_docs/09_dataset_explorer.md`.
@@ -5,10 +5,10 @@
## Scope
Owns the `/flights` route. Lets the user:
1. Browse / create / delete `Flight` rows (`POST/DELETE /api/flights/...`).
1. Browse / create / delete `Flight` rows via `endpoints.flights.collection()` (POST) and `endpoints.flights.flight(id)` (DELETE).
2. Plan a mission on a Leaflet map: add waypoints, draw work-area / no-go rectangles, edit altitude + purpose per point, see live total distance, time, battery %.
3. Toggle into GPS-Denied mode — opens an SSE stream `/api/flights/{id}/live-gps` (and the panel for orthophoto upload + correction once that backend wiring lands; today only the live-GPS readout is connected).
4. Save waypoints back to the Flights API (`/api/flights/{id}/waypoints`).
3. Toggle into GPS-Denied mode — opens an SSE stream `endpoints.flights.flightLiveGps(id)` (= `/api/flights/{id}/live-gps`) (and the panel for orthophoto upload + correction once that backend wiring lands; today only the live-GPS readout is connected).
4. Save waypoints back to the Flights API via `endpoints.flights.flightWaypoints(id)` and `endpoints.flights.flightWaypoint(flightId, waypointId)`.
5. Import / export the plan as JSON.
Currently handles only the planning surface; the gps-denied orthophoto upload / correction inputs in `_docs/ui_design/flights.html` are not yet implemented.
@@ -43,14 +43,14 @@ Currently handles only the planning surface; the gps-denied orthophoto upload /
## External integrations
| Endpoint / origin | Where | Direction | Notes |
| Builder → Path | Where | Direction | Notes |
|---|---|---|---|
| `GET /api/flights/aircrafts` | `FlightsPage` | egress | Aircraft selector population. |
| `GET /api/flights/{id}/waypoints` | `FlightsPage` | egress | On flight select. |
| `POST /api/flights` | `FlightsPage` | egress | Create flight from sidebar. |
| `DELETE /api/flights/{id}` | `FlightsPage` | egress | After ConfirmDialog. |
| `POST/DELETE /api/flights/{id}/waypoints[/{wp}]` | `FlightsPage.handleSave` | egress | Delete-all-then-recreate, sequentially. |
| `GET /api/flights/{id}/live-gps` (SSE) | `FlightsPage` | egress | Open while in GPS mode + flight selected. |
| `endpoints.flights.aircrafts()``GET /api/flights/aircrafts` | `FlightsPage` | egress | Aircraft selector population. |
| `endpoints.flights.flightWaypoints(id)``GET /api/flights/{id}/waypoints` | `FlightsPage` | egress | On flight select. |
| `endpoints.flights.collection()``POST /api/flights` | `FlightsPage` | egress | Create flight from sidebar. |
| `endpoints.flights.flight(id)``DELETE /api/flights/{id}` | `FlightsPage` | egress | After ConfirmDialog. |
| `endpoints.flights.flightWaypoints(id)` + `endpoints.flights.flightWaypoint(flightId, wp)``POST/DELETE /api/flights/{id}/waypoints[/{wp}]` | `FlightsPage.handleSave` | egress | Delete-all-then-recreate, sequentially. |
| `endpoints.flights.flightLiveGps(id)``GET /api/flights/{id}/live-gps` (SSE) | `FlightsPage` | egress | Open while in GPS mode + flight selected. |
| `https://api.openweathermap.org/...` | `flightPlanUtils.getWeatherData` | egress | Direct browser→3rd-party. **Hardcoded API key.** See Findings. |
| `tile.openstreetmap.org` (`TILE_URLS.classic`) | `FlightMap`, `MiniMap` | egress | Direct, no proxy. |
| `server.arcgisonline.com/.../World_Imagery` (`TILE_URLS.satellite`) | `FlightMap`, `MiniMap` | egress | Direct, no proxy. |
@@ -86,7 +86,8 @@ These are the real findings; the per-module rationale is in git history of the d
23. **`handleImport` silently drops the file picker** if the user cancels (`if (!file) return`) — fine. But `handleJsonSave`'s catch uses `alert(...)` for a UX-grade error — replace with the project's modal/toast pattern in Step 4.
24. **`MapPoint` popup recomputes the marker DOM offset on every drag move** to choose dx/dy for the moving-point indicator. Acceptable, but the `(marker as unknown as { _icon: HTMLElement })._icon` cast leaks Leaflet internals.
25. **`DrawControl` registers global `mousedown`/`mousemove`/`mouseup` on the map** while a draw mode is active and disables `map.dragging` for the duration — fine, but no Esc-to-cancel mid-draw.
26. **`FlightContext` ceiling**: `FlightsPage` reads `flights` from `useFlight()` which fetches with `pageSize=1000` (already flagged in `FlightContext` doc). Won't surface here, but `selectFlight` is fire-and-forget — if the PUT to `/api/flights/select` fails the next page reload reverts the choice without notice.
26. **`FlightContext` ceiling**: `FlightsPage` reads `flights` from `useFlight()` which fetches with `pageSize=1000` (already flagged in `FlightContext` doc). Won't surface here, but `selectFlight` is fire-and-forget — if the PUT to `endpoints.annotations.settingsUser()` (= `/api/annotations/settings/user`) fails the next page reload reverts the choice without notice. (Note: the underlying call goes to the annotations settings store, not a hypothetical `/api/flights/select`; see `src__components__FlightContext.md` for the actual PUT path.)
27. **Path builders (since AZ-486 / F7)**: every callsite in this page family now imports `endpoints` from `../../api` (barrel). The wire contract (the path strings) is unchanged; only the JS source surface migrated. Static gate `STC-ARCH-02` forbids re-introducing literal `/api/flights/...` strings.
## What's intentionally NOT here
@@ -29,18 +29,20 @@ No props.
- **State**:
- `system: SystemSettings | null` — loaded from
`GET /api/annotations/settings/system`. `null` until the GET
resolves; the panel does not render until then (`{system && (...)}`).
`GET endpoints.annotations.settingsSystem()` (= `/api/annotations/settings/system`).
`null` until the GET resolves; the panel does not render until
then (`{system && (...)}`).
- `dirs: DirectorySettings | null` — analogous, from
`GET /api/annotations/settings/directories`.
- `aircrafts: Aircraft[]` — from `GET /api/flights/aircrafts`.
`GET endpoints.annotations.settingsDirectories()` (= `/api/annotations/settings/directories`).
- `aircrafts: Aircraft[]` — from `GET endpoints.flights.aircrafts()`
(= `/api/flights/aircrafts`).
- `saving: boolean` — disables the two Save buttons during a PUT.
- **Bootstrap effect** (`useEffect([])`):
```ts
api.get<SystemSettings>('/api/annotations/settings/system').then(setSystem).catch(() => {})
api.get<DirectorySettings>('/api/annotations/settings/directories').then(setDirs).catch(() => {})
api.get<Aircraft[]>('/api/flights/aircrafts').then(setAircrafts).catch(() => {})
api.get<SystemSettings>(endpoints.annotations.settingsSystem()).then(setSystem).catch(() => {})
api.get<DirectorySettings>(endpoints.annotations.settingsDirectories()).then(setDirs).catch(() => {})
api.get<Aircraft[]>(endpoints.flights.aircrafts()).then(setAircrafts).catch(() => {})
```
Three independent calls, all silently swallowed on error. Empty UI
@@ -48,7 +50,7 @@ No props.
- **`saveSystem()`**:
1. Guard: `if (!system) return`.
2. `setSaving(true)`.
3. `await api.put('/api/annotations/settings/system', system)`.
3. `await api.put(endpoints.annotations.settingsSystem(), system)`.
4. `setSaving(false)`.
No optimistic update needed (the PUT body **is** the local state).
@@ -56,10 +58,10 @@ No props.
path is missing**: a thrown PUT leaves `saving: true` permanently
(no `try/finally`). Flag for Step 4.
- **`saveDirs()`** — analogous against
`PUT /api/annotations/settings/directories`. Same missing
`PUT endpoints.annotations.settingsDirectories()`. Same missing
`try/finally` issue.
- **`handleToggleDefault(a)`** — duplicate of the same handler in
`AdminPage`: `PATCH /api/flights/aircrafts/${a.id}` with
`AdminPage`: `PATCH endpoints.flights.aircraft(a.id)` with
`{ isDefault: !a.isDefault }` then optimistic local flip. Two copies
of the same logic in two pages — extract to a shared helper or to
`FlightContext` in Step 8 (the legacy WPF had a single
@@ -79,7 +81,7 @@ No props.
## Dependencies
- **Internal**:
- `../../api/client` — `api`.
- `../../api` (barrel) — `api`, `endpoints`. (Since AZ-485 / F4 + AZ-486 / F7.)
- `../../types` — `SystemSettings`, `DirectorySettings`, `Aircraft`.
- **External**: `react` (`useState`, `useEffect`),
`react-i18next` (`useTranslation`).
@@ -117,16 +119,16 @@ No props.
## External integrations
| Method | Path | Purpose |
| Method | Builder → Path | Purpose |
|---|---|---|
| `GET` | `/api/annotations/settings/system` | Load tenant config |
| `PUT` | `/api/annotations/settings/system` | Save tenant config |
| `GET` | `/api/annotations/settings/directories` | Load directory paths |
| `PUT` | `/api/annotations/settings/directories` | Save directory paths |
| `GET` | `/api/flights/aircrafts` | Load aircraft list |
| `PATCH` | `/api/flights/aircrafts/{id}` | Toggle `isDefault` |
| `GET` | `endpoints.annotations.settingsSystem()` → `/api/annotations/settings/system` | Load tenant config |
| `PUT` | `endpoints.annotations.settingsSystem()` → `/api/annotations/settings/system` | Save tenant config |
| `GET` | `endpoints.annotations.settingsDirectories()` → `/api/annotations/settings/directories` | Load directory paths |
| `PUT` | `endpoints.annotations.settingsDirectories()` → `/api/annotations/settings/directories` | Save directory paths |
| `GET` | `endpoints.flights.aircrafts()` → `/api/flights/aircrafts` | Load aircraft list |
| `PATCH` | `endpoints.flights.aircraft(id)` → `/api/flights/aircrafts/{id}` | Toggle `isDefault` |
Routed by `nginx.conf` to `annotations/` and `flights/` backends.
Path builders live in `src/api/endpoints.ts` (since AZ-486 / F7). Routed by `nginx.conf` to `annotations/` and `flights/` backends.
## Security
+46
View File
@@ -0,0 +1,46 @@
# Documentation Ripple Log — Cycle 1 (Phase B)
> Generated during Step 13 (Update Docs) of the autodev existing-code flow, cycle 1 (refactor-only).
> Task specs in scope: `AZ-485_phase_b_barrel_files.md`, `AZ-486_refactor_endpoint_builders.md` (both in `_docs/02_tasks/done/`).
## Scope analysis (Task Step 0)
Direct source files changed by Cycle 1 batches 9 + 10:
| Source file | Changed in | Touched module doc |
|---|---|---|
| `src/api/client.ts` | AZ-485 + AZ-486 | `modules/src__api__client.md` |
| `src/api/sse.ts` | AZ-485 | `modules/src__api__sse.md` |
| `src/api/endpoints.ts` (NEW) | AZ-486 | `modules/src__api__endpoints.md` (NEW) |
| `src/api/index.ts` (barrel) | AZ-485 + AZ-486 | covered in `components/01_api-transport/description.md` §2 |
| `src/auth/AuthContext.tsx` | AZ-486 | `modules/src__auth__AuthContext.md` |
| `src/components/FlightContext.tsx` | AZ-486 | `modules/src__components__FlightContext.md` |
| `src/components/DetectionClasses.tsx` | AZ-486 | `modules/src__components__DetectionClasses.md` |
| `src/features/admin/AdminPage.tsx` | AZ-486 | `modules/src__features__admin__AdminPage.md` |
| `src/features/settings/SettingsPage.tsx` | AZ-486 | `modules/src__features__settings__SettingsPage.md` |
| `src/features/dataset/DatasetPage.tsx` | AZ-486 | `modules/src__features__dataset__DatasetPage.md` |
| `src/features/flights/FlightsPage.tsx` | AZ-486 | `modules/src__features__flights.md` (group doc) |
| `src/features/annotations/{AnnotationsPage,AnnotationsSidebar,CanvasEditor,MediaList,VideoPlayer}.tsx` | AZ-486 | `modules/src__features__annotations.md` (group doc) |
System-level docs (`system-flows.md`, `data_model.md`, `architecture.md`): **not touched** — cycle 1 was a pure structural refactor (import paths + URL-literal centralisation). No flow diagrams, no entity shapes, no integration patterns changed.
Problem-level docs: **not touched** — cycle 1 introduced no new product acceptance criteria, no new input parameters, no new restrictions.
## Import-graph ripple (Task Step 0.5)
The reverse-dependency set of the changed files is **already captured in the direct list above**. Specifically:
- `src/api/index.ts` (barrel) is imported by every consumer module that uses `api`, `endpoints`, `createSSE`, `setToken`, `getToken`. After AZ-485 those imports moved to the barrel; after AZ-486 they additionally pulled in `endpoints`. The barrel itself has no separate module doc — its public surface is enumerated in `components/01_api-transport/description.md` §2.
- `src/api/endpoints.ts` is imported by `src/api/client.ts` (for the internal `refreshToken()` helper) and by every consumer module already in the direct list. No additional ripple.
- `src/api/client.ts` is imported by the consumer modules already in the direct list; no further ripple.
Therefore: **no additional doc was added to the refresh set by ripple analysis**. The direct file set is closed under the import graph.
## Tooling notes
- Ripple analysis was performed by reading `src/api/index.ts` and the changed files directly, plus the existing `_docs/02_document/components/01_api-transport/description.md` "Downstream consumers" enumeration. The repo has no `madge` / `depcruise` configured; this counts as the "directory-proximity + manual import inspection" fallback path from `document/workflows/task.md` Task Step 0.5 #6 — but with full coverage of the import graph because the changed file set is small.
- No static analyzer was used to discover indirect importers. None was needed: the consumer set of `src/api/index.ts` is small and already enumerated in `01_api-transport/description.md`.
## Outcome
All 12 affected module docs + 1 component doc + 1 NEW module doc updated in-place. Refresh set is complete.
@@ -0,0 +1,53 @@
# Implementation Report — Phase B Cycle 1 (Refactoring)
**Cycle**: Phase B, cycle 1 (`state.cycle = 1`)
**Date close**: 2026-05-11
**Epic**: AZ-447 (`01-testability-refactoring`)
**Findings closed**: F4 (Public API barrels) + F7 (Endpoint builders) — both from `_docs/02_document/architecture_compliance_baseline.md`
**Total complexity**: 10 pts (5 + 5)
**Verdict**: PASS
## Tasks
| Task | Spec | Batch | Commit | Verdict | AC Coverage |
|------|------|-------|--------|---------|-------------|
| AZ-485 — Public API barrels + STC-ARCH-01 | `_docs/02_tasks/done/AZ-485_refactor_public_api_barrels.md` | batch 9 | `23746ec` | PASS | 7 / 7 |
| AZ-486 — Endpoint builders + STC-ARCH-02 | `_docs/02_tasks/done/AZ-486_refactor_endpoint_builders.md` | batch 10 | `8a461a2` | PASS | 7 / 7 |
Batch reports: `_docs/03_implementation/batch_09_report.md`, `_docs/03_implementation/batch_10_report.md` (canonical per-batch source of truth — design decisions, modified-files inventory, AC test mapping).
## Architecture Outcome
After cycle 1, the `src/` codebase has two coupled static gates that lock in the architecture vision:
1. **`STC-ARCH-01`** (`scripts/check-arch-imports.mjs --mode=arch-imports`) — every cross-component import MUST go through the component's barrel (`src/<component>/index.ts`). Closes F4. One F3-pending exemption (`features/annotations/classColors`) documented in 5 places.
2. **`STC-ARCH-02`** (`scripts/check-arch-imports.mjs --mode=api-literals`) — no hardcoded `/api/<service>/<...>` literals in production source. The single source of truth is `src/api/endpoints.ts`, re-exported via the `01_api-transport` barrel. Closes F7. Exemptions: the contract owner (`endpoints.ts`) and `*.test.tsx?` files under `src/`.
The two gates are symmetric (single shared script, side-by-side `--mode` flags, identical fixture-driven test harness in `tests/architecture_imports.test.ts`). Adding a future STC-ARCH-03 / -04 follows the same pattern.
## Test Suite Delta
| Metric | End of Phase A (Step 7) | End of cycle 1 (Step 11) | Delta |
|--------|-------------------------|---------------------------|-------|
| Fast profile PASS | 163 | **209** | +46 |
| Fast profile SKIP | 13 | 13 | 0 |
| Fast profile FAIL | 0 | 0 | 0 |
| Static profile gates | 29 / 29 PASS | **31 / 31 PASS** | +2 (STC-ARCH-01, STC-ARCH-02) |
No regressions. All 46 new fast tests are additive — 4 new STC-ARCH-01 architecture cases (AZ-485), 6 new STC-ARCH-02 architecture cases (AZ-486), 36 new endpoint contract assertions (AZ-486).
## Code Review Trace
- Per-batch self-review: PASS (0 Critical / 0 High / 0 Medium / 0 Low on both batches).
- Cumulative review (K=3 trigger): not fired — cycle 1 had only 2 batches. Next cumulative review at the next 3-batch window close.
## Productivity Notes (Retro Input)
- **Single script, two modes** (Design Decision #1 in batch 10 report) replaced the obvious-but-wrong choice of forking `check-arch-imports.mjs` into a second script. Saved ~150 LOC of duplicated walker/comment-skip machinery and eliminated a drift surface.
- **All-quote-style regex** (`[`'"]/api/<service>/`) caught a class of regressions the spec's illustrative single-quote ripgrep would have missed. Locked in with 3 quote-style-specific test cases.
- **Resume of in-progress AZ-486 work** at the start of this session: the user's prior session left the working tree with most of AZ-486 done but unrecorded. The autodev orchestrator detected the state/working-tree disagreement and surfaced it as a Choose block before continuing — this is exactly what the state-reconciliation rule in `state.md` is for.
## Next
Auto-chain → Step 12 (Test-Spec Sync, `test-spec/SKILL.md` cycle-update mode).
@@ -0,0 +1,49 @@
# Test Run Report — Phase B Cycle 1 (Step 11)
**Date**: 2026-05-11
**Mode**: functional
**Runner**: `scripts/run-tests.sh` (default profiles: static + fast; e2e env-blocked, see Step 7 report)
**Verdict**: PASS_WITH_DOCUMENTED_GATE
**Handoff**: re-uses the suite run performed under Step 10 batch 10 (implement skill Step 16: avoid duplicate full runs when next flow step is Run Tests). Source state at run-time === source state at commit `8a461a2`.
## Profile Outcomes
| Profile | Status | Counts | Wall-clock | Report file |
|---------|--------|--------|------------|-------------|
| static | PASS | **31 / 31** including new `STC-ARCH-02` | ~14 s | `test-output/summary.csv` |
| fast | PASS | 28 files / **209 PASS / 13 SKIP / 0 FAIL** | ~22.6 s | `test-output/fast-report.xml` |
| e2e | env-blocked (deferred — same registry-access block as Step 7) | n/a | n/a | n/a |
## Delta vs Step 7 baseline
- Fast: 163 / 13 → 209 / 13 (+46 over Phase A close, +42 over end-of-batch-9):
- +4 STC-ARCH-01 architecture tests (added in batch 9 / AZ-485)
- +36 STC-ARCH-02 contract assertions in `src/api/endpoints.test.ts` (this cycle / AZ-486)
- +6 STC-ARCH-02 architecture tests in `tests/architecture_imports.test.ts` (this cycle / AZ-486)
- Static: 29 / 29 → 31 / 31 (+2 new gates: `STC-ARCH-01` AZ-485, `STC-ARCH-02` AZ-486)
- Skip count unchanged at 13 — no new skips introduced this cycle.
## System-Under-Test Reality Gate
PASS (same shape as Step 7):
- `_docs/00_problem/input_data/expected_results/results_report.md` still exists; `_docs/02_document/tests/traceability-matrix.md` still maps every AC. No internal product module was faked, monkeypatched, or replaced with a deterministic fallback by this cycle's batches — verified by self-review for batch 9 and batch 10.
- The refactor surface this cycle (`endpoints.*` + STC-ARCH-02) is pure rewrite-of-string-literals through a typed accessor object; no behavior change, no external system replaced.
- CSV report inspected — all 31 static rows PASS; fast profile rolled-up PASS row points at the JUnit XML.
## Skipped Tests — Same 13 As Step 7, Still Legitimate
The 13 skips are byte-for-byte the same set documented in `test_run_report.md`'s "Skipped Tests — All 13 Accepted as Legitimate" section. None of this cycle's two tasks (AZ-485, AZ-486) touched any of the skip conditions: F4 (barrels) and F7 (URL builders) are pure mechanical refactors of the import path and string-literal layer; they do not change which features ship to production. The user-approved acceptance from Step 7 still applies.
## Environment Block — e2e Profile
Same as Step 7: registry-access block on `azaion/{admin,flights,annotations,detect,loader,resource}:test` images. F4 / F7 changes do not affect Docker images or compose configuration (no Dockerfile or compose edits this cycle), so re-running e2e would not produce different results. Defer to the merge-lane CI per Step 7's user-approved option A.
## Outcome
Step 11 **passes**. Auto-chain to Step 12 (Test-Spec Sync).
## Open Items
Unchanged from Step 7:
- F-CUM-5 production-drift backlog — Phase B / Step 9 work continues into cycle 2.
- F-CUM-4 long-running-soak Playwright config tag — recommended fold-in to merge-lane config.
+19 -48
View File
@@ -2,57 +2,28 @@
## Current Step
flow: existing-code
step: 10
name: Implement
status: in_progress
step: 16
name: Deploy
status: not_started
sub_step:
phase: 7
name: batch-10-cycle1-az486-complete
detail: "AZ-486 (F7 endpoint builders + STC-ARCH-02) implemented + reviewed; batch_10_report saved; archive done; awaiting user approval to commit and then auto-chain to Step 11"
phase: 0
name: awaiting-invocation
detail: ""
retry_count: 0
cycle: 1
tracker: jira
step_4_5_glossary_vision: confirmed
step_2_baseline_routing: per-finding-recommended (option A)
step_3_results_report_authoring: agent (option A)
step_3_ac_gap_handling: rollback-to-6c (option A)
## Notes
- Phase A baseline cycle. Step 1 (Document) complete; see
`_docs/02_document/state.json`, `FINAL_report.md`, `architecture.md`,
`glossary.md`, plus `_docs/01_solution/solution.md` and
`_docs/00_problem/{problem,acceptance_criteria,restrictions,security_approach}.md`.
- Implement-skill batch reports at
`_docs/03_implementation/batch_0{1..9}_report.md` + `batch_10_report.md`
(batch 09 = AZ-485 cycle-1 batch-1; batch 10 = AZ-486 cycle-1 batch-2).
- Cumulative reviews PASS_WITH_WARNINGS at
`_docs/03_implementation/cumulative_review_batches_01-03_report.md`,
`_docs/03_implementation/cumulative_review_batches_04-06_cycle1_report.md`,
`_docs/03_implementation/cumulative_review_batches_07-08_cycle1_report.md`
(cycle close — Phase A wrap, no batch 9).
- Phase B cycle 1 closed (2 batches, both AC + static + fast green):
- AZ-485 (F4 — Public API barrels + STC-ARCH-01, 5 pts) — committed 23746ec
- AZ-486 (F7 — Endpoint builders + STC-ARCH-02, 5 pts) — batch 10 done,
uncommitted, awaiting user approval.
- Step 10 (Implement) batch 10 (AZ-486) done:
- 2 new files (`src/api/endpoints.ts` 25 builders, `src/api/endpoints.test.ts` 36 cases).
- 1 barrel update (`src/api/index.ts` re-exports `endpoints`).
- 13 production files migrated to `endpoints.*` (admin, annotations,
flights, settings, dataset, auth, client, FlightContext,
DetectionClasses, CanvasEditor, VideoPlayer, MediaList,
AnnotationsSidebar, AnnotationsPage).
- `scripts/check-arch-imports.mjs` extended with `--mode=api-literals`
(STC-ARCH-02) alongside `--mode=arch-imports` (STC-ARCH-01);
`scripts/run-tests.sh` wires both modes.
- `tests/architecture_imports.test.ts` extended with 6 STC-ARCH-02 cases
(single/double/template-literal fail paths, *.test.* exemption,
line-comment skip, migrated-codebase pass).
- `_docs/02_document/module-layout.md` `01_api-transport` Public API now
lists `endpoints`; Verification Needed item #3a records F7 resolution.
- Test counts: 167 → 209 PASS / 13 SKIP / 0 FAIL (+42).
- Static: 31 / 31 PASS including new STC-ARCH-02.
- Cumulative code review (K=3): no trigger — Phase B cycle 1 had only 2
batches (9, 10).
- Next on commit of batch 10: auto-chain to Step 11 (Run Tests) via
`test-run/SKILL.md`. Final cycle-1 implementation report (`implementation_report_phase_b_cycle1.md`) is written at that point per
implement skill Step 16 handoff rule.
- Cycle 1 (Phase B) refactor cycle: F4 (AZ-485, `23746ec`) and F7
(AZ-486, `8a461a2`) closed.
- Step 13 (Update Docs) DONE in working tree, NOT YET COMMITTED. User
skipped the commit gate; 12 module docs + new `src__api__endpoints.md`
+ `01_api-transport/description.md` + ripple log + 2 cycle reports
pending review. Commit subject for when ready:
`[AZ-485] [AZ-486] Cycle 1 docs refresh (Step 13)`.
- Step 14 (Security Audit) SKIPPED — cycle 1 was structural refactor
only; URL strings character-identical to pre-refactor (pinned by
`endpoints.test.ts`), no new auth / wire / permission surface.
- Step 15 (Performance Test) SKIPPED — same rationale; no
latency-/throughput-relevant code paths changed.
- Next: Step 16 (Deploy) — destructive; awaiting user decision.