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>
11 KiB
Module: src/features/admin/AdminPage.tsx
Source:
src/features/admin/AdminPage.tsx(209 lines) Topo batch: B4 (depends on B3:api/client,components/ConfirmDialog,types/index)
Purpose
The administrator console of the SPA. Bundles four management surfaces
into a single page: detection-classes catalogue (CRUD-lite),
AI-recognition tuning form, GPS-device endpoint config, user
list (CRUD-lite), and aircraft default-selector. Replaces the WPF
AdminWindow.xaml cluster (_docs/legacy/wpf-era.md §§4, 5).
Behind the /admin route, gated by Header's ADM permission
filter (a route-level permission re-check is the responsibility of the
admin/ service — Header gating is UI-only; see Security below).
Public interface
export default function AdminPage(): JSX.Element
No props. Reads everything via api/client and local state.
Internal logic
-
State:
classes: DetectionClass[]— the detection-class table.aircrafts: Aircraft[]— the aircraft list withisDefaultflag.users: User[]— the user table.newClass: { name; shortName; color; maxSizeM }— staging buffer for the "add detection class" form (initial:{ name: '', shortName: '', color: '#FF0000', maxSizeM: 7 }).newUser: { name; email; password; role }— staging buffer for "add user" (initial:{ name: '', email: '', password: '', role: 'Annotator' }).deactivateId: string | null— drives theConfirmDialog's open state for user deactivation.
-
Bootstrap effect (
useEffect([])— runs once at mount):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, no error UI, no loading state — empty arrays render as empty tables. Flag for Step 4 against the user-feedback patterns in
_docs/ui_design/README.md. -
handleAddClass():- Guard:
if (!newClass.name) return. await api.post(endpoints.admin.classes(), newClass)(=/api/admin/classes).- Refetch via
api.get(endpoints.annotations.classes())— note the read path is the publicannotations/endpoint (/api/annotations/classes), while the write path is theadmin/endpoint (/api/admin/classes). Architectural caveat: two different services own the same logical entity. Document inarchitecture.md§integration-points (Step 3a). - Reset
newClassto its initial values. No error path — a failed POST throws (becauseclient.tsthrows on non-2xx); the throw is uncaught and reaches React's error boundary (none configured). Flag.
- Guard:
-
handleDeleteClass(id): optimistic local update —await api.delete(endpoints.admin.class(id))(=/api/admin/classes/${id}) thensetClasses(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.mdconfirmation-dialog spec. -
handleAddUser()— analogous tohandleAddClassagainstPOST endpoints.admin.users()andGET endpoints.admin.users()(both →/api/admin/users). Guards onemail && password. -
handleDeactivate()— fired from the ConfirmDialog confirm:PATCH endpoints.admin.user(deactivateId)(=/api/admin/users/${deactivateId}) with{ isActive: false }.- Optimistic local update: marks the row inactive.
- Closes the dialog (
setDeactivateId(null)). No "reactivate" path — onceisActive: false, the row only renders the badge and no Deactivate button. Verify withadmin/service: is reactivation an admin task or out of scope?
-
handleToggleDefault(a)—PATCH endpoints.flights.aircraft(a.id)(=/api/flights/aircrafts/${a.id}) with{ isDefault: !a.isDefault }, then optimistic local flip. Note this allows multipleisDefault: trueaircraft 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 settings form, users table + add row. The AI and GPS forms havedefaultValueonly — there is no state, noSavehandler wired up. The buttons render but do nothing. Flag. - Right column (
w-[280px]): aircraft list with star toggle. <ConfirmDialog>mounted at the end, controlled bydeactivateId.
- Left column (
Dependencies
- Internal:
../../api(barrel) —api,endpoints. (Since AZ-485 / F4 + AZ-486 / F7.)../../components/ConfirmDialog— for user deactivation.../../types—DetectionClass,Aircraft,User.
- External:
react(useState,useEffect),react-i18next(useTranslation).
Consumers (intra-repo)
src/App.tsx— mounted at the/adminroute inside the protected tree.
Data models
Stateful local copies of DetectionClass[], Aircraft[], User[].
The newClass and newUser buffers are anonymous shapes —
intentionally narrower than DetectionClass / User because the
backend assigns id and other server-managed fields.
Configuration
- i18n keys consumed:
admin.classes,admin.aiSettings,admin.gpsSettings,admin.users,admin.aircrafts,admin.deactivate,common.save. (Confirmed present insrc/i18n/en.jsonadmin/common groups.) Plenty of hardcoded English strings — placeholders ("Name", "Email", "Password"), table headers (#,Name,Color,Email,Role,Status), role options (Annotator,Admin,Viewer), the GPS protocol options (TCP,UDP), the AI tuning labels ("Frame Period Recognition", etc.), and the deactivation confirmation message. Step 4 candidate. - Hardcoded defaults:
maxSizeM: 7for new detection classes.color: '#FF0000'for new detection classes.Frame Period Recognition: 5,Frame Recognition Seconds: 1,Probability Threshold: 0.5(step: 0.05,min: 0,max: 1).Device Address: 192.168.1.100,Port: 5535, default protocolTCP. Hardcoded internal IP is a smell; should come fromsystem_settingsor be a placeholder. Flag for Step 4 / Step 6 (security_approach.md).
- Tailwind tokens:
bg-az-panel,bg-az-bg,bg-az-orange,bg-az-blue/20,bg-az-green/20,text-az-{text,muted,red,green, blue,orange},border-az-border. Defined insrc/index.css.
External integrations
| Method | Builder → Path | Purpose |
|---|---|---|
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) |
Path builders live in src/api/endpoints.ts (since AZ-486 / F7). Routed by nginx.conf to admin/, annotations/, flights/ backends.
Security
/adminis access-controlled by Header'sADMpermission filter AND by every backend endpoint (each/api/admin/*is the authority). The page itself does not callhasPermission— a user who deep-links to/adminwould render the page but every fetch would 401/403. Acceptable for production; flag a UX improvement (server-error → friendly redirect) for Step 8.- Password input has
type="password"— masked. The new-user password is held in plaintext component state until the POST resolves. Acceptable; cleared bysetNewUser({...})after success. color: '#FF0000'is a hex string — when posted to/api/admin/classes, the backend must validate. UI-side validation (regex /<input type="color">) is enforced because the input is a color-picker.- No CSRF — relies on the bearer-token scheme. Same posture as the
rest of the SPA (see
client.tsSecurity section). - No row-level access checks visible to the UI: the user list
shows every row the backend returns; if the
admin/service ever filtered by tenant, the UI would not need changes.
Tests
None.
Notes / open questions
- Detection-class read/write split between
annotations/andadmin/is unusual. Verify with the suite ADRs whether/api/annotations/classesand/api/admin/classesare the same underlying entity. If yes, the split is a legacy artifact; if no, the UI is conflating two collections. Step 3a / Step 4 verification. - AI settings & GPS settings forms are wired to nothing — every
<input>usesdefaultValue(uncontrolled), there is no submit handler, and the Save button does nothing. Either:- The legacy WPF AI/GPS settings have not been ported yet (most
likely —
_docs/legacy/wpf-era.md§"What is intentionally NOT being ported" might apply), OR - The backend endpoint exists but the wiring was lost during the port. Flag prominently in Step 4 problem-extraction. Probably a missing feature, not a bug.
- The legacy WPF AI/GPS settings have not been ported yet (most
likely —
handleDeleteClasshas no confirmation — UX inconsistency with user deactivation. Consider unifying viaConfirmDialog. Step 4.- Aircraft default-toggle race: clicking two aircraft in quick
succession will fire two parallel
PATCHes. The backend may end up with bothisDefault: trueif it does not enforce exclusivity at the persistence layer. Flag for Step 6 problem-extraction. Trust the backend; do not add UI debouncing. - The "Active" / "Inactive" badge text is hardcoded English even
though the surrounding column header (
Status) is too. Either localize all of them or accept the inconsistency. Step 4. u.isActiveas the only mutable user field — no rename, no password reset, no role change. Document the gap; may be a feature cycle item for Phase B.newClass.shortNameis collected but not displayed in the table — the table only showsid, name, color, ×. TheshortNamelives only in the staging buffer and is sent to the backend. Verify the backend stores it.- Hardcoded GPS device address
192.168.1.100is a non-routable RFC1918 default that should not ship with the production bundle. Step 4 flag.