Masters-four-Tab-OpenAI / frontend /src /components /HealthStatusModal.tsx
Pete Dunn
Lock final workspace UI and guided flows
c2a37bc
Raw
History Blame Contribute Delete
9.66 kB
import React, { useEffect, useMemo, useState } from "react";
import Modal from "./Modal";
import { Badge, ButtonSoft, Card } from "./ui";
import { apiGetJson } from "../api/client";
import type { ApiHealthResponse, ApiObservabilityMetricsResponse } from "../api/contracts";
export default function HealthStatusModal({
open,
onClose,
}: {
open: boolean;
onClose: () => void;
}) {
const [loading, setLoading] = useState(false);
const [data, setData] = useState<ApiHealthResponse | null>(null);
const [metrics, setMetrics] = useState<ApiObservabilityMetricsResponse | null>(null);
const [error, setError] = useState<string>("");
const envSorted = useMemo(() => {
if (!data?.env) return [] as Array<[string, boolean]>;
return Object.entries(data.env).sort((a, b) => a[0].localeCompare(b[0]));
}, [data]);
const authAllowedDomains = useMemo(() => {
const domains = Array.isArray(data?.auth?.allowed_domains) ? data?.auth?.allowed_domains : [];
return [...domains].sort((a, b) => String(a).localeCompare(String(b)));
}, [data]);
const fetchHealth = async () => {
setLoading(true);
setError("");
try {
const [healthJson, metricsJson] = await Promise.all([
apiGetJson<ApiHealthResponse>("/api/health", { cache: "no-store" }),
apiGetJson<ApiObservabilityMetricsResponse>("/api/observability/metrics?limit=25", { cache: "no-store" }).catch(
() => null
),
]);
setData(healthJson);
setMetrics(metricsJson);
} catch (e: any) {
setError(e?.message || String(e));
setData(null);
setMetrics(null);
} finally {
setLoading(false);
}
};
useEffect(() => {
if (!open) return;
// Fetch on open.
fetchHealth();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open]);
const overallBadge = data?.ok ? (
<Badge tone="green">Healthy</Badge>
) : (
<Badge tone="red">Issues found</Badge>
);
return (
<Modal open={open} onClose={onClose} title="System status">
<div className="space-y-4">
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="flex items-center gap-2">
{data ? overallBadge : <Badge tone="slate">Unknown</Badge>}
{data?.summary ? (
<span className="text-sm text-slate-600">
Required checks: {Number(data.summary.required_ok || 0)}/{Number(data.summary.required_total || 0)}
</span>
) : null}
</div>
<div className="flex items-center gap-2">
<ButtonSoft onClick={fetchHealth} disabled={loading}>
{loading ? "Refreshing…" : "Refresh"}
</ButtonSoft>
<ButtonSoft onClick={onClose}>
Close
</ButtonSoft>
</div>
</div>
{error ? (
<div className="mt-panel-danger rounded-lg border p-3 text-sm">
Failed to load status: <span className="font-mono">{error}</span>
</div>
) : null}
<Card>
<div className="p-4">
<div className="mb-2 text-sm font-semibold text-slate-800">Checks</div>
{!data ? (
<div className="text-sm text-slate-600">No data loaded yet.</div>
) : (
<div className="space-y-2">
{data.checks.map((c, idx) => (
<div
key={`${c.name}-${idx}`}
className="flex flex-col gap-1 rounded-md border border-slate-200 bg-white p-3"
>
<div className="flex items-center justify-between gap-2">
<div className="font-medium text-slate-900">{c.name}</div>
{c.ok ? <Badge tone="green">OK</Badge> : <Badge tone="red">Fail</Badge>}
</div>
<div className="text-sm text-slate-600">
{[c.kind, c.path].filter(Boolean).join(" • ")}
</div>
{c.error ? <div className="mt-text-danger text-sm">{c.error}</div> : null}
</div>
))}
</div>
)}
</div>
</Card>
<Card>
<div className="p-4">
<div className="mb-2 text-sm font-semibold text-slate-800">Environment (presence only)</div>
{!data ? (
<div className="text-sm text-slate-600">No data loaded yet.</div>
) : (
<div className="space-y-2">
{envSorted.map(([k, present]) => (
<div
key={k}
className="flex items-center justify-between gap-2 rounded-md border border-slate-200 bg-white p-3"
>
<div className="font-mono text-sm text-slate-900">{k}</div>
{present ? <Badge tone="green">Present</Badge> : <Badge tone="amber">Missing</Badge>}
</div>
))}
</div>
)}
</div>
</Card>
<Card>
<div className="p-4">
<div className="mb-2 text-sm font-semibold text-slate-800">Authentication</div>
{!data?.auth ? (
<div className="text-sm text-slate-600">Auth diagnostics unavailable in this response.</div>
) : (
<div className="space-y-2">
<div className="flex flex-wrap items-center gap-2">
{data.auth.required ? <Badge tone="amber">Auth required</Badge> : <Badge tone="slate">Auth optional</Badge>}
{data.auth.enabled ? <Badge tone="green">Auth enabled</Badge> : <Badge tone="red">Auth not enabled</Badge>}
</div>
<div className="rounded-md border border-slate-200 bg-white p-3 text-xs text-slate-700">
<div>
<span className="font-semibold text-slate-900">Issuer:</span> {data.auth.issuer || "Not configured"}
</div>
<div className="mt-1">
<span className="font-semibold text-slate-900">Audience:</span> {data.auth.audience || "Not configured"}
</div>
<div className="mt-1">
<span className="font-semibold text-slate-900">Email claim:</span> {data.auth.email_claim || "Not configured"}
</div>
<div className="mt-1">
<span className="font-semibold text-slate-900">Allowed domains:</span>{" "}
{authAllowedDomains.length ? authAllowedDomains.join(", ") : "Not configured"}
</div>
</div>
{data.auth.config_error ? (
<div className="mt-panel-danger rounded-md border p-3 text-xs">
<div className="font-semibold">Config error</div>
<div className="mt-1">{data.auth.config_error}</div>
</div>
) : null}
{Array.isArray(data.auth.config_details) && data.auth.config_details.length ? (
<div className="mt-panel-warning rounded-md border p-3 text-xs">
<div className="font-semibold">Config details</div>
<div className="mt-2 space-y-1">
{data.auth.config_details.map((line, idx) => (
<div key={`${line}-${idx}`}>• {line}</div>
))}
</div>
</div>
) : null}
{Array.isArray(data.auth.config_warnings) && data.auth.config_warnings.length ? (
<div className="mt-panel-primary rounded-md border p-3 text-xs">
<div className="font-semibold">Config warnings</div>
<div className="mt-2 space-y-1">
{data.auth.config_warnings.map((line, idx) => (
<div key={`${line}-${idx}`}>• {line}</div>
))}
</div>
</div>
) : null}
</div>
)}
</div>
</Card>
{metrics ? (
<Card>
<div className="p-4">
<div className="mb-2 text-sm font-semibold text-slate-800">API observability</div>
<div className="mb-3 text-xs text-slate-600">
Requests: {metrics.totals.requests} • Errors: {metrics.totals.errors} • Error rate:{" "}
{(metrics.totals.error_rate * 100).toFixed(2)}%
</div>
<div className="space-y-2">
{metrics.endpoints.slice(0, 8).map((m) => (
<div key={m.endpoint} className="rounded-md border border-slate-200 bg-white p-3">
<div className="flex items-center justify-between gap-2">
<div className="font-mono text-xs text-slate-900">{m.endpoint}</div>
{m.error_requests > 0 ? (
<Badge tone="red">{m.error_requests} errors</Badge>
) : (
<Badge tone="green">No errors</Badge>
)}
</div>
<div className="mt-1 text-xs text-slate-600">
req={m.total_requests} • avg={m.avg_latency_ms}ms • p95={m.p95_latency_ms}ms • max={m.max_latency_ms}ms
</div>
</div>
))}
</div>
</div>
</Card>
) : null}
</div>
</Modal>
);
}