66 lines
4.4 KiB
TypeScript
66 lines
4.4 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import {
|
|
DismissibleAlert,
|
|
LoadingFrame,
|
|
MetricCard,
|
|
StatusBadge,
|
|
adminErrorMessage,
|
|
type ApiSettings
|
|
} from "@govoplan/core-webui";
|
|
import { fetchOpsStatus, type OpsStatus } from "../../api/ops";
|
|
|
|
export default function OpsHealthWidget({ settings, refreshKey }: { settings: ApiSettings; refreshKey: number }) {
|
|
const [status, setStatus] = useState<OpsStatus | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState("");
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
setLoading(true);
|
|
setError("");
|
|
fetchOpsStatus(settings).
|
|
then((next) => {if (!cancelled) setStatus(next);}).
|
|
catch((err) => {if (!cancelled) setError(adminErrorMessage(err));}).
|
|
finally(() => {if (!cancelled) setLoading(false);});
|
|
return () => {cancelled = true;};
|
|
}, [settings.apiBaseUrl, settings.apiKey, settings.accessToken, refreshKey]);
|
|
|
|
const checks = status?.checks ?? [];
|
|
const warningCount = checks.filter((item) => item.state === "warning").length;
|
|
const errorCount = checks.filter((item) => item.state === "error").length;
|
|
const ready = status?.readiness.ready ?? false;
|
|
const workerMetrics = status?.summary.worker_metrics;
|
|
const queueDepths = workerMetrics?.queue_depths ?? {};
|
|
const queuedTasks = Object.values(queueDepths).reduce((sum, value) => sum + value, 0);
|
|
const missingQueues = workerMetrics?.missing_queues ?? [];
|
|
const storageUsage = status?.summary.storage_metrics?.capacity_used_percent;
|
|
|
|
return (
|
|
<LoadingFrame loading={loading} label="Loading operations status">
|
|
{error && <DismissibleAlert tone="warning" resetKey={error}>{error}</DismissibleAlert>}
|
|
<div className="metric-grid inside dashboard-widget-metrics">
|
|
<MetricCard label="Readiness" value={ready ? "ready" : "blocked"} tone={ready ? "good" : "danger"} detail={status?.readiness.profile ?? "-"} />
|
|
<MetricCard label="Workers" value={status?.summary.celery_enabled ? workerMetrics?.workers ?? 0 : "off"} tone={status?.summary.celery_enabled && !missingQueues.length ? "good" : "warning"} detail={missingQueues.length ? `${missingQueues.length} queue(s) without a consumer` : status?.summary.celery_enabled ? "All configured queues covered" : "Single-process mode"} />
|
|
<MetricCard label="Active tasks" value={workerMetrics?.active_tasks ?? 0} tone="info" detail={status?.summary.celery_enabled ? "Reported by live workers" : "Workers disabled"} />
|
|
<MetricCard label="Queued tasks" value={queuedTasks} tone={queuedTasks ? "warning" : "neutral"} detail={Object.keys(queueDepths).length ? `${Object.keys(queueDepths).length} measured queue(s)` : "Queue depth unavailable"} />
|
|
<MetricCard label="Storage" value={storageUsage === undefined ? status?.summary.file_storage_backend ?? "-" : `${storageUsage}%`} tone={storageUsage !== undefined && storageUsage >= 90 ? "danger" : storageUsage !== undefined && storageUsage >= 75 ? "warning" : "neutral"} detail="Managed Files backend capacity" />
|
|
<MetricCard label="Failed" value={status?.summary.failed_operation_count ?? 0} tone={status?.summary.failed_operation_count ? "danger" : "good"} detail="Recovery-ledger operations" />
|
|
<MetricCard label="Unknown" value={status?.summary.outcome_unknown_count ?? 0} tone={status?.summary.outcome_unknown_count ? "danger" : "good"} detail="Outcome-unknown operations" />
|
|
<MetricCard label="Backup" value={status?.summary.backup_state ?? "unknown"} tone={status?.summary.backup_state === "ok" ? "good" : "warning"} detail="Backup and restore evidence" />
|
|
<MetricCard label="Probes" value={status?.summary.operational_probe_count ?? 0} tone="neutral" detail="Module-owned operational checks" />
|
|
<MetricCard label="Warnings" value={warningCount + errorCount} tone={errorCount ? "danger" : warningCount ? "warning" : "good"} detail="Current health checks" />
|
|
</div>
|
|
{status?.readiness.blockers.length ?
|
|
<dl className="detail-list dashboard-compact-list below-grid">
|
|
{status.readiness.blockers.slice(0, 3).map((blocker) =>
|
|
<div key={blocker.id}>
|
|
<dt><StatusBadge status={blocker.state === "error" ? "error" : "warning"} label={blocker.state} /></dt>
|
|
<dd><strong>{blocker.label}</strong><span className="muted"> · {blocker.detail}</span></dd>
|
|
</div>
|
|
)}
|
|
</dl> :
|
|
<p className="muted below-grid">No readiness blockers reported.</p>
|
|
}
|
|
</LoadingFrame>);
|
|
}
|