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) }