Files
server-26/drb-frontend/app/nodes/page.tsx
T
Logan CusanoandClaude Opus 5 d041c8648d
Build & Deploy / Build & push images (push) Successful in 4m6s
Build & Deploy / Deploy to VM (push) Successful in 2m25s
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 <noreply@anthropic.com>
2026-08-18 23:25:24 -04:00

79 lines
2.9 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { useNodes } from "@/lib/useNodes";
import { useSystems } from "@/lib/useSystems";
import { NodeCard } from "@/components/NodeCard";
import { NodeConfigModal } from "@/components/NodeConfigModal";
import { useAuth } from "@/components/AuthProvider";
import type { NodeRecord } from "@/lib/types";
export default function NodesPage() {
const { isAdmin, isOperator, loading: authLoading } = useAuth();
const router = useRouter();
const { nodes, loading } = useNodes();
const { systems } = useSystems();
useEffect(() => {
if (!authLoading && !isAdmin && !isOperator) router.replace("/dashboard");
}, [authLoading, isAdmin, isOperator, router]);
const [configNode, setConfigNode] = useState<NodeRecord | null>(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);
return (
<div className="space-y-6">
<h1 className="text-xl font-bold text-white font-mono">Nodes</h1>
{pending.length > 0 && (
<div className="space-y-2">
<h2 className="text-sm font-semibold text-indigo-400 uppercase tracking-wider">
Needs Configuration ({pending.length})
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{pending.map((n) => (
<div key={n.node_id} onClick={() => setConfigNode(n)} className="cursor-pointer">
<NodeCard node={n} system={systemMap[n.assigned_system_id ?? ""]} />
</div>
))}
</div>
</div>
)}
<div className="space-y-2">
<h2 className="text-sm font-semibold text-gray-400 uppercase tracking-wider">
All Nodes ({nodes.length})
</h2>
{loading ? (
<p className="text-gray-600 text-sm font-mono">Loading…</p>
) : nodes.length === 0 ? (
<p className="text-gray-600 text-sm font-mono">No nodes registered yet. Boot a Pi to get started.</p>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{nodes.map((n) => (
<NodeCard key={n.node_id} node={n} system={systemMap[n.assigned_system_id ?? ""]} />
))}
</div>
)}
</div>
{configNode && (
<NodeConfigModal
node={configNode}
systems={systems}
onClose={() => setConfigNode(null)}
/>
)}
</div>
);
}