Skip to content

feat(desktop): in-app admin console for relay operators - #4768

Open
wpfleger96 wants to merge 1 commit into
mainfrom
wpfleger/desktop-admin-surface
Open

feat(desktop): in-app admin console for relay operators#4768
wpfleger96 wants to merge 1 commit into
mainfrom
wpfleger/desktop-admin-surface

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented Aug 4, 2026

Copy link
Copy Markdown
Member

Adds an in-app admin console for relay operators, surfaced as an "Admin console" section under Settings. Operators view deployment-wide moderation reports and product feedback, resolve or dismiss reports, update feedback status, and manage the operator/moderator staffing roster — all authenticated with the operator's own Nostr key over NIP-98, with no browser extension or bearer token.

Discovery and connection

  • On mount, the console fetches the connected relay's NIP-11 information document and reads the optional admin_api field to auto-discover the admin origin — zero manual input on the happy path.
  • The advertised origin is untrusted input: it is revalidated through AdminOrigin::parse before use, and an absent or invalid value falls back to a manual origin field (pre-filled when discovery succeeds).
  • A saved manual origin takes precedence over discovery — an operator's explicit choice is never overridden.

Auth and transport (Rust, src-tauri/src/commands/admin/)

  • AdminOrigin value object: validates scheme + host + optional port, rejects credentials, path, query, and fragment; http:// only for loopback hosts, https:// otherwise.
  • AdminRoute closed enum: no IPC surface accepts arbitrary URLs or paths, so the NIP-98-signed URL is byte-identical to the fetched URL.
  • Dedicated no-redirect reqwest client as an SSRF guard: a relay 3xx is surfaced as an error and the NIP-98 header is never forwarded across origins.
  • admin_probe returns a typed state (Nip98Authorized / Denied, TokenMode, Disabled, NotAdminApi, NetworkOrIntercepted); Nip98Authorized only on an authenticated 2xx.
  • NIP-98 signing via AppState::signing_keys() (errors cleanly in recovery mode), with one retry on 401 using a fresh signed event.
  • Response size bounded by a Content-Length preflight and a streaming byte counter; per-pubkey origin storage with atomic 0o600 writes.

