From 405f9cd921f0c4868a4e2c4e774f2a598002b001 Mon Sep 17 00:00:00 2001 From: Anso Date: Tue, 28 Apr 2026 09:02:08 -0400 Subject: [PATCH] perf(frontend): parallelize auth bootstrap fetches (#826) * perf(frontend): parallelize auth bootstrap fetches AuthContext.checkAuth previously chained three sequential fetches: /api/auth/status, then /api/auth/check, then /api/permissions/me. The status check has to come first because its response decides whether to early-return on the setup or mfa-pending path. The next two are independent for an authenticated session and were costing an extra round-trip on every cold load. Run /api/auth/check and /api/permissions/me in parallel via Promise.all. The permissions request is wasted on the rare not-authenticated path (cookie expired or logged out) but that trade-off is worth saving the round-trip on the common success path. The .catch(() => null) on the permissions fetch and the inner try/catch around .json() preserve the original fault tolerance: a network failure or malformed body falls back to no permissions data, with the global role still authoritative. * fix(frontend): commit auth state without waiting on /permissions/me The previous Promise.all awaited both fetches before calling setAppStatus('authenticated'), so a slow /api/permissions/me would delay the dashboard commit relative to the old serial code. The E2E deploy-feedback tests at e2e/deploy-log-panel.spec.ts:168 and :236 race the dashboard render after page.reload() and were timing out waiting for GET /api/stacks/ because the click target was not yet wired when the slower permissions response held up state. Keep both requests in parallel on the wire, but await only the auth check before committing state. The permissions promise resolves in the background and updates state via void permsPromise.then() so the non-critical request never gates the bootstrap. --- frontend/src/context/AuthContext.tsx | 29 ++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/frontend/src/context/AuthContext.tsx b/frontend/src/context/AuthContext.tsx index ed02af48..0185c21f 100644 --- a/frontend/src/context/AuthContext.tsx +++ b/frontend/src/context/AuthContext.tsx @@ -71,25 +71,30 @@ export function AuthProvider({ children }: { children: ReactNode }) { return; } - // Then check if already authenticated - const authResponse = await fetch('/api/auth/check', { - credentials: 'include', - }); + // Auth check and permissions fetch are independent for an authenticated + // session, so fire both on the wire at the same time. Await only the + // auth check before committing app state — otherwise a slow + // /permissions/me delays setAppStatus('authenticated') and races + // post-reload UI that expects the dashboard to commit promptly. The + // permissions promise updates state in the background when it resolves. + const authPromise = fetch('/api/auth/check', { credentials: 'include' }); + const permsPromise = fetch('/api/permissions/me', { credentials: 'include' }).catch(() => null); + const authResponse = await authPromise; if (authResponse.ok) { const data = await authResponse.json(); setUser(data.user ?? null); setAppStatus('authenticated'); - // Fetch effective permissions - try { - const permsRes = await fetch('/api/permissions/me', { credentials: 'include' }); - if (permsRes.ok) { - setPermissions(await permsRes.json()); + void permsPromise.then(async (res) => { + if (res?.ok) { + try { + setPermissions(await res.json()); + } catch { + // Permissions fetch is non-critical — fallback to global role only + } } - } catch { - // Permissions fetch is non-critical — fallback to global role only - } + }); } else { setUser(null); setPermissions(null);