feat: add automatic cluster sync and IPAM discovery with configurable intervals, firewall log file parsing, and enhanced flow prioritization

Add RuntimeSettingsRead/RuntimeSettingsUpdate schemas with auto_node_sync_enabled/auto_node_sync_interval_minutes/auto_ipam_sync_enabled/auto_ipam_sync_interval_minutes/last_node_auto_sync_at/last_ipam_auto_sync_at fields, implement firewall_log_lines_from_files to parse /var/log/pve-firewall.log with max_lines limit and error collection, extend collect_firewall_log
This commit is contained in:
2026-07-10 08:13:40 +02:00
parent 1531b7ea47
commit 67eee0662a
10 changed files with 493 additions and 65 deletions
+6
View File
@@ -169,6 +169,12 @@ export type RuntimeSettings = {
firewall_apply_requires_preview: boolean;
agent_optional: boolean;
flow_retention_hours: number;
auto_node_sync_enabled: boolean;
auto_node_sync_interval_minutes: number;
auto_ipam_sync_enabled: boolean;
auto_ipam_sync_interval_minutes: number;
last_node_auto_sync_at: string | null;
last_ipam_auto_sync_at: string | null;
};
export type WorkloadInsight = {
+22 -2
View File
@@ -1,8 +1,8 @@
import { FormEvent, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Database, Download, Pencil, Plus } from "lucide-react";
import { Database, Download, Pencil, Plus, RefreshCcw } from "lucide-react";
import { api, authorizedFetch, IpAddress, Network, Subnet } from "../api/client";
import { api, authorizedFetch, IpAddress, Network, RuntimeSettings, Subnet } from "../api/client";
import { DataTable } from "../components/DataTable";
import { buttonClass, Field, iconButtonClass, inputClass, secondaryButtonClass, selectClass } from "../components/FormControls";
import { LoadingOverlay } from "../components/LoadingOverlay";
@@ -16,6 +16,7 @@ export function Ipam() {
const networks = useQuery({ queryKey: ["networks"], queryFn: () => api<Network[]>("/networks") });
const subnets = useQuery({ queryKey: ["subnets"], queryFn: () => api<Subnet[]>("/ipam/subnets") });
const addresses = useQuery({ queryKey: ["addresses"], queryFn: () => api<IpAddress[]>("/ipam/addresses") });
const settings = useQuery({ queryKey: ["settings"], queryFn: () => api<RuntimeSettings>("/settings") });
const [message, setMessage] = useState("");
const [subnetForm, setSubnetForm] = useState(emptySubnetForm);
const [ipForm, setIpForm] = useState({ subnet_id: "", address: "10.50.0.20", status: "reserved", note: "" });
@@ -46,6 +47,13 @@ export function Ipam() {
},
onError: (error) => setMessage(error instanceof Error ? error.message : "IP reservation failed."),
});
const toggleAutoDiscover = useMutation({
mutationFn: () => api<RuntimeSettings>("/settings", {
method: "PATCH",
body: JSON.stringify({ auto_ipam_sync_enabled: !settings.data?.auto_ipam_sync_enabled }),
}),
onSuccess: () => settings.refetch(),
});
async function submitSubnet(event: FormEvent) {
event.preventDefault();
@@ -110,6 +118,18 @@ export function Ipam() {
<PageHeader title="IPAM" subtitle="Manage subnets, reservations, assignments, conflicts, and export state." />
<LoadingOverlay open={Boolean(busyMessage)} message={busyMessage} />
<div className="space-y-4">
<div className="flex flex-wrap items-center justify-between gap-3 rounded-md border border-border bg-panel p-3">
<div>
<div className="text-sm font-medium">Automatic IPAM discovery</div>
<div className="text-xs text-slate-500">
{settings.data?.auto_ipam_sync_enabled ? `Enabled every ${settings.data.auto_ipam_sync_interval_minutes} minutes` : "Disabled"}
</div>
</div>
<button className={secondaryButtonClass} disabled={toggleAutoDiscover.isPending || !settings.data} onClick={() => toggleAutoDiscover.mutate()}>
<RefreshCcw size={16} />
{settings.data?.auto_ipam_sync_enabled ? "Disable Auto Discover" : "Enable Auto Discover"}
</button>
</div>
<div className="flex flex-wrap gap-2">
<button className={buttonClass} onClick={addSubnet}><Plus size={16} /> Add Subnet</button>
<button className={buttonClass} onClick={() => setIpOpen(true)}><Plus size={16} /> Reserve IP</button>
+22 -2
View File
@@ -1,8 +1,8 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import { Activity, Cpu, Copy, RadioTower, ScrollText } from "lucide-react";
import { Activity, Cpu, Copy, RadioTower, RefreshCcw, ScrollText } from "lucide-react";
import { useState } from "react";
import { AgentInstallInfo, api, Node } from "../api/client";
import { AgentInstallInfo, api, Node, RuntimeSettings } from "../api/client";
import { DataTable } from "../components/DataTable";
import { iconButtonClass, secondaryButtonClass } from "../components/FormControls";
import { LoadingOverlay } from "../components/LoadingOverlay";
@@ -11,6 +11,7 @@ import { PageHeader } from "../components/PageHeader";
export function Nodes() {
const nodes = useQuery({ queryKey: ["nodes-agents"], queryFn: () => api<Node[]>("/nodes/agents") });
const settings = useQuery({ queryKey: ["settings"], queryFn: () => api<RuntimeSettings>("/settings") });
const [selectedNode, setSelectedNode] = useState<Node | null>(null);
const [detailNode, setDetailNode] = useState<Node | null>(null);
const [copied, setCopied] = useState(false);
@@ -21,6 +22,13 @@ export function Nodes() {
setCopied(false);
},
});
const toggleAutoSync = useMutation({
mutationFn: () => api<RuntimeSettings>("/settings", {
method: "PATCH",
body: JSON.stringify({ auto_node_sync_enabled: !settings.data?.auto_node_sync_enabled }),
}),
onSuccess: () => settings.refetch(),
});
async function copyCommand() {
if (!installInfo.data) {
@@ -53,6 +61,18 @@ export function Nodes() {
<>
<PageHeader title="Nodes" subtitle="Cluster nodes, capacity, health, and NexaFabric agent enrollment." />
<LoadingOverlay open={installInfo.isPending} message="Generating node agent installer..." />
<div className="mb-4 flex flex-wrap items-center justify-between gap-3 rounded-md border border-border bg-panel p-3">
<div>
<div className="text-sm font-medium">Automatic node sync</div>
<div className="text-xs text-slate-500">
{settings.data?.auto_node_sync_enabled ? `Enabled every ${settings.data.auto_node_sync_interval_minutes} minutes` : "Disabled"}
</div>
</div>
<button className={secondaryButtonClass} disabled={toggleAutoSync.isPending || !settings.data} onClick={() => toggleAutoSync.mutate()}>
<RefreshCcw size={16} />
{settings.data?.auto_node_sync_enabled ? "Disable Auto Sync" : "Enable Auto Sync"}
</button>
</div>
{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 ? (
+89 -43
View File
@@ -1,6 +1,6 @@
import { FormEvent, useEffect, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Database, Save, ShieldCheck } from "lucide-react";
import { Database, RefreshCcw, Save, ShieldCheck } from "lucide-react";
import { api, RuntimeSettings } from "../api/client";
import { buttonClass, Field, inputClass } from "../components/FormControls";
@@ -10,10 +10,20 @@ export function Settings() {
const queryClient = useQueryClient();
const settings = useQuery({ queryKey: ["settings"], queryFn: () => api<RuntimeSettings>("/settings") });
const [retentionHours, setRetentionHours] = useState("24");
const [nodeAutoSync, setNodeAutoSync] = useState(false);
const [nodeInterval, setNodeInterval] = useState("60");
const [ipamAutoSync, setIpamAutoSync] = useState(false);
const [ipamInterval, setIpamInterval] = useState("60");
const update = useMutation({
mutationFn: () => api<RuntimeSettings>("/settings", {
method: "PATCH",
body: JSON.stringify({ flow_retention_hours: Number(retentionHours) }),
body: JSON.stringify({
flow_retention_hours: Number(retentionHours),
auto_node_sync_enabled: nodeAutoSync,
auto_node_sync_interval_minutes: Number(nodeInterval),
auto_ipam_sync_enabled: ipamAutoSync,
auto_ipam_sync_interval_minutes: Number(ipamInterval),
}),
}),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["settings"] }),
});
@@ -21,6 +31,10 @@ export function Settings() {
useEffect(() => {
if (settings.data) {
setRetentionHours(String(settings.data.flow_retention_hours));
setNodeAutoSync(settings.data.auto_node_sync_enabled);
setNodeInterval(String(settings.data.auto_node_sync_interval_minutes));
setIpamAutoSync(settings.data.auto_ipam_sync_enabled);
setIpamInterval(String(settings.data.auto_ipam_sync_interval_minutes));
}
}, [settings.data]);
@@ -32,49 +46,81 @@ export function Settings() {
return (
<>
<PageHeader title="Settings" subtitle="Runtime settings and safety defaults." />
<div className="grid gap-4 xl:grid-cols-[minmax(0,560px)_1fr]">
<form onSubmit={submit} className="rounded-md border border-border bg-panel p-4">
<div className="mb-4 flex items-center gap-2 font-medium"><Database size={18} /> Flow Retention</div>
<Field label="Keep flow telemetry for">
<div className="grid grid-cols-[1fr_auto] gap-2">
<input
className={inputClass}
min={1}
max={8760}
type="number"
value={retentionHours}
onChange={(event) => setRetentionHours(event.target.value)}
/>
<span className="inline-flex h-10 items-center rounded-md border border-border px-3 text-sm text-slate-500">hours</span>
<form onSubmit={submit} className="space-y-4">
<div className="grid gap-4 xl:grid-cols-[minmax(0,560px)_1fr]">
<section className="rounded-md border border-border bg-panel p-4">
<div className="mb-4 flex items-center gap-2 font-medium"><Database size={18} /> Flow Retention</div>
<Field label="Keep flow telemetry for">
<div className="grid grid-cols-[1fr_auto] gap-2">
<input
className={inputClass}
min={1}
max={8760}
type="number"
value={retentionHours}
onChange={(event) => setRetentionHours(event.target.value)}
/>
<span className="inline-flex h-10 items-center rounded-md border border-border px-3 text-sm text-slate-500">hours</span>
</div>
</Field>
<div className="mt-3 rounded-md border border-border bg-canvas p-3 text-sm text-slate-500">
New heartbeats update existing flows and remove entries older than this retention window.
</div>
</Field>
<div className="mt-3 rounded-md border border-border bg-canvas p-3 text-sm text-slate-500">
New heartbeats update existing flows and remove entries older than this retention window.
</div>
{update.error ? <div className="mt-3 rounded-md border border-danger p-3 text-sm text-danger">Settings could not be saved. Super Admin permission is required.</div> : null}
<button className={`${buttonClass} mt-4`} disabled={update.isPending || !retentionHours}>
<Save size={16} />
Save Settings
</button>
</form>
<section className="rounded-md border border-border bg-panel p-4">
<div className="mb-4 flex items-center gap-2 font-medium"><ShieldCheck size={18} /> Safety Defaults</div>
<div className="grid gap-3 md:grid-cols-3">
<div className="rounded-md border border-border bg-canvas p-3">
<div className="text-xs text-slate-500">Product</div>
<div className="mt-1 font-medium">{settings.data?.product ?? "NexaFabric"}</div>
</section>
<section className="rounded-md border border-border bg-panel p-4">
<div className="mb-4 flex items-center gap-2 font-medium"><ShieldCheck size={18} /> Safety Defaults</div>
<div className="grid gap-3 md:grid-cols-3">
<div className="rounded-md border border-border bg-canvas p-3">
<div className="text-xs text-slate-500">Product</div>
<div className="mt-1 font-medium">{settings.data?.product ?? "NexaFabric"}</div>
</div>
<div className="rounded-md border border-border bg-canvas p-3">
<div className="text-xs text-slate-500">Preview Required</div>
<div className="mt-1 font-medium">{String(settings.data?.firewall_apply_requires_preview ?? true)}</div>
</div>
<div className="rounded-md border border-border bg-canvas p-3">
<div className="text-xs text-slate-500">Agent Optional</div>
<div className="mt-1 font-medium">{String(settings.data?.agent_optional ?? true)}</div>
</div>
</div>
<div className="rounded-md border border-border bg-canvas p-3">
<div className="text-xs text-slate-500">Preview Required</div>
<div className="mt-1 font-medium">{String(settings.data?.firewall_apply_requires_preview ?? true)}</div>
</div>
<div className="rounded-md border border-border bg-canvas p-3">
<div className="text-xs text-slate-500">Agent Optional</div>
<div className="mt-1 font-medium">{String(settings.data?.agent_optional ?? true)}</div>
</div>
</div>
</section>
</div>
</section>
</div>
<div className="grid gap-4 xl:grid-cols-2">
<section className="rounded-md border border-border bg-panel p-4">
<div className="mb-4 flex items-center gap-2 font-medium"><RefreshCcw size={18} /> Nodes Auto Sync</div>
<label className="mb-3 flex items-center gap-2 text-sm">
<input type="checkbox" checked={nodeAutoSync} onChange={(event) => setNodeAutoSync(event.target.checked)} />
Enable automatic cluster inventory sync
</label>
<Field label="Interval">
<div className="grid grid-cols-[1fr_auto] gap-2">
<input className={inputClass} min={1} max={10080} type="number" value={nodeInterval} onChange={(event) => setNodeInterval(event.target.value)} />
<span className="inline-flex h-10 items-center rounded-md border border-border px-3 text-sm text-slate-500">minutes</span>
</div>
</Field>
<div className="mt-3 text-xs text-slate-500">Last run: {settings.data?.last_node_auto_sync_at ? new Date(settings.data.last_node_auto_sync_at).toLocaleString() : "never"}</div>
</section>
<section className="rounded-md border border-border bg-panel p-4">
<div className="mb-4 flex items-center gap-2 font-medium"><RefreshCcw size={18} /> IPAM Auto Discover</div>
<label className="mb-3 flex items-center gap-2 text-sm">
<input type="checkbox" checked={ipamAutoSync} onChange={(event) => setIpamAutoSync(event.target.checked)} />
Enable automatic IPAM discovery from Proxmox
</label>
<Field label="Interval">
<div className="grid grid-cols-[1fr_auto] gap-2">
<input className={inputClass} min={1} max={10080} type="number" value={ipamInterval} onChange={(event) => setIpamInterval(event.target.value)} />
<span className="inline-flex h-10 items-center rounded-md border border-border px-3 text-sm text-slate-500">minutes</span>
</div>
</Field>
<div className="mt-3 text-xs text-slate-500">Last run: {settings.data?.last_ipam_auto_sync_at ? new Date(settings.data.last_ipam_auto_sync_at).toLocaleString() : "never"}</div>
</section>
</div>
{update.error ? <div className="rounded-md border border-danger p-3 text-sm text-danger">Settings could not be saved. Super Admin permission is required.</div> : null}
<button className={buttonClass} disabled={update.isPending || !retentionHours || !nodeInterval || !ipamInterval}>
<Save size={16} />
Save Settings
</button>
</form>
</>
);
}