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:
@@ -1,4 +1,4 @@
|
||||
import { Button, Card, Form, Input, message, Typography } from 'antd'
|
||||
import { Alert, Button, Card, Form, Input, Space, Typography, message } from 'antd'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@@ -17,13 +17,27 @@ interface SetupValues {
|
||||
license_key?: string
|
||||
}
|
||||
|
||||
// FQDN-Regex: erlaubt RFC-1123-Labels (a-z 0-9 -) durch Punkte getrennt,
|
||||
// 1+ Labels, keine führenden/abschließenden Bindestriche, kein TLD-Zwang
|
||||
// (wir verifizieren live nicht die DNS-Existenz, nur die Form-Plausibilität).
|
||||
const FQDN_RE = /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/
|
||||
|
||||
export default function SetupPage({ onComplete: _onComplete }: Props) {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const onFinish = async (vals: SetupValues) => {
|
||||
try {
|
||||
await apiClient.post('/setup/complete', vals)
|
||||
// FQDN immer lower-casen — der Server erwartet das auch im
|
||||
// EqualFold-Vergleich beim Login, also vereinheitlichen wir hier
|
||||
// damit FQDN + ACME-Cert-Subject identisch werden.
|
||||
const normalised: SetupValues = {
|
||||
...vals,
|
||||
admin_email: vals.admin_email.trim().toLowerCase(),
|
||||
acme_email: vals.acme_email.trim().toLowerCase(),
|
||||
fqdn: vals.fqdn.trim().toLowerCase(),
|
||||
}
|
||||
await apiClient.post('/setup/complete', normalised)
|
||||
message.success(t('setup.successTitle'))
|
||||
// Setup doesn't issue a session — the operator must log in.
|
||||
navigate('/login', { replace: true })
|
||||
@@ -34,47 +48,75 @@ export default function SetupPage({ onComplete: _onComplete }: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', minHeight: '100vh', alignItems: 'center', justifyContent: 'center', background: '#f0f2f5' }}>
|
||||
<Card style={{ width: 520 }}>
|
||||
<Typography.Title level={3}>{t('setup.title')}</Typography.Title>
|
||||
<Typography.Paragraph type="secondary">{t('setup.intro')}</Typography.Paragraph>
|
||||
<div style={{ display: 'flex', minHeight: '100vh', alignItems: 'center', justifyContent: 'center', background: '#f0f2f5', padding: 24 }}>
|
||||
<Card style={{ width: 560 }}>
|
||||
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||
<Typography.Title level={3} style={{ marginBottom: 0 }}>{t('setup.title')}</Typography.Title>
|
||||
<Typography.Paragraph type="secondary" style={{ marginBottom: 12 }}>
|
||||
{t('setup.intro')}
|
||||
</Typography.Paragraph>
|
||||
</Space>
|
||||
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message={t('setup.preflightTitle')}
|
||||
description={t('setup.preflightDesc')}
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
|
||||
<Form layout="vertical" onFinish={onFinish}>
|
||||
<Form.Item
|
||||
label={t('setup.adminEmail')}
|
||||
name="admin_email"
|
||||
extra={t('setup.adminEmailHint')}
|
||||
rules={[{ required: true, type: 'email' }]}
|
||||
>
|
||||
<Input autoComplete="email" autoFocus />
|
||||
<Input autoComplete="email" autoFocus placeholder="admin@example.com" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('setup.adminPassword')}
|
||||
name="admin_password"
|
||||
extra={t('setup.passwordRule')}
|
||||
rules={[{ required: true, min: 12, message: t('setup.passwordRule') }]}
|
||||
help={t('setup.passwordRule')}
|
||||
>
|
||||
<Input.Password autoComplete="new-password" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('setup.fqdn')}
|
||||
name="fqdn"
|
||||
rules={[{ required: true }]}
|
||||
extra={t('setup.fqdnHint')}
|
||||
rules={[
|
||||
{ required: true },
|
||||
{
|
||||
pattern: FQDN_RE,
|
||||
message: t('setup.fqdnInvalid'),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="eg.example.com" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('setup.acmeEmail')}
|
||||
name="acme_email"
|
||||
extra={t('setup.acmeEmailHint')}
|
||||
rules={[{ required: true, type: 'email' }]}
|
||||
>
|
||||
<Input />
|
||||
<Input placeholder="ops@example.com" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('setup.licenseKey')}
|
||||
name="license_key"
|
||||
extra={t('setup.licenseKeyHint')}
|
||||
>
|
||||
<Input />
|
||||
<Input placeholder="EG-XXXX-XXXX-XXXX" />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
|
||||
<Form.Item style={{ marginBottom: 0 }}>
|
||||
<Button type="primary" htmlType="submit" block>
|
||||
{t('setup.submit')}
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user