Skip to content

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-second refetchInterval. A "Refreshes in Xs" countdown lives next to the date-range selector; metric numbers ease between values via a useCountUp tween (AnimatedNumber), and the Mantine BarChart + DonutChart re-enable Recharts animations via the barProps / pieProps escape 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/:id renders 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_review step (server-driven kind).
  • Image collection step — accepts an optional flag on dermoscopic_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 via patientAccount.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 pending PatientMergeCandidate rows 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/roles defaults 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-123 keeps "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 shared LocalizedStringInput component lives in apps/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). See services/orchestrator.md#i18n for the wire shape + ?lang= flag.

Tech stack

  • Build: Vite 5
  • Framework: React 18, TypeScript (tsconfig extends @sa-platform/tsconfig/base.json)
  • UI: Mantine 7 (AppShell provides 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 /login on 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:

  1. Password stage — POSTs /api/auth/login. A response that carries user means the session cookie is set and we navigate to /. A response that carries mfaToken + factors means the user has at least one MFA factor enrolled (or the platform requires it), and we switch to stage 2.
  2. MFA stage — the page renders whichever factor buttons the server listed. The passkey button calls startAuthentication from @simplewebauthn/browser and 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