diff --git a/src/lib/nodes.ts b/src/lib/nodes.ts index 55f4b14..d1d9aa1 100644 --- a/src/lib/nodes.ts +++ b/src/lib/nodes.ts @@ -125,6 +125,15 @@ export const useNodes = (): Node[] => { ); }; +// useNode is the reactive single-node lookup for per-node pages. It subscribes +// via useNodes so the live fleet is fetched and the page re-renders when it +// arrives — a bare getNode() reads the module store synchronously and, on a +// direct load or refresh in live mode, finds an empty store and never recovers. +export const useNode = (name: string | undefined): Node | undefined => { + const all = useNodes(); + return name ? all.find((n) => n.name === name) : undefined; +}; + // The trust chain from the root down to this node, following parentCn. Guards a // missing parent link and a cycle so a broken fixture can't loop forever. export const chainToRoot = (node: Node): Node[] => { diff --git a/src/pages/node-config.tsx b/src/pages/node-config.tsx index 65c7f5f..6790293 100644 --- a/src/pages/node-config.tsx +++ b/src/pages/node-config.tsx @@ -20,11 +20,11 @@ import { Link, Navigate, useParams } from "react-router-dom"; import { ConfigForm } from "@/components/config-form"; import { Button } from "@/components/ui/button"; -import { getNode } from "@/lib/nodes"; +import { useNode } from "@/lib/nodes"; export const NodeConfigPage = () => { const { name } = useParams<{ name: string }>(); - const node = name ? getNode(name) : undefined; + const node = useNode(name); if (!node || node.identityState === "REVOKED") { return ; diff --git a/src/pages/node-detail.tsx b/src/pages/node-detail.tsx index 461d435..e0887e8 100644 --- a/src/pages/node-detail.tsx +++ b/src/pages/node-detail.tsx @@ -22,7 +22,7 @@ import { CertInventory } from "@/components/cert-inventory"; import { NodeDetailPanel } from "@/components/node-detail-panel"; import { Button } from "@/components/ui/button"; import { type IdentityState, roleLabels } from "@/lib/mock"; -import { chainToRoot, getNode } from "@/lib/nodes"; +import { chainToRoot, useNode } from "@/lib/nodes"; import { cn } from "@/lib/utils"; const stateTone: Record = { @@ -33,7 +33,7 @@ const stateTone: Record = { export const NodeDetailPage = () => { const { name } = useParams<{ name: string }>(); - const node = name ? getNode(name) : undefined; + const node = useNode(name); if (!node) { return ( diff --git a/src/pages/node-issue.tsx b/src/pages/node-issue.tsx index 0691483..b6e4ca2 100644 --- a/src/pages/node-issue.tsx +++ b/src/pages/node-issue.tsx @@ -22,11 +22,11 @@ import { Link, Navigate, useParams } from "react-router-dom"; import { IssueForm } from "@/components/issue-form"; import { Button } from "@/components/ui/button"; import { canIssue, type Cert } from "@/lib/certs"; -import { getNode } from "@/lib/nodes"; +import { useNode } from "@/lib/nodes"; export const NodeIssuePage = () => { const { name } = useParams<{ name: string }>(); - const node = name ? getNode(name) : undefined; + const node = useNode(name); const [issued, setIssued] = useState(null); // Remount key: bumping it drops the form's issued/export state so "Issue // another" starts from a clean form (and a fresh, unexportable key). diff --git a/src/pages/node-profiles.test.tsx b/src/pages/node-profiles.test.tsx index 3933370..65b21f4 100644 --- a/src/pages/node-profiles.test.tsx +++ b/src/pages/node-profiles.test.tsx @@ -52,7 +52,7 @@ vi.mock("@/lib/config", () => ({ getNodeConfig: (...args: unknown[]) => getNodeConfig(...args), })); vi.mock("@/lib/nodes", () => ({ - getNode: (name: string) => ({ identityState: "ESTABLISHED", name }), + useNode: (name: string) => ({ identityState: "ESTABLISHED", name }), })); vi.mock("@/lib/profiles", async (importOriginal) => { const actual = await importOriginal(); diff --git a/src/pages/node-profiles.tsx b/src/pages/node-profiles.tsx index 87700ac..78d4f29 100644 --- a/src/pages/node-profiles.tsx +++ b/src/pages/node-profiles.tsx @@ -23,7 +23,7 @@ import { Button } from "@/components/ui/button"; import { useAuth } from "@/context/auth"; import { getNodeConfig } from "@/lib/config"; import { fleetMode } from "@/lib/fleet/mode"; -import { getNode } from "@/lib/nodes"; +import { useNode } from "@/lib/nodes"; import { computeDrift, type DriftRow, type DriftStatus } from "@/lib/profile-drift"; import { applyProfileToNode, fromProtoProfile, useProfiles } from "@/lib/profiles"; import { cn } from "@/lib/utils"; @@ -151,7 +151,7 @@ const DriftView = ({ nodeName }: { nodeName: string }) => { export const NodeProfilesPage = () => { const { name } = useParams<{ name: string }>(); - const node = name ? getNode(name) : undefined; + const node = useNode(name); if (!node) { return ; diff --git a/src/pages/node-rekey.tsx b/src/pages/node-rekey.tsx index 253c24c..5cf5b96 100644 --- a/src/pages/node-rekey.tsx +++ b/src/pages/node-rekey.tsx @@ -20,11 +20,11 @@ import { Link, Navigate, useParams } from "react-router-dom"; import { RekeyWizard } from "@/components/rekey-wizard"; import { Button } from "@/components/ui/button"; -import { getNode } from "@/lib/nodes"; +import { useNode } from "@/lib/nodes"; export const NodeRekeyPage = () => { const { name } = useParams<{ name: string }>(); - const node = name ? getNode(name) : undefined; + const node = useNode(name); if (!node || node.identityState === "REVOKED") { return ; diff --git a/src/pages/root-detail.tsx b/src/pages/root-detail.tsx index ac04007..5baa69d 100644 --- a/src/pages/root-detail.tsx +++ b/src/pages/root-detail.tsx @@ -21,7 +21,7 @@ import { Link, Navigate, useParams } from "react-router-dom"; import { ConfigForm } from "@/components/config-form"; import { RekeyWizard } from "@/components/rekey-wizard"; import { Button } from "@/components/ui/button"; -import { getNode } from "@/lib/nodes"; +import { useNode } from "@/lib/nodes"; const Panel = ({ children, label }: { children: React.ReactNode; label: string }) => (
@@ -43,7 +43,7 @@ const Field = ({ children, label }: { children: React.ReactNode; label: string } export const RootDetailPage = () => { const { name } = useParams<{ name: string }>(); - const node = name ? getNode(name) : undefined; + const node = useNode(name); if (!node || node.role !== "root") { return ;