---
path: /trust-safety/voice-abuse
surface: admin
domain: safety
auth: signed-in (admin) + scope:admin:workspace:moderation
source: apps/oshun/admin/src/app/trust-safety/voice-abuse/page.tsx
status: drafted
last_walked: '—'
---

# Safety · Voice abuse

## 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
