feat: PF States Chart (Sparkline letzte 8h) — Backend schreibt pf_states in TimescaleDB, Frontend zeigt SVG-Sparkline
This commit is contained in:
parent
e01fd7698c
commit
aca013b848
@ -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 != "" {
|
||||
|
||||
@ -107,7 +107,7 @@ export default function AgentPanel({ agentId, customers, onClose, onReload }) {
|
||||
const renderContent = () => {
|
||||
if (!sys) return <div className="text-gray-500">Keine Systemdaten</div>
|
||||
const tab = subTab
|
||||
if (tab === 'overview') return <OverviewTab sys={sys} />
|
||||
if (tab === 'overview') return <OverviewTab sys={sys} agentId={agentId} />
|
||||
if (tab === 'interfaces') return <InterfacesTab sys={sys} />
|
||||
if (tab === 'services') return <ServicesTab sys={sys} />
|
||||
if (tab === 'tunnels') return <TunnelTab agentId={agentId} agent={agent} />
|
||||
@ -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 (
|
||||
<svg width={w} height={h} viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none">
|
||||
<polyline points={pts} fill="none" stroke={color} strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="round" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function MetricCard({ icon: Icon, label, value, sub, bar, barColor, chart }) {
|
||||
return (
|
||||
<div className="bg-gray-800/50 rounded-lg border border-gray-700/50 p-4">
|
||||
<div className="flex items-center gap-2 text-gray-500 mb-2">
|
||||
{Icon && <Icon className="w-4 h-4" />}
|
||||
<span className="text-xs uppercase">{label}</span>
|
||||
</div>
|
||||
<div className="text-xl font-bold text-white">{value}</div>
|
||||
{sub && <div className="text-xs text-gray-500 mt-0.5">{sub}</div>}
|
||||
{bar !== undefined && bar !== null && (
|
||||
<div className="mt-2 h-1.5 bg-gray-700 rounded overflow-hidden">
|
||||
<div className={`h-full rounded ${barColor || 'bg-blue-500'}`} style={{ width: `${Math.min(bar, 100)}%` }} />
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 text-gray-500 mb-2">
|
||||
{Icon && <Icon className="w-4 h-4" />}
|
||||
<span className="text-xs uppercase">{label}</span>
|
||||
</div>
|
||||
<div className="text-xl font-bold text-white">{value}</div>
|
||||
{sub && <div className="text-xs text-gray-500 mt-0.5">{sub}</div>}
|
||||
{bar !== undefined && bar !== null && (
|
||||
<div className="mt-2 h-1.5 bg-gray-700 rounded overflow-hidden">
|
||||
<div className={`h-full rounded ${barColor || 'bg-blue-500'}`} style={{ width: `${Math.min(bar, 100)}%` }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{chart && (
|
||||
<div className="ml-3 opacity-70 flex-shrink-0">
|
||||
{chart}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<MetricCard
|
||||
icon={Shield}
|
||||
label="PF States"
|
||||
value={pf.hard_limit > 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={<Sparkline data={chartData} height={44} color="#3b82f6" />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
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)} />
|
||||
<MetricCard icon={Clock} label="Uptime" value={sys.uptime_seconds ? formatUptime(sys.uptime_seconds) : '—'} />
|
||||
{sys.pf_states && (
|
||||
<MetricCard icon={Shield} label="PF States"
|
||||
value={sys.pf_states.hard_limit > 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)}
|
||||
/>
|
||||
)}
|
||||
<PFStatesCard agentId={agentId} sys={sys} />
|
||||
</div>
|
||||
|
||||
<h3 className="text-sm font-medium text-gray-300 mt-4">System Status</h3>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user