From d041c8648d7c17872868772c3dcbcce42d4cd1b7 Mon Sep 17 00:00:00 2001 From: Logan Cusano Date: Tue, 18 Aug 2026 23:25:24 -0400 Subject: [PATCH] Run every hook before the admin guard on /nodes and /systems Both pages crashed to a blank "client-side exception" screen in production. React error #310: the useState calls sat *below* `if (authLoading || (!isAdmin && !isOperator)) return null`, so the first render returned before reaching them and the next render, once auth resolved, ran more hooks than the previous one. React tracks hooks by call order and refuses. The guard itself is fine and stays where it is -- only the hook declarations move above it. Behaviour is unchanged for a user who passes the guard, and a user who fails it still renders nothing before the effect redirects them. Found by walking the deployed site: /nodes and /systems were the only two routes that failed outright rather than merely showing empty data. The empty data everywhere else is the org_id backfill, which is a separate problem. npx tsc --noEmit clean. Co-Authored-By: Claude Opus 5 --- drb-frontend/app/nodes/page.tsx | 7 ++++++- drb-frontend/app/systems/page.tsx | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/drb-frontend/app/nodes/page.tsx b/drb-frontend/app/nodes/page.tsx index 9391b03..3478b87 100644 --- a/drb-frontend/app/nodes/page.tsx +++ b/drb-frontend/app/nodes/page.tsx @@ -19,9 +19,14 @@ export default function NodesPage() { if (!authLoading && !isAdmin && !isOperator) router.replace("/dashboard"); }, [authLoading, isAdmin, isOperator, router]); - if (authLoading || (!isAdmin && !isOperator)) return null; const [configNode, setConfigNode] = useState(null); + // Every hook must run before this guard. React tracks hooks by call order, + // so returning early on the first render and then reaching a useState on the + // next one is error #310 ("rendered more hooks than during the previous + // render") -- which crashed this whole page to a blank client-exception + // screen the moment auth resolved. + if (authLoading || (!isAdmin && !isOperator)) return null; const systemMap = Object.fromEntries(systems.map((s) => [s.system_id, s])); const pending = nodes.filter((n) => !n.configured); diff --git a/drb-frontend/app/systems/page.tsx b/drb-frontend/app/systems/page.tsx index 1d2758c..9643032 100644 --- a/drb-frontend/app/systems/page.tsx +++ b/drb-frontend/app/systems/page.tsx @@ -1188,10 +1188,15 @@ export default function SystemsPage() { if (!authLoading && !isAdmin && !isOperator) router.replace("/dashboard"); }, [authLoading, isAdmin, isOperator, router]); - if (authLoading || (!isAdmin && !isOperator)) return null; const [editing, setEditing] = useState(null); const [editIsDuplicate, setEditIsDuplicate] = useState(false); + // Every hook must run before this guard. React tracks hooks by call order, + // so returning early on the first render and then reaching a useState on the + // next one is error #310 ("rendered more hooks than during the previous + // render") -- which crashed this whole page to a blank client-exception + // screen the moment auth resolved. + if (authLoading || (!isAdmin && !isOperator)) return null; async function handleDelete(id: string) { if (!confirm("Delete this system?")) return; await c2api.deleteSystem(id);