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:
@@ -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>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user