fix(backends): Backend-Löschen verweigern wenn Routing-Regeln darauf zeigen (v1.1.142)

- Service: Pre-Check auf routing_rules.backend_id → ErrInUse vor DELETE
- Handler: ErrInUse → HTTP 409 Conflict statt 500 (response.Conflict helper)
- UI: Delete-Button deaktiviert + Tooltip "Von N Regeln verwendet" wenn Referenzen existieren

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-05-29 07:15:16 +02:00
parent 40414d472b
commit 7dd76d4b58
10 changed files with 52 additions and 12 deletions

View File

@@ -387,6 +387,7 @@
"serverHintCreate": "Speichern legt nur den Pool an. Server kommen im nächsten Schritt — Pool öffnen → „Server hinzufügen\".",
"actions": "Aktionen",
"deleteConfirm": "Backend-Pool {{name}} wirklich löschen? Alle Server im Pool werden mitentfernt.",
"deleteInUse": "Von {{n}} Routing-Regel(n) verwendet — zuerst diese löschen",
"backToList": "Zurück zur Übersicht",
"settingsCard": "Pool-Einstellungen",
"healthCheckHint": "HTTP-Pfad den HAProxy alle 5s pro Server prüft. Leer lassen = kein Health-Check.",

View File

@@ -387,6 +387,7 @@
"serverHintCreate": "Saving creates the pool only. Add servers in the next step — open the pool and click “Add server”.",
"actions": "Actions",
"deleteConfirm": "Really delete backend pool {{name}}? All servers in the pool will be removed too.",
"deleteInUse": "Used by {{n}} routing rule(s) — remove those first",
"backToList": "Back to backends",
"settingsCard": "Pool settings",
"healthCheckHint": "HTTP path HAProxy probes every 5s per server. Leave empty to disable health checks.",

View File

@@ -97,6 +97,15 @@ async function listDomains(): Promise<DomainFull[]> {
return (r.data.data as { domains?: DomainFull[] }).domains ?? []
}
interface RoutingRule { id: number; backend_id: number }
async function listRoutingRules(): Promise<RoutingRule[]> {
try {
const r = await apiClient.get('/routing-rules')
if (!isEnvelope(r.data)) return []
return (r.data.data as { routing_rules?: RoutingRule[] }).routing_rules ?? []
} catch { return [] }
}
interface HAProxyStat {
backend: string; server: string; status: string
sessions: number; bytes_in: number; bytes_out: number
@@ -124,6 +133,7 @@ export default function BackendsPage() {
const { data, isLoading } = useQuery({ queryKey: ['backends'], queryFn: listBackends })
const { data: domains } = useQuery({ queryKey: ['domains'], queryFn: listDomains })
const { data: routingRules } = useQuery({ queryKey: ['routing-rules'], queryFn: listRoutingRules })
const { data: services } = useQuery({
queryKey: ['system', 'services'],
queryFn: async () => {
@@ -177,6 +187,9 @@ export default function BackendsPage() {
const domainsForBackend = (id: number) =>
(domains ?? []).filter(d => d.primary_backend_id === id)
const ruleCountForBackend = (id: number) =>
(routingRules ?? []).filter(r => r.backend_id === id).length
const [creating, setCreating] = useState(false)
const [form] = Form.useForm<BackendFormValues>()
@@ -296,13 +309,18 @@ export default function BackendsPage() {
},
{
title: t('common.actions'), key: 'actions',
render: (_, row) => (
<ActionButtons
onEdit={() => navigate(`/backends/${row.id}`)}
onDelete={() => del.mutate(row.id)}
deleteConfirm={t('backends.deleteConfirm', { name: row.name })}
/>
),
render: (_, row) => {
const n = ruleCountForBackend(row.id)
return (
<ActionButtons
onEdit={() => navigate(`/backends/${row.id}`)}
onDelete={() => del.mutate(row.id)}
deleteConfirm={t('backends.deleteConfirm', { name: row.name })}
deleteDisabled={n > 0}
deleteDisabledReason={t('backends.deleteInUse', { n })}
/>
)
},
},
]