import { useEffect, useState, useCallback } from 'react' import JSZip from 'jszip' import { useParams, useNavigate } from 'react-router-dom' import { Alert, Box, Button, Chip, CircularProgress, Container, Dialog, DialogActions, DialogContent, DialogTitle, Divider, FormControl, FormControlLabel, Grid, IconButton, InputLabel, FormHelperText, MenuItem, Paper, Select, Snackbar, Switch, Table, TableBody, TableCell, TableHead, TableRow, TextField, Tooltip, Typography, } from '@mui/material' import { Add as AddIcon, ArrowBack, Cancel as CancelIcon, CheckCircle as CheckCircleIcon, Delete as DeleteIcon, Edit as EditIcon, MonitorHeart as MonitorHeartIcon, PlayArrow as PlayArrowIcon, Remove as RemoveIcon, Schedule as ScheduleIcon, VpnKey as VpnKeyIcon, ContentCopy as CopyIcon, VerifiedUser as VerifiedUserIcon, Security as SecurityIcon, WarningAmber as WarningAmberIcon, } from '@mui/icons-material' import { apiClient, hostsApi, maintenanceWindowsApi, healthChecksApi, certsApi } from '../api/client' import { useAuthStore } from '../store/authStore' import type { CreateHostRequest, IssuedCert, MaintenanceWindow, WindowRecurrence, HealthCheckType, HealthCheckWithResult, CreateHealthCheckRequest, UpdateHealthCheckRequest, } from '../types' // ── Helpers ─────────────────────────────────────────────────────────────────── const DAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] function recurrenceLabel(r: WindowRecurrence): string { const map: Record = { once: 'One-Time', daily: 'Daily', weekly: 'Weekly', monthly: 'Monthly', } return map[r] } function scheduleDescription(w: MaintenanceWindow): string { const dur = `${w.duration_minutes} min` const time = new Date(w.start_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', timeZoneName: 'short', }) switch (w.recurrence) { case 'once': return `Once at ${new Date(w.start_at).toLocaleString()} for ${dur}` case 'daily': return `Every day at ${time} for ${dur}` case 'weekly': { const day = w.recurrence_day != null ? DAY_NAMES[w.recurrence_day] ?? `Day ${w.recurrence_day}` : '?' // eslint-disable-line eqeqeq return `Every ${day} at ${time} for ${dur}` } case 'monthly': { const day = w.recurrence_day != null ? w.recurrence_day : '?' // eslint-disable-line eqeqeq return `Monthly on day ${day} at ${time} for ${dur}` } } } // ── Window form value type ──────────────────────────────────────────────────── interface FormValues { label: string recurrence: WindowRecurrence start_at: string duration_minutes: number recurrence_day: number | '' enabled: boolean } function defaultForm(): FormValues { return { label: '', recurrence: 'once', start_at: new Date().toISOString().slice(0, 16), duration_minutes: 60, recurrence_day: '', enabled: true, } } // ── Window form dialog ──────────────────────────────────────────────────────── interface WindowFormDialogProps { open: boolean title: string initial: FormValues onClose: () => void onSubmit: (values: FormValues) => Promise } function WindowFormDialog({ open, title, initial, onClose, onSubmit }: WindowFormDialogProps) { const [form, setForm] = useState(initial) const [saving, setSaving] = useState(false) const [err, setErr] = useState(null) useEffect(() => { setForm(initial); setErr(null) }, [open, initial]) const set = (field: keyof FormValues, value: FormValues[keyof FormValues]) => setForm(prev => ({ ...prev, [field]: value })) const needsDay = form.recurrence === 'weekly' || form.recurrence === 'monthly' const handleSubmit = async () => { if (!form.label.trim()) { setErr('Label is required'); return } if (needsDay && form.recurrence_day === '') { setErr('Recurrence day is required'); return } setSaving(true); setErr(null) try { await onSubmit(form) } catch (e: unknown) { const msg = (e as { response?: { data?: { error?: { message?: string } } } }) ?.response?.data?.error?.message ?? 'Failed to save' setErr(msg) } finally { setSaving(false) } } return ( {title} {err && {err}} set('label', e.target.value)} required fullWidth /> Recurrence set('start_at', e.target.value)} fullWidth slotProps={{ inputLabel: { shrink: true } }} /> set('duration_minutes', parseInt(e.target.value, 10) || 60)} fullWidth slotProps={{ htmlInput: { min: 1, max: 1440 } }} /> {form.recurrence === 'weekly' && ( Day of Week )} {form.recurrence === 'monthly' && ( set('recurrence_day', parseInt(e.target.value, 10) || 1)} fullWidth slotProps={{ htmlInput: { min: 1, max: 31 } }} /> )} set('enabled', e.target.checked)} />} label="Enabled" /> ) } // ── Health Check form value type ───────────────────────────────────────────── interface HealthCheckFormValues { name: string check_type: HealthCheckType service_name: string url: string expected_body: string ignore_cert_errors: boolean basic_auth_user: string basic_auth_pass: string enabled: boolean target_host_id: string } function defaultHealthCheckForm(): HealthCheckFormValues { return { name: '', check_type: 'service', service_name: '', url: '', expected_body: '', ignore_cert_errors: false, basic_auth_user: '', basic_auth_pass: '', enabled: true, target_host_id: '', } } // ── Health Check form dialog ────────────────────────────────────────────────── interface HealthCheckFormDialogProps { open: boolean title: string initial: HealthCheckFormValues hosts: { id: string; display_name: string; fqdn: string }[] currentHostId: string onClose: () => void onSubmit: (values: HealthCheckFormValues) => Promise } function HealthCheckFormDialog({ open, title, initial, hosts, currentHostId, onClose, onSubmit }: HealthCheckFormDialogProps) { const [form, setForm] = useState(initial) const [saving, setSaving] = useState(false) const [err, setErr] = useState(null) useEffect(() => { setForm(initial); setErr(null) }, [open, initial]) const set = (field: keyof HealthCheckFormValues, value: HealthCheckFormValues[keyof HealthCheckFormValues]) => setForm(prev => ({ ...prev, [field]: value })) const handleSubmit = async () => { if (!form.name.trim()) { setErr('Name is required'); return } if (form.check_type === 'service' && !form.service_name.trim()) { setErr('Service name is required'); return } if (form.check_type === 'http' && !form.url.trim()) { setErr('URL is required'); return } setSaving(true); setErr(null) try { await onSubmit(form) } catch (e: unknown) { const msg = (e as { response?: { data?: { error?: { message?: string } } } }) ?.response?.data?.error?.message ?? 'Failed to save' setErr(msg) } finally { setSaving(false) } } return ( {title} {err && {err}} set('name', e.target.value)} required fullWidth /> Check Type {form.check_type === 'service' && ( <> set('service_name', e.target.value)} required fullWidth helperText="Systemd service unit name to check" /> Target Host (optional) Query a service on a different host's agent (for redundant services) )} {form.check_type === 'http' && ( <> set('url', e.target.value)} required fullWidth helperText="Full URL to check (e.g. https://example.com/health)" /> set('expected_body', e.target.value)} fullWidth helperText="Substring expected in response body" /> set('ignore_cert_errors', e.target.checked)} />} label="Ignore Certificate Errors" /> set('basic_auth_user', e.target.value)} fullWidth /> set('basic_auth_pass', e.target.value)} fullWidth helperText="Leave blank to keep existing password" /> )} set('enabled', e.target.checked)} />} label="Enabled" /> ) } // ── Create Host Form ────────────────────────────────────────────────────────── // ── One-Time Key Display Dialog ─────────────────────────────────────────────── interface KeyDisplayDialogProps { open: boolean cert: IssuedCert | null hostname?: string onClose: () => void } function KeyDisplayDialog({ open, cert, hostname, onClose }: KeyDisplayDialogProps) { const [copiedField, setCopiedField] = useState<'ca' | 'server-cert' | 'server-key' | null>(null) const [downloading, setDownloading] = useState(false) const handleCopy = async (text: string, field: 'ca' | 'server-cert' | 'server-key') => { await navigator.clipboard.writeText(text) setCopiedField(field) setTimeout(() => setCopiedField(null), 2000) } const handleDownloadBundle = async () => { if (!cert) return setDownloading(true) try { const zip = new JSZip() zip.file('ca.crt', cert.ca_root_pem) zip.file('server.crt', cert.server_cert_pem) zip.file('server.key', cert.server_key_pem) const blob = await zip.generateAsync({ type: 'blob' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = `${hostname || 'host'}-agent-certs.zip` a.click() URL.revokeObjectURL(url) } finally { setDownloading(false) } } const preStyle = { p: 2, bgcolor: 'grey.100', borderRadius: 1, fontSize: 12, overflow: 'auto', maxHeight: 150, fontFamily: 'monospace' as const, whiteSpace: 'pre-wrap' as const, wordBreak: 'break-all' as const, } return ( Agent Certificates Issued — Save Your Private Key The server private key will NOT be shown again. Download the bundle or copy it now. {cert && ( <> Server Serial: {cert.server_serial_number}  |  Expires: {new Date(cert.expires_at).toLocaleDateString()} {/* CA Root Certificate */} CA Root Certificate (ca.crt) {cert.ca_root_pem} {/* Server Certificate (Agent TLS) */} Server Certificate — Agent TLS (server.crt) {cert.server_cert_pem} {/* Server Private Key */} Server Private Key (server.key) {cert.server_key_pem} )} ) } // ── Create Host Form ────────────────────────────────────────────────────────── function CreateHostForm() { const navigate = useNavigate() const [form, setForm] = useState({ fqdn: '', display_name: '', agent_port: 12443, notes: '', }) const [saving, setSaving] = useState(false) const [err, setErr] = useState(null) const set = (field: keyof CreateHostRequest, value: CreateHostRequest[keyof CreateHostRequest]) => setForm(prev => ({ ...prev, [field]: value })) const handleSubmit = async () => { if (!form.fqdn.trim()) { setErr('FQDN is required'); return } setSaving(true); setErr(null) try { const body: CreateHostRequest = { fqdn: form.fqdn.trim(), } if (form.display_name?.trim()) body.display_name = form.display_name.trim() if (form.agent_port) body.agent_port = form.agent_port if (form.notes?.trim()) body.notes = form.notes.trim() const res = await hostsApi.register(body) const newId = res.data?.id ?? res.data?.host?.id if (newId) { navigate(`/hosts/${newId}`) } else { navigate('/hosts') } } catch (e: unknown) { const msg = (e as { response?: { data?: { error?: { message?: string } } } }) ?.response?.data?.error?.message ?? 'Failed to register host' setErr(msg) } finally { setSaving(false) } } return ( Register New Host {err && {err}} set('fqdn', e.target.value)} required fullWidth helperText="Fully qualified domain name (IP address resolved automatically)" /> set('display_name', e.target.value)} fullWidth helperText="Optional friendly name for this host" /> set('agent_port', parseInt(e.target.value, 10) || 12443)} fullWidth slotProps={{ htmlInput: { min: 1, max: 65535 } }} helperText="Port the patch agent listens on (default 12443)" /> set('notes', e.target.value)} fullWidth multiline rows={3} helperText="Optional notes about this host" /> ) } // ── Main page ────────────────────────────────────────────────────────────────── export default function HostDetailPage() { const { id } = useParams<{ id: string }>() const navigate = useNavigate() const user = useAuthStore(state => state.user) const canWrite = user?.role === 'admin' || user?.role === 'operator' const [host, setHost] = useState | null>(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) // Maintenance windows state const [windows, setWindows] = useState([]) const [winLoading, setWinLoading] = useState(false) const [snackbar, setSnackbar] = useState<{ open: boolean; message: string; severity: 'success' | 'error' }>({ open: false, message: '', severity: 'success', }) // Create window dialog const [createOpen, setCreateOpen] = useState(false) const [createForm, setCreateForm] = useState(defaultForm()) // Edit window dialog const [editOpen, setEditOpen] = useState(false) const [editWindow, setEditWindow] = useState(null) const [editForm, setEditForm] = useState(defaultForm()) // Delete window dialog const [deleteOpen, setDeleteOpen] = useState(false) const [deleteTarget, setDeleteTarget] = useState(null) // Health checks state const [healthChecks, setHealthChecks] = useState([]) const [hcLoading, setHcLoading] = useState(false) const [testingId, setTestingId] = useState(null) // Create health check dialog const [hcCreateOpen, setHcCreateOpen] = useState(false) const [hcCreateForm, setHcCreateForm] = useState(defaultHealthCheckForm()) // Edit health check dialog const [hcEditOpen, setHcEditOpen] = useState(false) const [hcEditTarget, setHcEditTarget] = useState(null) const [hcEditForm, setHcEditForm] = useState(defaultHealthCheckForm()) // Delete health check dialog const [hcDeleteOpen, setHcDeleteOpen] = useState(false) const [hcDeleteTarget, setHcDeleteTarget] = useState(null) // Certificate state const [certExists, setCertExists] = useState(false) const [issueCertOpen, setIssueCertOpen] = useState(false) const [issuedCert, setIssuedCert] = useState(null) const [issueCertLoading, setIssueCertLoading] = useState(false) const [keyDialogOpen, setKeyDialogOpen] = useState(false) const [issueCertHostname, setIssueCertHostname] = useState('') const [issueCertError, setIssueCertError] = useState(null) // Re-issue certificate state const [reissueConfirmOpen, setReissueConfirmOpen] = useState(false) const [reissueLoading, setReissueLoading] = useState(false) const [reissueError, setReissueError] = useState(null) // Hosts list for target_host_id dropdown const [hosts, setHosts] = useState<{ id: string; display_name: string; fqdn: string }[]>([]) // ── Host editing state ──────────────────────────────────────────────────── const [editing, setEditing] = useState(false) const [editFqdn, setEditFqdn] = useState('') const [editIp, setEditIp] = useState('') const [editDisplayName, setEditDisplayName] = useState('') const [savingHost, setSavingHost] = useState(false) const enterEdit = () => { setEditFqdn(String(host?.fqdn ?? '')) setEditIp(String(host?.ip_address ?? '')) setEditDisplayName(String(host?.display_name ?? '')) setEditing(true) } const cancelEdit = () => { setEditing(false) setSavingHost(false) } const handleSaveHost = async () => { if (!id) return setSavingHost(true) try { const res = await hostsApi.update(id, { fqdn: editFqdn !== String(host?.fqdn ?? '') ? editFqdn : undefined, ip_address: editIp !== String(host?.ip_address ?? '') ? editIp : undefined, display_name: editDisplayName !== String(host?.display_name ?? '') ? editDisplayName : undefined, }) setHost(res.data) setEditing(false) showSnack('Host updated', 'success') } catch (e: unknown) { const msg = (e as { response?: { data?: { error?: { message?: string } } } }) ?.response?.data?.error?.message ?? 'Failed to update host' showSnack(msg, 'error') } finally { setSavingHost(false) } } // ── Fetch host ──────────────────────────────────────────────────────────── useEffect(() => { if (id === 'new') { setLoading(false); return } apiClient.get(`/hosts/${id}`) .then(r => setHost(r.data)) .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(() => { if (!id || id === 'new') return certsApi.list({ host_id: id }) .then(res => { const certs = res.data const hasActive = Array.isArray(certs) && certs.some((c: { status: string }) => c.status === 'active') setCertExists(hasActive) }) .catch(() => setCertExists(false)) }, [id]) // ── Fetch windows ───────────────────────────────────────────────────────── const fetchWindows = useCallback(async () => { if (!id) return setWinLoading(true) try { const res = await maintenanceWindowsApi.list(id) setWindows(res.data?.windows ?? []) } catch { /* ignore */ } finally { setWinLoading(false) } }, [id]) useEffect(() => { fetchWindows() }, [fetchWindows]) // ── Fetch health checks ─────────────────────────────────────────────────── const fetchHealthChecks = useCallback(async () => { if (!id) return setHcLoading(true) try { const res = await healthChecksApi.list(id) setHealthChecks(res.data?.checks ?? []) } catch { /* ignore */ } finally { setHcLoading(false) } }, [id]) useEffect(() => { fetchHealthChecks() }, [fetchHealthChecks]) const showSnack = (message: string, severity: 'success' | 'error') => setSnackbar({ open: true, message, severity }) // ── Create window ───────────────────────────────────────────────────────── const handleCreateSubmit = async (values: FormValues) => { if (!id) return await maintenanceWindowsApi.create(id, { label: values.label, recurrence: values.recurrence, start_at: new Date(values.start_at).toISOString(), duration_minutes: values.duration_minutes, recurrence_day: values.recurrence_day === '' ? undefined : values.recurrence_day, enabled: values.enabled, }) setCreateOpen(false) showSnack('Window created', 'success') await fetchWindows() } // ── Edit window ─────────────────────────────────────────────────────────── const handleEditClick = (w: MaintenanceWindow) => { setEditWindow(w) setEditForm({ label: w.label, recurrence: w.recurrence, start_at: new Date(w.start_at).toISOString().slice(0, 16), duration_minutes: w.duration_minutes, recurrence_day: w.recurrence_day ?? '', enabled: w.enabled, }) setEditOpen(true) } const handleEditSubmit = async (values: FormValues) => { if (!id || !editWindow) return await maintenanceWindowsApi.update(id, editWindow.id, { label: values.label, recurrence: values.recurrence, start_at: new Date(values.start_at).toISOString(), duration_minutes: values.duration_minutes, recurrence_day: values.recurrence_day === '' ? undefined : values.recurrence_day, enabled: values.enabled, }) setEditOpen(false) showSnack('Window updated', 'success') await fetchWindows() } // ── Delete window ───────────────────────────────────────────────────────── const handleDeleteConfirm = async () => { if (!id || !deleteTarget) return try { await maintenanceWindowsApi.remove(id, deleteTarget.id) setDeleteOpen(false) showSnack('Window deleted', 'success') await fetchWindows() } catch { showSnack('Failed to delete window', 'error') } } // ── Issue client certificate ────────────────────────────────────────────── const handleOpenIssueCert = () => { setIssueCertHostname(String(host?.fqdn ?? '')) setIssueCertError(null) setIssueCertOpen(true) } const handleIssueCertSubmit = async () => { if (!id || !issueCertHostname.trim()) { setIssueCertError('Hostname is required'); return } setIssueCertLoading(true) setIssueCertError(null) try { const res = await certsApi.issue(id, issueCertHostname.trim()) setIssuedCert(res.data) setIssueCertOpen(false) setKeyDialogOpen(true) setCertExists(true) } catch (e: unknown) { const msg = (e as { response?: { data?: { error?: { message?: string } } } }) ?.response?.data?.error?.message ?? 'Failed to issue certificate' setIssueCertError(msg) } finally { setIssueCertLoading(false) } } // ── Re-issue certificate ──────────────────────────────────────────────── const handleReissue = async () => { if (!id) return setReissueLoading(true) setReissueError(null) try { const res = await certsApi.reissue(id) setIssuedCert(res.data) setReissueConfirmOpen(false) setKeyDialogOpen(true) setCertExists(true) } catch (e: unknown) { const msg = (e as { response?: { data?: { error?: { message?: string } } } }) ?.response?.data?.error?.message ?? 'Failed to re-issue certificate' setReissueError(msg) } finally { setReissueLoading(false) } } // ── Create health check ────────────────────────────────────────────────── const handleHcCreateSubmit = async (values: HealthCheckFormValues) => { if (!id) return const body: CreateHealthCheckRequest = { name: values.name, check_type: values.check_type, } 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 body.ignore_cert_errors = values.ignore_cert_errors || undefined body.basic_auth_user = values.basic_auth_user || undefined body.basic_auth_pass = values.basic_auth_pass || undefined } await healthChecksApi.create(id, body) setHcCreateOpen(false) showSnack('Health check created', 'success') await fetchHealthChecks() } // ── Edit health check ──────────────────────────────────────────────────── const handleHcEditClick = (check: HealthCheckWithResult) => { setHcEditTarget(check) setHcEditForm({ name: check.name, check_type: check.check_type, service_name: check.service_name ?? '', url: check.url ?? '', expected_body: check.expected_body ?? '', ignore_cert_errors: check.ignore_cert_errors, basic_auth_user: check.basic_auth_user ?? '', basic_auth_pass: '', enabled: check.enabled, target_host_id: check.target_host_id ?? '', }) setHcEditOpen(true) } const handleHcEditSubmit = async (values: HealthCheckFormValues) => { if (!id || !hcEditTarget) return const body: UpdateHealthCheckRequest = { name: values.name, enabled: values.enabled, } 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 body.ignore_cert_errors = values.ignore_cert_errors body.basic_auth_user = values.basic_auth_user || undefined body.basic_auth_pass = values.basic_auth_pass || undefined } await healthChecksApi.update(id, hcEditTarget.id, body) setHcEditOpen(false) showSnack('Health check updated', 'success') await fetchHealthChecks() } // ── Delete health check ────────────────────────────────────────────────── const handleHcDeleteConfirm = async () => { if (!id || !hcDeleteTarget) return try { await healthChecksApi.delete(id, hcDeleteTarget.id) setHcDeleteOpen(false) showSnack('Health check deleted', 'success') await fetchHealthChecks() } catch { showSnack('Failed to delete health check', 'error') } } // ── Toggle health check enabled ────────────────────────────────────────── const handleToggleEnabled = async (check: HealthCheckWithResult) => { if (!id) return try { await healthChecksApi.update(id, check.id, { enabled: !check.enabled }) await fetchHealthChecks() } catch { showSnack('Failed to toggle health check', 'error') } } // ── Test health check ──────────────────────────────────────────────────── const handleTestCheck = async (check: HealthCheckWithResult) => { if (!id) return setTestingId(check.id) try { await healthChecksApi.test(id, check.id) await fetchHealthChecks() showSnack('Health check test completed', 'success') } catch { showSnack('Health check test failed', 'error') } finally { setTestingId(null) } } // ── Render ──────────────────────────────────────────────────────────────── if (loading) return if (error) return {error} // ── New host creation form ───────────────────────────────────────────────── if (id === 'new') { return } return ( {/* ── Host details ─────────────────────────────────────────────────── */} {String(host?.fqdn ?? '')} {canWrite && !editing && ( )} {canWrite && editing && ( <> )} {!editing && canWrite && !certExists && ( )} {!editing && canWrite && certExists && ( )} {host && (<> FQDN {editing ? ( setEditFqdn(e.target.value)} /> ) : ( {String(host.fqdn)} )} IP ADDRESS {editing ? ( setEditIp(e.target.value)} /> ) : ( {String(host.ip_address)} )} DISPLAY NAME {editing ? ( setEditDisplayName(e.target.value)} /> ) : ( {String(host.display_name)} )} {Object.entries(host).filter(([k]) => !['fqdn','ip_address','display_name'].includes(k)).map(([k, v]) => v !== null && v !== '' ? ( {k.replace(/_/g, ' ').toUpperCase()} {String(v)} ) : null )} )} {/* ── CRL Status ─────────────────────────────────────────────────── */} CRL Status {host?.crl_status === undefined || host?.crl_status === null ? ( CRL status not available (agent version does not support CRL) ) : ( Status {host.crl_status === 'valid' ? ( } label="Valid" color="success" size="small" /> ) : host.crl_status === 'expired' ? ( } label="Expired" color="warning" size="small" /> ) : host.crl_status === 'missing' ? ( } label="Missing" color="warning" size="small" /> ) : host.crl_status === 'invalid' ? ( } label="Invalid" color="error" size="small" /> ) : ( {String(host.crl_status)} )} CRL Age {host.crl_age_seconds !== null ? (() => { const s = Number(host.crl_age_seconds); return s < 3600 ? `${Math.round(s / 60)} minutes ago` : s < 86400 ? `${Math.round(s / 3600)} hours ago` : `${Math.round(s / 86400)} days ago`; })() : '—'} Next Update {host.crl_next_update ? new Date(host.crl_next_update as string).toLocaleString() : '—'} )} {/* ── Maintenance Windows ──────────────────────────────────────────── */} Maintenance Windows {canWrite && } Queued patch jobs execute only when an enabled maintenance window is open. {winLoading ? ( ) : windows.length === 0 ? ( No maintenance windows. Queued jobs will not run until a window is configured. ) : ( Label Schedule Recurrence Status {canWrite && Actions} {windows.map(w => ( {w.label} {scheduleDescription(w)} {canWrite && handleEditClick(w)}> { setDeleteTarget(w); setDeleteOpen(true) }} > } ))}
)}
{/* ── Health Checks ────────────────────────────────────────────────── */} Health Checks {canWrite && } Monitor host health with service and HTTP checks. Maximum 5 checks per host. {hcLoading ? ( ) : healthChecks.length === 0 ? ( No health checks configured. Add a check to monitor this host's health. ) : ( Name Type Status Target Enabled Detail Latency Last Checked {canWrite && Actions} {healthChecks.map(check => ( {check.name} {check.check_type === 'service' ? (check.service_name ?? '—') : (check.url ?? '—')} {check.last_result ? ( check.last_result.healthy ? ( ) : ( ) ) : ( )} handleToggleEnabled(check) : undefined} disabled={!canWrite} /> {check.last_result?.detail ?? '—'} {check.last_result?.latency_ms !== null && check.last_result?.latency_ms !== undefined ? `${check.last_result.latency_ms} ms` : '—'} {check.last_result?.checked_at ? new Date(check.last_result.checked_at).toLocaleString() : '—'} {canWrite && handleTestCheck(check)} > {testingId === check.id ? : } handleHcEditClick(check)}> { setHcDeleteTarget(check); setHcDeleteOpen(true) }} > } ))}
)}
{/* ── Dialogs ─────────────────────────────────────────────────────── */} setCreateOpen(false)} onSubmit={handleCreateSubmit} /> setEditOpen(false)} onSubmit={handleEditSubmit} /> setDeleteOpen(false)} maxWidth="xs" fullWidth> Delete Window Delete {deleteTarget?.label}? This cannot be undone. {/* Health Check Dialogs */} setHcCreateOpen(false)} onSubmit={handleHcCreateSubmit} /> setHcEditOpen(false)} onSubmit={handleHcEditSubmit} /> setHcDeleteOpen(false)} maxWidth="xs" fullWidth> Delete Health Check Delete {hcDeleteTarget?.name}? This cannot be undone. {/* Issue Certificate Dialog */} setIssueCertOpen(false)} maxWidth="sm" fullWidth> Issue Client Certificate {issueCertError && {issueCertError}} setIssueCertHostname(e.target.value)} required fullWidth helperText="Common name for the certificate (usually the host FQDN)" /> {/* Re-issue Certificate Confirmation Dialog */} setReissueConfirmOpen(false)} maxWidth="sm" fullWidth> Re-issue Certificate {reissueError && {reissueError}} This will revoke all existing certificates for this host and issue a new set. {' '}The new private key will only be shown once. Continue? {/* One-time key display dialog */} setKeyDialogOpen(false)} /> {/* Snackbar */} setSnackbar(p => ({ ...p, open: false }))} anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} > setSnackbar(p => ({ ...p, open: false }))}> {snackbar.message}
) }