feat: umfangreiches UI+API-Polish (v1.1.36–1.1.42)

Backend:
- Audit-Log: Search-Endpoint mit ILIKE-Filter (actor/action/subject/date)
- NTP: /ntp/status via chronyc tracking (Stratum, Offset, Quelle)
- System: /service-restart mit Allowlist (haproxy/squid/unbound/chrony/scheduler)
- Domain-Response-Headers + Rate-Limit (Migration 0024)
- Join-Tokens (Migration 0025), Cluster-mTLS, Aggregator-Fan-Out
- apt-Service für Update-Banner (apt-get update + Versionsprüfung)
- Backup-Retry mit exponential backoff (retry_apt 3×)
- publish.sh fail-fast + cleanup-old.sh (max 10 Versionen)

Frontend:
- Audit-Log-Page (/audit) mit Filter + Pagination
- ErrorBoundary an React-Root + Vite build-target festgenagelt (iOS 15+)
- Storage-Schema-Stamp: auto-wipe bei Versions-Mismatch (blank-page-Fix)
- EmptyState-Komponente überall ausgerollt
- SSL: Aggregate-Karte (total/expiring/expired/errors)
- Backups: Aggregate-Karte (letzter Backup/Größe/Fehlschläge 24h) + Backup-Now
- NTP: Sync-Status-Karte (chronyc tracking live)
- Domains: Backend-UP/DOWN-Chip aus HAProxy-Stats
- Backends: HAProxy-Status-Spalte (UP/DEGRADED/DOWN)
- Settings: Service-Neustart-Karte (haproxy/squid/unbound/chrony/scheduler)
- Settings: Upgrade-Status-Card, Wartungsmodus, Auto-Update, Retention
- Dashboard: Recent-Alerts, Cluster-Health, License-Chip, Onboarding-Hint
- System-Regeln im Firewall als eigener Tab

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-05-19 16:18:41 +02:00
parent 3178e25e78
commit 35b7308ce2
82 changed files with 8408 additions and 392 deletions

View File

