admin-ui¶
Purpose¶
admin-ui is the Phase 1 internal admin SPA. Vite + React + Mantine. Talks only to services/admin-api over same-origin /api/*; the SPA never holds a platform JWT, and all auth state is the httpOnly session cookie issued by the BFF.
Phase 1 shipped a read-only dashboard + per-organisation drill-down. Phases 2-3 have since landed organisation / user / role / product CRUD, the audit-log viewer, the MFA flows, the per-(org, product) integration page (credentials + Postman + scopes), and the workflow definition editor (list / detail / draft editor + canvas-based step authoring). The workflow editor's Assignment card lets operators reassign a draft to a different (org, product) slot without re-creating it (or promote it to product-wide default via the Default for all organisations checkbox — only cross-tenant admins, refused if another active default already exists for the product). See runbooks/workflow-assignment.md for the operator playbook + the underlying version-renumber semantics.
Recent additions on top of the Phase 2-3 baseline:
- Dashboard auto-refresh — the four top-line tiles (
useHealth,useVolume,useAiReview,useHumanReview) poll on a 30-secondrefetchInterval. A "Refreshes in Xs" countdown lives next to the date-range selector; metric numbers ease between values via auseCountUptween (AnimatedNumber), and the Mantine BarChart + DonutChart re-enable Recharts animations via thebarProps/piePropsescape hatch. - Workflow editor visual aids — start step renders with a green border + light-green fill, terminal kinds (
output/emit_final) get red border + red fill, under-configured steps prefix their label with⚠. The side panel exposes a "Next step (on_complete)" picker for linear-flow kinds so operators don't need to learn React Flow's edge-drag affordance. - Workflow detail flow chart —
/workflows/:idrenders a read-only React Flow viewer with the same colours + badges as the editor; nodes are draggable for legibility (positions don't persist). Each node shows per-kind config inline (question-set name, image patterns + counts, decision branches, output fields). - Send for AI analysis step — the editor palette adds a
run_ai_reviewstep (server-driven kind). - Image collection step — accepts an
optionalflag ondermoscopic_count/macroscopic_count; the side panel surfaces explicit per-type "{type} images are optional" checkboxes and renders min/max bounds with a clear-constraints affordance. - Users page surfaces clinical patients — patients ingested via case-creation appear in a dedicated section under the platform-users table, with a Promote button that creates a
user_type='patient'platform user linked back to the clinical record viapatientAccount.clinicalPatientId. - 'User' user-type — third radio option on Create User alongside Practitioner and Patient; carries capabilities purely via role membership (no profile, no patient account).
- Patient duplicate review (
/users/duplicates) — admin queue listing pendingPatientMergeCandidaterows with provisional + ranked candidate PII, the features that triggered the flag, plus Merge / "Not a duplicate" actions (each behind a confirmation modal). - Roles list cross-org —
/rolesdefaults to the cross-org view with an Organisation column; the dropdown filter is clearable. - Navbar grouping — items grouped into Day-to-day / Configuration / People + permissions / Platform admin with visible dividers; parent items stay highlighted on sub-route pages (e.g.
/workflows/abc-123keeps "Workflows" active). - Header colour updated to brand dark blue
#110E4F. - Question-set translations — the question-set edit page exposes a per-field translations editor: each translatable string (set name, description, question prompt, option label) renders a default-English input plus a collapsible "Translations" section listing the platform's supported locales (
fr,de,es,pt-BR,it,nl,et). The sharedLocalizedStringInputcomponent lives inapps/admin-ui/src/components/localized-string-input.tsx; translations normalise to plain strings on save when no variants are present (back-compat with rows authored pre-i18n). Seeservices/orchestrator.md#i18nfor the wire shape +?lang=flag.
Tech stack¶
- Build: Vite 5
- Framework: React 18, TypeScript (
tsconfigextends@sa-platform/tsconfig/base.json) - UI: Mantine 7 (
AppShellprovides the sidebar + header layout) - Routing: React Router 6
- Data fetching: TanStack Query 5 (single source of cache; no Redux)
- Charts: Mantine Charts (recharts wrapper) — used in later phases
- Tests: Vitest + React Testing Library; Playwright for one e2e smoke
Key routes¶
| Path | Purpose |
|---|---|
/login |
Email + password sign-in (two-step when MFA enrolled — see below) |
/ |
Dashboard with health / volume / AI / HR cards |
/orgs/:orgId |
Per-organisation drill-down |
/orgs |
Organisations list (Phase 2) |
/orgs/new, /orgs/:orgId/edit |
Create / edit organisation (Phase 2) |
/admin-users, /admin-users/new, /admin-users/:id/edit |
Admin user CRUD (Phase 3a) |
/setup/:token |
Public admin-user activation page (Phase 3a) |
/users |
Platform users list + clinical-patients section + promote action |
/users/new |
Create platform user (clinician / patient / user types) |
/users/:id/edit |
Edit / deactivate / reset platform user |
/users/duplicates |
Patient duplicate review queue (admin merge + dismissal) |
/audit |
Audit-log viewer (admin-only) |
/roles |
Per-org roles list (admin-only) |
/roles/new |
Create role (admin-only) |
/roles/:id/edit |
Edit role + delete (admin-only) |
/products |
Per-org products list (admin-only) |
/products/new |
Create product (admin-only) |
/products/:id/edit |
Edit product + soft-delete (admin-only) |
/orgs/:orgId/products |
Org's product assignments + workflow status column |
/orgs/:orgId/products/:productId/integration |
Per-(org, product) integration view — credentials, Postman, etc. |
/workflows |
Workflow list (filterable by org / product / status) |
/workflows/new |
Create draft workflow (accepts ?org_id=…&product_id=… prefill) |
/workflows/:id |
Read-only detail (publish / archive / clone / new-version) |
/workflows/:id/edit |
Editor — canvas + step palette + Assignment card (draft only) |
/settings |
Self-serve admin password change (Phase 3a) |
Auth boundary¶
<RequireSession> wraps every authenticated route. It calls useMe() (GET /api/me via TanStack Query, retry: false) and:
- shows a centred loader while the request is in flight,
- redirects to
/loginon a 401 / network error / no data, - renders the children when the response is
200.
useLogout() POSTs /api/auth/logout, clears the TanStack Query cache, and forces a hard navigation to /login so a previously-fetched /api/me payload can't survive the sign-out.
Login + MFA¶
/login is a two-stage form:
- Password stage — POSTs
/api/auth/login. A response that carriesusermeans the session cookie is set and we navigate to/. A response that carriesmfaToken + factorsmeans the user has at least one MFA factor enrolled (or the platform requires it), and we switch to stage 2. - MFA stage — the page renders whichever factor buttons the server listed. The passkey button calls
startAuthenticationfrom@simplewebauthn/browserand POSTs the assertion to/api/auth/mfa/passkey/verify. The TOTP form POSTs the 6-digit code to/api/auth/mfa/totp. Either resolves to the same{ user }payload and finalises the session.
MFA enrolment lives on the Settings page (/settings). Users can enrol passkeys (Touch ID / Face ID / hardware keys via @simplewebauthn/browser), set up TOTP (QR + manual secret + 6-digit confirmation), and admins can flip the platform-wide "Require MFA for all admin users" toggle.
Degraded-state handling¶
Each metric card runs its own TanStack Query, so one failing upstream doesn't blank the others — the affected card shows "Stats unavailable" and the page banner aggregates degradedFor across all sources.
Where to learn more¶
- Design spec
- Implementation plan
- Source:
apps/admin-ui/(in this repo)