feat(web): add orchestrator readiness badge and resilient events parsing
All checks were successful
ci/woodpecker/push/web Pipeline was successful
All checks were successful
ci/woodpecker/push/web Pipeline was successful
This commit is contained in:
@@ -32,6 +32,7 @@ export function OrchestratorEventsWidget({
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [streamConnected, setStreamConnected] = useState(false);
|
||||
const [backendReady, setBackendReady] = useState<boolean | null>(null);
|
||||
|
||||
const loadRecentEvents = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
@@ -39,8 +40,15 @@ export function OrchestratorEventsWidget({
|
||||
if (!response.ok) {
|
||||
throw new Error(`Unable to load events: HTTP ${String(response.status)}`);
|
||||
}
|
||||
const payload = (await response.json()) as RecentEventsResponse;
|
||||
setEvents(payload.events);
|
||||
const payload = (await response.json()) as unknown;
|
||||
const events =
|
||||
payload &&
|
||||
typeof payload === "object" &&
|
||||
"events" in payload &&
|
||||
Array.isArray(payload.events)
|
||||
? (payload.events as RecentEventsResponse["events"])
|
||||
: [];
|
||||
setEvents(events);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Unable to load events.");
|
||||
@@ -49,8 +57,18 @@ export function OrchestratorEventsWidget({
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadHealth = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
const response = await fetch("/api/orchestrator/health");
|
||||
setBackendReady(response.ok);
|
||||
} catch {
|
||||
setBackendReady(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void loadRecentEvents();
|
||||
void loadHealth();
|
||||
|
||||
const eventSource =
|
||||
typeof EventSource !== "undefined" ? new EventSource("/api/orchestrator/events") : null;
|
||||
@@ -60,6 +78,7 @@ export function OrchestratorEventsWidget({
|
||||
};
|
||||
eventSource.onmessage = (): void => {
|
||||
void loadRecentEvents();
|
||||
void loadHealth();
|
||||
};
|
||||
eventSource.onerror = (): void => {
|
||||
setStreamConnected(false);
|
||||
@@ -68,13 +87,14 @@ export function OrchestratorEventsWidget({
|
||||
|
||||
const interval = setInterval(() => {
|
||||
void loadRecentEvents();
|
||||
void loadHealth();
|
||||
}, 15000);
|
||||
|
||||
return (): void => {
|
||||
clearInterval(interval);
|
||||
eventSource?.close();
|
||||
};
|
||||
}, [loadRecentEvents]);
|
||||
}, [loadHealth, loadRecentEvents]);
|
||||
|
||||
const matrixSignals = useMemo(
|
||||
() => events.filter((event) => isMatrixSignal(event)).length,
|
||||
@@ -102,13 +122,24 @@ export function OrchestratorEventsWidget({
|
||||
return (
|
||||
<div className="flex flex-col h-full space-y-3">
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<div className="flex items-center gap-1 text-gray-600 dark:text-gray-300">
|
||||
<div className="flex items-center gap-2 text-gray-600 dark:text-gray-300">
|
||||
{streamConnected ? (
|
||||
<Wifi className="w-3 h-3 text-green-500" />
|
||||
) : (
|
||||
<WifiOff className="w-3 h-3 text-gray-400" />
|
||||
)}
|
||||
<span>{streamConnected ? "Live stream connected" : "Polling mode"}</span>
|
||||
<span
|
||||
className={`rounded px-1.5 py-0.5 ${
|
||||
backendReady === true
|
||||
? "bg-green-100 text-green-700 dark:bg-green-950 dark:text-green-300"
|
||||
: backendReady === false
|
||||
? "bg-amber-100 text-amber-700 dark:bg-amber-950 dark:text-amber-300"
|
||||
: "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-300"
|
||||
}`}
|
||||
>
|
||||
{backendReady === true ? "ready" : backendReady === false ? "degraded" : "unknown"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 rounded bg-blue-50 dark:bg-blue-950 px-2 py-1 text-blue-700 dark:text-blue-300">
|
||||
<DatabaseZap className="w-3 h-3" />
|
||||
|
||||
Reference in New Issue
Block a user