feat(web): show latest orchestrator event in task progress widget
Some checks failed
ci/woodpecker/push/web Pipeline failed

This commit is contained in:
Jason Woltje
2026-02-17 16:12:40 -06:00
parent ab902250f8
commit 5bce7dbb05
3 changed files with 87 additions and 1 deletions

View File

@@ -27,6 +27,13 @@ interface QueueStats {
delayed: number;
}
interface RecentOrchestratorEvent {
type: string;
timestamp: string;
taskId?: string;
agentId?: string;
}
function getElapsedTime(spawnedAt: string, completedAt?: string): string {
const start = new Date(spawnedAt).getTime();
const end = completedAt ? new Date(completedAt).getTime() : Date.now();
@@ -103,6 +110,7 @@ function getAgentTypeLabel(agentType: string): string {
export function TaskProgressWidget({ id: _id, config: _config }: WidgetProps): React.JSX.Element {
const [tasks, setTasks] = useState<AgentTask[]>([]);
const [queueStats, setQueueStats] = useState<QueueStats | null>(null);
const [recentEvents, setRecentEvents] = useState<RecentOrchestratorEvent[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [isQueuePaused, setIsQueuePaused] = useState(false);
@@ -135,6 +143,17 @@ export function TaskProgressWidget({ id: _id, config: _config }: WidgetProps): R
}
}, []);
const fetchRecentEvents = useCallback(async (): Promise<void> => {
try {
const res = await fetch("/api/orchestrator/events/recent?limit=5");
if (!res.ok) throw new Error(`HTTP ${String(res.status)}`);
const data = (await res.json()) as { events: RecentOrchestratorEvent[] };
setRecentEvents(data.events);
} catch {
// Optional enhancement path; do not fail widget if recent-events endpoint is unavailable.
}
}, []);
const setQueueState = useCallback(
async (action: "pause" | "resume"): Promise<void> => {
setIsActionPending(true);
@@ -157,10 +176,12 @@ export function TaskProgressWidget({ id: _id, config: _config }: WidgetProps): R
useEffect(() => {
void fetchTasks();
void fetchQueueStats();
void fetchRecentEvents();
const interval = setInterval(() => {
void fetchTasks();
void fetchQueueStats();
void fetchRecentEvents();
}, 15000);
const eventSource =
@@ -169,6 +190,7 @@ export function TaskProgressWidget({ id: _id, config: _config }: WidgetProps): R
eventSource.onmessage = (): void => {
void fetchTasks();
void fetchQueueStats();
void fetchRecentEvents();
};
eventSource.onerror = (): void => {
// Polling remains the resilience path.
@@ -179,7 +201,9 @@ export function TaskProgressWidget({ id: _id, config: _config }: WidgetProps): R
clearInterval(interval);
eventSource?.close();
};
}, [fetchTasks, fetchQueueStats]);
}, [fetchTasks, fetchQueueStats, fetchRecentEvents]);
const latestEvent = recentEvents.length > 0 ? recentEvents[recentEvents.length - 1] : null;
const stats = {
total: tasks.length,
@@ -226,6 +250,13 @@ export function TaskProgressWidget({ id: _id, config: _config }: WidgetProps): R
</button>
</div>
{latestEvent && (
<div className="rounded bg-gray-50 dark:bg-gray-800 px-2 py-1 text-xs text-gray-600 dark:text-gray-300">
Latest: {latestEvent.type}
{latestEvent.taskId ? ` · ${latestEvent.taskId}` : ""}
</div>
)}
{/* Summary stats */}
<div className="grid grid-cols-4 gap-1 text-center text-xs">
<div className="bg-gray-50 dark:bg-gray-800 rounded p-2">

View File

@@ -242,4 +242,58 @@ describe("TaskProgressWidget", (): void => {
expect(taskElements[1]?.textContent).toBe("COMPLETED-TASK");
});
});
it("should display latest orchestrator event when available", async (): Promise<void> => {
mockFetch.mockImplementation((input: RequestInfo | URL) => {
let url = "";
if (typeof input === "string") {
url = input;
} else if (input instanceof URL) {
url = input.toString();
} else {
url = input.url;
}
if (url.includes("/api/orchestrator/agents")) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve([]),
} as unknown as Response);
}
if (url.includes("/api/orchestrator/queue/stats")) {
return Promise.resolve({
ok: true,
json: () =>
Promise.resolve({
pending: 0,
active: 0,
completed: 0,
failed: 0,
delayed: 0,
}),
} as unknown as Response);
}
if (url.includes("/api/orchestrator/events/recent")) {
return Promise.resolve({
ok: true,
json: () =>
Promise.resolve({
events: [
{
type: "task.executing",
timestamp: new Date().toISOString(),
taskId: "TASK-123",
},
],
}),
} as unknown as Response);
}
return Promise.reject(new Error("Unknown endpoint"));
});
render(<TaskProgressWidget id="task-progress-1" />);
await waitFor(() => {
expect(screen.getByText(/Latest: task.executing · TASK-123/i)).toBeInTheDocument();
});
});
});