From f400959a4b633f3a230ecd88bc17f7331d3405e4 Mon Sep 17 00:00:00 2001 From: SaelixCode Date: Thu, 19 Mar 2026 18:36:27 -0400 Subject: [PATCH] fix(frontend): sync NodeContext with localStorage on initial load --- CHANGELOG.md | 1 + frontend/src/context/NodeContext.tsx | 25 +++++++++++++++---------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 780d3781..54fc65c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +- **Fixed:** A bug where the active node UI dropdown would desync from the actual API requests on initial page load by properly hydrating state from localStorage. - **Fixed:** Remote node proxy forwarding the browser's `sencho_token` cookie to the remote Sencho instance — the remote's `authMiddleware` evaluates `cookieToken || bearerToken` and the cookie (signed with the local JWT secret) was validated before the valid Bearer token, causing 401 on all proxied API calls. Fixed by stripping the `cookie` header in `proxyReq` so only the Bearer token is used for remote authentication. - **Fixed:** `nodeContextMiddleware` blocking `/api/nodes` when `x-node-id` references a deleted/non-existent node — the nodes list endpoint must always succeed so the frontend can re-sync a stale node ID in localStorage; exempted alongside `/api/auth/`. - **Fixed:** Remote node proxy stripping the `/api` path prefix — `remoteNodeProxy` is mounted at `app.use('/api/', ...)` so Express strips that prefix from `req.url` before `http-proxy-middleware` sees it; added `pathRewrite: (path) => '/api' + path` to restore the full path when forwarding to the remote Sencho instance (e.g. `/stats` → `/api/stats`). This was the root cause of all remote API calls returning the remote's SPA HTML instead of JSON. diff --git a/frontend/src/context/NodeContext.tsx b/frontend/src/context/NodeContext.tsx index e70ffc84..2d368ca4 100644 --- a/frontend/src/context/NodeContext.tsx +++ b/frontend/src/context/NodeContext.tsx @@ -40,24 +40,29 @@ export function NodeProvider({ children }: { children: React.ReactNode }) { const currentActive = activeNodeRef.current; if (!currentActive) { - const defaultNode = data.find((n: Node) => n.is_default); - if (defaultNode) { - setActiveNodeState(defaultNode); - } else if (data.length > 0) { - setActiveNodeState(data[0]); + // On initial load, restore from localStorage before falling back to default. + // This keeps the UI dropdown in sync with the node ID that apiFetch is already + // injecting via x-node-id (read directly from localStorage on every request). + const storedId = localStorage.getItem('sencho-active-node'); + const storedNode = storedId ? data.find((n: Node) => n.id === parseInt(storedId, 10)) : null; + const nodeToActivate = storedNode ?? data.find((n: Node) => n.is_default) ?? data[0] ?? null; + if (nodeToActivate) { + setActiveNodeState(nodeToActivate); + localStorage.setItem('sencho-active-node', String(nodeToActivate.id)); } } else { const updatedActive = data.find((n: Node) => n.id === currentActive.id); if (updatedActive) { setActiveNodeState(updatedActive); + localStorage.setItem('sencho-active-node', String(updatedActive.id)); } else { - const defaultNode = data.find((n: Node) => n.is_default); - if (defaultNode) { - setActiveNodeState(defaultNode); - } else if (data.length > 0) { - setActiveNodeState(data[0]); + const fallback = data.find((n: Node) => n.is_default) ?? data[0] ?? null; + if (fallback) { + setActiveNodeState(fallback); + localStorage.setItem('sencho-active-node', String(fallback.id)); } else { setActiveNodeState(null); + localStorage.removeItem('sencho-active-node'); } } }