feat(web): add orchestrator events widget with matrix signal visibility
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:
159
apps/web/src/components/widgets/OrchestratorEventsWidget.tsx
Normal file
159
apps/web/src/components/widgets/OrchestratorEventsWidget.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Activity, DatabaseZap, Loader2, Wifi, WifiOff } from "lucide-react";
|
||||
import type { WidgetProps } from "@mosaic/shared";
|
||||
|
||||
interface OrchestratorEvent {
|
||||
type: string;
|
||||
timestamp: string;
|
||||
agentId?: string;
|
||||
taskId?: string;
|
||||
data?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface RecentEventsResponse {
|
||||
events: OrchestratorEvent[];
|
||||
}
|
||||
|
||||
function isMatrixSignal(event: OrchestratorEvent): boolean {
|
||||
const text = JSON.stringify(event).toLowerCase();
|
||||
return (
|
||||
text.includes("matrix") ||
|
||||
text.includes("room") ||
|
||||
text.includes("channel") ||
|
||||
text.includes("thread")
|
||||
);
|
||||
}
|
||||
|
||||
export function OrchestratorEventsWidget({
|
||||
id: _id,
|
||||
config: _config,
|
||||
}: WidgetProps): React.JSX.Element {
|
||||
const [events, setEvents] = useState<OrchestratorEvent[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [streamConnected, setStreamConnected] = useState(false);
|
||||
|
||||
const loadRecentEvents = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
const response = await fetch("/api/orchestrator/events/recent?limit=25");
|
||||
if (!response.ok) {
|
||||
throw new Error(`Unable to load events: HTTP ${String(response.status)}`);
|
||||
}
|
||||
const payload = (await response.json()) as RecentEventsResponse;
|
||||
setEvents(payload.events);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Unable to load events.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void loadRecentEvents();
|
||||
|
||||
const eventSource =
|
||||
typeof EventSource !== "undefined" ? new EventSource("/api/orchestrator/events") : null;
|
||||
if (eventSource) {
|
||||
eventSource.onopen = (): void => {
|
||||
setStreamConnected(true);
|
||||
};
|
||||
eventSource.onmessage = (): void => {
|
||||
void loadRecentEvents();
|
||||
};
|
||||
eventSource.onerror = (): void => {
|
||||
setStreamConnected(false);
|
||||
};
|
||||
}
|
||||
|
||||
const interval = setInterval(() => {
|
||||
void loadRecentEvents();
|
||||
}, 15000);
|
||||
|
||||
return (): void => {
|
||||
clearInterval(interval);
|
||||
eventSource?.close();
|
||||
};
|
||||
}, [loadRecentEvents]);
|
||||
|
||||
const matrixSignals = useMemo(
|
||||
() => events.filter((event) => isMatrixSignal(event)).length,
|
||||
[events]
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Loader2 className="w-5 h-5 text-gray-400 animate-spin" />
|
||||
<span className="ml-2 text-gray-500 text-sm">Loading orchestrator events...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full text-center">
|
||||
<WifiOff className="w-5 h-5 text-amber-500 mb-2" />
|
||||
<span className="text-sm text-amber-600">{error}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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">
|
||||
{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>
|
||||
</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" />
|
||||
<span>Matrix signals: {matrixSignals}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto space-y-2">
|
||||
{events.length === 0 ? (
|
||||
<div className="text-center text-sm text-gray-500 py-4">
|
||||
No recent orchestration events.
|
||||
</div>
|
||||
) : (
|
||||
events
|
||||
.slice()
|
||||
.reverse()
|
||||
.map((event, index) => (
|
||||
<div
|
||||
key={`${event.timestamp}-${event.type}-${String(index)}`}
|
||||
className="rounded border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 px-2 py-2"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<Activity className="w-3 h-3 text-blue-500 shrink-0" />
|
||||
<span className="text-xs font-medium text-gray-900 dark:text-gray-100 truncate">
|
||||
{event.type}
|
||||
</span>
|
||||
{isMatrixSignal(event) && (
|
||||
<span className="text-[10px] rounded bg-indigo-100 dark:bg-indigo-950 text-indigo-700 dark:text-indigo-300 px-1.5 py-0.5">
|
||||
matrix
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-[10px] text-gray-500">
|
||||
{new Date(event.timestamp).toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 text-[11px] text-gray-600 dark:text-gray-300">
|
||||
{event.taskId ? `Task ${event.taskId}` : "Task n/a"}
|
||||
{event.agentId ? ` · Agent ${event.agentId.slice(0, 8)}` : ""}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user