@@ -3,7 +3,10 @@ import { Button, Form, Input, Modal, Popconfirm, Select, Space, Tag, message } f
import type { ColumnsType } from 'antd/es/table'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import { GroupOutlined } from '@ant-design/icons'
import DataTable from '../../components/DataTable'
import EmptyState from '../../components/EmptyState'
import apiClient, { isEnvelope } from '../../api/client'
import type { AddressGroup, AddressObject } from './types'
@@ -84,15 +87,30 @@ export default function AddressGroupsTab() {
},
]
const openCreate = () => {
setCreating(true); form.resetFields()
form.setFieldsValue({ member_ids: [] })
}
return (
<>
<Button type="primary" className="mb-16" onClick={() => {
setCreating(true); form.resetFields()
form.setFieldsValue({ member_ids: [] })
}}>
<Button type="primary" className="mb-16" onClick={openCreate}>
{t('fw.ag.add')}
</Button>
<DataTable rowKey="id" loading={isLoading} dataSource={groups ?? []} columns={columns} />
<DataTable
rowKey="id"
loading={isLoading}
dataSource={groups ?? []}
columns={columns}
emptyContent={
<EmptyState
icon={<GroupOutlined />}
title={t('fw.ag.emptyTitle')}
description={t('fw.ag.emptyDesc')}
action={<Button type="primary" onClick={openCreate}>{t('fw.ag.add')}</Button>}
/>
}
/>
<Modal
title={editing ? t('fw.ag.edit') : t('fw.ag.add')}
open={editing !== null || creating}

View File

@@ -3,7 +3,10 @@ import { Button, Form, Input, Modal, Popconfirm, Select, Space, Tag, message } f
import type { ColumnsType } from 'antd/es/table'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import { EnvironmentOutlined } from '@ant-design/icons'
import DataTable from '../../components/DataTable'
import EmptyState from '../../components/EmptyState'
import apiClient, { isEnvelope } from '../../api/client'
import type { AddressObject } from './types'
@@ -77,15 +80,32 @@ export default function AddressObjectsTab() {
},
]
const openCreate = () => {
setCreating(true); form.resetFields()
form.setFieldsValue({ kind: 'host' })
}
return (
<>
<Button type="primary" className="mb-16" onClick={() => {
setCreating(true); form.resetFields()
form.setFieldsValue({ kind: 'host' })
}}>
<Button type="primary" className="mb-16" onClick={openCreate}>
{t('fw.ao.add')}
</Button>
<DataTable rowKey="id" loading={isLoading} dataSource={data ?? []} columns={columns} />
<DataTable
rowKey="id"
loading={isLoading}
dataSource={data ?? []}
columns={columns}
emptyContent={
<EmptyState
icon={<EnvironmentOutlined />}
title={t('fw.ao.emptyTitle')}
description={t('fw.ao.emptyDesc')}
action={
<Button type="primary" onClick={openCreate}>{t('fw.ao.add')}</Button>
}
/>
}
/>
<Modal
title={editing ? t('fw.ao.edit') : t('fw.ao.add')}
open={editing !== null || creating}

View File

@@ -3,7 +3,10 @@ import { Button, Form, Input, InputNumber, Modal, Popconfirm, Select, Space, Swi
import type { ColumnsType } from 'antd/es/table'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import { BranchesOutlined } from '@ant-design/icons'
import DataTable from '../../components/DataTable'
import EmptyState from '../../components/EmptyState'
import apiClient, { isEnvelope } from '../../api/client'
import type { FwZone, NATRule } from './types'
@@ -132,15 +135,30 @@ export default function NATRulesTab() {
},
]
const openCreate = () => {
setCreating(true); form.resetFields()
form.setFieldsValue({ priority: 100, enabled: true, kind: 'dnat' })
}
return (
<>
<Button type="primary" className="mb-16" onClick={() => {
setCreating(true); form.resetFields()
form.setFieldsValue({ priority: 100, enabled: true, kind: 'dnat' })
}}>
<Button type="primary" className="mb-16" onClick={openCreate}>
{t('fw.nat.add')}
</Button>
<DataTable rowKey="id" loading={isLoading} dataSource={data ?? []} columns={columns} />
<DataTable
rowKey="id"
loading={isLoading}
dataSource={data ?? []}
columns={columns}
emptyContent={
<EmptyState
icon={<BranchesOutlined />}
title={t('fw.nat.emptyTitle')}
description={t('fw.nat.emptyDesc')}
action={<Button type="primary" onClick={openCreate}>{t('fw.nat.add')}</Button>}
/>
}
/>
<Modal
title={editing ? t('fw.nat.edit') : t('fw.nat.add')}
open={editing !== null || creating}

View File

@@ -3,8 +3,10 @@ import { Button, Form, Input, InputNumber, Modal, Popconfirm, Select, Space, Swi
import type { ColumnsType } from 'antd/es/table'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import { FireOutlined } from '@ant-design/icons'
import DataTable from '../../components/DataTable'
import SystemRulesCard from './SystemRules'
import EmptyState from '../../components/EmptyState'
import apiClient, { isEnvelope } from '../../api/client'
import type { AddressGroup, AddressObject, FwRule, FwService, FwZone, ServiceGroup, Zone } from './types'
@@ -197,20 +199,34 @@ export default function RulesTab() {
},
]
const openCreate = () => {
setCreating(true); form.resetFields()
form.setFieldsValue({
priority: 100, enabled: true, action: 'accept', log: false,
src_zone: 'any', dst_zone: 'any',
src_kind: 'any', dst_kind: 'any', service_kind: 'any',
})
}
return (
<>
<SystemRulesCard />
<Button type="primary" className="mb-16" onClick={() => {
setCreating(true); form.resetFields()
form.setFieldsValue({
priority: 100, enabled: true, action: 'accept', log: false,
src_zone: 'any', dst_zone: 'any',
src_kind: 'any', dst_kind: 'any', service_kind: 'any',
})
}}>
<Button type="primary" className="mb-16" onClick={openCreate}>
{t('fw.rule.add')}
</Button>
<DataTable rowKey="id" loading={isLoading} dataSource={rules ?? []} columns={columns} />
<DataTable
rowKey="id"
loading={isLoading}
dataSource={rules ?? []}
columns={columns}
emptyContent={
<EmptyState
icon={<FireOutlined />}
title={t('fw.rule.emptyTitle')}
description={t('fw.rule.emptyDesc')}
action={<Button type="primary" onClick={openCreate}>{t('fw.rule.add')}</Button>}
/>
}
/>
<Modal
title={editing ? t('fw.rule.edit') : t('fw.rule.add')}
open={editing !== null || creating}

View File

@@ -3,7 +3,10 @@ import { Button, Form, Input, Modal, Popconfirm, Select, Space, Tag, message } f
import type { ColumnsType } from 'antd/es/table'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import { GroupOutlined } from '@ant-design/icons'
import DataTable from '../../components/DataTable'
import EmptyState from '../../components/EmptyState'
import apiClient, { isEnvelope } from '../../api/client'
import type { FwService, ServiceGroup } from './types'
@@ -84,15 +87,30 @@ export default function ServiceGroupsTab() {
},
]
const openCreate = () => {
setCreating(true); form.resetFields()
form.setFieldsValue({ member_ids: [] })
}
return (
<>
<Button type="primary" className="mb-16" onClick={() => {
setCreating(true); form.resetFields()
form.setFieldsValue({ member_ids: [] })
}}>
<Button type="primary" className="mb-16" onClick={openCreate}>
{t('fw.sg.add')}
</Button>
<DataTable rowKey="id" loading={isLoading} dataSource={groups ?? []} columns={columns} />
<DataTable
rowKey="id"
loading={isLoading}
dataSource={groups ?? []}
columns={columns}
emptyContent={
<EmptyState
icon={<GroupOutlined />}
title={t('fw.sg.emptyTitle')}
description={t('fw.sg.emptyDesc')}
action={<Button type="primary" onClick={openCreate}>{t('fw.sg.add')}</Button>}
/>
}
/>
<Modal
title={editing ? t('fw.sg.edit') : t('fw.sg.add')}
open={editing !== null || creating}

View File

@@ -3,7 +3,10 @@ import { Button, Form, Input, InputNumber, Modal, Popconfirm, Select, Space, Tag
import type { ColumnsType } from 'antd/es/table'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import { ApiOutlined } from '@ant-design/icons'
import DataTable from '../../components/DataTable'
import EmptyState from '../../components/EmptyState'
import apiClient, { isEnvelope } from '../../api/client'
import type { FwService } from './types'
@@ -88,15 +91,30 @@ export default function ServicesTab() {
},
]
const openCreate = () => {
setCreating(true); form.resetFields()
form.setFieldsValue({ proto: 'tcp' })
}
return (
<>
<Button type="primary" className="mb-16" onClick={() => {
setCreating(true); form.resetFields()
form.setFieldsValue({ proto: 'tcp' })
}}>
<Button type="primary" className="mb-16" onClick={openCreate}>
{t('fw.svc.add')}
</Button>
<DataTable rowKey="id" loading={isLoading} dataSource={data ?? []} columns={columns} />
<DataTable
rowKey="id"
loading={isLoading}
dataSource={data ?? []}
columns={columns}
emptyContent={
<EmptyState
icon={<ApiOutlined />}
title={t('fw.svc.emptyTitle')}
description={t('fw.svc.emptyDesc')}
action={<Button type="primary" onClick={openCreate}>{t('fw.svc.add')}</Button>}
/>
}
/>
<Modal
title={editing ? t('fw.svc.edit') : t('fw.svc.add')}
open={editing !== null || creating}

View File

@@ -4,8 +4,11 @@ import type { ColumnsType } from 'antd/es/table'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import { ApartmentOutlined } from '@ant-design/icons'
import apiClient, { isEnvelope } from '../../api/client'
import DataTable from '../../components/DataTable'
import EmptyState from '../../components/EmptyState'
import type { FwZone } from './types'
interface FormValues {
@@ -77,14 +80,29 @@ export default function ZonesTab() {
},
]
const openCreate = () => { setCreating(true); form.resetFields() }
return (
<>
<Button type="primary" className="mb-16" onClick={() => {
setCreating(true); form.resetFields()
}}>
<Button type="primary" className="mb-16" onClick={openCreate}>
{t('fw.zone.add')}
</Button>
<DataTable rowKey="id" loading={isLoading} dataSource={data ?? []} columns={columns} />
<DataTable
rowKey="id"
loading={isLoading}
dataSource={data ?? []}
columns={columns}
emptyContent={
<EmptyState
icon={<ApartmentOutlined />}
title={t('fw.zone.emptyTitle')}
description={t('fw.zone.emptyDesc')}
action={
<Button type="primary" onClick={openCreate}>{t('fw.zone.add')}</Button>
}
/>
}
/>
<Modal
title={editing ? t('fw.zone.edit') : t('fw.zone.add')}

View File

@@ -9,6 +9,7 @@ import ServicesTab from './Services'
import ServiceGroupsTab from './ServiceGroups'
import RulesTab from './Rules'
import NATRulesTab from './NATRules'
import SystemRulesCard from './SystemRules'
import ZonesTab from './Zones'
export default function FirewallPage() {
@@ -22,6 +23,7 @@ export default function FirewallPage() {
{ key: 'addrGrp', label: t('fw.tabs.addrGrp'), children: <AddressGroupsTab /> },
{ key: 'services', label: t('fw.tabs.services'), children: <ServicesTab /> },
{ key: 'svcGrp', label: t('fw.tabs.svcGrp'), children: <ServiceGroupsTab /> },
{ key: 'system', label: t('fw.tabs.system'), children: <SystemRulesCard /> },
]
return (