Skip to content

Admin correctness and accessibility fixes - #40

Closed
theobong wants to merge 21 commits into
fix/admin-campaign-03-security-crashfrom
fix/admin-campaign-04-correctness
Closed

theobong wants to merge 21 commits into
fix/admin-campaign-03-security-crashfrom
fix/admin-campaign-04-correctness

Conversation

@theobong

@theobong theobong commented Sep 24, 2026 •

Copy link
Copy Markdown
Member

What changed

Correctness and accessibility fixes across the operator dashboard.

Errors

  • Every failed action shows exactly one error toast: a warning icon that stays about 8 seconds, with a real "Dismiss" button.

Selection

  • A deep link opens its item even when the item is not on the first page.
  • A filter or search that hides the picked item keeps it open.
  • After an action the item stays until it leaves the list; the next item never replaces it.

Keyboard

  • Every list row can be reached with Tab and opened with Enter or Space.
  • Tabs, radio switches and map markers work from the keyboard.

Everywhere

  • The source-code link appears on every section page.

Section-by-section fixes

  • Organizations offer Verify and Reject only for pending applications.
  • Mail's "Fix routing contact" finds the jurisdiction.
  • Contacts can be cleared.
  • Report chat pages older messages.
  • Bags counts are validated.
  • Home tiles still show previews when the summary fails.
  • Analytics times, percentages and populations read correctly.
  • Signup pages show their real public path.

Visible in: admin.

Before you start

Verify

[Admin]

Error toasts

  1. Open "Reports". In DevTools set the network to Offline, then click "Flag" in the report detail. Expect: one toast (not two): "Could not reach the server. Check your connection and try again." It has a warning triangle and stays about 8 seconds. Hovering it keeps it open.
  2. Repeat, then press Tab until the toast's "Dismiss" button has focus and press Enter. Expect: the toast closes and focus returns to where you were. Switch back online.
  3. "Organizations" → "New organization": enter an existing slug, pick an owner, and click "Create organization". Expect: "This slug is already taken." appears under Slug, and no toast.

Keyboard rows
4. On "Users", press Tab to reach a second account row, then press Enter. Expect: that account opens.
5. Repeat with Space on "Reports", "Events", "Moderation" (including "Gov claims"), "Mail" (Outreach and Inbox), "Jurisdictions", "Organizations", "Host messaging" and "Signup pages". Expect: the row opens and the page does not scroll.

Deep links
6. Open #/reports/<id of an old report>. Expect: that report opens, pinned above the list as "Linked report"; the first row does not take over.
7. Repeat with #/users/<id>, #/events/<id> and #/moderation/<id> for items that are not on page 1. Expect: each item opens.
8. Open #/users/<made-up uuid>. Expect: "User not found" / "No account has this ID.", with no "Try again".

Filters keep the pick
9. On "Reports", under "In progress", click the second row, then the "Completed" chip. Expect: the same report stays open, pinned as "Selected report".
10. On "Users", pick an account and type a search that matches nothing. Expect: "Nothing matches" appears in the list and the account stays open.
11. On "Host messaging", pick a host and search for a name that does not match. Expect: the pane clears to "No host selected" (hosts cannot be reopened by id).

After an action
12. On "Users" under the "Active" chip, pick the test account, click "Ban account" and then "Ban". Expect: the toast " · account banned". Once the list refreshes, the detail reads "No user selected" and no other account opens. Undo with "Un-ban" under the "All" chip.
13. On "Moderation", pick an item under "All" and click "Keep". Expect: a dialog titled "Keep ". After you submit, the buttons stay busy until the list refreshes, then the toast " · kept" appears and the detail reads "No item selected".
14. On "Mail", switch to "Inbox", click the "Unread" chip and open a message. Expect: the message stays open and readable even though it leaves the Unread list.

Organizations
15. Click the "Pending review" chip. Expect: the detail opens on the Verification tab with the "Verify as ..." and "Reject" buttons.
16. Pick a verified org and open Verification. Expect: no decision buttons, and the note "No application is awaiting a decision."
17. On the tab list, press the Left, Right, Home and End keys. Expect: the selected tab moves.

