mirror of
https://github.com/azaion/ui.git
synced 2026-06-21 11:51:10 +00:00
[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>
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
# 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
|
||||
|
||||
```ts
|
||||
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):
|
||||
|
||||
```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(() => {})
|
||||
```
|
||||
|
||||
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/client` — `api`.
|
||||
- `../../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 `/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 `PATCH`es. 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.
|
||||
Reference in New Issue
Block a user