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:
Debian
2026-05-24 08:55:22 +02:00
parent 386972366b
commit 99982ef9d1
23 changed files with 755 additions and 63 deletions

View File

@@ -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} />