Skip to content

feat(desktop): browse and moderate every community in the Admin Console - #8027

Draft
wpfleger96 wants to merge 20 commits into
duncan/admin-community-readsfrom
hayt/admin-console-communities
Draft

wpfleger96 wants to merge 20 commits into
duncan/admin-community-readsfrom
hayt/admin-console-communities

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

Stack: #8011 → this PR. This PR targets main, so until #8011 merges its diff also shows #8011's crates/ commits. Only desktop/ belongs to this PR.

The Admin Console only ever saw the community the desktop happened to be connected to. With #8011's community reads, the console can now browse every community on the relay and, for signed staff, moderate any of them by name without switching connections. 🤖

What changes

  • Communities tab. Lists every live community on the relay, searchable by host prefix with "Load more" paging. The connected community is pinned at the top by exact host match. Report rows get a community badge that opens that community.
  • Community page. Reports, Restrictions, Members and Actions, each scoped to the community on the page. The Actions form has no host field: it always acts on the page's community, and the TypeScript host normalization is gone from the Actions tab.
  • Native reads (admin_list_communities, admin_search_members, admin_get_member, admin_get_event). Every request validates the explicit community host and NIP-98-signs the real method and URL. Failures come back as a structured error (status, bodyComplete, bodyEmpty, code), so the UI can tell an older relay's empty 404, a coded event_not_found and a transport failure apart, and never says "not found" unless the relay did.
  • Explicit-host lifts. admin_lift_restriction sends the page's community host, not the active relay's. It freezes the relay and signer the intent was reviewed under, and refuses to send if either has changed.
  • Pending actions survive navigation. A panel-level DirectActionsProvider holds the frozen intent, so switching communities or tabs mid-confirm doesn't lose it. Only Discard drops it. A signer change remounts the controller and drops it.
  • Fenced lookups. Member and event lookups are keyed by origin, signer, community, target and a generation counter, so a late answer for one community can't unlock Review in another.
  • Shared member picker (MemberSearchResult, also used by Add Member), showing the truncated npub so members with the same name can be told apart. Delete accepts a note link and previews the message before Review. Relay staff are blocked before Review.
  • Disabled admin auth is read-only. With BUZZ_ADMIN_AUTH=disabled the Communities tab and community pages still render, and report badges still open them, but every ban, timeout, delete, add-member and lift control is disabled. The settings badge says the console is read-only and that acting needs nip98. Staffing stays hidden.
  • feat(desktop): add Admin Console Actions tab for direct staff actions #7904's protections in the Actions tab are kept: frozen intent, requestId, Retry, notSent and the key-backup guard.
  • just admin-seed for multi-community testing. Besides the local community, it seeds beta.localhost:3000, gamma.localhost:3000 and 50 filler communities, with named members, a ban and a timeout in each named community, a report with Kick available, and a message for the delete preview. Rerunning it restores all of that to the starting state. It refuses, writing nothing, while a moderation action is unfinished on a seeded report.
  • The bodyless GET /events/{id} preview is added to the events-URL egress inventory. It sends no body, so it carries no key material.

Tests

Native (Rust):

  • empty_404_and_405_read_as_complete_and_empty, coded_404_carries_the_relay_code, truncated_and_over_cap_bodies_are_incomplete, transport_failure_has_no_status: the structured read error tells an older relay, a coded refusal, a cut-off body and a transport failure apart.
  • a_401_retry_is_signed_by_the_same_keys, lift_signs_the_send_and_401_retry_with_the_checked_keys: the 401 retry never switches keys.
  • read_urls_validate_the_explicit_host_and_encode_the_query, lift_url_sends_the_page_host_not_the_active_relay, list_url_carries_host_and_cursor_and_refuses_a_changed_relay, lift_refuses_before_sending_on_signer_change_or_bad_host: requests target the explicit host and refuse to send after a relay or signer change.

