Skip to content

🚩 fix: Non-Admin Org Recovery Follow-Ups (loader gate, switch completion, switcher UX) - #107

Closed
dustinhealy wants to merge 4 commits into
agent/admin-org-access-recoveryfrom
agent/admin-org-access-recovery-fixes
Closed

🚩 fix: Non-Admin Org Recovery Follow-Ups (loader gate, switch completion, switcher UX)#107
dustinhealy wants to merge 4 commits into
agent/admin-org-access-recoveryfrom
agent/admin-org-access-recovery-fixes

Conversation

@dustinhealy

@dustinhealy dustinhealy commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Follow-ups to the non-admin organization recovery flow on #106. Exercising that flow end to end surfaced two functional gaps in its current implementation and led to several UX refinements to the organization switcher.

Functional changes:

  1. The admin capability check runs in the component while capabilities are still loading, so the full panel renders for a moment before the access-denied screen. This PR resolves the check in the route loader, so the layout only ever renders a decided state.
  2. A successful org switch invalidated only the org-list query, so the newly minted admin session was not reflected and the screen stayed on access-denied until a manual reload. This PR invalidates all queries and re-runs the loaders, so the switch completes client-side.

Switcher UX refinements:

  1. One administered org now shows a single Continue action instead of a one-item dropdown.
  2. The dropdown shows its placeholder when nothing is selected, and the screen is vertically centered.
  3. Selecting an org no longer switches on its own; a separate Switch button confirms, and a named Switching to <Org>... interstitial covers the transition.

Change Type

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)

What changed

  • Resolve the capability gate in the _app route loader with queryClient.ensureQueryData, read it via useSuspenseQuery, and share one capabilitiesQueryOptions between the loader and useCapabilities.
  • Type the root route context (createRootRouteWithContext<{ queryClient }>) so the loader can reach the query client.
  • On switch, invalidateQueries() (all) against the new session and re-run the loaders; no hard reload, no stale cross-org data.
  • Rework OrganizationSwitcher: single Continue button for one org; placeholder dropdown plus a separate Switch button for several (selection is inert); a full-screen Switching to <Org>... interstitial; prefetch the org list in the loader.
  • Vertically center the recovery screen; render the dropdown placeholder when nothing is selected.

Testing

Test configuration

Verified end to end in an isolated container (node:22): a stateful mock of the admin API (/api/admin/verify, /grants/effective, /orgs, /orgs/switch) with runtime toggles for zero / one / multiple admin orgs, driven by a Playwright script through a genuine login. A MutationObserver installed before app JS runs timestamps DOM markers, which is the only reliable way to catch a sub-frame chrome flash.

1. Access-denied gate (before / after)

MutationObserver timeline at real timing. Base branch: Loading (824ms) -> full dashboard (1316ms) -> Access denied (1360ms), a ~44 ms window with the panel on screen for a non-admin. This PR: Loading (661ms) -> Access denied (1192ms), CHROME_MARKERS=[].

Base branch: the full panel renders while capabilities load, then is replaced by the access-denied screen

clip-base-flash.webm

With this change: neutral loading only, then the access-denied screen; no panel is shown.

02-flash-after

