Private
Public Access
1
0

feat: add target_host_id to service health checks
All checks were successful
CI Pipeline / Rust Format Check (push) Successful in 6s
CI Pipeline / Clippy Lints (push) Successful in 45s
CI Pipeline / Rust Unit Tests (push) Successful in 1m2s
CI Pipeline / Security Audit (push) Successful in 3s
CI Pipeline / Frontend Lint & Type Check (push) Successful in 13s
CI Pipeline / Build .deb & Release (push) Has been skipped

- Add target_host_id column to host_health_checks table (nullable UUID FK)
- Allow service checks to query a different host agent
- Backend models, API routes, and poller updated
- Frontend: host selector dropdown for service checks
- Validation: target host must exist and be healthy
- FK ON DELETE SET NULL: revert to own host if target deleted
This commit is contained in:
2026-05-06 21:38:42 +00:00
parent 4889ab5d0a
commit 0279caf5d2
8 changed files with 234 additions and 265 deletions

View File

@ -18,6 +18,7 @@ import {
Grid,
IconButton,
InputLabel,
FormHelperText,
MenuItem,
Paper,
Select,
@ -213,6 +214,7 @@ interface HealthCheckFormValues {
basic_auth_user: string
basic_auth_pass: string
enabled: boolean
target_host_id: string
}
function defaultHealthCheckForm(): HealthCheckFormValues {
@ -226,6 +228,7 @@ function defaultHealthCheckForm(): HealthCheckFormValues {
basic_auth_user: '',
basic_auth_pass: '',
enabled: true,
target_host_id: '',
}
}
@ -235,11 +238,13 @@ interface HealthCheckFormDialogProps {
open: boolean
title: string
initial: HealthCheckFormValues
hosts: { id: string; display_name: string; fqdn: string }[]
currentHostId: string
onClose: () => void
onSubmit: (values: HealthCheckFormValues) => Promise<void>
}
function HealthCheckFormDialog({ open, title, initial, onClose, onSubmit }: HealthCheckFormDialogProps) {
function HealthCheckFormDialog({ open, title, initial, hosts, currentHostId, onClose, onSubmit }: HealthCheckFormDialogProps) {
const [form, setForm] = useState<HealthCheckFormValues>(initial)
const [saving, setSaving] = useState(false)
const [err, setErr] = useState<string | null>(null)
@ -276,8 +281,20 @@ function HealthCheckFormDialog({ open, title, initial, onClose, onSubmit }: Heal
</Select>
</FormControl>
{form.check_type === 'service' && (
<TextField label="Service Name" value={form.service_name} onChange={e => set('service_name', e.target.value)} required fullWidth
helperText="Systemd service unit name to check" />
<>
<TextField label="Service Name" value={form.service_name} onChange={e => set('service_name', e.target.value)} required fullWidth
helperText="Systemd service unit name to check" />
<FormControl fullWidth>
<InputLabel>Target Host (optional)</InputLabel>
<Select label="Target Host (optional)" value={form.target_host_id} onChange={e => set('target_host_id', e.target.value)}>
<MenuItem value="">Own Host (default)</MenuItem>
{hosts.filter(h => h.id !== currentHostId).map(h => (
<MenuItem key={h.id} value={h.id}>{h.display_name || h.fqdn}</MenuItem>
))}
</Select>
<FormHelperText>Query a service on a different host's agent (for redundant services)</FormHelperText>
</FormControl>
</>
)}
{form.check_type === 'http' && (
<>
@ -591,6 +608,9 @@ export default function HostDetailPage() {
const [reissueLoading, setReissueLoading] = useState(false)
const [reissueError, setReissueError] = useState<string | null>(null)
// Hosts list for target_host_id dropdown
const [hosts, setHosts] = useState<{ id: string; display_name: string; fqdn: string }[]>([])
// ── Fetch host ────────────────────────────────────────────────────────────
useEffect(() => {
if (id === 'new') { setLoading(false); return }
@ -599,6 +619,13 @@ export default function HostDetailPage() {
.catch(() => setError('Host not found or access denied.'))
.finally(() => setLoading(false))
}, [id])
// ── Fetch hosts list (for target_host_id dropdown) ──────────────────────
useEffect(() => {
hostsApi.list()
.then(res => setHosts(res.data?.hosts ?? []))
.catch(() => { /* ignore */ })
}, [])
// ── Check cert existence ───────────────────────────────────────────────────
useEffect(() => {
@ -754,6 +781,7 @@ export default function HostDetailPage() {
}
if (values.check_type === 'service') {
body.service_name = values.service_name || undefined
body.target_host_id = values.target_host_id || undefined
} else {
body.url = values.url || undefined
body.expected_body = values.expected_body || undefined
@ -780,6 +808,7 @@ export default function HostDetailPage() {
basic_auth_user: check.basic_auth_user ?? '',
basic_auth_pass: '',
enabled: check.enabled,
target_host_id: check.target_host_id ?? '',
})
setHcEditOpen(true)
}
@ -792,6 +821,7 @@ export default function HostDetailPage() {
}
if (values.check_type === 'service') {
body.service_name = values.service_name || undefined
body.target_host_id = values.target_host_id || undefined
} else {
body.url = values.url || undefined
body.expected_body = values.expected_body || undefined
@ -1145,6 +1175,8 @@ export default function HostDetailPage() {
open={hcCreateOpen}
title="Add Health Check"
initial={hcCreateForm}
hosts={hosts}
currentHostId={id ?? ''}
onClose={() => setHcCreateOpen(false)}
onSubmit={handleHcCreateSubmit}
/>
@ -1152,6 +1184,8 @@ export default function HostDetailPage() {
open={hcEditOpen}
title="Edit Health Check"
initial={hcEditForm}
hosts={hosts}
currentHostId={id ?? ''}
onClose={() => setHcEditOpen(false)}
onSubmit={handleHcEditSubmit}
/>

View File

@ -282,6 +282,7 @@ export interface HealthCheck {
expected_body?: string
ignore_cert_errors: boolean
basic_auth_user?: string
target_host_id?: string | null
created_at: string
updated_at: string
}
@ -313,6 +314,7 @@ export interface CreateHealthCheckRequest {
ignore_cert_errors?: boolean
basic_auth_user?: string
basic_auth_pass?: string
target_host_id?: string | null
}
export interface UpdateHealthCheckRequest {
@ -324,4 +326,5 @@ export interface UpdateHealthCheckRequest {
ignore_cert_errors?: boolean
basic_auth_user?: string
basic_auth_pass?: string
target_host_id?: string | null
}