Files
ui/_docs/02_document/modules/src__api__endpoints.md
Oleksandr Bezdieniezhnykh 09449bda2c
ci/woodpecker/push/build-arm Pipeline failed
[AZ-510][AZ-511][AZ-512][AZ-513] Cycle 3 Steps 12-15 + admin prereq
Wrap up cycle 3 across the autodev existing-code Phase B steps that
follow Implement (Steps 12-15), plus the cross-workspace prerequisite
ticket filed for AZ-512.

Step 12 - Test-Spec Sync:
- Un-quarantine FT-P-01 in traceability-matrix (closed by AZ-510)
- Add AZ-510 chained /users/me failure-path test reference under AC-23
- Note AZ-512 deferral status under O9 (P12 Phase B target)

Step 13 - Update Docs (task mode):
- Refresh src__auth__AuthContext module doc with AZ-510 wire shape
  (POST refresh + chained /users/me + bootstrapInflight guard)
- Add usersMe() to src__api__endpoints module doc + consumer note
- Rename src__features__annotations__classColors module doc to
  src__class-colors__classColors (matches AZ-511 git mv); refresh header
- Refresh src__components__DetectionClasses + src__features__annotations
  module group doc for the new class-colors barrel import path
- Update components/11_class-colors Module Inventory to point at the
  renamed module doc filename
- Rewrite system-flows.md Flow F2 (Bearer auto-refresh) with the AZ-510
  POST + chained /users/me sequence; close Finding B3 references
- Generate ripple_log_cycle3 documenting all changed source files,
  their reverse-dependency search results, and the docs touched

Step 14 - Security Audit (cycle-3 delta):
- Resume mode against cycle-2 baseline; cycle-2 artifacts untouched
- Re-run bun audit on both roots: clean (cycle-2 inline fix held)
- Re-rate OWASP A06: FAIL -> PASS; A07: PASS_WITH_KNOWN -> PASS (B3
  closed by AZ-510)
- New finding F-SAST-CY3-1 (LOW): __resetBootstrapInflightForTests
  exposed via src/auth public barrel; defer to hygiene cycle
- Verdict: FAIL -> PASS_WITH_WARNINGS; one HIGH (F-SAST-1
  mission-planner git-history key, unchanged) remains
- Add amendment banner to cycle-2 security_report.md

Step 15 - Performance Test:
- Static profile NFT-PERF-01 PASS (290 575 B gzipped vs 2 MB budget;
  ~14% of budget; no regression from AZ-510 surface additions)
- E2E profile SKIP (Playwright perf project still pending AZ-457..AZ-482);
  legitimate skip per test-run skill, gap acknowledged in report
- AZ-510 200ms p95 chain NFR verified at spec level only - no CI gate
  yet (covered by future AZ-457..AZ-482 work)

Cross-workspace prerequisite (AZ-513 just filed):
- Updated _docs/_process_leftovers/2026-05-13_az-512-admin-classes-prereq.md
  to reflect AZ-513 filing on admin/ workspace (parent epic AZ-509,
  Blocks link to AZ-512). Companion task spec added in admin/ repo
  (separate commit there, owned by admin/ workspace).

State file: advanced to Step 16 (Deploy) per autodev existing-code flow.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-13 03:58:21 +03:00

9.4 KiB

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), this module enforces "no hardcoded API path literals in src/" as a build-time invariant rather than a code-review aspiration.

Public interface

export const endpoints = {
  admin: {
    authRefresh: () => string
    authLogin:   () => string
    authLogout:  () => string
    users:       () => string
    user:        (id: string) => string
    usersMe:     () => string  // added 2026-05-13 by AZ-510 — chained read after POST refresh
    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.tsxauthRefresh, authLogin, authLogout, usersMe (added by AZ-510).
  • src/components/FlightContext.tsxflights.collection, flights.flight, annotations.settingsUser.
  • src/components/DetectionClasses.tsxadmin.classes, admin.class.
  • src/features/admin/AdminPage.tsxadmin.users, admin.user.
  • src/features/annotations/AnnotationsPage.tsx — annotation CRUD endpoints, detect.media.
  • src/features/annotations/AnnotationsSidebar.tsxannotations.annotationEvents (SSE), bulk-status, dataset endpoints.
  • src/features/annotations/CanvasEditor.tsxannotations.annotationImage, annotations.annotationThumbnail.
  • src/features/annotations/MediaList.tsxannotations.media, annotations.mediaFile, annotations.mediaItem, annotations.mediaBatch.
  • src/features/annotations/VideoPlayer.tsxannotations.mediaFile.
  • src/features/dataset/DatasetPage.tsxannotations.dataset* family, annotations.classes, annotations.annotationImage.
  • src/features/flights/FlightsPage.tsx — full flights.* surface + annotations.settingsUser.
  • src/features/settings/SettingsPage.tsxannotations.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 undersrc/`.
  • 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.