feat(ui): (a) Backends + Routing-Rules + Settings pages + Sidebar

CRUD-Pages analog Domains:
* Backends: AntD Table + Modal-Form mit name, scheme, address, port,
  health_check_path, active. TanStack-Query gegen /api/v1/backends.
* RoutingRules: Table mit Domain-Name- und Backend-Label-Resolution,
  Modal mit Select-Pickern für Domain + Backend, Path-Prefix,
  Priority, Active. Drei parallele Queries (rules, domains, backends)
  liefern die Listen.
* Settings: read-only Descriptions-Cards mit /system/health und
  /setup/status. Editable Werte folgen später.

Sidebar erweitert um Backends, Routing-Rules, Settings (mit AntD-
Icons). i18n de/en für alle drei neuen Seiten.

bun run build + npx tsc -b strict (0 errors). Live-Smoke gegen API:
SPA-Routes /backends, /routing-rules, /settings antworten 200 mit
index.html (NoRoute fallback wirkt).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Debian
2026-05-09 11:23:00 +02:00
parent b507d2a7d5
commit 0b45b23d45
7 changed files with 491 additions and 3 deletions

View File

@@ -0,0 +1,66 @@
import { Card, Descriptions, Spin, Typography } from 'antd'
import { useQuery } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import apiClient, { isEnvelope } from '../../api/client'
interface SetupStatus {
completed: boolean
admin_email: string
fqdn: string
}
interface SystemHealth {
status: string
version: string
}
export default function SettingsPage() {
const { t } = useTranslation()
const { data: setupStatus, isLoading: loadingSetup } = useQuery({
queryKey: ['setup', 'status'],
queryFn: async () => {
const r = await apiClient.get('/setup/status')
if (isEnvelope(r.data)) return r.data.data as SetupStatus
return null
},
})
const { data: health, isLoading: loadingHealth } = useQuery({
queryKey: ['system', 'health'],
queryFn: async () => {
const r = await apiClient.get('/system/health')
if (isEnvelope(r.data)) return r.data.data as SystemHealth
return null
},
})
if (loadingSetup || loadingHealth) {
return <Spin />
}
return (
<div>
<Typography.Title level={3}>{t('settings.title')}</Typography.Title>
<Typography.Paragraph type="secondary">{t('settings.intro')}</Typography.Paragraph>
<Card title={t('settings.systemInfo')} style={{ marginBottom: 16 }}>
<Descriptions column={1}>
<Descriptions.Item label={t('settings.version')}>{health?.version ?? '—'}</Descriptions.Item>
<Descriptions.Item label={t('settings.status')}>{health?.status ?? '—'}</Descriptions.Item>
</Descriptions>
</Card>
<Card title={t('settings.setupInfo')}>
<Descriptions column={1}>
<Descriptions.Item label={t('settings.adminEmail')}>{setupStatus?.admin_email ?? '—'}</Descriptions.Item>
<Descriptions.Item label={t('settings.fqdn')}>{setupStatus?.fqdn ?? '—'}</Descriptions.Item>
<Descriptions.Item label={t('settings.setupCompleted')}>
{setupStatus?.completed ? t('common.yes') : t('common.no')}
</Descriptions.Item>
</Descriptions>
</Card>
</div>
)
}