Merge pull request #37 from AnsoCode/fix/node-context-hydration

fix(frontend): sync NodeContext with localStorage on initial load
This commit is contained in:
Anso
2026-03-19 19:23:13 -04:00
committed by GitHub
2 changed files with 16 additions and 10 deletions
+1
View File
@@ -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.
+15 -10
View File
@@ -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');
}
}
}