feat(web): show latest orchestrator event in task progress widget
Some checks failed
ci/woodpecker/push/web Pipeline failed
Some checks failed
ci/woodpecker/push/web Pipeline failed
This commit is contained in:
@@ -27,6 +27,13 @@ interface QueueStats {
|
|||||||
delayed: number;
|
delayed: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RecentOrchestratorEvent {
|
||||||
|
type: string;
|
||||||
|
timestamp: string;
|
||||||
|
taskId?: string;
|
||||||
|
agentId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
function getElapsedTime(spawnedAt: string, completedAt?: string): string {
|
function getElapsedTime(spawnedAt: string, completedAt?: string): string {
|
||||||
const start = new Date(spawnedAt).getTime();
|
const start = new Date(spawnedAt).getTime();
|
||||||
const end = completedAt ? new Date(completedAt).getTime() : Date.now();
|
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 {
|
export function TaskProgressWidget({ id: _id, config: _config }: WidgetProps): React.JSX.Element {
|
||||||
const [tasks, setTasks] = useState<AgentTask[]>([]);
|
const [tasks, setTasks] = useState<AgentTask[]>([]);
|
||||||
const [queueStats, setQueueStats] = useState<QueueStats | null>(null);
|
const [queueStats, setQueueStats] = useState<QueueStats | null>(null);
|
||||||
|
const [recentEvents, setRecentEvents] = useState<RecentOrchestratorEvent[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [isQueuePaused, setIsQueuePaused] = useState(false);
|
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(
|
const setQueueState = useCallback(
|
||||||
async (action: "pause" | "resume"): Promise<void> => {
|
async (action: "pause" | "resume"): Promise<void> => {
|
||||||
setIsActionPending(true);
|
setIsActionPending(true);
|
||||||
@@ -157,10 +176,12 @@ export function TaskProgressWidget({ id: _id, config: _config }: WidgetProps): R
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void fetchTasks();
|
void fetchTasks();
|
||||||
void fetchQueueStats();
|
void fetchQueueStats();
|
||||||
|
void fetchRecentEvents();
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
void fetchTasks();
|
void fetchTasks();
|
||||||
void fetchQueueStats();
|
void fetchQueueStats();
|
||||||
|
void fetchRecentEvents();
|
||||||
}, 15000);
|
}, 15000);
|
||||||
|
|
||||||
const eventSource =
|
const eventSource =
|
||||||
@@ -169,6 +190,7 @@ export function TaskProgressWidget({ id: _id, config: _config }: WidgetProps): R
|
|||||||
eventSource.onmessage = (): void => {
|
eventSource.onmessage = (): void => {
|
||||||
void fetchTasks();
|
void fetchTasks();
|
||||||
void fetchQueueStats();
|
void fetchQueueStats();
|
||||||
|
void fetchRecentEvents();
|
||||||
};
|
};
|
||||||
eventSource.onerror = (): void => {
|
eventSource.onerror = (): void => {
|
||||||
// Polling remains the resilience path.
|
// Polling remains the resilience path.
|
||||||
@@ -179,7 +201,9 @@ export function TaskProgressWidget({ id: _id, config: _config }: WidgetProps): R
|
|||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
eventSource?.close();
|
eventSource?.close();
|
||||||
};
|
};
|
||||||
}, [fetchTasks, fetchQueueStats]);
|
}, [fetchTasks, fetchQueueStats, fetchRecentEvents]);
|
||||||
|
|
||||||
|
const latestEvent = recentEvents.length > 0 ? recentEvents[recentEvents.length - 1] : null;
|
||||||
|
|
||||||
const stats = {
|
const stats = {
|
||||||
total: tasks.length,
|
total: tasks.length,
|
||||||
@@ -226,6 +250,13 @@ export function TaskProgressWidget({ id: _id, config: _config }: WidgetProps): R
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</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 */}
|
{/* Summary stats */}
|
||||||
<div className="grid grid-cols-4 gap-1 text-center text-xs">
|
<div className="grid grid-cols-4 gap-1 text-center text-xs">
|
||||||
<div className="bg-gray-50 dark:bg-gray-800 rounded p-2">
|
<div className="bg-gray-50 dark:bg-gray-800 rounded p-2">
|
||||||
|
|||||||
@@ -242,4 +242,58 @@ describe("TaskProgressWidget", (): void => {
|
|||||||
expect(taskElements[1]?.textContent).toBe("COMPLETED-TASK");
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -386,3 +386,4 @@
|
|||||||
| ORCH-OBS-007 | done | Add `OrchestratorEventsWidget` for live/recent orchestration visibility with Matrix signal hints | #411 | web | feature/mosaic-stack-finalization | ORCH-OBS-002 | | orch | 2026-02-17T16:55Z | 2026-02-17T17:03Z | 12K | 9K |
|
| ORCH-OBS-007 | done | Add `OrchestratorEventsWidget` for live/recent orchestration visibility with Matrix signal hints | #411 | web | feature/mosaic-stack-finalization | ORCH-OBS-002 | | orch | 2026-02-17T16:55Z | 2026-02-17T17:03Z | 12K | 9K |
|
||||||
| ORCH-OBS-008 | done | Integrate new widget into HUD/WidgetRegistry and extend widget regression coverage | #411 | web | feature/mosaic-stack-finalization | ORCH-OBS-007 | | orch | 2026-02-17T17:03Z | 2026-02-17T17:08Z | 10K | 7K |
|
| ORCH-OBS-008 | done | Integrate new widget into HUD/WidgetRegistry and extend widget regression coverage | #411 | web | feature/mosaic-stack-finalization | ORCH-OBS-007 | | orch | 2026-02-17T17:03Z | 2026-02-17T17:08Z | 10K | 7K |
|
||||||
| ORCH-OBS-009 | done | Seed default/reset local HUD layout with orchestration widgets so visibility works out-of-box | #411 | web | feature/mosaic-stack-finalization | ORCH-OBS-008 | | orch | 2026-02-17T17:10Z | 2026-02-17T17:14Z | 8K | 6K |
|
| ORCH-OBS-009 | done | Seed default/reset local HUD layout with orchestration widgets so visibility works out-of-box | #411 | web | feature/mosaic-stack-finalization | ORCH-OBS-008 | | orch | 2026-02-17T17:10Z | 2026-02-17T17:14Z | 8K | 6K |
|
||||||
|
| ORCH-OBS-010 | done | Enrich `TaskProgressWidget` with latest recent-event context from `/api/orchestrator/events/recent` | #411 | web | feature/mosaic-stack-finalization | ORCH-OBS-009 | | orch | 2026-02-17T17:15Z | 2026-02-17T17:20Z | 8K | 6K |
|
||||||
|
|||||||
Reference in New Issue
Block a user