Frontend UX: session-expiry handling loses in-progress work; auth feedback gaps #48
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Severity: High. From a frontend UX audit. The session-expiry path is the most damaging daily failure mode for a long-session tool.
Problems
redirectToLogin()(web/src/api/client.ts:6-13) →window.location.assign("/login")(auth-redirect.ts:3-6), a full page reload. If a session expires mid-edit (e.g. duringuseUpdateObject/useSetFields), the form state is destroyed and the login page gives no reason ("session expired"). After re-login the user lands on/objects, not where they were. (The code comment even notes it could be a router navigation.)login-page.tsx:21hardcodesnavigate("/objects");require-auth.tsx:10redirects without capturing the attempted location. Shared deep links always require manual re-navigation after login.require-auth.tsx:8renders an empty<div role="status">during theuseMefetch — a blank white page on first load reads as "broken/hung", plus layout shift when the shell appears.isPending); logout (app-shell.tsx:13-16) navigates only on success with no pending/error feedback.Suggested fixes
?reason=expired&from=<path>so login shows "Your session expired — please sign in again" and returns the user afterward. Preserve SPA state where possible.locationinRequireAuth; honor it on login success.Source: frontend UX/design audit, 2026-06-06.
Fixed in merge
7a43f79.401 → soft redirect (no full reload): the openapi-fetch 401 middleware can't use
useNavigate, so a small navigate-bridge module (api/auth-redirect.ts:setNavigate/redirectToLogin) holds a settable navigate ref; a one-lineNavigationBridgecomponent (mounted via a pathlessRootLayoutroute covering/login+ the authed area) registers React Router'snavigate. A 401 now router-navigates to/login?reason=expired&from=<path>— the React app + query cache survive (nowindow.location.assignteardown). Hard-nav fallback only if a 401 fires before first paint; no-op when already on/login.Return-to-destination: login reads
reason/from, shows a "session expired" notice whenreason=expired, and on success returns to a validatedfrom(open-redirect guardsafeFrom— single-leading-slash local paths only;//host/absolute →/objects).RequireAuthnow redirects unauthenticated users to/login?from=<attempted path>so deep links return after login.Feedback gaps: login submit is disabled while either field is empty; the Sign out item shows a pending state (
auth.signingOut, disabled, menu kept open viacloseOnClick={false}) while logging out — logout errors already surface via the global toast.Note: the audit's "blank screen during auth check" was already fixed earlier —
RequireAuthrenders<AppShellSkeleton/>.Gate: 237 tests pass; typecheck/lint/build clean; check:size 215.9 KB gz; check:colors clean; en/sv parity (2 new keys:
auth.sessionExpired,auth.signingOut); no new dependency;client.tsuntouched.Deferred follow-up: an in-place re-auth modal that keeps the edit form mounted for full field-level preservation of in-progress work (this milestone ships the soft redirect; typed values in an unsaved form are still lost on expiry, but you land back on the same record without a reload).