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.

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

  1. Active Task: Show currentTask() from SSE. Add phase progress bar showing ANALYZE → PLAN → EXECUTE → VERIFY from phaseResults().

  2. "Execution Agents" → "Active Delegations": Show delegations() from SSE. Each card: agentType, status, handoffId, progress.

  3. "Training Agents" → "System Agents": Show agentDefinitions() from REST. Static cards: name, role, successRate, totalExecutions.

  4. 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.

  5. Event Log: Uses mergedEventLog() computed signal instead of local-only eventLog.

New Panels

  1. Checkpoints: Shows pendingCheckpoints() with approve/reject buttons (calls VittoMonitorService.resolveCheckpoint()).

  2. Proactive Alerts: Shows proactiveAlerts() with severity badges and grouped deduplication.

Unchanged Panels

Dependencies

New imports in component

Removed imports

Backend changes

Risks

  1. 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.

  2. 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.

  3. Event log merge performance: computed() merging two arrays of up to 100 items each, sorted by timestamp. Negligible — Angular signals only recompute on change.

  4. 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.

  5. SSE state is ephemeral vs. autoFlashState persistence: SSE-sourced signals (task, phases, delegations) are lost on page refresh. VittoMonitorService recovers by calling loadInitialState() (REST) on every EventSource reconnect, so state is restored within seconds. WebSocket-sourced signals (decisions, learning stats, pipeline) survive refresh via IndexedDB persistence through autoFlashState. This asymmetry is acceptable: engine state is transient by nature; decision history is worth persisting.

  6. AI_API_URL regex strip for WebSocket: The BACKEND_URL getter strips /ai-api suffix via regex for Socket.io connection. Fragile if the URL format changes. Known tech debt, unchanged in Phase A.

  7. setInterval in approval timer: The approval countdown uses setInterval (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)

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