Console UI (TypeScript, src/features/admin-console/)

  • AdminConsoleSettingsCard: discovery + probe flow, per-pubkey state, role and source badges when authorized, honest copy for every probe state.
  • AdminConsolePanel with Reports / Feedback / Staffing tabs (Staffing gated on role === "operator").
  • Reports: full action matrix per target_kind (event → delete/kick/ban/timeout/dismiss/escalate; pubkey → ban/timeout/dismiss/escalate; blob → dismiss/escalate). Enforcement state renders processing as not-actionable and pending/enforcing/succeeded/failed with retry; cancel is offered only pre-mutation, and a rejected cancel is treated as server-authoritative. Each attempt carries a client-generated request_id, reused on retry after a lost response.
  • Feedback: list, detail, and status control (new/reviewed/archived) with a generation-fenced attachment viewer (image/* auto-loads; other MIME types load on demand).
  • Staffing: add operator (pubkey + role), roster with config / owner_fallback / db source badges; config-backed entries disable remove client-side and surface a 409 conflict as a human-readable message.

Settings refactor

  • SettingsPanels.tsx split along panel boundaries (ThemeSettingsCard.tsx extracted) to stay under the 1000-line file-size ratchet; the PTT global-shortcut plugin builder is extracted from lib.rs for the same reason. Mechanical, no behavior change.

UI invariants

  • processing reports are never presented as actionable.
  • Post-mutation delivery states render on a resolved report, never as an enforcement failure.
  • token and disabled auth modes render read-only, with no action or staffing capabilities.

Related: #3777 (relay OPERATOR/MODERATOR role model + NIP-98 auth + admin_api NIP-11 advertisement — provides the runtime and the discovery field this console consumes)
Related: bb-public#339 (Phase 4 rollout config)

@wpfleger96
wpfleger96 requested a review from a team as a code owner August 4, 2026 18:46
@wpfleger96
wpfleger96 force-pushed the wpfleger/desktop-admin-surface branch 4 times, most recently from 42e67e3 to 9bb1666 Compare August 6, 2026 17:49
wpfleger96 added a commit that referenced this pull request Aug 7, 2026
…orcement states, feedback status, staffing tab

Implement Plan v4 Phase 3 for the desktop admin console panel (#4768).

## Probe
- AdminProbeResult::Nip98Authorized now carries optional role and source
  fields (Rust enum variant updated to struct variant).
- AdminConsoleSettingsCard propagates role/source from probe result to
  AdminConsolePanel; AdminConsolePanel renders a role+source badge strip
  when role is present.

## Report actions (frozen v4 matrix)
- Event reports: delete/kick/ban/timeout/dismiss/escalate
- Pubkey reports: ban/timeout/dismiss/escalate
- Blob reports: dismiss/escalate
- ResolveReportForm generates a client UUID request_id per submission
  attempt (v4 §6a amendment 2); 409/processing errors preserve the
  request_id for retry idempotency.
- Timeout action shows a duration (expiration_secs) input; submit is
  disabled until a value is provided.

## Enforcement states
- processing reports are disabled (non-actionable) in the list with a
  spinner.
- EnforcementStateBlock renders pending/enforcing/succeeded/failed states.
- Failed actions surface Retry (reuses same request_id) and Cancel
  (dismiss with fresh request_id; server-rejected cancel treated as
  authoritative).

## Feedback status
- FeedbackStatusControl: new/reviewed/archived PATCH buttons with
  optimistic local-state sync; server error surfaces inline.
- FeedbackTab list shows non-new status as a badge.

## Staffing tab
- Operator-only (gated by role === 'operator' from probe).
- SourceBadge distinguishes config/owner_fallback (immutable) from db.
- Config-backed operator rows have disabled remove buttons with title
  explaining why.
- PUT 409 (config-backed add conflict) and DELETE 409 surfaced clearly.

## File structure
AdminConsolePanel.tsx split into four files to satisfy the 1000-line
ratchet (all new files under the limit):
- AdminConsolePanelHelpers.tsx: AsyncState, useAsyncLoad, formatTimestamp,
  DetailRow, LoadingSpinner, ErrorMessage, AttachmentMeta,
  parseImetaAttachments
- AdminConsoleFeedbackTab.tsx: FeedbackTab, FeedbackDetail, and related
  sub-components
- AdminConsoleStaffingTab.tsx: StaffingTab, SourceBadge
- AdminConsolePanel.tsx: ReportsTab, ReportDetail, report action
  components, TabBar, AdminConsolePanel root
src-tauri/src/commands/admin/helpers.rs extracted from mod.rs to keep
mod.rs under 1000 lines.

## Tests
- 7 new tests: probe-role-source-badge, probe-moderator-role,
  probe-operator-role, probe-no-role, processing-report-not-actionable,
  action-matrix-types, plus reportButton.disabled assertion.
- All 4511 TS tests pass; all 12 jsdom tests pass; Rust compiles clean;
  desktop-check, desktop-tauri-check, desktop-tauri-test all green.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96 wpfleger96 changed the title feat(desktop): add in-app admin console for relay operators feat(desktop): role-based admin console — Phase 3 rework (v4 contract) Aug 7, 2026
@wpfleger96
wpfleger96 force-pushed the wpfleger/desktop-admin-surface branch from fa29ae1 to 99527b1 Compare August 11, 2026 00:04
@wpfleger96 wpfleger96 changed the title feat(desktop): role-based admin console — Phase 3 rework (v4 contract) feat(desktop): in-app admin console for relay operators Aug 11, 2026
Add a NIP-98 client for the /api/admin/v1 relay API, surfaced as an
"Admin console" section in Settings. Relay operators view
deployment-wide moderation reports and product feedback, resolve/dismiss
reports, update feedback status, and manage the operator/moderator
staffing roster — no browser extension or bearer token required.

The console auto-discovers the relay's admin origin from its NIP-11
document (admin_api field): on mount it fetches the connected relay's
relay-information document, validates the advertised origin through
AdminOrigin::parse, and auto-probes. The manual origin field remains as
a pre-filled fallback for relays that do not advertise.

Rust:
- AdminOrigin value object: validates scheme+host+optional-port, rejects
  credentials/path/query/fragment; http:// only for loopback hosts
- AdminRoute closed enum: no IPC surface accepts arbitrary URLs or paths;
  the signed URL is byte-identical to the fetched URL
- Dedicated no-redirect reqwest client (SSRF guard: relay 3xx surfaced as
  error, NIP-98 header never forwarded across origins)
- admin_probe: typed state enum (Nip98Authorized/Denied, TokenMode,
  Disabled, NotAdminApi, NetworkOrIntercepted); Nip98Authorized only on
  authenticated 2xx
- NIP-11 admin-origin discovery command; advertised value treated as
  untrusted input and revalidated before use
- NIP-98 signing via AppState::signing_keys() (Err in recovery mode);
  one retry on 401 with a fresh event
- Response bounds enforced by Content-Length preflight and streaming
  byte counter; per-pubkey origin storage (atomic write, 0o600)

TypeScript:
- admin-console API wrappers for all Tauri commands; attachments return a
  Blob URL from caller-supplied MIME
- AdminConsoleSettingsCard: auto-discovery + probe flow, per-pubkey state,
  honest copy for every probe state
- AdminConsolePanel with Reports / Feedback / Staffing tabs
- Settings panels split under the file-size ratchet

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant