Skip to main content

Admin Dashboard — Design of Record (M27, #485)

These boards are the Al-approved (2026-08-15) design of record for milestone M27 — Admin Dashboard. The UI build slice (#486) implements these patterns and nothing beyond them.

Canonical Penpot study

The five patterns are reproduced as a version-controlled Penpot file on the self-hosted server:

Known limitation: the deployment's Penpot exporter service (headless Chromium) is unavailable (/api/export → 400), so PNG exports could not be produced from Penpot. The PNGs below are the approved renders of the same boards and serve as the previews of record.

The five patterns

#PatternPreview
1Admin shell — nav (7 resources + Overview, counts from each resource's total), read-only banner, identity + role pill, self sign-outAdmin shell
2Resource list (Users) — only whitelisted sort/search/filter controls; view affordance per rowResource list
3Record detail (User) — read-only fields + related panels cross-linked by user_id; actions limited to Copy ID / View raw JSONRecord detail
4Overview — resource total tiles, grants-by-role, recent sessions; no fabricated metricsOverview
5JSON inspector — the redaction contract made visibleJSON inspector

All five at once: reference/admin-all-boards.png. Full layout source: reference/admin-design-study.html.

The hard rule: no fake affordances

Every control in these frames maps 1:1 to what the M27-7 read-only resource protocol (apps/api/src/bloom/domain/admin_resources.py, apps/api/src/bloom/persistence/admin_resources.py) actually supports. The admin surface is read-only in this slice — no create/edit/delete/bulk controls exist anywhere, because the backend exposes no mutation routes.

The frames are bound to exactly this whitelist:

ResourceFiltersSearchSortable columns
usershas_grant, created_since, created_untilemail, nameemail, created_at
admin_grantsrolegranted_at, role
sessionsuser_id, surface, activecreated_at, expires_at
oauth_identitiesprovideremailprovider, user_id
telegram_linksverifiedcreated_at
workspacesuser emailcreated_at, user_email
global_credentialsuser_idnameprovided_at, name
jobs (M27-12)kind, statusrun_after, attempts
outbound_messages (M27-12)channel, kind, status, thread_idcreated_at
runtime_correlations (M27-12)runtime, statusexternal idcreated_at, updated_at
projects (#488)phase, lifecycle, owner_user_idtitle, thread idupdated_at, title
audit_log (M27-13)actor_id, action, resource, resource_id, outcome, since, untilat

Redaction is structural (fields absent from the read models cannot serialize):

  • Session id is an 8-character prefix — the raw 43-char cookie id never leaves the store.
  • Telegram link one-time code is omitted entirely.
  • Global credentials carry metadata only — there is no value field.
  • Audit before/after snapshots are redacted at append time (M27-3); the audit screens (M27-13) render them verbatim, read-only.
  • Job payloads (M27-12) render read-only, always: they are immutable server-side (RCE-adjacent - they drive Claude Code execution on a worker VM). The only job verbs are retry / cancel (+ bulk cancel), and no verb accepts a payload - retry re-enqueues the stored payload byte-identically.
  • Outbound messages (M27-12) carry a truncated preview + content hash only - the full body was never stored (#216), so it has no field to leak from.
  • Runtime correlations' free-form metadata dict is omitted entirely (M27-12).

The JSON-inspector board exists to make this contract visible in the UI.

Forward-looking: forms standard for write slices

When later milestones add mutation routes, admin forms use the project standard: shadcn Form + react-hook-form + zod. Nothing in the current frames anticipates those routes.