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