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 byuseState<'alerts' | 'cascades'>('alerts') - Alerts table (when
tab === 'alerts'): columns Alert / Kind / Profile / Severity / Status / Reported; rows sorted byseveritydescending;data-testid="alert-rows"on<tbody> - Cascades table (when
tab === 'cascades'): per cascade, progress reported viareportCascadeProgress({ cascade, nowUnixSeconds })
- Tablist: two tabs — "Alerts (
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
AdminShellchrome — note that this subroute is the only one in the assigned set that bypasses the canonical workspace pattern (shell/04-workspace-pattern.md); nogetAdminServerSessionredirect, noWorkspaceEntryPointfallback, 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"
- Function: switches state
- "Revocation cascades" tab (button,
role="tab",aria-selected)- Function: switches state
tab → 'cascades' - Label:
Revocation cascades (<props.cascades.length>) data-tab="cascades"
- Function: switches state
- 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 viabindVoiceAbuseLoader(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
getAdminServerSessionor checkcanEnterAdminWorkspace)
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/voiceexports —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, noWorkspaceEntryPoint). 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 —
AdminBreadcrumbProviderfrom the shell does not wrap this page -
bindVoiceAbuseLoaderis 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