Source link
18. Open any section and scroll to the bottom. Expect: "Source code (AGPL-3.0)" with the short commit hash. The Dashboard shows the same footer once.

Home and analytics
19. In DevTools block the request whose URL contains /admin/home/summary and reload the Dashboard. Expect: only the analytics cell shows "Could not load the dashboard summary"; the other tiles still list their preview rows. Unblock it and click "Try again".
20. On the Dashboard, check the Mail tile. Expect: ages read like "5m" or "2h". Populations on the Jurisdictions tile read like "3.9M".
21. Open "Analytics". Expect: "Median resolution time" never shows "24h" or "1d 24h". Click "Export": in the CSV, Resolved reads like "42%", and accented names open correctly in Excel.

Mail and Jurisdictions
22. "Mail" → Outreach → "Needs attention" → open a bounced thread → "Fix routing contact". Expect: "Jurisdictions" opens on "All" with the GEOID in the search box and that jurisdiction open.
23. In that jurisdiction, empty a " contact email" field and click "Save draft". Expect: the toast "Draft saved for ", and after a reload that category has no contact.
24. Type "@ SF" in the discussion handle. Expect: the hint shows "@sf". Typing just "a" shows "Handle must be 2-40 characters using lowercase letters, numbers, or underscores." and disables saving.
25. Open a mail thread, type a reply, and press Ctrl+Enter (Cmd+Enter on Mac) three times quickly. Expect: exactly one reply is sent.

Reports and events
26. Open a report with a long chat. Expect: the chat count reads "50+". Click "Load older messages" and older messages appear.
27. Open a completed cleanup and type 2.5 in "Bags collected". Expect: "Log outcome" is disabled. Type 1 and save. Expect: the toast "Outcome logged · 1 bag".

Signup pages
28. Open "Signup pages". Expect: rows, the detail and the toasts show "/e/".

People and moderation dialogs
29. "Organizations" → Members → "Add member". Expect: the placeholder "Search by name, handle or city…". Searching for an existing member shows "Everyone matching is already a member."
30. On "Moderation", open a user report and click "Hold". Expect: a dialog titled "Hold for review".

Regression

Success toasts: [Admin]

  1. Run one success action per section: Reports "Approve"; Events "Log outcome"; Users "Un-ban"; Moderation "Keep"; Organizations "Suspend" then "Restore"; Host messaging "Suspend messaging" then "Restore messaging"; Signup pages "Flag"; Mail reply; Jurisdictions "Save draft". Expect: one success toast each, with the check icon, gone after about 2.6 seconds.

First load: [Admin]

  1. Open each section from the Dashboard with no link. Expect: the first row is selected automatically, as before.

Dashboard: [Admin]

  1. Click every tile's header, its "Open" button and a preview row, including an Inbox row in the Mail tile. Expect: each opens its section focused on that item.
  2. Rows in the "Host platform" card open "Organizations", "Host messaging" and "Signup pages".

Signing out: [Admin]

  1. Click "Sign out". Expect: "Signing out..." shows, then the Cloudflare Access logout.

Not covered

  • Screen-reader announcements (live-region toast, aria-current, accessible names) need a screen reader; automated tests assert the attributes.
  • Removing messages or approving gov claims against production-like data: use staging test records only.

Findings addressed