2. Recovery screen on the base branch (#106)

Base branch: recovery screen is top-aligned, the dropdown trigger renders blank, and a single administered org still shows a dropdown.

03-base-recovery

Base branch: the dashboard appeared only after a manual reload.

clip-base-stuck-then-reload.webm

3. Recovery screen with this change

One administered org: a Continue button; screen vertically centered.

06-one-org-continue

Multiple orgs: placeholder dropdown plus a separate Switch button; centered.

07-multi-landing

Multiple orgs: expanded dropdown.

08-multi-expanded

Multiple orgs: selecting only fills the dropdown and enables Switch; no navigation yet.

09-multi-picked

4. Org switch transition (with this change)

A named Switching to <Org>... interstitial covers the mutation, refetch, and reroute

clip-switch-flow.webm
10-switching-interstitial

Lands on the dashboard client-side, no reload.

11-landed-dashboard

5. No administered orgs (behavior unchanged, verified)

Only the Return to LibreChat link, no switcher.

12-zero-orgs

Iterations, in order

Layout and placeholder: on the base branch the screen was top-aligned and the dropdown trigger rendered blank (section 2); this PR centers the screen and shows the placeholder (section 3).

Single administered org: the base branch always rendered a dropdown, so one org meant a one-item dropdown (section 2); this PR shows a direct Continue button (section 3).

Selecting vs switching: on the base branch selecting an org switched immediately, which also left the user on access-denied because the switch did not complete (section 2); this PR makes selection inert, adds an explicit Switch button (section 3), and completes the switch client-side through a named interstitial (section 4).

Local gates

eslint src/ --fix, prettier --write, and tsc --noEmit are clean for src/.

Checklist

  • My code adheres to this project's style guidelines
  • I have performed a self-review of my own code
  • I have commented in any complex areas of my code
  • My changes do not introduce new warnings
  • I have written tests demonstrating that my changes are effective or that my feature works

Behavioral verification used a reusable Playwright plus mock-backend harness (above) rather than committed specs. Porting it into e2e/ as regression specs (non-admin recovery, target-org authorization, no-admin fallback, switch transition) is a sensible follow-up and would satisfy the AI-1663 acceptance criterion for tests.

Resolve the admin capability check in the _app route loader via
ensureQueryData and read it with useSuspenseQuery, so the layout renders a
decided state instead of flashing the full panel before the access-denied
screen. Type the root route context so the loader can reach the query
client.

Complete an org switch entirely client-side: invalidate every query
against the newly minted session and re-run the loaders, so the panel
transitions to the dashboard without a hard reload and without leaving the
user stranded on the recovery screen.

Make the recovery screen legible: a single Continue action when the actor
administers one org, a select-then-confirm dropdown when they administer
several (selecting no longer switches on its own), and a named "Switching
to X" interstitial during the transition. Show the placeholder in the
empty dropdown, vertically center the screen, and prefetch the org list so
the control is present on first paint.
@dustinhealy dustinhealy changed the title fix(admin): non-admin org recovery follow-ups (loader gate, switch completion, switcher UX) 🚩 fix: Non-Admin Org Recovery Follow-Ups (loader gate, switch completion, switcher UX) Aug 5, 2026
The 403 branch preserves the authenticated session and returns
accessDenied so the panel can offer org recovery, rather than clearing
the session. Update the test that still asserted the removed
clear-session outcome.
…nts, sort imports

The switch transition already spans the mutation and refetch, so the
minimum-interstitial timer is removed. Imports sorted per the repo script.
@dustinhealy
dustinhealy force-pushed the agent/admin-org-access-recovery-fixes branch from d620f64 to d436d43 Compare August 5, 2026 02:35
@danny-avila
danny-avila marked this pull request as ready for review August 5, 2026 13:42
Comment thread src/routes/_app.tsx Outdated
Comment thread src/routes/_app.tsx
Comment thread src/components/shared/OrganizationSwitcher.tsx
Comment thread src/components/shared/OrganizationSwitcher.tsx
Comment thread src/components/shared/OrganizationSwitcher.tsx
Keep the recovery screen reachable when the admin-org prefetch fails by
letting that warm-up reject silently, so a backend hiccup no longer routes
non-admin users to the error page instead of the access-denied screen.

Guard the switch itself: disable the switch actions while a switch is
pending, clear the interstitial if the post-switch invalidation or
navigation rejects, and evaluate the interstitial before the empty-list
short-circuit so it stays visible for the whole transition.
@dustinhealy

Copy link
Copy Markdown
Contributor Author

@codex review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 7208a01. Configure here.

Comment thread src/routes/_app.tsx
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. What shall we delve into next?

Reviewed commit: 7208a01efa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@dustinhealy

Copy link
Copy Markdown
Contributor Author

Continued in #109, which carries the generic part of this change against main. This PR's base branch belongs to the closed #106, so it cannot merge as-is.

@dustinhealy dustinhealy closed this Aug 7, 2026
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