2026 03 23 Command Center Realtime Migration Design
Documentation for Plans
Command Center Real-Time Migration — Phase A Design
Date: 2026-03-23
Session: S262
Status: Approved
Scope: Fix Mission Control tab (/command/mission) to use real backend data via SSE + WebSocket dual transport
Problem Statement
The Mission Control tab at /command/mission shows "OFFLINE" because it relies exclusively on a Socket.io WebSocket connection to backend-ai-api:3001 at namespace /command-center. This was built in early sessions (S60-70) when the MCP server pushed events to command-center.controller.ts, which relayed them via WebSocket.
Since then, Vitto became the super-orchestrator. All real task/agent/delegation activity flows through backend-vitto-api:3003 via SSE (/engine/events). The VittoMonitorService handles this SSE connection and all other command center tabs (Approvals, Audit, Trust, Costs) use it successfully.
The Mission Control tab never migrated. It's waiting for events on a WebSocket that nobody feeds data to.
Architecture Decision
Dual transport, clean separation.
- SSE (VittoMonitorService): Vitto engine reality — task lifecycle, phases, delegations, checkpoints, engine status, proactive alerts
- WebSocket (existing gateway): MCP intelligence layer — decisions, learning stats, pipeline status, approval workflow, interactive commands (pause/resume/cancel)
Each event type has exactly one canonical source. No duplication. Both are push.
Data Source Mapping
| UI Panel | New Source | Transport |
|---|---|---|
| Connection status | SSE + WebSocket combined | Both |
| Active Task | VittoMonitorService.currentTask |
SSE |
| Task Phases (ANALYZE→VERIFY) | VittoMonitorService.phaseResults |
SSE |
| Active Delegations (was Execution Agents) | VittoMonitorService.delegations |
SSE |
| System Agents (was Training Agents) | VittoApiService.listAgentDefinitions() |
REST on init |
| Recent Decisions | WebSocket decision.made + decision.changed |
WebSocket |
| Learning Stats | WebSocket learning.stats + golden.changed |
WebSocket |
| MCP Workflow Pipeline | WebSocket mcp.* events |
WebSocket |
| Memory Tiers | WebSocket mcp.memory-tier |
WebSocket |
| Event Log | Merged from SSE + WebSocket | Both |
| Pause/Resume/Cancel | WebSocket emit | WebSocket |
| Approval Workflow | WebSocket decision.approval-required |
WebSocket |
| Checkpoints (new) | VittoMonitorService.pendingCheckpoints |
SSE |
| Proactive Alerts (new) | VittoMonitorService.groupedAlerts |
SSE |
| Engine Status (new) | VittoMonitorService.engineStatus + REST init |
SSE + REST |
Key Design Decisions
1. "Execution Agents" replaced by "Active Delegations"
The old component tracked agents as ephemeral WebSocket events (agent.spawned → agent.thinking → agent.completed). Nobody pushes these events anymore.
In the Vitto architecture, the equivalent is delegations — Sub-Vitto tasks spawned by DelegationManagerService and tracked via SSE vitto.delegation events. The DelegationChild type already exists in vitto.types.ts with handoffId, agentType, status, progress.
2. "Training Agents" replaced by "System Agents"
Training agents were phantom WebSocket events that never fired. Real agents are the 4 seeded system agents (Module Gen, Widget, Architecture, Code Quality) persisted in MongoDB via AgentSeedService. They're permanent definitions queried via VittoApiService.listAgentDefinitions(). Show as a static registry with performance stats, not as blinking spawn/complete animations.
3. Task control is cosmetic in Phase A
Pause/resume/cancel still only affects the gateway's in-memory boolean, not the Vitto engine. The VittoCoreController doesn't expose pause/cancel endpoints for individual tasks yet. Phase B adds real Vitto task control.
4. Inline template/styles extracted to separate files
The component is 2145 lines with everything inline. Code standards require separate .ts, .html, .scss files. ChangeDetectionStrategy.OnPush must also be added (currently missing — project standard violation).
5. Duplicate file must be kept in sync
An identical copy exists at libs/libs-ai/vitto-hub/src/lib/components/dashboards/command-center/command-center.component.ts (2144 lines). The canonical file is in gograb-ai-lib (used by routing at app.routes.ts line 150). The vitto-hub copy must be deleted or replaced with a re-export. Phase A deletes the stale copy and re-exports from the canonical location if needed.
Component Architecture
libs/libs-ai/gograb-ai-lib/src/lib/components/command-center/
command-center.component.ts — orchestrator, connects SSE + WS, provides signals
command-center.component.html — extracted template (structural changes for new panels)
command-center.component.scss — extracted styles (from inline, minimal changes)
No sub-components in Phase A. The template stays structurally similar but wired to new signals. Sub-component extraction is Phase B.
Signal Architecture
// Services
private readonly monitor = inject(VittoMonitorService); // SSE — via relative import from ../../services/
private readonly vittoApi = inject(VittoApiService); // REST — via @libs-ai/vitto-core
private readonly destroyRef = inject(DestroyRef);
// SSE-sourced signals (delegate to VittoMonitorService)
// These are EPHEMERAL — lost on page refresh but recovered on SSE reconnect
// (VittoMonitorService calls loadInitialState() on EventSource.onopen)
readonly sseConnected = this.monitor.isConnected;
readonly currentTask = this.monitor.currentTask;
readonly currentPhase = this.monitor.currentPhase;
readonly phaseResults = this.monitor.phaseResults;
readonly delegations = this.monitor.delegations;
readonly pendingCheckpoints = this.monitor.pendingCheckpoints;
readonly taskHistory = this.monitor.taskHistory;
readonly sseEventLog = this.monitor.eventLog;
readonly engineStatus = this.monitor.engineStatus;
readonly proactiveAlerts = this.monitor.groupedAlerts;
// WebSocket-sourced signals — PERSISTED via autoFlashState (survive page refresh)
readonly wsConnected = signal(false);
readonly recentDecisions = autoFlashState<DecisionEvent[]>(...);
readonly learningStats = autoFlashState<LearningStats>(...);
readonly mcpInitStatus = autoFlashState<McpInitStatus | null>(...);
readonly preflightStatus = autoFlashState<PreflightStatus | null>(...);
readonly pipelineStatus = autoFlashState<PipelineStatus>(...);
readonly memoryTierStats = autoFlashState<MemoryTierStats>(...);
readonly timelineEvents = autoFlashState<TimelineEvent[]>(...);
readonly liveDecisions = autoFlashState<LiveDecision[]>(...);
// Combined connection status
// Uses OR: dashboard is useful if either transport is active
readonly isConnected = computed(() => this.sseConnected() || this.wsConnected());
// Merged event log (chronologically sorted from both sources)
readonly mergedEventLog = computed(() => {
const sseEvents = this.sseEventLog().map(e => ({
type: e.type,
message: this.formatSSEEventMessage(e),
timestamp: new Date(e.timestamp)
}));
const wsEvents = this.localEventLog();
return [...sseEvents, ...wsEvents]
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())
.slice(0, 100);
});
// Agent definitions (loaded once on init via REST)
// Type derived from VittoApiService.listAgentDefinitions() response shape
readonly agentDefinitions = signal<Array<{
id: string;
name: string;
role: string;
systemPrompt?: string;
allowedTools?: string[];
triggerKeywords?: string[];
performance?: { totalExecutions: number; successRate: number; avgQualityScore: number };
}>>([]);
Init Flow
ngOnInit(): void {
// 1. Connect SSE (VittoMonitorService handles reconnection)
// If another tab already connected, this increments the reference counter
// and reuses the existing EventSource connection.
this.monitor.connect();
// 2. Connect WebSocket for MCP events (existing logic, unchanged)
this.connectWebSocket();
// 3. Load system agent definitions via REST
this.vittoApi.listAgentDefinitions().pipe(
catchError(() => of({ data: [] })),
takeUntilDestroyed(this.destroyRef),
).subscribe(res => this.agentDefinitions.set(res.data || []));
}
ngOnDestroy(): void {
this.monitor.disconnect(); // decrements ref count; closes only if last consumer
this.socket?.disconnect();
if (this.approvalTimer) clearInterval(this.approvalTimer);
}
Template Structural Changes
Modified Panels
Active Task: Show
currentTask()from SSE. Add phase progress bar showing ANALYZE → PLAN → EXECUTE → VERIFY fromphaseResults()."Execution Agents" → "Active Delegations": Show
delegations()from SSE. Each card: agentType, status, handoffId, progress."Training Agents" → "System Agents": Show
agentDefinitions()from REST. Static cards: name, role, successRate, totalExecutions.Connection indicator: Header status badge uses
isConnected()computed (green if either transport live). Diagnostic label shows "SSE: LIVE | WS: LIVE" (or partial states) for operational visibility.Event Log: Uses
mergedEventLog()computed signal instead of local-onlyeventLog.
New Panels
Checkpoints: Shows
pendingCheckpoints()with approve/reject buttons (callsVittoMonitorService.resolveCheckpoint()).Proactive Alerts: Shows
proactiveAlerts()with severity badges and grouped deduplication.
Unchanged Panels
- Recent Decisions (WebSocket
decision.made) - MCP Workflow Pipeline (WebSocket
mcp.*) - Memory Tiers (WebSocket
mcp.memory-tier) - Learning Stats (WebSocket
learning.stats) - Approval Workflow modal (WebSocket)
- Live Timeline (WebSocket)
- Code Preview (WebSocket)
- Session Recording/Replay (WebSocket)
Dependencies
New imports in component
VittoMonitorService— via relative path../../services/vitto-monitor.service(local re-export in gograb-ai-lib, matching existing pattern inagent-observatory.component.tsandvitto-hub.component.ts)VittoApiService— via@libs-ai/vitto-core(global tsconfig path available, service isprovidedIn: 'root')DestroyRef+takeUntilDestroyedfrom@angular/core/@angular/core/rxjs-interopcatchError,offromrxjs
Removed imports
- None removed — Socket.io stays for WebSocket connection
Backend changes
- None. Phase A is purely frontend. Zero backend modifications.
Risks
SSE ticket auth: VittoMonitorService connects without JWT token (SSE endpoint is
@Public()). Same pattern as all other tabs. If the endpoint gets locked down, all tabs break — not unique to this change.VittoMonitorService reference counting: Service is
providedIn: 'root'with_connectCount. If another tab already connected, we share the connection. On destroy, we decrement. Correct behavior by design. If Mission Control is the first tab loaded, it initiates the connection; otherwise it joins an existing one.Event log merge performance:
computed()merging two arrays of up to 100 items each, sorted by timestamp. Negligible — Angular signals only recompute on change.Task control is cosmetic: Pause/resume/cancel affects gateway state but doesn't stop a running Vitto agent loop. Known gap, documented for Phase B.
SSE state is ephemeral vs. autoFlashState persistence: SSE-sourced signals (task, phases, delegations) are lost on page refresh.
VittoMonitorServicerecovers by callingloadInitialState()(REST) on every EventSource reconnect, so state is restored within seconds. WebSocket-sourced signals (decisions, learning stats, pipeline) survive refresh via IndexedDB persistence throughautoFlashState. This asymmetry is acceptable: engine state is transient by nature; decision history is worth persisting.AI_API_URLregex strip for WebSocket: TheBACKEND_URLgetter strips/ai-apisuffix via regex for Socket.io connection. Fragile if the URL format changes. Known tech debt, unchanged in Phase A.setIntervalin approval timer: The approval countdown usessetInterval(line 2072), which violates the project standard of RxJS-only async. Out of scope for Phase A; flagged for Phase B cleanup.
Phase B Preview (Out of Scope)
- Sub-component extraction (each panel → own component)
- Real task control via Vitto engine endpoints
- Sub-Vitto mesh visualization (delegation tree)
- Cost per task display
- Bridge MCP decisions into Vitto SSE stream (eliminate dual connection)
- Agent communication dashboard (handoff flow visualization)
- Inter-agent communication improvements
- Replace
setIntervalapproval timer with RxJSinterval
Files Modified
| File | Change |
|---|---|
libs/libs-ai/gograb-ai-lib/src/lib/components/command-center/command-center.component.ts |
Rewrite: add VittoMonitorService + VittoApiService, extract template/styles, add OnPush, rewire signals |
libs/libs-ai/gograb-ai-lib/src/lib/components/command-center/command-center.component.html |
New file: extracted + modified template |
libs/libs-ai/gograb-ai-lib/src/lib/components/command-center/command-center.component.scss |
New file: extracted styles |
libs/libs-ai/vitto-hub/src/lib/components/dashboards/command-center/command-center.component.ts |
Delete or replace with re-export from canonical location |