Files
ui/_docs/02_document/modules/src__features__admin__AdminPage.md
T
Oleksandr Bezdieniezhnykh 510df68bcf [AZ-447] autodev Steps 1-4 baseline: docs, tests, refactor specs
Captures the full output of autodev existing-code Phase A through
Step 4 (Code Testability Revision) for the Azaion UI workspace:

- Step 1 Document: _docs/02_document/ (FINAL_report, architecture,
  glossary, components/, modules/, diagrams/, system-flows,
  module-layout) plus _docs/00_problem/ + _docs/01_solution/ +
  _docs/legacy/ + _docs/how_to_test + README.
- Step 2 Architecture Baseline: architecture_compliance_baseline.md.
- Step 3 Test Spec: _docs/02_document/tests/ (environment,
  test-data, blackbox/performance/resilience/security/
  resource-limit tests, traceability-matrix), enum_spec_snapshot,
  expected_results/results_report.md (98 rows), plus the
  run-tests.sh + run-performance-tests.sh runners.
- Step 4 Code Testability Revision: 01-testability-refactoring/
  run dir (list-of-changes C01-C07, deferred_to_refactor,
  analysis/research_findings + refactoring_roadmap) and the 7
  child task specs AZ-448..AZ-454 under _docs/02_tasks/todo/
  plus _dependencies_table.md.
- _docs/_autodev_state.md pins the cursor at Step 4 / refactor
  Phase 4 entry so /autodev resumes cleanly.

Epic AZ-447 (UI testability gates) tracks the 7 child tasks that
will land in subsequent commits.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 00:38:49 +03:00

10 KiB
Raw Blame History

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 with isDefault flag.
    • 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 the ConfirmDialog's open state for user deactivation.
  • Bootstrap effect (useEffect([]) — runs once at mount):

    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(() => {})
    

    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():

    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: two different services own the same logical entity. Document in architecture.md §integration-points (Step 3a).
    4. Reset newClass to its initial values. No error path — a failed POST throws (because client.ts throws on 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 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.

  • handleDeactivate() — fired from the ConfirmDialog confirm:

    1. PATCH /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).

  • 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 have defaultValue only — there is no state, no Save handler wired up. The buttons render but do nothing. Flag.
    • Right column (w-[280px]): aircraft list with star toggle.
    • <ConfirmDialog> mounted at the end, controlled by deactivateId.

Dependencies

  • Internal:
    • ../../api/clientapi.
    • ../../components/ConfirmDialog — for user deactivation.
    • ../../typesDetectionClass, Aircraft, User.
  • External: react (useState, useEffect), react-i18next (useTranslation).

Consumers (intra-repo)

  • src/App.tsx — mounted at the /admin route 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 in src/i18n/en.json admin/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: 7 for 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 protocol TCP. Hardcoded internal IP is a smell; should come from system_settings or 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 in src/index.css.

External integrations

Method 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)

Routed by nginx.conf to admin/, annotations/, flights/ backends.

Security

  • /admin is access-controlled by Header's ADM permission filter AND by every backend endpoint (each /api/admin/* is the authority). The page itself does not call hasPermission — a user who deep-links to /admin would 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 by setNewUser({...}) 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.ts Security 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/ and admin/ is unusual. Verify with the suite ADRs whether /api/annotations/classes and /api/admin/classes are 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> uses defaultValue (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.
  • handleDeleteClass has no confirmation — UX inconsistency with user deactivation. Consider unifying via ConfirmDialog. Step 4.
  • Aircraft default-toggle race: clicking two aircraft in quick succession will fire two parallel PATCHes. The backend may end up with both isDefault: true if 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.isActive as 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.shortName is collected but not displayed in the table — the table only shows id, name, color, ×. The shortName lives only in the staging buffer and is sent to the backend. Verify the backend stores it.
  • Hardcoded GPS device address 192.168.1.100 is a non-routable RFC1918 default that should not ship with the production bundle. Step 4 flag.