V1 Web PWA · Surface walkthrough

Operator · Admin · inbox

A per-surface walkthrough of the V1 Web PWA operator surface: layout, states, interactions, data, and cross-references.

walked
9sections6 minread

On this page

Context. surface operator · domain operator (house — incidents, citation drift, tenant requests, support routing) · route /operator/admin · auth signed-in · source apps/oshun/web/src/app/operator/admin/page.tsx

Last walked. 2026-07-03 Operator admin inbox decision addendum — page/header, filter-chip, queue row-cell, detail-card, drift-source, decision-panel, reject/retract rationale gate with BFF readback, incident-action, and anonymous redirect-before-render coverage in apps/oshun/web/e2e/incident-triage.spec.ts and apps/oshun/web/e2e/operator-editorial-review-approval.spec.ts. Evidence: WALKTHROUGH/results/v1-real-infra-run-2026-06-22.md §88

Purpose#

The operator admin inbox — "Lilith Admin · House · Inbox" in the plate-of-record register. A single page that surfaces incidents, citation drift, tenant requests, and support routing as a triage queue with a detail pane. Robots-noindex.

Entry points#

  • From /operator (the index) — surface link routed via getOperatorIndex()
  • Profile menu → Operator → Admin
  • Direct URL — session-gated only (proxy.ts redirects sessionless → /welcome?redirect=); no operator-role gate in the proxy
  • Auto-routed incidents — alerts/auto-generated INC tickets deep-link here

robots: { index: false, follow: false } keeps it out of public SEO.

Layout regions#

The page uses Lilith design primitives directly (LGraphPaper, LOperatorBar, LCard, LCode, LChip, LEyebrow, LDataCell, LSeverityPill, L, SERIF_L, SANS_L) rather than ShellLayout.

  • Top: LOperatorBar — product="Lilith Admin", section="House", breadcrumb="Inbox"
  • Header band:
    • Eyebrow accent "Admin · inbox"
    • H1-like (Garamond 28) "6 open · 1 critical" (italic open count)
    • Right-aligned ghost link: Audit log/operator/audit
  • Admin inbox console (data-operator-admin-console) — client island grid (2fr / 1fr) with filterable queue and selected-incident detail.
    • Filter chips (data-operator-filter) — real buttons: All, Editorial, Tenant, Auto; active state is recorded in data-operator-active-filter.
    • Items column (2fr) (data-operator-inbox-list) — list rows, each laid out as 110px 90px 1fr 130px 100px (code / severity / title+sub / assignee / when):
      • LCode ticket code
      • LSeverityPill level={sev} — S1 → "critical", S2 → "degraded", anything else → "advisory"
      • Title (Garamond 17 medium) + italic subtitle (Garamond 13 muted)
      • LDataCell mono assignee
      • LDataCell mono time, right-aligned
      • Each row exposes data-operator-item, category/selected attributes, and cell anchors for code, severity, title, subtitle, assignee, and when.
    • Detail card (1fr) (data-operator-incident-detail="<INC>") — accent-bordered, aligned start:
      • LCode header, e.g. "INC-2041 · 14:22 · auto · Lilith"
      • Garamond 22 selected title
      • Italic selected subtitle
      • Severity pill, optional public-scene deep-link for abuse-derived INCs, and optional provenance handoff for citation-drift/generated-artifact INCs
      • Drift sources list — LEyebrow heading + rows mapped from data.driftSources only for the citation-drift incident (each [source, when, weight]: italic source + LCode when + weight)
      • AdminInboxDecisionPanel (incidentId="INC-2041") — three verdicts: approved-republish / request-rereview / rejected-retract, keyed to the currently selected incident.
      • AdminInboxIncidentActions — acknowledge, assign, escalate, snooze, and resolve controls against the selected incident.
      • AdminEvidenceQueuePanel (data={data.evidenceReview}) renders below the grid (operator.tsx:661)

States#

  • Loading — source-verified: no local loading.tsx; page.tsx awaits getAdminInbox() server-side before rendering.
  • Anonymous user — redirected to /welcome?redirect= (proxy.ts) before [data-operator-admin-page], rows, or the decision panel render (Playwright, 2026-06-25)
  • Signed-in user — full inbox renders (session-gated only; no operator-role gate in the proxy)
  • Inbox with items + selection — selected row has accent ring; detail card and decision/action panels match the selected incident
  • Inbox with no items — no full-page empty fixture is available; filter empty state copy exists in AdminInboxQueue but is not reachable from the seeded all/editorial/tenant/auto categories.
  • getAdminInbox() fails — no local segment error.tsx is present; getAdminInbox() fail-softs live abuse-report fetches back to the static fixture.
  • Offline — write-heavy surface; queued or disabled (verify)
  • Different operator personas — items filtered by role