jsdom:

  • actions-pending-nav: a pending intent survives a community change, and only Discard drops it.
  • actions-fenced-lookup, fenced-load: a late lookup for an old key never shows under, or unlocks, the new one.
  • actions-same-ids: the same pubkey and event id are read separately in each community.
  • actions-community-change-drops-member, actions-page-host, actions-success, actions-identity: the form acts only in the page's community, and confirm sends the frozen intent.
  • actions-preview-errors, actions-delete-link, actions-staff-target, actions-disabled-auth: preview errors, delete by note link, the staff block, and community pages rendering read-only with every action control disabled under disabled admin auth.
  • communities-directory, communities-unsupported, community-badge, community-badge-disabled-auth: the directory, older relays, and report badges, which open community pages under disabled admin auth too.
  • restrictions-lift-signer-change. The existing restriction tests now run on the community page and use admin_lift_restriction.

Mutation checks: removing the pending-navigation guard turns actions-pending-nav red, and dropping the host from the fence key turns actions-fenced-lookup, actions-same-ids and fenced-load red. Ungating restriction lifts on canMutate turns actions-disabled-auth red.

@wpfleger96
wpfleger96 force-pushed the hayt/admin-console-communities branch 2 times, most recently from 9ad6292 to ebbb8db Compare October 1, 2026 20:57
wpfleger96 pushed a commit that referenced this pull request Oct 1, 2026
@wpfleger96

Copy link
Copy Markdown
Member Author

🤖 Captured headless from the e2e build at ebbb8dbd2. Tauri IPC is mocked in the capture script, so admin API responses, community lists, members, message previews and the native error strings are all simulated. Community hosts are placeholders (alpha.example.com, beta.example.com, ...), and the app is connected to a different community than the one being viewed in most shots, so the "not the community you're connected to" warning shows.

Reports, grouped by community

Reports from every community the relay hosts, grouped under one community heading per group. The badge sits in the heading only, not on each row.
01-reports-list-badges

Resolving a report from another community

The confirm names the community and warns that it isn't the one you're connected to.
02-reports-resolve-confirm-not-connected

Feedback, grouped by community

Same grouping as Reports.
03-feedback-list-badges

Operators

The operator roster tab.
04-operators-tab

Communities tab

First page of the relay's community directory.
05-communities-list

Communities tab, after "Load more"

The second page is appended below the first.
06-communities-load-more

Communities search

Searching by host. The community you're connected to is pinned at the top only when its host starts with the query, so it is absent for team1 here.
07-communities-search

A community's page: Reports

Reports for just this community. There is no group heading because the banner at the top already names the community.
08-community-page-reports

A community's page: Restrictions

Active bans and timeouts in this community, each with its own lift button.
09-community-restrictions

Lifting a restriction

The confirm names the member and the community the ban is lifted in.
10-restriction-lift-confirm

A community's page: Members search

Searching this community's members by name.
11-community-members-search

A member selected

Picking a member offers Ban and Time out.
12-community-members-selected

Actions: member lookup

Looking up a member before acting. An existing timeout shows its end as an absolute date and time.
13-actions-member-lookup

Actions: message preview

Pasting an nevent loads the message so you can see what you're about to delete.
14-actions-event-preview

Actions: delete confirm

The delete confirm repeats the community, the author and the message text.
15-actions-delete-confirm

Message lookup errors

Four failure cases for the message preview: a relay without the new admin routes, a message that isn't in this community, a response the app couldn't read, and a relay it couldn't reach.
16-read-error-unsupported
17-read-error-not-found
18-read-error-incomplete-body
19-read-error-transport

Communities tab errors

A relay without the community directory route, and a relay the app couldn't reach.
20-communities-unsupported
21-communities-transport-error