The per-row verdicts (FIXED, REFUTED, DEFER) are in the campaign ledger. The main themes:

  • Query keys (ADM-BUG-010):
    • Flat and infinite lists share no cache key in events, inbox, mail and moderation.
    • Key factories take the real contract query types instead of unknown.
  • Selection (ADM-BUG-004, 028, 005, 055, 085, 097 and related): one rule on every master-detail page.
    • The deep link wins.
    • A filtered-out pick stays open where the page can read it by id.
    • A pick that drops out after a refetch clears.
    • The first row never replaces a pick.
  • Errors (ADM-BUG-007, 009, 022, 037, 044):
    • A MutationCache handler shows one error toast per failure, with per-mutation copy and inline field errors never toasting.
    • The toast has an error tone and a longer duration.
    • ErrorState and the user picker go through errorMessage.
    • A second dialog settles the first.
  • Accessibility:
    • Every master row is operable by keyboard with aria-current (ADM-A11Y-002).
    • The toast is a live region with a real Dismiss button (A11Y-001).
    • Dialogs are named by their title, prompt labels are associated, and prompts restore focus (A11Y-004, 900, 901, 014).
    • The create-organization panel has focus management (A11Y-006).
    • Fields carry aria-invalid and aria-describedby (A11Y-007).
    • Tabs and radio switches support the arrow keys (A11Y-009).
    • Map markers are named and can be focused (A11Y-003).
    • Search boxes have labels (A11Y-008).
  • Auth UX (ADM-BUG-040, 041, 042), client state only:
    • A "Signing out..." state.
    • A non-operator session is treated as forbidden, and a forbidden or failed exchange clears the stored operator.
    • Concurrent bootstraps share one exchange.
    • The Access exchange, CSRF and cookies are unchanged.
  • Per-section fixes:
    • Org decisions only for pending applications (ADM-BUG-012).
    • Clearable contacts, and only edited contacts are sent (ADM-BUG-021).
    • The report chat pages older messages (ADM-BUG-018).
    • Verify-and-send is chained with an awaited call (ADM-BUG-017, client part).
    • Double-submit guards on Ctrl/Cmd+Enter.
    • Contract-validated bag counts.
    • Home previews no longer depend on the summary (ADM-BUG-031); the mail tile merges newest first (030) with relative ages (900).
    • Analytics rounding and CSV percentages.
    • The public page path shows as /e/slug.
    • Mail and inbox list counts are honest.
    • Attachment links are refetched before their 15-minute presign expires.
    • Unknown page and broadcast statuses fall back to a neutral pill (ADM-BUG-906).
  • The AGPL source link appears on every section page (ADM-BUG-046).
  • The Users copy-ID menu item's hover color referenced an undefined --ink-1 variable; it now darkens to --ink (ADM-CSS-004).

Decisions for the reviewer

  • Selection rule:
    • A pick hidden by a filter stays open (Host messaging has no single-host read, so it clears instead).
    • An item that drops out after a refetch clears.
    • Moderation and gov-claim decisions and "Remove report" clear the pane right away, because the resolved item cannot show that it is resolved.
    • Nothing else opens by itself.
    • Action buttons stay busy until the list has refreshed.
    • Opening an unread message under "Unread" or "Needs attention" keeps it open even though reading it removes it from that filter.
  • Error toasts: a warning icon, 8 seconds, paused while hovered or focused. Mail's per-action fallbacks ("Couldn't send the reply...") are replaced by the server message or "Something went wrong. Please try again."
  • Discovery contacts: "Save draft" now sends only the contacts you edited, so it never overwrites another operator's untouched contact. "Save & route" sends every contact shown, plus a cleared value for any contact you emptied. Re-saving the default email still clears its bounce; that is server behavior and is noted in the handoff to the contract owners.
  • Toasts:
    • Success and error toasts both come from the query cache, so they still show after the pane that fired them closes.
    • Action buttons stay busy until the list refresh lands.
    • "Verify and send" now shows "#id · approved" and then "Sent to …" once the send completes.
  • Queries: requests no longer retry RATE_LIMITED or CONFLICT responses.
  • CSV: files carry a UTF-8 byte order mark and charset=utf-8.
  • Auth, client state only: a session whose user is not an operator shows the "Not authorized" screen instead of an empty shell. The backend already refuses these, so this is defense in depth.

User-visible copy changes