Interactions#

Filter chips#

  • All chip (accent active) — shows all six seeded INC rows
  • Editorial chip — filters to the two editorial-class rows
  • Tenant chip — filters to the two tenant-class rows
  • Auto chip — filters to the two auto-generated rows

Filter state is component-local. It updates data-operator-active-filter and data-operator-inbox-count; it does not write URL state.

Items list#

  • Each row — click selects; selected row paints with data-operator-item-selected="true"; detail card, decision panel, and incident actions swap to the selected incident.
    • Keyboard: row is role="button" and Enter/Space selects; arrow-key roving focus is not implemented.
    • Touch ≥ 44×44 px by row padding/grid height.
  • Row cells — all six fixture rows are asserted cell-by-cell (code, severity, title, subtitle, assignee, when) via data-operator-item-cell.
  • LSeverityPill — non-interactive; conveys severity via shape + label, not color alone
  • LCode ticket code — non-interactive display

Detail card#

  • AdminInboxDecisionPanel verdict picker — three radio-style verdicts (approved-republish, request-rereview, rejected-retract); posts to the real audited BFF decision route in E2E and renders data-decision-audit-id. The reject/retract branch now proves no/short rationale leaves the action placeholder-disabled with no browser POST; valid rationale records and BFF GET rehydrates the same rejected decision, rationale, and audit id.
  • Drift-sources list — non-interactive source / when / weight rows, visible for the default citation-drift incident and removed when a non-drift row is selected.
  • Provenance handoff — citation-drift rows can render data-operator-incident-provenance-link="<INC>"; INC-2041 links to /operator/admin/isis/provenance?inc=INC-2041 and is covered by citation-drift-resolution.spec.ts.
  • AdminEvidenceQueuePanel — evidence review queue below the grid; Playwright covers source/retrieval inspection, citation validation, and grounded-report promotion.
  • Incident lifecycle actions — acknowledge, escalate, and resolve are covered through real BFF writes; server rationale floors are covered by pure-HTTP tests.
  • Cross-reference link to fixture artifact codes like TR-047 — not present for the static drift fixture. Abuse-derived rows can expose a public-scene "Open offering" link.
  • Audit-trail visible — the page exposes an Audit log entry point and decision writes render the real audit id.

E2E coverage#

  • apps/oshun/web/e2e/incident-triage.spec.ts covers signed-in /operator/admin, page/header/audit-entry anchors, filter chips, all six fixture rows cell-by-cell, detail-card selection, drift-source rows, decision-panel rationale gates and real audited BFF writes, incident lifecycle actions with an opt-in BFF harness reseed, server-side rationale-floor failures, and anonymous redirect-before-render.
  • apps/oshun/web/e2e/operator-editorial-review-approval.spec.ts covers the shared decision panel's editorial review approve/reopen path, reject/retract rationale gate with audited BFF readback, and local rehydrate behavior.
  • apps/oshun/web/e2e/operator-admin-evidence-queue.spec.ts covers the evidence review queue below the inbox grid.
  • apps/oshun/web/e2e/citation-drift-resolution.spec.ts covers the citation-drift row's "Open provenance" handoff from the selected incident detail into /operator/admin/isis/provenance?inc=INC-2041.

Data & contracts#

  • Reads: getAdminInbox() from @/lib/lilith-data/operator-depth returning AdminInboxData. It prepends live public-abuse reports from /v1/admin/abuse-reports when present and otherwise falls back to the static fixture baseline. Rows may carry shortCode for scene review or provenanceHref for provenance inspection handoff.
  • Writes: AdminInboxDecisionPanel posts to /v1/admin/operator-inbox/:id/decision and .../reopen; AdminInboxIncidentActions posts to /v1/admin/operator-incidents/:id/<verb> for acknowledge, assign, escalate, snooze, and resolve.
  • Realtime: incidents stream in via oshun-sw-update or similar push channel (verify against shell/05-notifications.md)
  • Caching: server-rendered; SW excludes operator routes from runtime cache (verify)
  • Auth/role check: session-gated only (proxy.ts redirects sessionless → /welcome?redirect=); no operator-role gate in the proxy

Cross-references#

Open questions / known gaps#

  • Audit the detail-card action set in current source (acknowledge, assign, escalate, snooze, resolve) and cover acknowledge/escalate/resolve in Playwright (2026-06-25)
  • Confirm filter chips wire to a real handler (component-local filter state, no URL state)
  • Document the realtime channel (Psyche topic) that feeds new incidents to this view
  • Confirm SW caching policy for /operator/* (likely network-only, audit-stamped writes)
  • An operator-role gate appears absent — proxy.ts only redirects sessionless users to /welcome; no role check in the proxy or page components (possible gap vs intended scope)