feat: agent cycle visibility — WebSocket emits + dashboard polling (#461)

- Wire WebSocketGateway into RunnerJobsService: emit job:created,
  job:status, and job:progress on create/cancel/retry/updateStatus/
  updateProgress operations
- Add 30s polling interval to dashboard page for near-real-time updates
- Enhance OrchestratorSessions widget with progress bars and step
  status labels
- Update test mocks with WebSocketGateway provider

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-22 19:02:51 -06:00
parent 63178df643
commit 5d3045ab4f
7 changed files with 110 additions and 4 deletions

View File

@@ -53,6 +53,28 @@ export default function DashboardPage(): ReactElement {
};
}, [workspaceId]);
useEffect(() => {
if (!workspaceId) return;
let cancelled = false;
const wsId = workspaceId;
const interval = setInterval(() => {
fetchDashboardSummary(wsId)
.then((summary) => {
if (!cancelled) setData(summary);
})
.catch((err: unknown) => {
console.error("[Dashboard] Refresh failed:", err);
});
}, 30_000);
return (): void => {
cancelled = true;
clearInterval(interval);
};
}, [workspaceId]);
if (isLoading) {
return (
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>

View File

@@ -27,6 +27,7 @@ interface AgentNode {
name: string;
task: string;
status: DotVariant;
statusLabel: string;
}
interface OrchestratorSession {
@@ -36,6 +37,7 @@ interface OrchestratorSession {
badge: string;
badgeVariant: BadgeVariant;
duration: string;
progress: number;
agents: AgentNode[];
}
@@ -105,6 +107,7 @@ function mapJobToSession(job: ActiveJob): OrchestratorSession {
name: step.name,
task: `Phase: ${step.phase}`,
status: statusToDotVariant(step.status),
statusLabel: step.status.toLowerCase(),
}));
return {
@@ -114,6 +117,7 @@ function mapJobToSession(job: ActiveJob): OrchestratorSession {
badge: job.status,
badgeVariant: statusToBadgeVariant(job.status),
duration: formatDuration(job.createdAt),
progress: job.progressPercent,
agents,
};
}
@@ -192,6 +196,16 @@ function AgentNodeItem({ agent }: AgentNodeItemProps): ReactElement {
</div>
</div>
<Dot variant={agent.status} />
<span
style={{
fontSize: "0.65rem",
fontFamily: "var(--mono)",
color: "var(--muted)",
textTransform: "uppercase",
}}
>
{agent.statusLabel}
</span>
</div>
);
}
@@ -251,6 +265,27 @@ function OrchCard({ session }: OrchCardProps): ReactElement {
{session.duration}
</span>
</div>
{session.progress > 0 && (
<div
style={{
height: 4,
borderRadius: 2,
background: "var(--border)",
marginBottom: 10,
overflow: "hidden",
}}
>
<div
style={{
height: "100%",
width: `${String(session.progress)}%`,
background: "var(--primary)",
borderRadius: 2,
transition: "width 0.3s ease",
}}
/>
</div>
)}
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
{session.agents.map((agent) => (
<AgentNodeItem key={agent.id} agent={agent} />