Admin Cockpit · Surface walkthrough

Safety · Voice abuse

A per-surface walkthrough of the Admin Cockpit admin surface: layout, states, interactions, data, and cross-references.

drafted
8sections2 minread

On this page

Context. surface admin · domain safety · route /trust-safety/voice-abuse · auth signed-in (admin) + scope:admin:workspace:moderation · source apps/oshun/admin/src/app/trust-safety/voice-abuse/page.tsx

Last walked.

Purpose#

Voice-abuse subsurface under trust-safety (§24.7): surfaces impersonation alerts, denylist matches, and revocation cascade triggers; the coordination edge with §22 consent revocation lives here too. Backed by @iris/voice abuse-detection types.

Entry points#

  • Subroute under /trust-safety — not a top-level sidebar item; reached by deep link / cross-link from cloned-voice review rows in /trust-safety
  • Direct URL — /trust-safety/voice-abuse

Layout regions#

This page does NOT wrap the body in AdminShell — unlike sibling /trust-safety. It exports bindVoiceAbuseLoader(loader) to inject the AbuseContext (alerts / cascades / nowUnixSeconds) and renders <VoiceAbusePanel> directly:

  • VoiceAbusePanel — top-level <main data-testid="voice-abuse-panel"> with:
    • Tablist: two tabs — "Alerts (<open> open / <total> total)" and "Revocation cascades (<count>)"; managed by useState<'alerts' | 'cascades'>('alerts')
    • Alerts table (when tab === 'alerts'): columns Alert / Kind / Profile / Severity / Status / Reported; rows sorted by severity descending; data-testid="alert-rows" on <tbody>
    • Cascades table (when tab === 'cascades'): per cascade, progress reported via reportCascadeProgress({ cascade, nowUnixSeconds })

States#

  • Loader not bound → page falls back to { alerts: [], cascades: [], nowUnixSeconds: Math.floor(Date.now()/1000) } — both tables render empty
  • Loader bound, alerts empty → "Alerts (0 open / 0 total)" tab; table body empty
  • Loader bound, cascades present → cascade tab shows progress from reportCascadeProgress
  • No AdminShell chrome — note that this subroute is the only one in the assigned set that bypasses the canonical workspace pattern (shell/04-workspace-pattern.md); no getAdminServerSession redirect, no WorkspaceEntryPoint fallback, no sidebar / header chrome
  • Middleware still gates the route — it is NOT in PUBLIC_PATHS, so an anonymous request still bounces via the global middleware (shell/02-routing-layouts.md)

Interactions#

  • "Alerts" tab (button, role="tab", aria-selected)
    • Function: switches state tab → 'alerts'
    • Label: Alerts (<openAlerts.length> open / <props.alerts.length> total)
    • data-tab="alerts"
  • "Revocation cascades" tab (button, role="tab", aria-selected)
    • Function: switches state tab → 'cascades'
    • Label: Revocation cascades (<props.cascades.length>)
    • data-tab="cascades"
  • Alert row (<tr data-alert-id>) — details surfaced inline; verify whether rows have click handlers or just display

Data & contracts#

  • Reads: loader-bound AbuseContext{ alerts: readonly AbuseAlert[], cascades: readonly RevocationCascade[], nowUnixSeconds: number }; production binds via bindVoiceAbuseLoader(loader) exported from the page module
  • Writes: None visible at page level.
  • Realtime: None.
  • Auth/role check: only the global middleware (the page does NOT call getAdminServerSession or check canEnterAdminWorkspace)

Cross-references#

  • Parent route: trust-safety.md
  • Sibling Isis route on the voice-cloning admin side: ../../isis/isis-voice-cloning.md (clone workflow surface)
  • Library: @iris/voice exports — AbuseAlert, RevocationCascade, reportCascadeProgress
  • Component sources: apps/oshun/admin/src/app/trust-safety/voice-abuse/VoiceAbusePanel.tsx
  • Outbound coordination: §22 consent-revocation surface (per the page comment); locate the corresponding consent admin route

Open questions / known gaps#

  • This page departs from the canonical workspace pattern (no AdminShell, no session check, no WorkspaceEntryPoint). Document whether this is intentional (it's a "raw" panel meant to be embedded elsewhere?) or a gap that should be reconciled with the rest of the cockpit
  • No top-level header / breadcrumb — AdminBreadcrumbProvider from the shell does not wrap this page
  • bindVoiceAbuseLoader is module-level mutable state; document where production binding happens (likely an app-init step or test setup) — this is unusual in the rest of the admin app, which fetches via BFF in the server component