"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 { c2api } from "@/lib/c2api"; 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("/"); }, [authLoading, isAdmin, isOperator, router]); const [configNode, setConfigNode] = useState(null); const [updatingAll, setUpdatingAll] = useState(false); async function handleUpdateAll() { if ( !confirm( "Update every approved node? Each one pulls the latest release it tracks and recreates its " + "containers (radio capture pauses for about a minute per node). Online nodes start within " + "seconds; offline nodes pick it up on their next check-in (up to ~15 minutes).", ) ) return; setUpdatingAll(true); try { const r = await c2api.requestUpdateAllNodes(); const offline = r.requested.filter((n) => !r.mqtt_sent.includes(n)); alert( `Update requested for ${r.requested.length} node(s).` + (offline.length ? ` Not reachable over MQTT right now (will update on next check-in): ${offline.join(", ")}.` : "") + (r.skipped.length ? ` Skipped (not approved): ${r.skipped.join(", ")}.` : ""), ); } catch (err) { alert(err instanceof Error ? err.message : "Update request failed."); } finally { setUpdatingAll(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; const systemMap = Object.fromEntries(systems.map((s) => [s.system_id, s])); const pending = nodes.filter((n) => !n.configured); return (

Nodes

{isAdmin && ( )}
{pending.length > 0 && (

Needs Configuration ({pending.length})

{pending.map((n) => ( // Configuring is admin-only on the server (server-26#222); for // everyone else the card links to the node page instead. isAdmin ? (
setConfigNode(n)} className="cursor-pointer">
) : ( ) ))}
)}

All Nodes ({nodes.length})

{loading ? (

Loading…

) : nodes.length === 0 ? (

No nodes registered yet. Boot a Pi to get started.

) : (
{nodes.map((n) => ( ))}
)}
{configNode && ( setConfigNode(null)} /> )}
); }