feat(web): add orchestrator readiness badge and resilient events parsing
All checks were successful
ci/woodpecker/push/web Pipeline was successful

This commit is contained in:
Jason Woltje
2026-02-17 16:20:03 -06:00
parent 5bce7dbb05
commit 9d9a01f5f7
5 changed files with 120 additions and 25 deletions

View File

@@ -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" />