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

@@ -1,7 +1,7 @@
import { useState } from 'react'
import { Button, Divider, Form, Input, InputNumber, Modal, Select, Space, Switch, Tag, Tooltip, Typography, message } from 'antd'
import type { ColumnsType } from 'antd/es/table'
import { GlobalOutlined, PlusOutlined } from '@ant-design/icons'
import { GlobalOutlined, PlusOutlined, StopOutlined, ThunderboltOutlined } from '@ant-design/icons'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useNavigate } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
@@ -28,6 +28,7 @@ interface Domain {
www_redirect: '' | 'to-naked' | 'to-www'
rate_limit_rps: number
max_body_kb: number
disable_h3: boolean
notes?: string
created_at: string
updated_at: string
@@ -176,9 +177,38 @@ export default function DomainsPage() {
void qc.invalidateQueries({ queryKey: ['domains'] })
},
})
const quickToggle = useMutation({
mutationFn: async ({ id, row, checked }: { id: number; row: Domain; checked: boolean }) => {
const { id: _id, created_at: _ca, updated_at: _ua, ...body } = row
await apiClient.put(`/domains/${id}`, { ...body, active: checked })
},
onSuccess: () => { void qc.invalidateQueries({ queryKey: ['domains'] }) },
onError: (e: Error) => message.error(e.message),
})
const columns: ColumnsType<Domain> = [
{ title: t('domains.name'), dataIndex: 'name', key: 'name' },
{
title: t('domains.name'), dataIndex: 'name', key: 'name',
render: (name: string, row: Domain) => (
<Space size={4} wrap>
<span>{name}</span>
{row.maintenance_mode && (
<Tooltip title={row.maintenance_message || t('domains.maintenanceTag')}>
<Tag icon={<StopOutlined />} color="orange" style={{ margin: 0 }}>
{t('domains.maintenanceTag')}
</Tag>
</Tooltip>
)}
{row.rate_limit_rps > 0 && (
<Tooltip title={`${row.rate_limit_rps} req/s`}>
<Tag icon={<ThunderboltOutlined />} color="purple" style={{ margin: 0, fontSize: 11 }}>
{t('domains.rateLimitTag')}
</Tag>
</Tooltip>
)}
</Space>
),
},
{
title: t('domains.primaryBackend'), dataIndex: 'primary_backend_id', key: 'primary_backend_id',
render: (id?: number | null) => {
@@ -196,7 +226,17 @@ export default function DomainsPage() {
)
},
},
{ title: t('domains.active'), dataIndex: 'active', key: 'active', render: (v: boolean) => <StatusDot active={v} /> },
{
title: t('domains.active'), dataIndex: 'active', key: 'active', width: 80,
render: (v: boolean, row: Domain) => (
<Switch
size="small"
checked={v}
loading={quickToggle.isPending && quickToggle.variables?.id === row.id}
onChange={(checked) => quickToggle.mutate({ id: row.id, row, checked })}
/>
),
},
{ title: t('domains.httpToHttps'), dataIndex: 'http_to_https', key: 'http_to_https', render: (v: boolean) => <StatusDot active={v} activeLabel="HTTPS" inactiveLabel="HTTP" /> },
{ title: t('domains.hsts'), dataIndex: 'hsts_enabled', key: 'hsts', render: (v: boolean) => <StatusDot active={v} /> },
{