feat: HA-Cluster v1.2.x — Split-Brain, TOTP, Enterprise-FW, Drift-Fix, VIP-Recovery
- keepalived: pg_role='standby' hat Vorrang vor role für BACKUP-Bestimmung - keepalived-master.sh: gecrasht Dienste beim MASTER-Übergang starten (nicht nur reload) - confighash: ip_addresses per Interface-Name hashen statt per FK (Cross-Node-Drift-Fix) - TOTP/2FA: RFC 6238 — Setup-Flow, QR-Code, Admin-Disable; two-step Login - Firewall-UI: Enterprise-Design — auto-Beschreibung, icon-only Actions, zero-hit Indikator - fe80-Filter: Link-local IPv6 aus NTP/DNS Listen-Dropdowns entfernen - VIP-Dashboard, Dual-Path VRRP, GW-Tracking (Migrations 0033/0034) - Forward Proxy + DNS erweiterte Einstellungen (Migrations 0031/0032) - unbound-control: edgeguard in unbound-Gruppe via postinst Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import {
|
||||
Alert, Button, Card, Col, Form, Input, InputNumber, Modal, Progress, Row, Select, Space, Statistic, Switch, Tag, Tooltip, Typography, message,
|
||||
Alert, Button, Card, Col, Form, Input, InputNumber, Modal, Progress, Row, Select, Space, Statistic, Switch, Tag, Tooltip, Typography, message, Divider,
|
||||
} from 'antd'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import { ArrowDownOutlined, ArrowUpOutlined, BarChartOutlined, CheckCircleOutlined, CloseCircleOutlined, CloudServerOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
@@ -31,6 +31,18 @@ interface ACL {
|
||||
|
||||
interface ServiceStatus { label: string; unit: string; active: boolean; state: string }
|
||||
|
||||
interface ProxySettings {
|
||||
id: number
|
||||
listen_addresses: string
|
||||
listen_port: number
|
||||
cache_mem_mb: number
|
||||
cache_dir_mb: number
|
||||
max_obj_size_mb: number
|
||||
connect_timeout: number
|
||||
read_timeout: number
|
||||
request_timeout: number
|
||||
}
|
||||
|
||||
interface FormValues {
|
||||
name: string
|
||||
acl_type: string
|
||||
@@ -98,6 +110,27 @@ export default function ForwardProxyPage() {
|
||||
refetchInterval: 30_000,
|
||||
})
|
||||
|
||||
const { data: settings, isLoading: settingsLoading } = useQuery({
|
||||
queryKey: ['fwd-proxy', 'settings'],
|
||||
queryFn: async () => {
|
||||
const r = await apiClient.get('/forward-proxy/settings')
|
||||
if (!isEnvelope(r.data)) return null
|
||||
return r.data.data as ProxySettings
|
||||
},
|
||||
})
|
||||
|
||||
const [settingsForm] = Form.useForm<ProxySettings>()
|
||||
const saveSettings = useMutation({
|
||||
mutationFn: async (v: ProxySettings) => {
|
||||
await apiClient.put('/forward-proxy/settings', v)
|
||||
},
|
||||
onSuccess: () => {
|
||||
message.success(t('common.save'))
|
||||
void qc.invalidateQueries({ queryKey: ['fwd-proxy', 'settings'] })
|
||||
},
|
||||
onError: () => message.error(t('fwd.settings.saveFailed')),
|
||||
})
|
||||
|
||||
const [editing, setEditing] = useState<ACL | null>(null)
|
||||
const [creating, setCreating] = useState(false)
|
||||
const [form] = Form.useForm<FormValues>()
|
||||
@@ -284,6 +317,97 @@ export default function ForwardProxyPage() {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Card
|
||||
size="small"
|
||||
className="mb-12"
|
||||
title={t('fwd.settings.title')}
|
||||
loading={settingsLoading}
|
||||
>
|
||||
<Form
|
||||
form={settingsForm}
|
||||
layout="vertical"
|
||||
initialValues={settings ?? {
|
||||
listen_addresses: '', listen_port: 3128,
|
||||
cache_mem_mb: 64, cache_dir_mb: 100, max_obj_size_mb: 4,
|
||||
connect_timeout: 60, read_timeout: 300, request_timeout: 300,
|
||||
}}
|
||||
key={settings?.id ?? 'loading'}
|
||||
onFinish={(v) => saveSettings.mutate(v)}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} md={16}>
|
||||
<Form.Item
|
||||
label={t('fwd.settings.listenAddresses')}
|
||||
name="listen_addresses"
|
||||
extra={t('fwd.settings.listenAddressesExtra')}
|
||||
>
|
||||
<Input placeholder="10.0.5.1, 10.0.20.1" disabled={isViewer} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={8}>
|
||||
<Form.Item
|
||||
label={t('fwd.settings.listenPort')}
|
||||
name="listen_port"
|
||||
extra={t('fwd.settings.listenPortExtra')}
|
||||
>
|
||||
<InputNumber min={1} max={65535} style={{ width: '100%' }} disabled={isViewer} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Divider plain>{t('fwd.settings.cacheSection')}</Divider>
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} sm={8}>
|
||||
<Form.Item label={t('fwd.settings.cacheMemMB')} name="cache_mem_mb" extra={t('fwd.settings.cacheMemMBExtra')}>
|
||||
<InputNumber min={16} max={8192} style={{ width: '100%' }} addonAfter="MB" disabled={isViewer} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} sm={8}>
|
||||
<Form.Item label={t('fwd.settings.cacheDirMB')} name="cache_dir_mb" extra={t('fwd.settings.cacheDirMBExtra')}>
|
||||
<InputNumber min={100} max={102400} style={{ width: '100%' }} addonAfter="MB" disabled={isViewer} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} sm={8}>
|
||||
<Form.Item label={t('fwd.settings.maxObjSizeMB')} name="max_obj_size_mb" extra={t('fwd.settings.maxObjSizeMBExtra')}>
|
||||
<InputNumber min={1} max={1024} style={{ width: '100%' }} addonAfter="MB" disabled={isViewer} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Divider plain>{t('fwd.settings.timeoutSection')}</Divider>
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} sm={8}>
|
||||
<Form.Item label={t('fwd.settings.connectTimeout')} name="connect_timeout" extra={t('fwd.settings.connectTimeoutExtra')}>
|
||||
<InputNumber min={5} max={3600} style={{ width: '100%' }} addonAfter="s" disabled={isViewer} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} sm={8}>
|
||||
<Form.Item label={t('fwd.settings.readTimeout')} name="read_timeout" extra={t('fwd.settings.readTimeoutExtra')}>
|
||||
<InputNumber min={30} max={3600} style={{ width: '100%' }} addonAfter="s" disabled={isViewer} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} sm={8}>
|
||||
<Form.Item label={t('fwd.settings.requestTimeout')} name="request_timeout" extra={t('fwd.settings.requestTimeoutExtra')}>
|
||||
<InputNumber min={30} max={3600} style={{ width: '100%' }} addonAfter="s" disabled={isViewer} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={saveSettings.isPending}
|
||||
disabled={isViewer}
|
||||
>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<Divider />
|
||||
|
||||
<DataTable
|
||||
rowKey="id"
|
||||
loading={isLoading}
|
||||
|
||||
Reference in New Issue
Block a user