Fix the UX issues the new test suite found - #596
Draft
sandhose wants to merge 13 commits into
Draft
Conversation
sandhose
force-pushed
the
quenting/ux-fixes
branch
from
August 24, 2026 08:08
3d8ef6b to
a4d25e7
Compare
Deploying admin-beta-element-dev with
|
| Latest commit: |
38f77a7
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://da79e010.admin-beta-element-dev.pages.dev |
| Branch Preview URL: | https://quenting-ux-fixes.admin-beta-element-dev.pages.dev |
sandhose
force-pushed
the
quenting/ux-fixes
branch
from
August 24, 2026 08:31
a4d25e7 to
61dc06a
Compare
sandhose
force-pushed
the
quenting/ux-fixes
branch
2 times, most recently
from
August 24, 2026 09:27
4abba5b to
81d039e
Compare
sandhose
force-pushed
the
quenting/ux-fixes
branch
2 times, most recently
from
August 25, 2026 08:39
7a3d256 to
9af3b28
Compare
A failing MAS request threw whatever the generated client decoded. A
structured error body came out as an object, so the error screen printed
the raw JSON with the message buried in it; a body that is not a MAS error
came out as the raw upstream text, with no hint of what went wrong; and an
empty body became the literal "{}".
The MAS client now throws `MasApiError` with the titles the service listed,
falls back to the HTTP status error when the body carries no usable list,
and the error screen replaces an empty stringified body with a sentence
saying the server gave no details. That fallback attaches the body as a
cause, which the error screen renders as a sibling paragraph rather than
nesting one inside another.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Declining consent at the authorization server sent the browser back with `error=access_denied`, which the callback rethrew as the provider's raw `error_description` — a technical sentence for something the user chose to do. On the login page, a browser outside a secure context withholds Web Crypto, so building the PKCE challenge throws, the redirect never happens and "Get started" looks broken with nothing said. A failure to start the flow passed silently either way. The callback throws a localized error, saying the sign-in was cancelled for `access_denied` — keeping the server's description on the non-localized message, where the error screen still exposes it — and quoting that description otherwise. The login form checks `isSecureContext` up front, disables the submit button and explains that the page has to be loaded over https, and a failed start is now logged and reported under the field, until the server name is edited. Neither of those marks the field itself invalid: the server name is not what is wrong. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three loaders fired requests the page then had no use for. The dashboard asked api.github.com for the latest ESS release on every deployment, even where the tile that shows it never renders — and the component asked again, unconditionally, undoing any gate the loader put on it. /supervision probed the adminbot endpoint before knowing the edition, where it 404s outside Pro. The federation destination drawer prefetched the destination's own well-known in parallel with looking the destination up, so any string in the URL made the admin's browser fetch that host. Each of the three now waits for the answer that decides whether the request is worth making: the ESS version before the GitHub release, in the loader and in the component, the edition before the adminbot config, and the destination itself before its well-known. The mocks pin the first two gates: the GitHub release is handled on the ESS deployments only and the adminbot config on ESS Pro only, so a run against a deployment which should not make those calls fails on the unhandled request. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both token list pages and both token drawers carried their own copy of a status badge, and the copies had already drifted: the registration one short-circuited on the server-computed `valid`, so a token that expired while the page was open kept reading "Active" until the server recomputed the flag, and it painted revoked, expired and used-up tokens red — the colour that should mean something went wrong, not that a token reached a normal end of life. The badges move to src/ui/token-status-badge.tsx, one per token kind. The attribute checks run before the server flag, so the first render after an expiry tells the truth without waiting on the server, and grey covers the ends of life with red kept for a token the server rejects for a reason none of the attributes explain. An expiry the browser cannot parse counts as past rather than as still active. The registration list's "Valid Until" column becomes "Expires at" to match the drawer, and the personal token columns drop their title casing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Removing a federation allowlist entry and revoking either kind of token happened on the first click, with no confirmation, even though the first stops the server federating with a whole domain and the second breaks everything using the token immediately. The allowlist trigger was an icon-only button whose label lived in a tooltip, so it had no accessible name. When either token mutation failed nothing was said at all: the revoke, unrevoke and edit handlers had no `onError`, so the request just sank. All three destructive actions now open a confirmation dialog naming what they affect, and the allowlist trigger carries an `aria-label`. A failure raised while a dialog is open is rendered inside it, through `Dialog.ErrorAlert`: an open dialog marks the app root `aria-hidden`, so a toast fired from there reaches nobody using a screen reader. The dialog stays open on the failure, which is what makes the alert the feedback. The allowlist remove already reported its failure as a toast, and that toast moves into the dialog with the rest. Successes are reported by toast: the personal token keeps the one it had, and revoking or editing a registration token gains one. Unrevoke has no dialog of its own, so its failure stays a toast too. The specs cover cancelling both revoke dialogs, and a failing revoke endpoint showing the error with the token untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A newly created or regenerated personal token shows its secret exactly once. The dialog holding it closed on Escape, on a click outside and on the corner close button, so the only copy of the token could be lost by a stray keypress — and the button that closed it read "Cancel" or "Close", as if the token had not already been created. `Dialog.Root` takes a `dismissible` prop that turns off Escape, outside clicks and the close button, and both token dialogs set it while the secret is on screen. The remaining button becomes a primary "Done", closing through the dialog's own open state: on a phone or tablet, where the dialog renders as a vaul drawer, a non-dismissible drawer swallows `Dialog.Close` too, which would leave no way out at all. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two buttons are disabled and need to explain why: Regenerate on a personal token you do not own, and Edit properties on a revoked registration token. Compound buttons are `aria-disabled` rather than natively disabled, so a tooltip on one does open — but Regenerate passed its explanation as `label`, which renamed the button to it, and Edit properties had no tooltip at all: on a revoked token it was a bare disabled dialog trigger, so the dialog could never be opened and nothing said so. The Regenerate tooltip becomes a `description`, leaving the button its own name, and the revoked branch of the edit modal returns the disabled button inside a new tooltip saying what to do about it, instead of a dialog nobody can reach. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Alias column fell back to the room ID when a room had no canonical alias, presenting an identifier as something it is not and duplicating the Room ID field in the drawer. The column headed "Type" showed Public, Restricted or Private, which is the join rule, not a room type. Member counts were interpolated raw in the list while the drawer formatted its own, so the same number appeared two ways. The drawer's creator was a bare Matrix ID with nothing to follow. The alias cell renders an em dash when there is none, the column is renamed Visibility, and both member counts go through the locale's number formatting. A creator from this server links to it in the user list, searching for the username — the filter matches that, not the full Matrix ID. A creator from another server stays plain text, and a room created under room version 11 or later, which reports no creator at all, gets the em dash. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A destination that has never been retried has `retry_last_ts` and `retry_interval` of 0, which the drawer rendered as an em dash in both fields — the same glyph it uses for a value it does not have, so "never retried" and "unknown" looked alike. The last successful stream ordering was interpolated raw, so a seven-digit number came out ungrouped. The drawer also repeated the destination name as its first data item, right under the heading that already says it. The two retry fields read "Never" and "None" — no backoff is the healthy state, so the interval avoids wording that sounds like a fault under a green badge — the stream ordering goes through `Data.NumericValue`, and the duplicated Destination item is gone. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The user drawer led with the mxid in a bold paragraph and put the display name underneath in smaller text, so the identifier outranked the name and neither was a heading. The Status badge sat directly inside the data item, next to its term but outside any definition: `Data.Grid` and its children are divs carrying `list`, `listitem`, `term` and `definition` roles, so a value outside `Data.Value` leaves the term with nothing paired to it. "Locked At" was the only title-cased label in the drawer. The display name becomes the drawer's h3, falling back to the mxid when there is none, with the mxid below it. The badge moves inside `Data.Value` and the label reads "Locked at". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Five strings said something other than what was true. The /supervision launch button reused the generic "Sign in" label, which reads as signing in to the console you are already in. Closing the supervision popup before it finished raised a critical alert blaming the user for closing it "too quickly". The disabled-feature alert told an ESS Pro admin the feature is part of their subscription without saying it is off on this deployment. The Secure Border Gateway alert pitched SBG as an ESS Pro feature to admins who already have ESS Pro. And the dashboard's rooms total carried the uptime tile's translator description. Each now says what it means: the button names the interface it opens, the closed-popup alert is informational and offers to try again, both deployment alerts point at the configuration — with the SBG description carrying only the action, since its title already says the feature is off — and the description matches its message. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The sidebar entry for Registration tokens links to `?revoked=false`, but the page's filter definition had no entry for that key, so the list arrived already filtered with nothing on screen saying so and no way to widen it short of editing the URL. `revoked: false` joins the filter definition as "Not revoked", which gives it a chip, a menu item and the usual "Clear". A test walks the sidebar link and checks the chip appears and Clear drops it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The application details drawer already links to the devices list filtered to that application, but there was no way to get from it to the people using it — the users list accepts exactly that filter, and only a hand-written URL reached it. The drawer gains a "View users on this application" button next to the existing devices one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
sandhose
force-pushed
the
quenting/ux-fixes
branch
from
August 25, 2026 10:26
9af3b28 to
38f77a7
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The quick fixes from the UX pass over the test suite (the larger items are in the follow-up backlog):
108 non-screenshot / 132 container tests at the tip, with three new specs for the confirm dialogs. Four screenshot baselines regenerate across this and the a11y PR, each in the commit that changes that page's pixels.