From aca013b8484e40883ca05fa2f9736b5c6d0c2f5a Mon Sep 17 00:00:00 2001 From: cynfo3000 Date: Fri, 6 Mar 2026 21:47:47 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20PF=20States=20Chart=20(Sparkline=20letz?= =?UTF-8?q?te=208h)=20=E2=80=94=20Backend=20schreibt=20pf=5Fstates=20in=20?= =?UTF-8?q?TimescaleDB,=20Frontend=20zeigt=20SVG-Sparkline?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/db/postgres.go | 8 +++ frontend/src/components/AgentPanel.jsx | 97 ++++++++++++++++++++------ 2 files changed, 84 insertions(+), 21 deletions(-) diff --git a/backend/db/postgres.go b/backend/db/postgres.go index 3ed9562..90be336 100644 --- a/backend/db/postgres.go +++ b/backend/db/postgres.go @@ -424,6 +424,14 @@ func (d *Database) writeMetrics(tx *sql.Tx, agentID string, ts time.Time, data * } } + // PF States + if data.PFStates != nil && data.PFStates.HardLimit > 0 { + stmt.Exec(ts, agentID, "pf_states_current", float64(data.PFStates.CurrentEntries), nil) + stmt.Exec(ts, agentID, "pf_states_limit", float64(data.PFStates.HardLimit), nil) + usedPct := float64(data.PFStates.CurrentEntries) / float64(data.PFStates.HardLimit) * 100 + stmt.Exec(ts, agentID, "pf_states_used_percent", usedPct, nil) + } + // Gateways (Loss und Delay sind Strings wie "0.0%" und "1.2ms") for _, gw := range data.Gateways { if gw.Delay != "" { diff --git a/frontend/src/components/AgentPanel.jsx b/frontend/src/components/AgentPanel.jsx index c020988..632431d 100644 --- a/frontend/src/components/AgentPanel.jsx +++ b/frontend/src/components/AgentPanel.jsx @@ -107,7 +107,7 @@ export default function AgentPanel({ agentId, customers, onClose, onReload }) { const renderContent = () => { if (!sys) return
Keine Systemdaten
const tab = subTab - if (tab === 'overview') return + if (tab === 'overview') return if (tab === 'interfaces') return if (tab === 'services') return if (tab === 'tunnels') return @@ -326,25 +326,87 @@ function GatewaysTab({ sys }) { ) } -function MetricCard({ icon: Icon, label, value, sub, bar, barColor }) { +function Sparkline({ data, height = 40, color = '#3b82f6' }) { + if (!data || data.length < 2) return null + const vals = data.map(d => d.value) + const min = Math.min(...vals) + const max = Math.max(...vals) + const range = max - min || 1 + const w = 200 + const h = height + const pts = vals.map((v, i) => { + const x = (i / (vals.length - 1)) * w + const y = h - ((v - min) / range) * (h - 4) - 2 + return `${x.toFixed(1)},${y.toFixed(1)}` + }).join(' ') + return ( + + + + ) +} + +function MetricCard({ icon: Icon, label, value, sub, bar, barColor, chart }) { return (
-
- {Icon && } - {label} -
-
{value}
- {sub &&
{sub}
} - {bar !== undefined && bar !== null && ( -
-
+
+
+
+ {Icon && } + {label} +
+
{value}
+ {sub &&
{sub}
} + {bar !== undefined && bar !== null && ( +
+
+
+ )}
- )} + {chart && ( +
+ {chart} +
+ )} +
) } -function OverviewTab({ sys }) { +function PFStatesCard({ agentId, sys }) { + const [chartData, setChartData] = useState([]) + + useEffect(() => { + if (!agentId) return + const from = new Date(Date.now() - 8 * 60 * 60 * 1000).toISOString() + api.getMetrics(agentId, 'pf_states_current', `&from=${from}&limit=200`) + .then(data => { + if (Array.isArray(data)) setChartData(data) + }) + .catch(() => {}) + }, [agentId]) + + if (!sys?.pf_states) return null + const pf = sys.pf_states + const usedPct = pf.hard_limit > 0 ? (pf.current_entries / pf.hard_limit * 100) : 0 + const barColor = usedPct > 90 ? 'bg-red-500' : usedPct > 70 ? 'bg-yellow-500' : 'bg-green-500' + + return ( + 0 + ? `${pf.current_entries.toLocaleString('de-DE')} / ${pf.hard_limit.toLocaleString('de-DE')}` + : `${pf.current_entries.toLocaleString('de-DE')}`} + sub={`Search: ${pf.search_rate?.toFixed(1)}/s · Insert: ${pf.insert_rate?.toFixed(1)}/s`} + bar={pf.hard_limit > 0 ? usedPct : null} + barColor={barColor} + chart={} + /> + ) +} + +function OverviewTab({ sys, agentId }) { const rootDisk = sys.disks?.find((d) => d.mount_point === '/') const diskPct = rootDisk && rootDisk.total_bytes > 0 ? (rootDisk.used_bytes / rootDisk.total_bytes * 100) : null @@ -375,14 +437,7 @@ function OverviewTab({ sys }) { sub={rootDisk ? `${formatBytes(rootDisk.used_bytes)} / ${formatBytes(rootDisk.total_bytes)}` : ''} bar={diskPct} barColor={barColor(diskPct || 0)} /> - {sys.pf_states && ( - 0 ? `${sys.pf_states.current_entries.toLocaleString('de-DE')} / ${sys.pf_states.hard_limit.toLocaleString('de-DE')}` : `${sys.pf_states.current_entries.toLocaleString('de-DE')}`} - sub={`Search: ${sys.pf_states.search_rate?.toFixed(1)}/s · Insert: ${sys.pf_states.insert_rate?.toFixed(1)}/s`} - bar={sys.pf_states.hard_limit > 0 ? (sys.pf_states.current_entries / sys.pf_states.hard_limit * 100) : null} - barColor={barColor(sys.pf_states.hard_limit > 0 ? (sys.pf_states.current_entries / sys.pf_states.hard_limit * 100) : 0)} - /> - )} +

System Status