@wpfleger96
wpfleger96 force-pushed the hayt/admin-console-communities branch from ebbb8db to cbba1e5 Compare October 2, 2026 18:39
@wpfleger96
wpfleger96 changed the base branch from main to duncan/admin-community-reads October 5, 2026 21:34
@wpfleger96
wpfleger96 force-pushed the duncan/admin-community-reads branch from 8a7f3b9 to cf351a4 Compare October 6, 2026 14:47
@wpfleger96
wpfleger96 force-pushed the hayt/admin-console-communities branch 4 times, most recently from d92937d to 315af0f Compare October 6, 2026 17:20
Hayt and others added 19 commits October 6, 2026 15:07
The community page needs staff reads whose failures stay distinguishable
(unsupported route vs absent target vs unknown), and lifts that target the
page community and cannot be signed by a different identity on retry.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Each console tab picked its community differently and never said which,
so a ban could land in the wrong community. Actions and Restrictions now
live on a community page that names its host everywhere; the direct-action
controller stays mounted across navigation, so a pending intent and its
requestId are only dropped by an explicit Discard, and community reads are
fenced by origin, signer, community, target and generation.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The bodyless GET /events/{id} preview carries no key material, so it is listed rather than guarded.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
A Load more answer for search A could land after the user moved to B and overwrite B's pages, cursor and busy state. Each search transition now gets its own identity, and late answers from an earlier one are discarded before any state is set.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…hosen

Confirm rebuilt the relay and signer from the list state at that moment, so a lift confirmed during a reload went out with empty expectations and was refused. The intent is now captured from the row's loaded list and sent unchanged.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The Actions section can change community while staying mounted. These
tests pin that a late member lookup, event preview or member search from
the old community never shows in, or unlocks Review in, the new one.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Group headings alone left a row ambiguous once scrolled past, and the
resolve confirm named only the host string. Each row now carries its own
community badge, and confirm shows the badge plus the not-connected warning.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The report-kick roster check re-derived the host in TypeScript from the
relay URL. It now asks the native side for the connected host, matching
the relay-normalized report host, and a failed lookup records nothing
instead of throwing after the relay has already resolved the report.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The b3 cursor fell through to page-one rows, so the final Load more
rendered a duplicate community and React warned about duplicate keys.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Rows repeated the group heading's badge, and a community page repeated its banner on top of both.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…arch

The relay matches a trimmed, case-insensitive host prefix; the pin now follows the same rule instead of showing under unrelated searches.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Member and message previews showed raw ISO timestamps.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Lets the multi-community admin console be tested against plain just dev: communities past one directory page, named members, a ban and timeout per community, grouped reports and feedback, and pre-signed messages for the delete preview.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Rerunning just admin-seed forced fixture reports back to open while an enforcement action could still be attached, stranding Cancel & reopen and letting a pending action re-apply after the reset. The seed now locks admin actions, refuses before any write while a fixture report or action is unfinished, and runs in one transaction. It also resets every fixture restriction, feedback status and message-author channel membership, and links event reports to their channel so Kick works.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
A report claim locks its report row before inserting an action, the reverse of the seed's order, so a claim racing a reseed could deadlock. The seed now takes the fixture report rows with FOR UPDATE NOWAIT right after the action-table lock and refuses with a clear message if one is held.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The ID-prefix match also caught unseeded reports that reuse a fixture-looking ID, which could make the seed refuse over an unrelated row. Select by the upserts' (community, report_event_id) keys instead.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The relay now serves every moderation read in disabled mode, including the
community reads, while still refusing writes. The console hid Communities
there; it now shows them, with every ban, timeout, delete, add and lift
control disabled. Restriction lifts were the one control not gated on
canMutate.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The community page renders its section nav only after async reads settle,
so the fixed 20ms delay raced on slow CI runners. The review-race test also
left a relay read pending forever when mounting outlasted its release,
which stalled every later test in the file.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The Staffing tab renders only for an Operator role, and only an authorized
probe reports a role, so canMutate is always true there. Community
navigation is always provided by the panel, so open is never null.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The relay returns isStaff: null in disabled auth mode, where it does not
read the staff roster. Both readers already treat null as not staff.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96
wpfleger96 force-pushed the hayt/admin-console-communities branch from 315af0f to 6ab34f2 Compare October 6, 2026 19:38

This branch has not been deployed

No deployments
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