import { useEffect, useMemo, useState } from "react"; import { HeartPulse, CheckCircle2, XCircle, Plus, Trash2, Play, Pencil, } from "lucide-react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { Button } from "@/components/button"; import { Sparkline } from "@/components/charts"; import { managerPost } from "@/main-axios"; import { useManagerData, extractError } from "./useManagerData"; import { ManagerCardShell } from "./ManagerCardShell"; import { Select2 } from "@/components/select2"; interface HealthCheck { id: string; name: string; type: "tcp" | "http"; target: string; port?: number; path?: string; } interface HealthResult { checkId: string; ok: boolean; latencyMs: number | null; detail: string; } interface HistoryRow { checkId: string; ts: string; ok: boolean; latencyMs: number | null; } interface HealthData { checks: HealthCheck[]; results: HealthResult[]; history: HistoryRow[]; } function newCheck(): HealthCheck { return { id: `c_${Math.random().toString(36).slice(2, 9)}`, name: "", type: "tcp", target: "", port: 80, }; } export function HealthCheckCard({ hostId }: { hostId: number | null }) { const { t } = useTranslation(); const { data, loading, error, refresh } = useManagerData( hostId, "health", ); const [editing, setEditing] = useState(false); const [draft, setDraft] = useState([]); const [saving, setSaving] = useState(false); const [running, setRunning] = useState(false); useEffect(() => { if (data?.checks && !editing) setDraft(data.checks); }, [data?.checks, editing]); const byCheck = useMemo(() => { const map = new Map(); for (const h of data?.history ?? []) { const arr = map.get(h.checkId) ?? []; arr.push(h); map.set(h.checkId, arr); } return map; }, [data?.history]); const save = async () => { if (hostId == null) return; for (const c of draft) { if (!c.name.trim() || !c.target.trim()) { toast.error(t("hostMetrics.managers.healthMissingFields")); return; } } setSaving(true); try { const res = await managerPost<{ success: boolean }>( hostId, "health", { checks: draft }, "config", ); if (res.success) { toast.success(t("hostMetrics.managers.healthSaved")); setEditing(false); refresh(); } else { toast.error(t("hostMetrics.managers.actionFailed")); } } catch (e) { toast.error(extractError(e).message); } finally { setSaving(false); } }; const runNow = async () => { if (hostId == null) return; setRunning(true); try { await managerPost(hostId, "health", {}, "run"); refresh(); } catch (e) { toast.error(extractError(e).message); } finally { setRunning(false); } }; const updateDraft = (id: string, patch: Partial) => setDraft((d) => d.map((c) => (c.id === id ? { ...c, ...patch } : c))); return ( } loading={loading} error={error} onRefresh={refresh} headerExtra={ editing ? ( <> ) : ( <> ) } > {editing ? (
{draft.map((c) => (
updateDraft(c.id, { name: e.target.value })} placeholder={t("hostMetrics.managers.healthName")} className="h-7 flex-1 border border-border bg-background px-2 text-xs outline-none focus:ring-1 focus:ring-ring" /> updateDraft(c.id, { type: e.target.value as "tcp" | "http", }) } className="h-7 border border-border bg-background px-1 text-xs" >
updateDraft(c.id, { target: e.target.value }) } placeholder={t("hostMetrics.managers.healthTarget")} className="h-7 flex-1 border border-border bg-background px-2 font-mono text-xs outline-none focus:ring-1 focus:ring-ring" /> {c.type === "tcp" ? ( updateDraft(c.id, { port: Number(e.target.value) }) } placeholder={t("hostMetrics.ports.port")} className="h-7 w-16 border border-border bg-background px-1.5 text-xs outline-none focus:ring-1 focus:ring-ring" /> ) : ( updateDraft(c.id, { path: e.target.value }) } placeholder="/health" className="h-7 w-24 border border-border bg-background px-1.5 font-mono text-xs outline-none focus:ring-1 focus:ring-ring" /> )}
))}
) : (data?.checks?.length ?? 0) === 0 ? (
{t("hostMetrics.managers.noHealthChecks")}
) : (
{(data?.checks ?? []).map((check) => { const result = data?.results?.find((r) => r.checkId === check.id); const hist = (byCheck.get(check.id) ?? []).slice().reverse(); const upPct = hist.length > 0 ? Math.round( (hist.filter((h) => h.ok).length / hist.length) * 100, ) : null; return (
{result?.ok ? ( ) : ( )} {check.name} {check.type === "tcp" ? `${check.target}:${check.port}` : check.target}
{result?.latencyMs != null ? `${result.latencyMs}ms` : "—"} {upPct != null ? ` · ${upPct}%` : ""}
{hist.length > 1 && ( h.latencyMs ?? 0)} height={28} showLastDot={false} /> )}
); })}
)}
); }