mirror of
https://github.com/azaion/ui.git
synced 2026-06-21 12:01:11 +00:00
[AZ-485] [AZ-486] Cycle 1 docs refresh (Step 13)
ci/woodpecker/push/build-arm Pipeline was successful
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:
@@ -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.
|
||||
@@ -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 0–1.
|
||||
4. Pick the active detection class (1–9 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.1–10×), pan (held in `pan` state — there is no Ctrl+drag pan code, only an unused `pan` state — see Findings), 8-handle resize, multi-select, draw-on-Ctrl-mousedown. Renders detections + a "time-window" preview of *other* annotations during video playback. |
|
||||
| `AnnotationsPage.tsx` | page | Orchestrator: 3-panel layout via `useResizablePanel` (left 250 / right 200, min/max bounds). Owns `selectedMedia`, `annotations`, `detections`, `selectedClassNum`, `photoMode`, `currentTime`. Wires `MediaList` → `CanvasEditor` ↔ `VideoPlayer` ↔ `AnnotationsSidebar`. Handles the `Save` POST + local-fallback. Builds the YOLO + PNG download. |
|
||||
|
||||
@@ -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 0–1. 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 / 200–400), `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 / 200–400), `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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user