Where Before After
Error toasts (mail) "Couldn't send the reply. Please try again." and similar server message, or "Something went wrong. Please try again.", in the error tone
Network failure (lists, picker) "Failed to fetch" "Could not reach the server. Check your connection and try again."
Users, missing account "Could not load this" "User not found" / "No account has this ID."
Users, message "[deleted by user]" "Removed"
Users, event roles "Joined the X" "Co-hosted / Coordinated / Staffed the X", plus "Co-host", "Coordinator" and "Staff" pills
Users, copy ID "Couldn't copy — select the ID manually" "Couldn't copy. Select the ID manually." (error tone)
Counts "1 neighbors", "1 reports", "1 bags", "1 attendees" singular forms
Moderation dialogs "Dismiss report" / "Approve", "Remove content", "Hold for review" "Keep " / "Approve ", "Remove ", "Hold for review"
Moderation toast " · report dismissed" " · kept"
User picker "Search by name, @handle or email…" "Search by name, handle or city…"; new "Everyone matching is already a member."
Org verification "This application was already decided. Deciding again overwrites..." "No application is awaiting a decision." (or the deleted-org note); Verify and Reject hidden
Org detail nothing "Could not refresh this organization. Showing what was last loaded." with "Try again"
Org owner row "Transfer ownership before removing" "Transfer ownership before changing this role or removing"
Home "Loading dashboard..." / "Could not load the dashboard" "Loading summary..." / "Could not load the dashboard summary"; new "Could not load outreach mail" / "Could not load the inbox"
Home, mail ages raw timestamp "5m" / "2h"
Populations "3899k" "3.9M"
Analytics "24h", "1d 24h"; CSV "41.6" "1d", "2d"; CSV "42%"; empty strip "No data yet"
Hosts "That host is not in this window" after filtering "No host selected"; "No events to show" when the log fails
Signup pages "/slug" "/e/slug"
Report detail tooltip-only reasons visible reasons; new "This report is already rejected."; "Load older messages"
Mail "Reply to …"; sizes "20480k" "Reply to …"; "20 MB"
Jurisdictions chips "0" while loading "Loading…"; new handle error; partial-save error; "Clearing the default email or form URL here does not remove the saved one."
Sign out error screen flash "Signing out..."

Tests changed

  • Flipped PR Admin test harness and characterization safety net #38 "(current behavior)" pins:
    • deep-link overrides (users, events, moderation, mail), which now keep the deep-linked item;
    • list/infinite cache collisions, which now use separate entries;
    • ui-store trailing slash, query string and nav("home", x), which now keep no focus;
    • prompt focus restore;
    • errorMessage quirks: a non-Error value gives the fallback, and the toString field bug is fixed.
  • Selection tests that pinned per-page variants now assert the one shared rule. The old and new names are listed in each commit, for example:
    • "selects the first row of the new list when a filter drops the selected account" became "keeps the selected account open when a filter drops it from the list";
    • "keeps a decided organization on screen after it leaves the verification queue" became "clears a decided organization that leaves the list, without opening another".
  • Discovery payload tests: untouched saved contacts are no longer sent.
  • CSV test: the content type is now text/csv;charset=utf-8.

Verification

  • Node v22.23.2. pnpm lint, pnpm typecheck, pnpm build and pnpm test pass: 98 files, 1,056 tests, 3 consecutive runs.
  • Every fix ships a test that failed on the base first. The output is recorded per area in the campaign notes.
  • Adversarial review: one reviewer per area plus a cross-cutting consistency pass, then a re-review of the fixes. It found and this PR fixed:
    • three error paths shown in the success tone;
    • a discovery save error lost when the pane changes;
    • clearing a contact broken after a save;
    • a created org being replaced by a stale cached row;
    • three inconsistent selection rules;
    • a Tailwind utility class in the markup;
    • the Dismiss button inside the live region;
    • "Save & route" sending no contacts;
    • resolved moderation items keeping live buttons;
    • unread mail closing as it was opened;
    • success toasts lost when the pane unmounted during the refresh.
  • This PR has no CI until Admin repo hygiene: docs, env example, unused deps, favicon #32 (CI on stacked PRs) merges.

🤖 Generated with Claude Code

…uth bootstrap, map and dialog fixes, source link on every page
@byteful byteful closed this Sep 24, 2026
@byteful
byteful deleted the fix/admin-campaign-04-correctness branch September 24, 2026 20:13
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.

2 participants