Feature: PF States, HAProxy + Caddy collectors and dashboard
This commit is contained in:
parent
31369381d0
commit
f4a28463b7
89
agent/collector/caddy.go
Normal file
89
agent/collector/caddy.go
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
package collector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CaddySite struct {
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
Upstream string `json:"upstream"`
|
||||||
|
TLS bool `json:"tls"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CaddyData struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
Sites []CaddySite `json:"sites"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const caddyFile = "/usr/local/etc/caddy/Caddyfile"
|
||||||
|
|
||||||
|
func CollectCaddy() *CaddyData {
|
||||||
|
if _, err := os.Stat(caddyFile); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data := &CaddyData{}
|
||||||
|
|
||||||
|
// Version
|
||||||
|
if out, err := exec.Command("/usr/local/bin/caddy", "version").Output(); err == nil {
|
||||||
|
fields := strings.Fields(strings.TrimSpace(string(out)))
|
||||||
|
if len(fields) > 0 {
|
||||||
|
data.Version = fields[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse Caddyfile
|
||||||
|
content, err := os.ReadFile(caddyFile)
|
||||||
|
if err != nil {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(string(content), "\n")
|
||||||
|
var currentDomain string
|
||||||
|
var currentTLS bool
|
||||||
|
depth := 0
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for domain block start: "domain.com {" or "http://domain.com {"
|
||||||
|
if depth == 0 && strings.Contains(trimmed, "{") {
|
||||||
|
parts := strings.Fields(trimmed)
|
||||||
|
if len(parts) >= 2 && parts[len(parts)-1] == "{" {
|
||||||
|
currentDomain = parts[0]
|
||||||
|
currentTLS = !strings.HasPrefix(currentDomain, "http://")
|
||||||
|
depth = 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if depth > 0 {
|
||||||
|
// Track nested braces
|
||||||
|
depth += strings.Count(trimmed, "{") - strings.Count(trimmed, "}")
|
||||||
|
|
||||||
|
if strings.HasPrefix(trimmed, "reverse_proxy") {
|
||||||
|
parts := strings.Fields(trimmed)
|
||||||
|
if len(parts) >= 2 {
|
||||||
|
upstream := parts[1]
|
||||||
|
data.Sites = append(data.Sites, CaddySite{
|
||||||
|
Domain: currentDomain,
|
||||||
|
Upstream: upstream,
|
||||||
|
TLS: currentTLS,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if depth <= 0 {
|
||||||
|
currentDomain = ""
|
||||||
|
depth = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
132
agent/collector/haproxy.go
Normal file
132
agent/collector/haproxy.go
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
package collector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HAProxyInfo struct {
|
||||||
|
Uptime string `json:"uptime"`
|
||||||
|
CurrentConns int `json:"current_conns"`
|
||||||
|
TotalConns int `json:"total_conns"`
|
||||||
|
TotalReqs int `json:"total_requests"`
|
||||||
|
CurrentSSL int `json:"current_ssl"`
|
||||||
|
TotalSSL int `json:"total_ssl"`
|
||||||
|
MaxConns int `json:"max_conns"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HAProxyEntry struct {
|
||||||
|
ProxyName string `json:"proxy_name"`
|
||||||
|
ServerName string `json:"server_name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
CurrentSessions int `json:"current_sessions"`
|
||||||
|
TotalSessions int `json:"total_sessions"`
|
||||||
|
BytesIn int64 `json:"bytes_in"`
|
||||||
|
BytesOut int64 `json:"bytes_out"`
|
||||||
|
RequestsTotal int `json:"requests_total"`
|
||||||
|
ErrorsReq int `json:"errors_req"`
|
||||||
|
ErrorsConn int `json:"errors_conn"`
|
||||||
|
ErrorsResp int `json:"errors_resp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HAProxyData struct {
|
||||||
|
Info HAProxyInfo `json:"info"`
|
||||||
|
Entries []HAProxyEntry `json:"entries"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const haproxySocket = "/var/run/haproxy.socket"
|
||||||
|
|
||||||
|
func CollectHAProxy() *HAProxyData {
|
||||||
|
if _, err := os.Stat(haproxySocket); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data := &HAProxyData{}
|
||||||
|
|
||||||
|
// show info
|
||||||
|
if out, err := exec.Command("/bin/sh", "-c", `echo "show info" | /usr/bin/nc -U `+haproxySocket).Output(); err == nil {
|
||||||
|
info := parseHAProxyInfo(string(out))
|
||||||
|
data.Info = info
|
||||||
|
}
|
||||||
|
|
||||||
|
// show stat
|
||||||
|
if out, err := exec.Command("/bin/sh", "-c", `echo "show stat" | /usr/bin/nc -U `+haproxySocket).Output(); err == nil {
|
||||||
|
data.Entries = parseHAProxyStats(string(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseHAProxyInfo(output string) HAProxyInfo {
|
||||||
|
info := HAProxyInfo{}
|
||||||
|
for _, line := range strings.Split(output, "\n") {
|
||||||
|
parts := strings.SplitN(line, ":", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := strings.TrimSpace(parts[0])
|
||||||
|
val := strings.TrimSpace(parts[1])
|
||||||
|
switch key {
|
||||||
|
case "Uptime":
|
||||||
|
info.Uptime = val
|
||||||
|
case "CurrConns":
|
||||||
|
info.CurrentConns, _ = strconv.Atoi(val)
|
||||||
|
case "CumConns":
|
||||||
|
info.TotalConns, _ = strconv.Atoi(val)
|
||||||
|
case "CumReq":
|
||||||
|
info.TotalReqs, _ = strconv.Atoi(val)
|
||||||
|
case "CurrSslConns":
|
||||||
|
info.CurrentSSL, _ = strconv.Atoi(val)
|
||||||
|
case "CumSslConns":
|
||||||
|
info.TotalSSL, _ = strconv.Atoi(val)
|
||||||
|
case "Maxconn":
|
||||||
|
info.MaxConns, _ = strconv.Atoi(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseHAProxyStats(output string) []HAProxyEntry {
|
||||||
|
var entries []HAProxyEntry
|
||||||
|
lines := strings.Split(output, "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.HasPrefix(line, "#") || strings.TrimSpace(line) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fields := strings.Split(line, ",")
|
||||||
|
if len(fields) < 18 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
svname := fields[1]
|
||||||
|
entryType := "server"
|
||||||
|
if svname == "FRONTEND" {
|
||||||
|
entryType = "frontend"
|
||||||
|
} else if svname == "BACKEND" {
|
||||||
|
entryType = "backend"
|
||||||
|
}
|
||||||
|
|
||||||
|
e := HAProxyEntry{
|
||||||
|
ProxyName: fields[0],
|
||||||
|
ServerName: svname,
|
||||||
|
Type: entryType,
|
||||||
|
Status: fields[17],
|
||||||
|
}
|
||||||
|
e.CurrentSessions, _ = strconv.Atoi(fields[4])
|
||||||
|
e.TotalSessions, _ = strconv.Atoi(fields[7])
|
||||||
|
e.BytesIn, _ = strconv.ParseInt(fields[8], 10, 64)
|
||||||
|
e.BytesOut, _ = strconv.ParseInt(fields[9], 10, 64)
|
||||||
|
e.ErrorsReq, _ = strconv.Atoi(fields[12])
|
||||||
|
e.ErrorsConn, _ = strconv.Atoi(fields[13])
|
||||||
|
e.ErrorsResp, _ = strconv.Atoi(fields[14])
|
||||||
|
if len(fields) > 48 {
|
||||||
|
e.RequestsTotal, _ = strconv.Atoi(fields[48])
|
||||||
|
}
|
||||||
|
|
||||||
|
entries = append(entries, e)
|
||||||
|
}
|
||||||
|
return entries
|
||||||
|
}
|
||||||
71
agent/collector/pfstates.go
Normal file
71
agent/collector/pfstates.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package collector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PFStates struct {
|
||||||
|
CurrentEntries int `json:"current_entries"`
|
||||||
|
HardLimit int `json:"hard_limit"`
|
||||||
|
SearchRate float64 `json:"search_rate"`
|
||||||
|
InsertRate float64 `json:"insert_rate"`
|
||||||
|
RemovalRate float64 `json:"removal_rate"`
|
||||||
|
MatchRate float64 `json:"match_rate"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func CollectPFStates() *PFStates {
|
||||||
|
result := &PFStates{}
|
||||||
|
|
||||||
|
// pfctl -si for state table info
|
||||||
|
if out, err := exec.Command("/sbin/pfctl", "-si").Output(); err == nil {
|
||||||
|
lines := strings.Split(string(out), "\n")
|
||||||
|
rateRe := regexp.MustCompile(`([\d.]+)/s`)
|
||||||
|
for _, line := range lines {
|
||||||
|
lower := strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(lower, "current entries") {
|
||||||
|
result.CurrentEntries = parseLastInt(lower)
|
||||||
|
} else if strings.HasPrefix(lower, "searches") {
|
||||||
|
if m := rateRe.FindStringSubmatch(line); len(m) > 1 {
|
||||||
|
result.SearchRate, _ = strconv.ParseFloat(m[1], 64)
|
||||||
|
}
|
||||||
|
} else if strings.HasPrefix(lower, "inserts") {
|
||||||
|
if m := rateRe.FindStringSubmatch(line); len(m) > 1 {
|
||||||
|
result.InsertRate, _ = strconv.ParseFloat(m[1], 64)
|
||||||
|
}
|
||||||
|
} else if strings.HasPrefix(lower, "removals") {
|
||||||
|
if m := rateRe.FindStringSubmatch(line); len(m) > 1 {
|
||||||
|
result.RemovalRate, _ = strconv.ParseFloat(m[1], 64)
|
||||||
|
}
|
||||||
|
} else if strings.HasPrefix(lower, "match") {
|
||||||
|
if m := rateRe.FindStringSubmatch(line); len(m) > 1 {
|
||||||
|
result.MatchRate, _ = strconv.ParseFloat(m[1], 64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pfctl -sm for hard limit
|
||||||
|
if out, err := exec.Command("/sbin/pfctl", "-sm").Output(); err == nil {
|
||||||
|
for _, line := range strings.Split(string(out), "\n") {
|
||||||
|
if strings.Contains(line, "states") && strings.Contains(line, "hard limit") {
|
||||||
|
result.HardLimit = parseLastInt(line)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseLastInt(line string) int {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
for i := len(fields) - 1; i >= 0; i-- {
|
||||||
|
if v, err := strconv.Atoi(fields[i]); err == nil {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
@ -128,6 +128,9 @@ func sendHeartbeat(c *client.Client, agentID string) error {
|
|||||||
cronJobs := collector.CollectCron()
|
cronJobs := collector.CollectCron()
|
||||||
license := collector.CollectLicense()
|
license := collector.CollectLicense()
|
||||||
security := collector.CollectSecurity()
|
security := collector.CollectSecurity()
|
||||||
|
pfStates := collector.CollectPFStates()
|
||||||
|
haproxy := collector.CollectHAProxy()
|
||||||
|
caddy := collector.CollectCaddy()
|
||||||
|
|
||||||
req := map[string]interface{}{
|
req := map[string]interface{}{
|
||||||
"agent_id": agentID,
|
"agent_id": agentID,
|
||||||
@ -169,6 +172,9 @@ func sendHeartbeat(c *client.Client, agentID string) error {
|
|||||||
"cron_jobs": cronJobs,
|
"cron_jobs": cronJobs,
|
||||||
"license": license,
|
"license": license,
|
||||||
"security": security,
|
"security": security,
|
||||||
|
"pf_states": pfStates,
|
||||||
|
"haproxy": haproxy,
|
||||||
|
"caddy": caddy,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -25,6 +25,9 @@ type SystemData struct {
|
|||||||
Proxmox map[string]interface{} `json:"proxmox,omitempty"`
|
Proxmox map[string]interface{} `json:"proxmox,omitempty"`
|
||||||
Backups map[string]interface{} `json:"backups,omitempty"`
|
Backups map[string]interface{} `json:"backups,omitempty"`
|
||||||
Security *SecurityInfo `json:"security,omitempty"`
|
Security *SecurityInfo `json:"security,omitempty"`
|
||||||
|
PFStates *PFStatesInfo `json:"pf_states,omitempty"`
|
||||||
|
HAProxy *HAProxyData `json:"haproxy,omitempty"`
|
||||||
|
Caddy *CaddyData `json:"caddy,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LicenseInfo struct {
|
type LicenseInfo struct {
|
||||||
@ -216,6 +219,56 @@ type SecuritySummary struct {
|
|||||||
LastLogin string `json:"last_login"`
|
LastLogin string `json:"last_login"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PFStatesInfo struct {
|
||||||
|
CurrentEntries int `json:"current_entries"`
|
||||||
|
HardLimit int `json:"hard_limit"`
|
||||||
|
SearchRate float64 `json:"search_rate"`
|
||||||
|
InsertRate float64 `json:"insert_rate"`
|
||||||
|
RemovalRate float64 `json:"removal_rate"`
|
||||||
|
MatchRate float64 `json:"match_rate"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HAProxyInfo struct {
|
||||||
|
Uptime string `json:"uptime"`
|
||||||
|
CurrentConns int `json:"current_conns"`
|
||||||
|
TotalConns int `json:"total_conns"`
|
||||||
|
TotalReqs int `json:"total_requests"`
|
||||||
|
CurrentSSL int `json:"current_ssl"`
|
||||||
|
TotalSSL int `json:"total_ssl"`
|
||||||
|
MaxConns int `json:"max_conns"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HAProxyEntry struct {
|
||||||
|
ProxyName string `json:"proxy_name"`
|
||||||
|
ServerName string `json:"server_name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
CurrentSessions int `json:"current_sessions"`
|
||||||
|
TotalSessions int `json:"total_sessions"`
|
||||||
|
BytesIn int64 `json:"bytes_in"`
|
||||||
|
BytesOut int64 `json:"bytes_out"`
|
||||||
|
RequestsTotal int `json:"requests_total"`
|
||||||
|
ErrorsReq int `json:"errors_req"`
|
||||||
|
ErrorsConn int `json:"errors_conn"`
|
||||||
|
ErrorsResp int `json:"errors_resp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HAProxyData struct {
|
||||||
|
Info HAProxyInfo `json:"info"`
|
||||||
|
Entries []HAProxyEntry `json:"entries"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CaddySite struct {
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
Upstream string `json:"upstream"`
|
||||||
|
TLS bool `json:"tls"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CaddyData struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
Sites []CaddySite `json:"sites"`
|
||||||
|
}
|
||||||
|
|
||||||
// HeartbeatRequest
|
// HeartbeatRequest
|
||||||
type HeartbeatRequest struct {
|
type HeartbeatRequest struct {
|
||||||
AgentID string `json:"agent_id"`
|
AgentID string `json:"agent_id"`
|
||||||
|
|||||||
@ -73,7 +73,17 @@ export default function AgentPanel({ agentId, customers, onClose, onReload }) {
|
|||||||
// Filter sub-items by platform and determine visible main categories
|
// Filter sub-items by platform and determine visible main categories
|
||||||
const getVisibleSubs = (catId) => {
|
const getVisibleSubs = (catId) => {
|
||||||
const items = subItems[catId] || []
|
const items = subItems[catId] || []
|
||||||
return items.filter(s => !s.platforms || s.platforms.includes(platform))
|
let filtered = items.filter(s => !s.platforms || s.platforms.includes(platform))
|
||||||
|
// Dynamic sub-tabs based on data availability
|
||||||
|
if (catId === 'netzwerk') {
|
||||||
|
if (sys?.haproxy) {
|
||||||
|
filtered = [...filtered, { id: 'haproxy', label: 'HAProxy', icon: Server }]
|
||||||
|
}
|
||||||
|
if (sys?.caddy) {
|
||||||
|
filtered = [...filtered, { id: 'caddy', label: 'Caddy', icon: Globe }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filtered
|
||||||
}
|
}
|
||||||
|
|
||||||
const visibleMainTabs = mainCategories.filter(cat => {
|
const visibleMainTabs = mainCategories.filter(cat => {
|
||||||
@ -111,6 +121,8 @@ export default function AgentPanel({ agentId, customers, onClose, onReload }) {
|
|||||||
if (tab === 'tasks') return <TasksTab agentId={agentId} />
|
if (tab === 'tasks') return <TasksTab agentId={agentId} />
|
||||||
if (tab === 'backups') return platform === 'linux' ? <PVEBackupsTab sys={sys} /> : <BackupsTab agentId={agentId} />
|
if (tab === 'backups') return platform === 'linux' ? <PVEBackupsTab sys={sys} /> : <BackupsTab agentId={agentId} />
|
||||||
if (tab === 'agent') return <AgentTab agent={data?.agent} status={data?.status} platform={platform} />
|
if (tab === 'agent') return <AgentTab agent={data?.agent} status={data?.status} platform={platform} />
|
||||||
|
if (tab === 'haproxy') return <HAProxyTab sys={sys} />
|
||||||
|
if (tab === 'caddy') return <CaddyTab sys={sys} />
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -363,6 +375,14 @@ function OverviewTab({ sys }) {
|
|||||||
sub={rootDisk ? `${formatBytes(rootDisk.used_bytes)} / ${formatBytes(rootDisk.total_bytes)}` : ''}
|
sub={rootDisk ? `${formatBytes(rootDisk.used_bytes)} / ${formatBytes(rootDisk.total_bytes)}` : ''}
|
||||||
bar={diskPct} barColor={barColor(diskPct || 0)} />
|
bar={diskPct} barColor={barColor(diskPct || 0)} />
|
||||||
<MetricCard icon={Clock} label="Uptime" value={sys.uptime_seconds ? formatUptime(sys.uptime_seconds) : '—'} />
|
<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)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 className="text-sm font-medium text-gray-300 mt-4">System Status</h3>
|
<h3 className="text-sm font-medium text-gray-300 mt-4">System Status</h3>
|
||||||
@ -2289,6 +2309,103 @@ function AgentTab({ agent, status, platform }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function HAProxyTab({ sys }) {
|
||||||
|
const ha = sys?.haproxy
|
||||||
|
if (!ha) return <div className="text-gray-500">Keine HAProxy-Daten</div>
|
||||||
|
const info = ha.info || {}
|
||||||
|
const entries = ha.entries || []
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-sm font-medium text-gray-300">HAProxy Info</h3>
|
||||||
|
<div className="grid grid-cols-4 gap-3">
|
||||||
|
<StatusCard icon={Clock} value={info.uptime || '—'} label="Uptime" />
|
||||||
|
<StatusCard icon={Network} value={info.current_conns} label="Aktive Verb." />
|
||||||
|
<StatusCard icon={Shield} value={info.current_ssl} label="SSL Verb." />
|
||||||
|
<StatusCard value={info.total_requests?.toLocaleString('de-DE') || '0'} label="Requests Total" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className="text-sm font-medium text-gray-300">Proxies</h3>
|
||||||
|
<div className="bg-gray-800/50 rounded-lg border border-gray-700/50 overflow-hidden">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="text-left text-gray-500 text-xs border-b border-gray-700">
|
||||||
|
<th className="px-3 py-2">Proxy</th>
|
||||||
|
<th className="px-3 py-2">Server</th>
|
||||||
|
<th className="px-3 py-2">Typ</th>
|
||||||
|
<th className="px-3 py-2">Status</th>
|
||||||
|
<th className="px-3 py-2">Sessions</th>
|
||||||
|
<th className="px-3 py-2">Requests</th>
|
||||||
|
<th className="px-3 py-2">Traffic In</th>
|
||||||
|
<th className="px-3 py-2">Traffic Out</th>
|
||||||
|
<th className="px-3 py-2">Fehler</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-700/50">
|
||||||
|
{entries.map((e, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td className="px-3 py-2 text-white">{e.proxy_name}</td>
|
||||||
|
<td className="px-3 py-2 text-gray-400">{e.server_name}</td>
|
||||||
|
<td className="px-3 py-2"><span className="text-xs px-1.5 py-0.5 rounded bg-gray-700 text-gray-300">{e.type}</span></td>
|
||||||
|
<td className="px-3 py-2">
|
||||||
|
<span className={`text-xs px-1.5 py-0.5 rounded ${
|
||||||
|
['OPEN', 'UP'].includes(e.status) ? 'bg-green-900/50 text-green-400' :
|
||||||
|
e.status === 'DOWN' ? 'bg-red-900/50 text-red-400' :
|
||||||
|
'bg-gray-700 text-gray-400'
|
||||||
|
}`}>{e.status}</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2 text-gray-400">{e.current_sessions} / {e.total_sessions?.toLocaleString('de-DE')}</td>
|
||||||
|
<td className="px-3 py-2 text-gray-400">{e.requests_total?.toLocaleString('de-DE') || '—'}</td>
|
||||||
|
<td className="px-3 py-2 text-gray-400">{formatBytes(e.bytes_in)}</td>
|
||||||
|
<td className="px-3 py-2 text-gray-400">{formatBytes(e.bytes_out)}</td>
|
||||||
|
<td className="px-3 py-2 text-gray-400">{(e.errors_req || 0) + (e.errors_conn || 0) + (e.errors_resp || 0)}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CaddyTab({ sys }) {
|
||||||
|
const caddy = sys?.caddy
|
||||||
|
if (!caddy) return <div className="text-gray-500">Keine Caddy-Daten</div>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-sm font-medium text-gray-300">Caddy Reverse Proxy</h3>
|
||||||
|
{caddy.version && (
|
||||||
|
<div className="text-xs text-gray-400">Version: <span className="text-white font-mono">{caddy.version}</span></div>
|
||||||
|
)}
|
||||||
|
<div className="bg-gray-800/50 rounded-lg border border-gray-700/50 overflow-hidden">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="text-left text-gray-500 text-xs border-b border-gray-700">
|
||||||
|
<th className="px-3 py-2">Domain</th>
|
||||||
|
<th className="px-3 py-2">Upstream</th>
|
||||||
|
<th className="px-3 py-2">TLS</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-700/50">
|
||||||
|
{(caddy.sites || []).map((s, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td className="px-3 py-2 text-white font-mono">{s.domain}</td>
|
||||||
|
<td className="px-3 py-2 text-gray-400 font-mono">{s.upstream}</td>
|
||||||
|
<td className="px-3 py-2">
|
||||||
|
<span className={`text-xs px-1.5 py-0.5 rounded ${
|
||||||
|
s.tls ? 'bg-green-900/50 text-green-400' : 'bg-red-900/50 text-red-400'
|
||||||
|
}`}>{s.tls ? 'Aktiv' : 'Aus'}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function gwColor(status) {
|
function gwColor(status) {
|
||||||
const s = (status || '').toLowerCase()
|
const s = (status || '').toLowerCase()
|
||||||
if (s === 'online' || s === 'none') return 'text-green-400'
|
if (s === 'online' || s === 'none') return 'text-green-400'
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user