feat(dashboard): HAProxy Listener-Stats + i18n-Relativzeit

- haproxy_stats.go: FRONTEND-Rows aus show-stat CSV auslesen und als
  frontends[] zurückgeben (Name, Sessions, MaxSess, Bytes, ReqTot/Rate)
- Dashboard: Listener-Sektion über Backend-Liste; query jetzt unified
  haproxyStats mit backends/frontends alias für Abwärtskompatibilität
- common.relTime.*-Keys (en+de) eingeführt; relativeTime/relativeFromIso
  in Dashboard + relTime in WireGuard Servers/Clients auf t-Parameter
  umgestellt statt hardcodierter Strings
- CertExpiry in Cluster-Seite nutzt jetzt certDaysRemaining/certExpiredDaysAgo

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-05-24 12:09:53 +02:00
parent e18037375f
commit c9caf35596
6 changed files with 79 additions and 18 deletions

View File

@@ -757,7 +757,8 @@
},
"haproxyCard": {
"title": "HAProxy-Backends (live)",
"empty": "Keine Backend-Stats erreichbar (HAProxy down oder admin.sock-permission)."
"empty": "Keine Backend-Stats erreichbar (HAProxy down oder admin.sock-permission).",
"frontends": "Listener"
},
"resCard": {
"load": "Load",

View File

@@ -757,7 +757,8 @@
},
"haproxyCard": {
"title": "HAProxy backends (live)",
"empty": "No backend stats reachable (HAProxy down or admin.sock permission)."
"empty": "No backend stats reachable (HAProxy down or admin.sock permission).",
"frontends": "Listeners"
},
"resCard": {
"load": "Load",

View File

@@ -104,6 +104,11 @@ interface HAProxyBackend {
req_tot: number; req_rate: number
last_change_sec: number; health?: string
}
interface HAProxyFrontend {
name: string; sessions: number; max_sess: number
bytes_in: number; bytes_out: number
req_tot: number; req_rate: number
}
// ── Fetchers ──────────────────────────────────────────────────
@@ -189,11 +194,19 @@ export default function DashboardPage() {
queryFn: () => fetchOne<Resources>('/system/resources'),
refetchInterval: 10_000,
})
const haproxyBackends = useQuery({
const haproxyStats = useQuery({
queryKey: ['haproxy', 'stats'],
queryFn: () => fetchList<HAProxyBackend>('/haproxy/stats', 'backends'),
queryFn: async () => {
try {
const r = await apiClient.get('/haproxy/stats')
if (!isEnvelope(r.data)) return { backends: [] as HAProxyBackend[], frontends: [] as HAProxyFrontend[] }
const d = r.data.data as { backends?: HAProxyBackend[]; frontends?: HAProxyFrontend[] }
return { backends: d.backends ?? [], frontends: d.frontends ?? [] }
} catch { return { backends: [] as HAProxyBackend[], frontends: [] as HAProxyFrontend[] } }
},
refetchInterval: 10_000,
})
const haproxyBackends = { data: haproxyStats.data?.backends }
const auditEntries = useAuditLive(15)
const domains = useQuery({ queryKey: ['domains'], queryFn: () => fetchList<Domain>('/domains', 'domains') })
@@ -450,6 +463,27 @@ export default function DashboardPage() {
{/* ── HAProxy backend live health ─────────────────── */}
<Col xs={24} lg={12}>
<Card size="small" title={<><DatabaseOutlined /> {t('dashboard.haproxyCard.title')}</>} className="h-100">
{(haproxyStats.data?.frontends ?? []).length > 0 && (
<div style={{ marginBottom: 8, paddingBottom: 8, borderBottom: '1px solid #E2E8F0' }}>
<Text type="secondary" style={{ fontSize: 10, textTransform: 'uppercase', letterSpacing: 0.5 }}>
{t('dashboard.haproxyCard.frontends')}
</Text>
<Space direction="vertical" size={2} style={{ width: '100%', marginTop: 4 }}>
{(haproxyStats.data?.frontends ?? []).map((f) => (
<div key={f.name} style={{ fontSize: 12, color: '#334155' }}>
<Space size={6} wrap>
<code style={{ fontSize: 11 }}>{f.name}</code>
<Text type="secondary" style={{ fontSize: 11 }}>
{f.sessions} sess
{f.req_rate > 0 ? ` · ${f.req_rate}/s` : ''}
{` · ↓${formatBytes(f.bytes_in)}${formatBytes(f.bytes_out)}`}
</Text>
</Space>
</div>
))}
</Space>
</div>
)}
{(haproxyBackends.data ?? []).length === 0 ? (
<Text type="secondary">{t('dashboard.haproxyCard.empty')}</Text>
) : (