From 458cac7cdd8bfac0f28b4566fb78dbbfcd0e4cb5 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Mon, 23 Feb 2026 01:07:29 +0000 Subject: [PATCH] Phase 3: Agent Cycle Visibility (#461) (#462) Co-authored-by: Jason Woltje Co-committed-by: Jason Woltje --- .../api/src/runner-jobs/runner-jobs.module.ts | 3 +- .../runner-jobs.service.concurrency.spec.ts | 11 ++++++ .../runner-jobs/runner-jobs.service.spec.ts | 11 ++++++ .../src/runner-jobs/runner-jobs.service.ts | 28 ++++++++++++++- apps/web/src/app/(authenticated)/page.tsx | 22 ++++++++++++ .../dashboard/OrchestratorSessions.tsx | 35 +++++++++++++++++++ docs/MISSION-MANIFEST.md | 21 +++++------ docs/TASKS.md | 3 ++ .../mosaic-stack-go-live-mvp-20260222.md | 14 ++++++++ 9 files changed, 136 insertions(+), 12 deletions(-) diff --git a/apps/api/src/runner-jobs/runner-jobs.module.ts b/apps/api/src/runner-jobs/runner-jobs.module.ts index 828fff2..0cda3f7 100644 --- a/apps/api/src/runner-jobs/runner-jobs.module.ts +++ b/apps/api/src/runner-jobs/runner-jobs.module.ts @@ -4,6 +4,7 @@ import { RunnerJobsService } from "./runner-jobs.service"; import { PrismaModule } from "../prisma/prisma.module"; import { BullMqModule } from "../bullmq/bullmq.module"; import { AuthModule } from "../auth/auth.module"; +import { WebSocketModule } from "../websocket/websocket.module"; /** * Runner Jobs Module @@ -12,7 +13,7 @@ import { AuthModule } from "../auth/auth.module"; * for asynchronous job processing. */ @Module({ - imports: [PrismaModule, BullMqModule, AuthModule], + imports: [PrismaModule, BullMqModule, AuthModule, WebSocketModule], controllers: [RunnerJobsController], providers: [RunnerJobsService], exports: [RunnerJobsService], diff --git a/apps/api/src/runner-jobs/runner-jobs.service.concurrency.spec.ts b/apps/api/src/runner-jobs/runner-jobs.service.concurrency.spec.ts index c5b4d54..9e96baf 100644 --- a/apps/api/src/runner-jobs/runner-jobs.service.concurrency.spec.ts +++ b/apps/api/src/runner-jobs/runner-jobs.service.concurrency.spec.ts @@ -3,6 +3,7 @@ import { Test, TestingModule } from "@nestjs/testing"; import { RunnerJobsService } from "./runner-jobs.service"; import { PrismaService } from "../prisma/prisma.service"; import { BullMqService } from "../bullmq/bullmq.service"; +import { WebSocketGateway } from "../websocket/websocket.gateway"; import { RunnerJobStatus } from "@prisma/client"; import { ConflictException, BadRequestException } from "@nestjs/common"; @@ -19,6 +20,12 @@ describe("RunnerJobsService - Concurrency", () => { getQueue: vi.fn(), }; + const mockWebSocketGateway = { + emitJobCreated: vi.fn(), + emitJobStatusChanged: vi.fn(), + emitJobProgress: vi.fn(), + }; + beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ @@ -37,6 +44,10 @@ describe("RunnerJobsService - Concurrency", () => { provide: BullMqService, useValue: mockBullMqService, }, + { + provide: WebSocketGateway, + useValue: mockWebSocketGateway, + }, ], }).compile(); diff --git a/apps/api/src/runner-jobs/runner-jobs.service.spec.ts b/apps/api/src/runner-jobs/runner-jobs.service.spec.ts index 2632192..ac0ca10 100644 --- a/apps/api/src/runner-jobs/runner-jobs.service.spec.ts +++ b/apps/api/src/runner-jobs/runner-jobs.service.spec.ts @@ -3,6 +3,7 @@ import { Test, TestingModule } from "@nestjs/testing"; import { RunnerJobsService } from "./runner-jobs.service"; import { PrismaService } from "../prisma/prisma.service"; import { BullMqService } from "../bullmq/bullmq.service"; +import { WebSocketGateway } from "../websocket/websocket.gateway"; import { RunnerJobStatus } from "@prisma/client"; import { NotFoundException, BadRequestException } from "@nestjs/common"; import { CreateJobDto, QueryJobsDto } from "./dto"; @@ -32,6 +33,12 @@ describe("RunnerJobsService", () => { getQueue: vi.fn(), }; + const mockWebSocketGateway = { + emitJobCreated: vi.fn(), + emitJobStatusChanged: vi.fn(), + emitJobProgress: vi.fn(), + }; + beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ @@ -44,6 +51,10 @@ describe("RunnerJobsService", () => { provide: BullMqService, useValue: mockBullMqService, }, + { + provide: WebSocketGateway, + useValue: mockWebSocketGateway, + }, ], }).compile(); diff --git a/apps/api/src/runner-jobs/runner-jobs.service.ts b/apps/api/src/runner-jobs/runner-jobs.service.ts index 8149a23..02d340d 100644 --- a/apps/api/src/runner-jobs/runner-jobs.service.ts +++ b/apps/api/src/runner-jobs/runner-jobs.service.ts @@ -3,6 +3,7 @@ import { Prisma, RunnerJobStatus } from "@prisma/client"; import { Response } from "express"; import { PrismaService } from "../prisma/prisma.service"; import { BullMqService } from "../bullmq/bullmq.service"; +import { WebSocketGateway } from "../websocket/websocket.gateway"; import { QUEUE_NAMES } from "../bullmq/queues"; import { ConcurrentUpdateException } from "../common/exceptions/concurrent-update.exception"; import type { CreateJobDto, QueryJobsDto } from "./dto"; @@ -14,7 +15,8 @@ import type { CreateJobDto, QueryJobsDto } from "./dto"; export class RunnerJobsService { constructor( private readonly prisma: PrismaService, - private readonly bullMq: BullMqService + private readonly bullMq: BullMqService, + private readonly wsGateway: WebSocketGateway ) {} /** @@ -56,6 +58,8 @@ export class RunnerJobsService { { priority } ); + this.wsGateway.emitJobCreated(workspaceId, job); + return job; } @@ -194,6 +198,13 @@ export class RunnerJobsService { throw new NotFoundException(`RunnerJob with ID ${id} not found after cancel`); } + this.wsGateway.emitJobStatusChanged(workspaceId, id, { + id, + workspaceId, + status: job.status, + previousStatus: existingJob.status, + }); + return job; }); } @@ -248,6 +259,8 @@ export class RunnerJobsService { { priority: existingJob.priority } ); + this.wsGateway.emitJobCreated(workspaceId, newJob); + return newJob; } @@ -530,6 +543,13 @@ export class RunnerJobsService { throw new NotFoundException(`RunnerJob with ID ${id} not found after update`); } + this.wsGateway.emitJobStatusChanged(workspaceId, id, { + id, + workspaceId, + status: updatedJob.status, + previousStatus: existingJob.status, + }); + return updatedJob; }); } @@ -606,6 +626,12 @@ export class RunnerJobsService { throw new NotFoundException(`RunnerJob with ID ${id} not found after update`); } + this.wsGateway.emitJobProgress(workspaceId, id, { + id, + workspaceId, + progressPercent: updatedJob.progressPercent, + }); + return updatedJob; }); } diff --git a/apps/web/src/app/(authenticated)/page.tsx b/apps/web/src/app/(authenticated)/page.tsx index 2105b48..dbaf667 100644 --- a/apps/web/src/app/(authenticated)/page.tsx +++ b/apps/web/src/app/(authenticated)/page.tsx @@ -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 (
diff --git a/apps/web/src/components/dashboard/OrchestratorSessions.tsx b/apps/web/src/components/dashboard/OrchestratorSessions.tsx index e654163..40e6659 100644 --- a/apps/web/src/components/dashboard/OrchestratorSessions.tsx +++ b/apps/web/src/components/dashboard/OrchestratorSessions.tsx @@ -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 {
+ + {agent.statusLabel} + ); } @@ -251,6 +265,27 @@ function OrchCard({ session }: OrchCardProps): ReactElement { {session.duration} + {session.progress > 0 && ( +
+
+
+ )}
{session.agents.map((agent) => ( diff --git a/docs/MISSION-MANIFEST.md b/docs/MISSION-MANIFEST.md index 7e0a815..8016b3f 100644 --- a/docs/MISSION-MANIFEST.md +++ b/docs/MISSION-MANIFEST.md @@ -8,10 +8,10 @@ **ID:** mosaic-stack-go-live-mvp-20260222 **Statement:** Ship Mosaic Stack MVP: operational dashboard with theming, task ingestion, one visible agent cycle, deployed and smoke-tested. Unblocks SagePHR, DYOR, Calibr, and downstream projects. **Phase:** Execution -**Current Milestone:** phase-2 (Task Ingestion Pipeline) -**Progress:** 1 / 4 milestones +**Current Milestone:** phase-3 (Agent Cycle Visibility) +**Progress:** 2 / 4 milestones **Status:** active -**Last Updated:** 2026-02-23 00:20 UTC +**Last Updated:** 2026-02-23 18:55 UTC ## Success Criteria @@ -34,12 +34,12 @@ This mission continues from that foundation. ## Milestones -| # | ID | Name | Status | Branch | Issue | Started | Completed | -| --- | ------- | -------------------------- | ----------- | ---------------------- | ----- | ---------- | ---------- | -| 1 | phase-1 | Dashboard Polish + Theming | completed | feat/phase-1-polish | #457 | 2026-02-22 | 2026-02-23 | -| 2 | phase-2 | Task Ingestion Pipeline | in-progress | feat/phase-2-ingestion | #459 | 2026-02-23 | — | -| 3 | phase-3 | Agent Cycle Visibility | pending | — | — | — | — | -| 4 | phase-4 | Deploy + Smoke Test | pending | — | — | — | — | +| # | ID | Name | Status | Branch | Issue | Started | Completed | +| --- | ------- | -------------------------- | ----------- | ----------------------------- | ----- | ---------- | ---------- | +| 1 | phase-1 | Dashboard Polish + Theming | completed | feat/phase-1-polish | #457 | 2026-02-22 | 2026-02-23 | +| 2 | phase-2 | Task Ingestion Pipeline | completed | feat/phase-2-ingestion | #459 | 2026-02-23 | 2026-02-23 | +| 3 | phase-3 | Agent Cycle Visibility | in-progress | feat/phase-3-agent-visibility | #461 | 2026-02-23 | — | +| 4 | phase-4 | Deploy + Smoke Test | pending | — | — | — | — | ## Deployment @@ -61,7 +61,8 @@ This mission continues from that foundation. | Session | Runtime | Started | Duration | Ended Reason | Last Task | | ------- | ------- | ---------------- | -------- | ------------ | --------- | -| S1 | Claude | 2026-02-22 17:50 | — | — | — | +| S1 | Claude | 2026-02-22 17:50 | — | context | MS-P2-002 | +| S2 | Claude | 2026-02-23 18:45 | — | — | — | ## Scratchpad diff --git a/docs/TASKS.md b/docs/TASKS.md index 09eb59c..33a42e8 100644 --- a/docs/TASKS.md +++ b/docs/TASKS.md @@ -11,3 +11,6 @@ | MS-P2-001 | done | phase-2 | Create dashboard summary API endpoint: aggregate task counts, project counts, recent activity, active jobs in single call | — | issue #459, commit e38aaa9, 7 files +430 lines | | MS-P2-002 | done | phase-2 | Wire dashboard widgets to real API data: ActivityFeed, DashboardMetrics, OrchestratorSessions replace mock with API calls | — | issue #459, commit 7c762e6 + remediation | | MS-P2-003 | done | phase-2 | Phase verification: create task via API, confirm visible in dashboard, all quality gates pass | — | issue #459, lint 8/8 typecheck 7/7 test 8/8 | +| MS-P3-001 | done | phase-3 | Wire WebSocket emits into RunnerJobsService: broadcast job status/progress/step events to workspace rooms | — | issue #461, commit 5d3045a | +| MS-P3-002 | done | phase-3 | Dashboard auto-refresh + enhanced OrchestratorSessions: polling interval, progress bars, step status indicators, timestamps | — | issue #461, commit 5d3045a | +| MS-P3-003 | done | phase-3 | Phase verification: all quality gates pass, demonstrate agent job cycle visibility end-to-end | — | issue #461, lint 8/8 typecheck 7/7 test 8/8 | diff --git a/docs/scratchpads/mosaic-stack-go-live-mvp-20260222.md b/docs/scratchpads/mosaic-stack-go-live-mvp-20260222.md index 1529d7e..24e5150 100644 --- a/docs/scratchpads/mosaic-stack-go-live-mvp-20260222.md +++ b/docs/scratchpads/mosaic-stack-go-live-mvp-20260222.md @@ -46,6 +46,20 @@ Estimated total: ~50K tokens - Net: -373 lines (legacy cleanup + responsive CSS additions) - Review: approve (0 blockers, 0 critical security) +### 2026-02-23: Phase-2 Completion Summary + +- PR #460 merged to main (squash), commit 7581d26 +- Issue #459 closed +- 3/3 tasks done (MS-P2-001 through MS-P2-003) +- New files: dashboard module (controller, service, DTOs, tests), API client, typed widget props +- Review blockers fixed: race condition (null workspaceId guard), TypeScript strict typing, error state UI +- Net: +1042 lines, -253 lines (18 files changed) +- All quality gates green: lint 8/8, typecheck 7/7, test 8/8 (no cache) + +| Session | Date | Milestone | Tasks Done | Outcome | +| ------- | ---------- | --------- | ---------- | ------------------------------------------------------ | +| S2 | 2026-02-23 | phase-2 | 3/3 | COMPLETE — PR #460 merged (7581d26), issue #459 closed | + ## Open Questions ## Corrections