feat: add node agent system with heartbeat collection, installer generation, and traffic flow telemetry
CI / backend (push) Failing after 3s
CI / frontend (push) Failing after 28s

Add NodeAgent and TrafficFlow models to track agent status and network flows, implement /agents/heartbeat endpoint to receive interface counters, conntrack flows, firewall status, and nftables ruleset hash from agents, add nexafabric-agent.py Python script to collect host telemetry including VM/LXC interface hints via tap/fwbr regex matching, conntrack flow parsing with protocol/state/byte counters, and pve-firewall status checks,
This commit is contained in:
2026-07-09 14:13:47 +02:00
parent 88badf1f22
commit e67174a4ae
8 changed files with 677 additions and 7 deletions
+2 -1
View File
@@ -11,6 +11,7 @@ import { Ipam } from "./pages/Ipam";
import { ListPage } from "./pages/ListPage";
import { Login } from "./pages/Login";
import { Networks } from "./pages/Networks";
import { Nodes } from "./pages/Nodes";
import { Policies } from "./pages/Policies";
import { PolicyDesigner } from "./pages/PolicyDesigner";
import { SecurityGroups } from "./pages/SecurityGroups";
@@ -46,7 +47,7 @@ function AppRoutes() {
<Route element={<Layout />}>
<Route index element={<Dashboard />} />
<Route path="clusters" element={<Clusters />} />
<Route path="nodes" element={<ListPage title="Nodes" subtitle="Cluster nodes, capacity, and health." path="/nodes" columns={[{ key: "name", label: "Name" }, { key: "status", label: "Status" }, { key: "cpu_count", label: "CPU" }, { key: "memory_mb", label: "Memory MB" }]} />} />
<Route path="nodes" element={<Nodes />} />
<Route path="workloads" element={<Workloads />} />
<Route path="networks" element={<Networks />} />
<Route path="ipam" element={<Ipam />} />
+25
View File
@@ -126,6 +126,31 @@ export type Workload = {
tags: string[];
};
export type NodeAgent = {
node_id: string;
status: string;
version: string | null;
last_seen_at: string | null;
install_count: number;
last_payload: Record<string, unknown> | null;
};
export type Node = {
id: string;
cluster_id: string;
name: string;
status: string;
cpu_count: number;
memory_mb: number;
agent: NodeAgent | null;
};
export type AgentInstallInfo = {
node_id: string;
install_url: string;
command: string;
};
export type WorkloadInsight = {
workload: Workload;
assigned_ips: IpAddress[];
+98
View File
@@ -0,0 +1,98 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import { Cpu, Copy, RadioTower } from "lucide-react";
import { useState } from "react";
import { AgentInstallInfo, api, Node } from "../api/client";
import { DataTable } from "../components/DataTable";
import { iconButtonClass, secondaryButtonClass } from "../components/FormControls";
import { Modal } from "../components/Modal";
import { PageHeader } from "../components/PageHeader";
export function Nodes() {
const nodes = useQuery({ queryKey: ["nodes-agents"], queryFn: () => api<Node[]>("/nodes/agents") });
const [selectedNode, setSelectedNode] = useState<Node | null>(null);
const [copied, setCopied] = useState(false);
const installInfo = useMutation({
mutationFn: (node: Node) => api<AgentInstallInfo>(`/nodes/${node.id}/agent/install-info`),
onSuccess: (_data, node) => {
setSelectedNode(node);
setCopied(false);
},
});
async function copyCommand() {
if (!installInfo.data) {
return;
}
await navigator.clipboard.writeText(installInfo.data.command);
setCopied(true);
}
return (
<>
<PageHeader title="Nodes" subtitle="Cluster nodes, capacity, health, and NexaFabric agent enrollment." />
{nodes.isLoading ? <div className="rounded-md border border-border bg-panel p-8 text-sm">Loading...</div> : null}
{nodes.error ? <div className="rounded-md border border-danger p-4 text-sm text-danger">Failed to load nodes.</div> : null}
{!nodes.isLoading && !nodes.error ? (
<DataTable
rows={(nodes.data ?? []) as unknown as Record<string, unknown>[]}
columns={[
{ key: "name", label: "Name" },
{ key: "status", label: "Status" },
{ key: "cpu_count", label: "CPU" },
{ key: "memory_mb", label: "Memory MB" },
{
key: "agent",
label: "Agent",
render: (row) => {
const node = row as unknown as Node;
return node.agent ? `${node.agent.status}${node.agent.version ? ` · ${node.agent.version}` : ""}` : "not_installed";
},
},
{
key: "actions",
label: "Actions",
render: (row) => {
const node = row as unknown as Node;
return (
<div className="flex justify-end gap-2">
<button
className={iconButtonClass}
title="Install node agent"
aria-label={`Install agent on ${node.name}`}
onClick={() => installInfo.mutate(node)}
>
<RadioTower size={16} />
</button>
</div>
);
},
},
]}
/>
) : null}
<Modal title="Install Node Agent" open={Boolean(selectedNode)} onClose={() => setSelectedNode(null)}>
<div className="space-y-4">
<div className="flex items-center gap-2 font-medium">
<Cpu size={18} />
{selectedNode?.name}
</div>
<div className="rounded-md border border-border bg-canvas p-3 text-xs text-slate-500 dark:text-slate-400">
Run this command as root on the Proxmox node. It installs the agent under <code>/opt/nexafabric-agent</code> and starts a systemd service.
</div>
<pre className="max-h-48 overflow-auto rounded-md border border-border bg-canvas p-3 text-xs">
{installInfo.data?.command ?? "Generating installer..."}
</pre>
<div className="grid gap-2 text-xs">
<span className="text-slate-500 dark:text-slate-400">Installer link</span>
<code className="break-all rounded-md border border-border bg-canvas p-3">{installInfo.data?.install_url ?? ""}</code>
</div>
<button className={secondaryButtonClass} disabled={!installInfo.data} onClick={copyCommand}>
<Copy size={16} />
{copied ? "Copied" : "Copy Command"}
</button>
</div>
</Modal>
</>
);
}