import { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { listHealthChecks, createHealthCheck, deleteHealthCheck, acknowledgeHealthCheck, getHealthCheckSummary, } from '../api/client'; import PageHeader from '../components/PageHeader'; import DataTable from '../components/DataTable'; import type { Column } from '../components/DataTable'; import ErrorState from '../components/ErrorState'; import StatusBadge from '../components/StatusBadge'; import { formatDateTime } from '../api/utils'; import type { EndpointHealthCheck, HealthCheckSummary } from '../api/types'; function CreateHealthCheckModal({ onClose, onCreate }: { onClose: () => void; onCreate: (data: Partial) => void; }) { const [endpoint, setEndpoint] = useState(''); const [expectedFingerprint, setExpectedFingerprint] = useState(''); const [checkInterval, setCheckInterval] = useState('300'); const [degradedThreshold, setDegradedThreshold] = useState('2'); const [downThreshold, setDownThreshold] = useState('5'); const handleSubmit = () => { onCreate({ endpoint, expected_fingerprint: expectedFingerprint, check_interval_seconds: parseInt(checkInterval, 10), degraded_threshold: parseInt(degradedThreshold, 10), down_threshold: parseInt(downThreshold, 10), enabled: true, }); }; return (
e.stopPropagation()}>

New Health Check

Monitor a TLS endpoint for certificate health

setEndpoint(e.target.value)} placeholder="e.g., example.com:443" className="w-full border border-surface-border rounded px-3 py-2 text-sm text-ink bg-white focus:outline-none focus:ring-2 focus:ring-brand-500" />
setExpectedFingerprint(e.target.value)} placeholder="Optional: auto-populated from deployment" className="w-full border border-surface-border rounded px-3 py-2 text-sm text-ink bg-white font-mono focus:outline-none focus:ring-2 focus:ring-brand-500" />

Leave empty to auto-detect from first successful probe

setCheckInterval(e.target.value)} min="60" className="w-full border border-surface-border rounded px-3 py-2 text-sm text-ink bg-white focus:outline-none focus:ring-2 focus:ring-brand-500" />
setDegradedThreshold(e.target.value)} min="1" className="w-full border border-surface-border rounded px-3 py-2 text-sm text-ink bg-white focus:outline-none focus:ring-2 focus:ring-brand-500" />
setDownThreshold(e.target.value)} min="1" className="w-full border border-surface-border rounded px-3 py-2 text-sm text-ink bg-white focus:outline-none focus:ring-2 focus:ring-brand-500" />
); } function SummaryBar({ summary }: { summary: HealthCheckSummary }) { const items = [ { label: 'Healthy', count: summary.healthy, color: 'text-green-600' }, { label: 'Degraded', count: summary.degraded, color: 'text-yellow-600' }, { label: 'Down', count: summary.down, color: 'text-red-600' }, { label: 'Cert Mismatch', count: summary.cert_mismatch, color: 'text-orange-600' }, { label: 'Unknown', count: summary.unknown, color: 'text-gray-500' }, ]; return (
{items.map(item => (

{item.count}

{item.label}

))}
); } export default function HealthMonitorPage() { const [showCreate, setShowCreate] = useState(false); const [statusFilter, setStatusFilter] = useState(); const queryClient = useQueryClient(); const { data, isLoading, error, refetch } = useQuery({ queryKey: ['health-checks', statusFilter], queryFn: () => listHealthChecks({ status: statusFilter, page: 1, per_page: 100 }), refetchInterval: 30000, }); const summaryQuery = useQuery({ queryKey: ['health-checks-summary'], queryFn: () => getHealthCheckSummary(), refetchInterval: 30000, }); const createMutation = useMutation({ mutationFn: createHealthCheck, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['health-checks'] }); queryClient.invalidateQueries({ queryKey: ['health-checks-summary'] }); setShowCreate(false); }, }); const deleteMutation = useMutation({ mutationFn: deleteHealthCheck, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['health-checks'] }); queryClient.invalidateQueries({ queryKey: ['health-checks-summary'] }); }, }); const acknowledgeMutation = useMutation({ mutationFn: acknowledgeHealthCheck, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['health-checks'] }); queryClient.invalidateQueries({ queryKey: ['health-checks-summary'] }); }, }); const columns: Column[] = [ { key: 'endpoint', label: 'Endpoint', render: (row) => row.endpoint, }, { key: 'status', label: 'Status', render: (row) => , }, { key: 'response_time_ms', label: 'Response Time (ms)', render: (row) => row.response_time_ms ? `${row.response_time_ms}ms` : '—', }, { key: 'last_checked_at', label: 'Last Checked', render: (row) => row.last_checked_at ? formatDateTime(row.last_checked_at) : '—', }, { key: 'last_transition_at', label: 'Last Transition', render: (row) => row.last_transition_at ? formatDateTime(row.last_transition_at) : '—', }, { key: 'acknowledged', label: 'Acknowledged', render: (row) => row.acknowledged ? '✓' : '—', }, { key: 'actions', label: 'Actions', render: (row) => (
{!row.acknowledged && row.status !== 'healthy' && ( )}
), }, ]; if (error) { return ; } return (
{summaryQuery.data && }
{isLoading ? (
Loading health checks...
) : data && data.data.length > 0 ? ( columns={columns} data={data.data} keyField="id" /> ) : (
No health checks configured
)}
{showCreate && ( setShowCreate(false)} onCreate={data => createMutation.mutate(data)} /> )}
); }