feat(ui): Quick-Toggles, Config-Preview, Dashboard-Alerts, Domain-Detail-Health
Quick-Toggle-Switches (kein Modal nötig) für: Backends, Backend-Server, DNS-Zonen, DNS-Records, Domains (active), Firewall-Rules, NAT-Rules, Forward-Proxy ACLs, Routing-Rules. Dashboard: Alert-Banner für komplett ausgefallene Backends (HAProxy-Stats) und Domains im Maintenance-Mode. Domain-Detail: HAProxy-Live-Health-Badge (15s Polling), TLS-Cert ausstellen/erneuern direkt aus dem Detail, Routing-Rules-Panel inline. Config-Preview (Settings): alle 4 Generatoren (haproxy, nftables, squid, unbound) rendern via RenderToString ohne Disk-Write — GET /system/config-preview. ActionButtons: Viewer-Rolle blendet Delete aus (RBAC-Ergänzung). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -63,7 +63,7 @@ function useAuditLive(keep = 15) {
|
||||
|
||||
// ── Wire shapes ───────────────────────────────────────────────
|
||||
|
||||
interface Domain { id: number; active: boolean; primary_backend_id?: number | null }
|
||||
interface Domain { id: number; name: string; active: boolean; primary_backend_id?: number | null; maintenance_mode: boolean }
|
||||
interface Backend { id: number; active: boolean; name: string }
|
||||
interface Iface { id: number; active: boolean; name: string; type: string }
|
||||
interface FwRule { id: number; enabled: boolean; action: string }
|
||||
@@ -251,6 +251,33 @@ export default function DashboardPage() {
|
||||
&& Date.now() / 1000 - s.last_handshake_unix < 180).length
|
||||
|
||||
const now = Date.now()
|
||||
const maintenanceDomains = (domains.data ?? []).filter(d => d.maintenance_mode)
|
||||
|
||||
// Backends die komplett DOWN sind (mind. 1 Server mit echtem Check,
|
||||
// aber kein einziger UP) — liefert friendly Backend-Namen.
|
||||
const downBackends = (() => {
|
||||
const stats = haproxyBackends.data ?? []
|
||||
const bklist = backends.data ?? []
|
||||
if (!stats.length || !bklist.length) return []
|
||||
// gruppiere Stat-Zeilen nach HAProxy-Backend-Name (eg_backend_<id>)
|
||||
const byBackend = new Map<string, typeof stats>()
|
||||
for (const s of stats) {
|
||||
const arr = byBackend.get(s.backend) ?? []; arr.push(s)
|
||||
byBackend.set(s.backend, arr)
|
||||
}
|
||||
const down: string[] = []
|
||||
for (const [haName, servers] of byBackend) {
|
||||
const hasRealCheck = servers.some(s => s.status !== 'no check')
|
||||
if (hasRealCheck && !servers.some(s => s.status === 'UP')) {
|
||||
const m = /^eg_backend_(\d+)$/.exec(haName)
|
||||
const id = m ? Number(m[1]) : null
|
||||
const b = id != null ? bklist.find(x => x.id === id) : null
|
||||
down.push(b?.name ?? haName)
|
||||
}
|
||||
}
|
||||
return down
|
||||
})()
|
||||
|
||||
const certsSoon = (tlsCerts.data ?? []).filter(c => {
|
||||
if (!c.not_after) return false
|
||||
const exp = new Date(c.not_after).getTime()
|
||||
@@ -303,6 +330,34 @@ export default function DashboardPage() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{downBackends.length > 0 && (
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
className="mb-12"
|
||||
message={t('dashboard.downBackendsAlert', { count: downBackends.length })}
|
||||
description={
|
||||
<Space wrap size={4}>
|
||||
{downBackends.map(name => <Link key={name} to="/backends">{name}</Link>)}
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{maintenanceDomains.length > 0 && (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
className="mb-12"
|
||||
message={t('dashboard.maintenanceAlert', { count: maintenanceDomains.length })}
|
||||
description={
|
||||
<Space wrap size={4}>
|
||||
{maintenanceDomains.map(d => <Link key={d.id} to={`/domains/${d.id}`}>{d.name}</Link>)}
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* ── KPI tiles (compact strip) ──────────────────── */}
|
||||
<Row gutter={[12, 12]} className="mb-12">
|
||||
<KPI icon={<GlobalOutlined />} label={t('dashboard.kpi.domains')} value={activeDomains} total={(domains.data ?? []).length} />
|
||||
|
||||
Reference in New Issue
Block a user