From 23e214a85c8736d271696ed1ac3c1df778414ff2 Mon Sep 17 00:00:00 2001 From: archipelago Date: Tue, 14 Jul 2026 09:16:25 -0400 Subject: [PATCH] =?UTF-8?q?fix(ui):=20login=20page=20refresh=20loop=20?= =?UTF-8?q?=E2=80=94=20guard=20401=20redirect=20+=20gate=20background=20po?= =?UTF-8?q?lls=20on=20auth?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The global mesh-detection poll (added for the device-detected modal) fired on the login page, 401'd, and the session-expired handler redirected to /login — reloading the page we were already on, forever. Background polls now run only when authenticated, and a 401 never redirects when the path is already /login. Co-Authored-By: Claude Fable 5 --- neode-ui/src/api/rpc-client.ts | 6 +++++- neode-ui/src/stores/mesh.ts | 9 +++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/neode-ui/src/api/rpc-client.ts b/neode-ui/src/api/rpc-client.ts index 3735d153..13d5b6b9 100644 --- a/neode-ui/src/api/rpc-client.ts +++ b/neode-ui/src/api/rpc-client.ts @@ -117,8 +117,12 @@ class RPCClient { try { localStorage.removeItem('neode-auth') } catch { /* noop */ } const isOnboarding = window.location.pathname.startsWith('/onboarding') + // Already on the login page: redirecting = a full reload of the + // page we're on. Any background poll that 401s would refresh + // /login in a loop forever (seen with the global mesh poll). + const isLogin = window.location.pathname.startsWith('/login') console.warn(`[RPC] 401 on ${method} | path=${window.location.pathname} | onboarding=${isOnboarding} | redirecting=${RPCClient._sessionExpiredRedirecting}`) - if (!isOnboarding && !RPCClient._sessionExpiredRedirecting) { + if (!isOnboarding && !isLogin && !RPCClient._sessionExpiredRedirecting) { RPCClient._sessionExpiredRedirecting = true console.warn(`[RPC] Session expired — redirecting to /login in 300ms`) setTimeout(() => { diff --git a/neode-ui/src/stores/mesh.ts b/neode-ui/src/stores/mesh.ts index ec6e2b6a..f8cfc670 100644 --- a/neode-ui/src/stores/mesh.ts +++ b/neode-ui/src/stores/mesh.ts @@ -280,9 +280,14 @@ export const useMeshStore = defineStore('mesh', () => { * (the Mesh view's own 5s poll takes over while it is mounted). */ function startGlobalDetection(intervalMs = 15000) { if (globalDetectTimer) return - void fetchStatus() + // Never poll unauthenticated — a 401 from a background poll on the + // login page caused a login-refresh loop (2026-07-14, .116). + const authed = () => { + try { return !!localStorage.getItem('neode-auth') } catch { return false } + } + if (authed()) void fetchStatus() globalDetectTimer = setInterval(() => { - if (document.visibilityState === 'visible') void fetchStatus() + if (document.visibilityState === 'visible' && authed()) void fetchStatus() }, intervalMs) }