mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-04 14:22:09 +03:00
Release v3.8.33 — full CHANGELOG in the PR body. Blocking gates green (Build, Lint, Unit Tests 8/8, Package Artifact, Quality Gates, Quality Ratchet, Docs Sync, PR Test Policy, test-vitest). Admin-merged over a Node 26 future-compat timer flake (1/4) + an E2E UI flake (3/9) — both verified non-deterministic; full test:unit validated locally (16936 pass).
90 lines
3.0 KiB
TypeScript
90 lines
3.0 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef, useState } from "react";
|
|
import type {
|
|
ProviderBreakerSnapshot,
|
|
ConnectionCooldownSnapshot,
|
|
} from "@/app/(dashboard)/dashboard/combos/live/comboFlowModel";
|
|
|
|
const DEFAULT_POLL_MS = 5000;
|
|
const POLL_TIMEOUT_MS = 8000;
|
|
|
|
export interface ResilienceHealthSnapshot {
|
|
/** Per-provider circuit-breaker state (`providerHealth`). */
|
|
providerHealth: Record<string, ProviderBreakerSnapshot>;
|
|
/** Per-provider connection-cooldown summary (`connectionHealth`). */
|
|
connectionHealth: Record<string, ConnectionCooldownSnapshot>;
|
|
}
|
|
|
|
const EMPTY: ResilienceHealthSnapshot = { providerHealth: {}, connectionHealth: {} };
|
|
|
|
/**
|
|
* Polls `GET /api/monitoring/health` and exposes the resilience overlays the Combo
|
|
* Live Studio consumes (U1b): per-provider circuit-breaker state (`providerHealth`)
|
|
* and per-provider connection-cooldown summary (`connectionHealth`).
|
|
*
|
|
* Fail-soft by design: any network/parse error keeps the last known snapshot (or the
|
|
* empty default), so the cascade simply shows no resilience badges instead of breaking.
|
|
* One poll covers both overlays. Polls every `pollMs` and on mount.
|
|
*/
|
|
export function useProviderBreakerHealth(pollMs = DEFAULT_POLL_MS): ResilienceHealthSnapshot {
|
|
const [snapshot, setSnapshot] = useState<ResilienceHealthSnapshot>(EMPTY);
|
|
const inFlightRef = useRef<AbortController | null>(null);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
|
|
const poll = async () => {
|
|
if (inFlightRef.current) return;
|
|
|
|
const controller = new AbortController();
|
|
inFlightRef.current = controller;
|
|
const timeoutId = setTimeout(
|
|
() => controller.abort("Provider breaker health timeout"),
|
|
POLL_TIMEOUT_MS
|
|
);
|
|
|
|
try {
|
|
const res = await fetch("/api/monitoring/health", {
|
|
signal: controller.signal,
|
|
cache: "no-store",
|
|
});
|
|
if (!res.ok) return;
|
|
const json = (await res.json()) as {
|
|
providerHealth?: Record<string, ProviderBreakerSnapshot>;
|
|
connectionHealth?: Record<string, ConnectionCooldownSnapshot>;
|
|
};
|
|
if (cancelled || !json || typeof json !== "object") return;
|
|
setSnapshot({
|
|
providerHealth:
|
|
typeof json.providerHealth === "object" && json.providerHealth
|
|
? json.providerHealth
|
|
: {},
|
|
connectionHealth:
|
|
typeof json.connectionHealth === "object" && json.connectionHealth
|
|
? json.connectionHealth
|
|
: {},
|
|
});
|
|
} catch {
|
|
// Fail-soft: keep the previous snapshot; cascade degrades to no badges.
|
|
} finally {
|
|
clearTimeout(timeoutId);
|
|
if (inFlightRef.current === controller) {
|
|
inFlightRef.current = null;
|
|
}
|
|
}
|
|
};
|
|
|
|
poll();
|
|
const id = setInterval(poll, pollMs);
|
|
return () => {
|
|
cancelled = true;
|
|
inFlightRef.current?.abort("Provider breaker health unmounted");
|
|
inFlightRef.current = null;
|
|
clearInterval(id);
|
|
};
|
|
}, [pollMs]);
|
|
|
|
return snapshot;
|
|
}
|