2026 03 04 Observatory Implementation Plan

Documentation for Plans

Observatory Redesign — Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Unify Observatory under Vitto as the canonical super-agent monitoring surface with full MCP convergence, cost persistence, unified alerts, and zero dead code.

Architecture: Vitto's Observatory (/vitto/observatory) becomes the single 9-tab monitoring surface. /observatory renders the same shell. Backend split (ai-api + vitto-api) stays but frontend abstracts it. MCP gets 3 new self-monitoring actions.

Tech Stack: Angular 20+ (standalone, signals), NestJS (Mongoose, EventEmitter2), MCP (TypeScript gateway pattern), SSE + WebSocket + REST polling.

Design Doc: docs/plans/2026-03-04-observatory-redesign.md


Execution Order

Tasks are ordered by dependency. Each task is independently committable.

  1. Tasks 1-6: Bug fixes (stabilize existing code)
  2. Tasks 7-9: Backend new features (LLM persistence, alerts, new endpoints)
  3. Tasks 10-14: Wire unwired frontend code
  4. Tasks 15-17: Unified surface (route consolidation)
  5. Tasks 18-20: MCP convergence
  6. Task 21: Wire unused backend endpoints to observatory tabs
  7. Task 22: Final verification

Task 1: Fix setInterval in Approvals Component

Files:

Step 1: Replace setInterval with RxJS timer

Replace the setInterval call (line 77) and clearInterval in ngOnDestroy with:

// In ngOnInit, after the initial loadData() call:
timer(30_000, 30_000).pipe(
  takeUntilDestroyed(this.destroyRef),
).subscribe(() => this.loadData());

Remove the refreshInterval property and ngOnDestroy clearInterval call.

Add import: import { timer } from 'rxjs';

Step 2: Verify no setInterval remains

Run: grep -r 'setInterval' libs/libs-ai/vitto-hub/src/lib/components/observatory-panel/ Expected: No results

Step 3: Commit

git add libs/libs-ai/vitto-hub/src/lib/components/observatory-panel/observatory-approvals.component.ts
git commit -m "fix: replace setInterval with RxJS timer in observatory approvals"

Task 2: Fix VittoMonitorService Singleton Race Condition

Files:

Step 1: Add reference counting to connect/disconnect

Add a private counter:

private _connectCount = 0;

Modify connect() (line 168):

connect(tokenProvider?: () => string | null): void {
  this._connectCount++;
  if (this._connectCount > 1) {
    // Already connected, just increment count
    return;
  }
  // ... existing connection logic
}

Modify disconnect() (line 221):

disconnect(): void {
  this._connectCount = Math.max(0, this._connectCount - 1);
  if (this._connectCount > 0) {
    // Other consumers still active, keep connection
    return;
  }
  // ... existing disconnect logic
}

Step 2: Commit

git add libs/libs-ai/gograb-ai-lib/src/lib/services/vitto-monitor.service.ts
git commit -m "fix: add reference counting to VittoMonitorService connect/disconnect"

Task 3: Fix Sandbox Tab Issues (3 fixes)

Files:

Step 1: Add env vars for server info

Add to .env.local:

SANDBOX_SERVER_LABEL=Hetzner AX42-U (Falkenstein, DE)
SANDBOX_SERVER_HARDWARE=Ryzen 7 PRO 8700GE, 128GB DDR5, 2x512GB NVMe

Step 2: Extend backend sandbox-providers endpoint to return serverInfo

In observatory.controller.ts, in the getSandboxProviders() method (line 72), extend the return value to include:

const sandboxServerUrl = this.config.get<string>('SANDBOX_SERVER_URL') || null;
const serverInfo = sandboxServerUrl ? {
  url: sandboxServerUrl,
  dns: new URL(sandboxServerUrl).hostname,
  label: this.config.get<string>('SANDBOX_SERVER_LABEL', 'Self-Hosted Server'),
  hardware: this.config.get<string>('SANDBOX_SERVER_HARDWARE', ''),
  isolation: 'Firecracker microVMs (KVM-backed)',
} : null;

Add serverInfo to the return object.

Step 3: Remove dead vittoApiUrl injection from sandbox-tab.component.ts

Remove line 26: private readonly vittoApiUrl = inject(VITTO_API_URL); Remove the VITTO_API_URL import if no longer used.

Step 4: Replace hardcoded HTML with data-driven template

In sandbox-tab.component.ts, add a computed signal:

readonly serverInfo = computed(() => this.providerData()?.serverInfo ?? null);

In sandbox-tab.component.html, replace lines 80-105 (the hardcoded "Self-Hosted Infrastructure" card) with:

@if (serverInfo(); as info) {
  <div class="card">
    <h3 class="card-title">Self-Hosted Infrastructure</h3>
    <div class="info-grid">
      <div class="info-row"><span class="info-key">Server</span><span class="info-val">{{ info.label }}</span></div>
      <div class="info-row"><span class="info-key">DNS</span><span class="info-val">{{ info.dns }}</span></div>
      @if (info.hardware) {
        <div class="info-row"><span class="info-key">Hardware</span><span class="info-val">{{ info.hardware }}</span></div>
      }
      <div class="info-row"><span class="info-key">Isolation</span><span class="info-val">{{ info.isolation }}</span></div>
    </div>
  </div>
}

Step 5: Add error handling to destroySession()

In sandbox-tab.component.ts, update destroySession() to show feedback:

destroySession(sessionId: string): void {
  this.data.destroySandbox(sessionId).subscribe({
    next: () => this.loadProviderData(), // immediate refresh
    error: (err) => console.error('Failed to destroy sandbox session:', err),
  });
}

Step 6: Commit

git add apps/gograb-ai/src/app/pages/observatory/sandbox-tab/ APIs/backend-ai-api/src/app/infrastructure/controllers/observatory.controller.ts .env.local
git commit -m "fix: make sandbox infrastructure data-driven, remove dead injection, add error handling"

Task 4: Fix System Dashboard (3 fixes)

Files:

Step 1: Convert getModelEntries() to computed signal

In the component class, replace the getModelEntries() method (line 75) with a computed signal:

readonly modelEntries = computed(() => {
  const d = this.dataService.dashboard();
  if (!d?.llmSpend?.byModel) return [];
  return Object.entries(d.llmSpend.byModel).map(([model, data]) => ({
    model,
    count: data.count,
    costUsd: Math.round(data.costUsd * 10000) / 10000,
  }));
});

Update HTML to use modelEntries() instead of getModelEntries(d).

Step 2: Convert legacy directives to control flow

Replace all *ngIf with @if, all *ngFor with @for, and [ngClass] with [class] binding where appropriate throughout the HTML template.

Step 3: Surface byUser and details fields

Add to the template after the model breakdown:

Step 4: Commit

git add libs/libs-ai/gograb-ai-lib/src/lib/components/observatory-dashboard/
git commit -m "fix: computed signal for model entries, modern control flow, surface hidden data"

Task 5: Fix Vitto Hub Observatory Tab Issues

Files:

Step 1: Add filter input to Failure Chains

In failure-chains.component.html, add before the chains list:

<div class="filter-bar">
  <input
    type="text"
    placeholder="Filter by task type..."
    [value]="taskTypeFilter()"
    (input)="taskTypeFilter.set($any($event.target).value)"
    (keyup.enter)="loadChains()"
  />
</div>

Wire loadChains() to call getFailureChains(this.taskTypeFilter() || undefined).

Step 2: Fix specialization typing and join HTTP calls

In specialization-matrix.component.ts:

Step 3: Commit

git add libs/libs-ai/vitto-hub/src/lib/components/observatory-panel/failure-chains/ libs/libs-ai/vitto-hub/src/lib/components/observatory-panel/specialization-matrix/
git commit -m "fix: add task type filter to failure chains, fix typing and forkJoin in specialization"

Task 6: Type VittoApiService Responses (Remove as any)

Files:

Step 1: Create shared DTO types

Create observatory-dtos.ts with typed response interfaces for:

Step 2: Update VittoApiService method return types

Change relevant methods from Observable<any> to typed observables using the new DTOs.

Step 3: Update all 6 observatory tab components

Remove all (res as any) patterns. Use the typed response interfaces.

Step 4: Commit

git add libs/libs-ai/vitto-core/src/lib/types/ libs/libs-ai/vitto-core/src/lib/services/vitto-api.service.ts libs/libs-ai/vitto-hub/src/lib/components/
git commit -m "refactor: type all VittoApiService observatory responses, remove (as any) casts"

Task 7: LLM Cost Persistence (Backend)

Files:

Step 1: Create LlmUsage schema

Following the project convention (@Schema, @Prop, SchemaFactory):

// APIs/backend-ai-api/src/app/schemas/llm-usage.schema.ts
@Schema({ collection: 'llm_usage', timestamps: true })
export class LlmUsage extends Document {
  @Prop({ required: true, index: true }) model: string;
  @Prop({ required: true }) tokensIn: number;
  @Prop({ required: true }) tokensOut: number;
  @Prop({ required: true }) estimatedCostUsd: number;
  @Prop({ required: true, index: true }) userId: string;
  @Prop() sessionId?: string;
  @Prop({ required: true, index: true }) timestamp: Date;
}
export const LlmUsageSchema = SchemaFactory.createForClass(LlmUsage);

// TTL index: auto-expire after 90 days
LlmUsageSchema.index({ timestamp: 1 }, { expireAfterSeconds: 90 * 24 * 60 * 60 });
// Compound index for daily aggregation
LlmUsageSchema.index({ timestamp: 1, model: 1 });

Step 2: Register schema in infrastructure module

In infrastructure.module.ts, add to MongooseModule.forFeature() under AI_CONNECTION_NAMES.AGENTS:

{ name: LlmUsage.name, schema: LlmUsageSchema },

Step 3: Update LlmCostTrackerService to persist

Inject the model: @InjectModel(LlmUsage.name, AI_CONNECTION_NAMES.AGENTS) private llmUsageModel: Model<LlmUsage>

In recordUsage() (line 61), after updating in-memory records, add fire-and-forget MongoDB write:

// Persist to MongoDB (non-blocking)
this.llmUsageModel.create({
  model, tokensIn, tokensOut, estimatedCostUsd: costUsd, userId, sessionId, timestamp: new Date(),
}).catch(err => this.logger.warn('Failed to persist LLM usage:', err.message));

Add new methods:

async getHistoricalCosts(days: number): Promise<DailyCostAggregate[]> {
  const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
  return this.llmUsageModel.aggregate([
    { $match: { timestamp: { $gte: since } } },
    { $group: {
      _id: { $dateToString: { format: '%Y-%m-%d', date: '$timestamp' } },
      totalCostUsd: { $sum: '$estimatedCostUsd' },
      totalTokensIn: { $sum: '$tokensIn' },
      totalTokensOut: { $sum: '$tokensOut' },
      count: { $sum: 1 },
    }},
    { $sort: { _id: 1 } },
  ]);
}

async getCostsByUser(days: number): Promise<UserCostAggregate[]> {
  const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
  return this.llmUsageModel.aggregate([
    { $match: { timestamp: { $gte: since } } },
    { $group: {
      _id: '$userId',
      totalCostUsd: { $sum: '$estimatedCostUsd' },
      totalTokensIn: { $sum: '$tokensIn' },
      totalTokensOut: { $sum: '$tokensOut' },
      count: { $sum: 1 },
    }},
    { $sort: { totalCostUsd: -1 } },
  ]);
}

Step 4: Add new endpoints to controller

@Get('llm-history')
@Public()
async getLlmHistory(@Query('days') days = '30') {
  return this.costTracker.getHistoricalCosts(parseInt(days, 10));
}

@Get('llm-by-user')
@Public()
async getLlmByUser(@Query('days') days = '7') {
  return this.costTracker.getCostsByUser(parseInt(days, 10));
}

Step 5: Commit

git add APIs/backend-ai-api/src/app/schemas/llm-usage.schema.ts APIs/backend-ai-api/src/app/infrastructure/
git commit -m "feat: persist LLM costs to MongoDB with historical aggregation endpoints"

Task 8: Unified Alert Pipeline (Backend)

Files:

Step 1: Create Alert schema

// APIs/backend-ai-api/src/app/schemas/alert.schema.ts
@Schema({ collection: 'alerts', timestamps: true })
export class Alert extends Document {
  @Prop({ required: true, enum: ['critical', 'warning', 'info'], index: true }) severity: string;
  @Prop({ required: true }) source: string; // pipeline | condition-monitor | health-check | vitto-proactive
  @Prop({ required: true }) check: string;
  @Prop({ required: true }) message: string;
  @Prop() details?: string;
  @Prop({ default: false, index: true }) resolved: boolean;
  @Prop() resolvedAt?: Date;
  @Prop({ required: true }) firstSeenAt: Date;
  @Prop({ required: true }) lastSeenAt: Date;
  @Prop({ default: 1 }) occurrenceCount: number;
  @Prop({ index: true }) messageHash: string; // For deduplication
}
export const AlertSchema = SchemaFactory.createForClass(Alert);
AlertSchema.index({ resolved: 1, lastSeenAt: -1 });
// Auto-expire resolved alerts after 30 days
AlertSchema.index({ resolvedAt: 1 }, { expireAfterSeconds: 30 * 24 * 60 * 60, partialFilterExpression: { resolved: true } });

Step 2: Create AlertAggregatorService

// APIs/backend-ai-api/src/app/infrastructure/services/alert-aggregator.service.ts
@Injectable()
export class AlertAggregatorService implements OnModuleInit {
  // Inject: Model<Alert>, EventEmitter2, ObservatoryService

  onModuleInit() {
    // Subscribe to all event sources:
    this.eventEmitter.on('pipeline.alert', (alert) => this.ingestAlert('pipeline', alert));
    this.eventEmitter.on('pipeline.unhealthy', (data) => this.ingestAlert('pipeline', { severity: 'critical', ...data }));
    this.eventEmitter.on('pipeline.degraded', (data) => this.ingestAlert('pipeline', { severity: 'warning', ...data }));
    this.eventEmitter.on('monitor.alert', (data) => this.ingestAlert('condition-monitor', data));
    this.eventEmitter.on('monitor.recovered', (data) => this.resolveByCheck(data.name));
  }

  async ingestAlert(source: string, data: { severity, check, message, details? }): Promise<void> {
    const hash = createHash('md5').update(`${source}:${data.check}:${data.message}`).digest('hex');
    // Upsert: increment count if same hash within 1h, otherwise create new
    await this.alertModel.findOneAndUpdate(
      { messageHash: hash, resolved: false },
      { $set: { lastSeenAt: new Date(), severity: data.severity, details: data.details },
        $inc: { occurrenceCount: 1 },
        $setOnInsert: { source, check: data.check, message: data.message, firstSeenAt: new Date() } },
      { upsert: true },
    );
  }

  async getAlerts(filter: { severity?, resolved?, limit? }): Promise<Alert[]> { ... }
  async resolveAlert(alertId: string): Promise<void> { ... }
  async resolveByCheck(check: string): Promise<void> { ... }
}

Step 3: Register in module and add endpoints

Register AlertAggregatorService in infrastructure.module.ts providers. Register Alert schema in MongooseModule.forFeature().

Add to controller:

@Get('alerts')
@Public()
async getAlerts(@Query('severity') severity?: string, @Query('resolved') resolved?: string, @Query('limit') limit = '50') {
  return this.alertService.getAlerts({
    severity: severity || undefined,
    resolved: resolved === 'true' ? true : resolved === 'false' ? false : undefined,
    limit: parseInt(limit, 10),
  });
}

@Post('alerts/:id/resolve')
async resolveAlert(@Param('id') id: string) {
  return this.alertService.resolveAlert(id);
}

Step 4: Commit

git add APIs/backend-ai-api/src/app/schemas/alert.schema.ts APIs/backend-ai-api/src/app/infrastructure/services/alert-aggregator.service.ts APIs/backend-ai-api/src/app/infrastructure/controllers/observatory.controller.ts APIs/backend-ai-api/src/app/infrastructure/infrastructure.module.ts
git commit -m "feat: unified alert pipeline with MongoDB persistence and deduplication"

Task 9: Frontend — LLM History Charts and Alert Section

Files:

Step 1: Add new data methods to ObservatoryDataService

fetchLlmHistory(days = 30): Observable<DailyCostAggregate[]> {
  return this.http.get<DailyCostAggregate[]>(`${this.aiApiUrl}/infrastructure/observatory/llm-history?days=${days}`);
}

fetchAlerts(): Observable<Alert[]> {
  return this.http.get<Alert[]>(`${this.aiApiUrl}/infrastructure/observatory/alerts?resolved=false&limit=20`);
}

Step 2: Add history chart and alerts to dashboard component

In the component:

Step 3: Commit

git add libs/libs-ai/gograb-ai-lib/src/lib/services/observatory-data.service.ts libs/libs-ai/gograb-ai-lib/src/lib/components/observatory-dashboard/
git commit -m "feat: add LLM cost history chart and alerts section to system dashboard"

Task 10: Wire OrchestrationClientService Dead Signals

Files:

Step 1: Add Intelligence section in full mode

In agent-observatory.component.html, after the existing events section (around line 330), add a new section visible only in full mode:

@if (mode() === 'full') {
  <!-- Agent Decisions -->
  @if (orchestration.recentDecisions().length > 0) {
    <section class="card">
      <div class="card-header">
        <span class="card-title">Agent Decisions</span>
        <span class="badge">{{ orchestration.recentDecisions().length }}</span>
      </div>
      @for (d of orchestration.recentDecisions().slice(0, 10); track d.decisionId) {
        <div class="decision-item" [class.high-confidence]="d.confidence >= 0.8">
          <span class="decision-question">{{ d.question | slice:0:80 }}</span>
          <span class="decision-chosen">{{ d.chosen | slice:0:50 }}</span>
          <span class="decision-confidence">{{ (d.confidence * 100) | number:'1.0-0' }}%</span>
        </div>
      }
    </section>
  }

  <!-- Learning System Status -->
  @if (orchestration.learningStats(); as stats) {
    <section class="card">
      <div class="card-header"><span class="card-title">Learning System</span></div>
      <div class="stats-grid">
        <div class="stat"><span class="stat-value">{{ stats.totalDecisions | number }}</span><span class="stat-label">Decisions</span></div>
        <div class="stat"><span class="stat-value">{{ stats.goldenDecisions }}</span><span class="stat-label">Golden</span></div>
        <div class="stat"><span class="stat-value">{{ stats.embeddings | number }}</span><span class="stat-label">Embeddings</span></div>
      </div>
    </section>
  }

  <!-- Context Loaded -->
  @if (orchestration.loadedContext().length > 0) {
    <section class="card">
      <div class="card-header"><span class="card-title">Context Loaded</span></div>
      @for (ctx of orchestration.loadedContext().slice(0, 5); track $index) {
        <div class="context-item">
          <span class="context-type">{{ ctx.type }}</span>
          <span class="context-count">{{ ctx.count }} items</span>
        </div>
      }
    </section>
  }

  <!-- Pattern Matches -->
  @if (orchestration.patternMatches().length > 0) {
    <section class="card">
      <div class="card-header"><span class="card-title">Pattern Matches</span></div>
      @for (p of orchestration.patternMatches().slice(0, 5); track p.patternId) {
        <div class="pattern-item">
          <span class="pattern-name">{{ p.name }}</span>
          <span class="pattern-confidence" [class.high]="p.confidence >= 0.8">{{ (p.confidence * 100) | number:'1.0-0' }}%</span>
        </div>
      }
    </section>
  }
}

Step 2: Add styles for new sections

Add .decision-item, .stats-grid, .context-item, .pattern-item styles to the SCSS file.

Step 3: Commit

git add libs/libs-ai/gograb-ai-lib/src/lib/components/agent-observatory/
git commit -m "feat: wire OrchestrationClientService intelligence signals to agent observatory"

Task 11: Merge VittoMonitorComponent Unique Features

Files:

Step 1: Add phase token counts to progress bar

In agent-observatory.component.html, in the phase progress loop (around line 136-138 where duration is shown), add:

@if (getPhaseTokens(phase); as tok) {
  <span class="phase-tokens">{{ tok }} tok</span>
}

The getPhaseTokens() helper already exists at line 180 of the component.

Step 2: Add model/provider to task history

In the task history expansion section (around line 352-364), add a row:

<div class="detail-row">
  <span>Model: {{ task.cost?.model || 'N/A' }}</span>
  <span>Provider: {{ task.cost?.provider || 'N/A' }}</span>
</div>

Step 3: Deprecate VittoMonitorComponent

Add JSDoc to the top of vitto-monitor.component.ts:

/**
 * @deprecated Session 176.3: Superseded by AgentObservatoryComponent.
 * Unique features (model/provider display, phase tokens) merged into AgentObservatoryComponent.
 * Keep for reference; do not use in new routes.
 */

Step 4: Commit

git add libs/libs-ai/gograb-ai-lib/src/lib/components/agent-observatory/ libs/libs-ai/gograb-ai-lib/src/lib/components/vitto-monitor/vitto-monitor.component.ts
git commit -m "feat: merge VittoMonitorComponent unique features into AgentObservatory, deprecate original"

Task 12: Wire createObservatoryPlugin to Chat

Files:

Step 1: Add observatory plugin to standard preset

In plugin-presets.ts, import:

import { createObservatoryPlugin } from './observatory.plugin';

Add observatoryComponent?: Type<unknown> to the PluginPresetConfig interface.

In createStandardPluginSet, after the learning plugin block (around line 105), add:

// Observatory plugin (T0 + T1 only)
if (config.observatoryComponent) {
  plugins.push(
    createObservatoryPlugin({
      dashboardComponent: config.observatoryComponent,
      compact: true,
    }),
  );
}

Step 2: Pass component from VittoChatPanelComponent

In the component that calls createStandardPluginSet, add AgentObservatoryComponent as the observatoryComponent config value. Check apps/gograb-ai/src/app/pages/vitto-chat-panel/vitto-chat-panel.component.ts for where createStandardPluginSet is called.

Step 3: Commit

git add libs/libs-ai/vitto-chat/src/lib/plugins/plugin-presets.ts
git commit -m "feat: wire createObservatoryPlugin into standard chat plugin set for T0+T1"

Task 13: Wire GoGrabObservatoryComponent to gograb-space

Files:

Step 1: Import the component

In main-layout.component.ts, add to imports array:

import { GoGrabObservatoryComponent } from '@libs-ux/embeddable-bridge';

Add GoGrabObservatoryComponent to the imports array in the @Component decorator.

Step 2: Add to template

Find the appropriate location in the layout template (near the AI panel embedding) and add:

<!-- Observatory embed for space admins -->
<gograb-observatory
  [mode]="'compact'"
  [spaceId]="currentSpaceId()"
/>

Note: The exact template position depends on the layout structure. Review the HTML to find the sidebar/panel area where <ai-panel-tabs> is rendered and add the observatory nearby or as a togglable panel.

Step 3: Commit

git add apps/gograb-space/src/app/components/layout/main-layout/
git commit -m "feat: wire GoGrabObservatoryComponent into gograb-space admin layout"

Task 14: Remove Overlap — Costs AI Infrastructure Health Cards

Files:

Step 1: Replace generic health cards with MCP-specific data

In the "AI Infrastructure" sub-tab of the cost dashboard, find the MongoDB/backend health status cards (lines ~71-95 in the HTML). Replace these generic backend health cards (which duplicate /observatory/system) with MCP-specific intelligence data:

Step 2: Commit

git add libs/libs-ai/vitto-hub/src/lib/components/cost-dashboard/
git commit -m "refactor: remove duplicate health cards from Costs tab, keep MCP-specific data"

Task 15: Unified Surface — Enhance ObservatoryPanelComponent

Files:

Step 1: Add tab group structure

Enhance the template to support 3 tab groups (Operations, Governance, Intelligence) with 9 tabs total. Use ux-tabs with group headers:

<div class="observatory-panel">
  <ux-tabs variant="floating" [level]="2" [routeIntegration]="true">
    <!-- Operations Group -->
    <ux-tab label="Agents" icon="material-icons monitor_heart" routeLink="agents" />
    <ux-tab label="System" icon="material-icons dns" routeLink="system" />
    <ux-tab label="Sandbox" icon="material-icons cloud" routeLink="sandbox" />

    <!-- Governance Group -->
    <ux-tab label="Approvals" icon="material-icons verified_user" routeLink="approvals">
      @if (pendingCount() > 0) {
        <span class="approval-badge">{{ pendingCount() }}</span>
      }
    </ux-tab>
    <ux-tab label="Audit" icon="material-icons security" routeLink="audit" />
    <ux-tab label="Trust" icon="material-icons verified" routeLink="trust" />

    <!-- Intelligence Group (developer/admin only) -->
    <ux-tab label="Costs" icon="material-icons payments" routeLink="costs" />
    <ux-tab label="Failures" icon="material-icons link" routeLink="failure-chains" />
    <ux-tab label="Specialization" icon="material-icons psychology" routeLink="specialization" />
  </ux-tabs>

  <div class="observatory-content">
    <router-outlet />
  </div>
</div>

Step 2: Commit

git add libs/libs-ai/vitto-hub/src/lib/components/observatory-panel/
git commit -m "feat: enhance ObservatoryPanelComponent with 9 tabs in 3 groups"

Task 16: Unified Surface — Add Operations Routes to Vitto Hub

Files:

Step 1: Add agents, system, sandbox child routes

In both VittoHubChildRoutes and VittoUnifiedChildRoutes observatory children, add:

{
  path: 'agents',
  loadComponent: () => import('../components/observatory-wrappers/observatory-agents-wrapper.component')
    .then(m => m.ObservatoryAgentsWrapperComponent),
},
{
  path: 'system',
  loadComponent: () => import('../components/observatory-wrappers/observatory-system-wrapper.component')
    .then(m => m.ObservatorySystemWrapperComponent),
},
{
  path: 'sandbox',
  loadComponent: () => import('../components/observatory-wrappers/observatory-sandbox-wrapper.component')
    .then(m => m.ObservatorySandboxWrapperComponent),
},

Create thin wrapper components in libs/libs-ai/vitto-hub/src/lib/components/observatory-wrappers/ that import and render the existing components from @libs-ai/gograb-ai-lib and apps/gograb-ai.

Update the default redirect from approvals to agents (Operations is the first group).

Step 2: Commit

git add libs/libs-ai/vitto-hub/src/lib/routes/vitto-hub.routes.ts libs/libs-ai/vitto-hub/src/lib/components/observatory-wrappers/
git commit -m "feat: add Operations tabs (agents, system, sandbox) to Vitto Hub observatory routes"

Task 17: Unified Surface — /observatory Renders Same Shell

Files:

Step 1: Update /observatory route to render ObservatoryPanelComponent

In app.routes.ts, update the /observatory route (lines 83-105) to use the same ObservatoryPanelComponent from @libs-ai/vitto-hub with the same child routes. This makes both /observatory and /vitto/observatory render identical content.

{
  path: 'observatory',
  loadComponent: () => import('@libs-ai/vitto-hub').then(m => m.ObservatoryPanelComponent),
  children: [
    { path: '', redirectTo: 'agents', pathMatch: 'full' },
    // ... same 9 child routes as vitto hub
  ],
},

The existing ObservatoryPageComponent can be deprecated (it was just a shell with tab routing).

Step 2: Update Chrome extension

In sidepanel.js line 202-204, update the URL if needed. Since /observatory now renders the same component, the existing URL may already work. Verify the mode=compact param is still respected.

Step 3: Commit

git add apps/gograb-ai/src/app/app.routes.ts apps/gograb-ai/src/app/pages/observatory/ apps/vitto-chrome-extension/sidepanel/sidepanel.js
git commit -m "feat: /observatory renders unified ObservatoryPanelComponent, same as /vitto/observatory"

Task 18: MCP Convergence — stats.alerts Action

Files:

Step 1: Add 'alerts' action

Add 'alerts' to the StatsAction type enum and inputSchema.properties.action.enum.

Add handler in handleStatsGateway:

case 'alerts': {
  const response = await apiRequest('GET', '/infrastructure/observatory/alerts?resolved=false&limit=20');
  return {
    success: true,
    alerts: response,
    summary: `${response.length} active alerts`,
  };
}

Step 2: Commit

git add MCP/mcp-gograb-ai/src/tools/gateways/stats-gateway.ts
git commit -m "feat: add stats.alerts MCP action for Vitto self-monitoring"

Task 19: MCP Convergence — stats.costs Action

Files:

Step 1: Add 'costs' action

Add 'costs' to StatsAction type and schema enum.

Add handler:

case 'costs': {
  const days = (args as any).days || 7;
  const [history, spend] = await Promise.all([
    apiRequest('GET', `/infrastructure/observatory/llm-history?days=${days}`),
    apiRequest('GET', '/infrastructure/observatory/llm-spend'),
  ]);
  return {
    success: true,
    currentWindow: spend,
    dailyTrend: history,
    summary: `$${spend.totalCostUsd?.toFixed(2) || '0'} in last 24h, ${history.length} days of history`,
  };
}

Add days to the inputSchema properties.

Step 2: Commit

git add MCP/mcp-gograb-ai/src/tools/gateways/stats-gateway.ts
git commit -m "feat: add stats.costs MCP action for LLM spend monitoring"

Task 20: MCP Convergence — intelligence.trust Action

Files:

Step 1: Add 'trust' action

Add 'trust' to the IntelligenceAction type enum and schema.

Add handler in handleIntelligenceGateway:

case 'trust': {
  const actionType = args.actionType;
  const url = actionType
    ? `/engine/trust-scores/${encodeURIComponent(actionType)}`
    : '/engine/trust-scores';
  // This goes to vitto-api, so use the vitto API URL
  const response = await vittoApiRequest('GET', url);
  return {
    success: true,
    trustData: response,
    summary: actionType
      ? `Trust for ${actionType}: ${response.currentTrustScore || 'N/A'}`
      : `${response.scores?.length || 0} action types tracked`,
  };
}

Add actionType to inputSchema properties.

Step 2: Commit

git add MCP/mcp-gograb-ai/src/tools/gateways/intelligence-gateway.ts
git commit -m "feat: add intelligence.trust MCP action for autonomy trust queries"

Task 21: Wire High-Value Unused Backend Endpoints

Files:

Step 1: Wire cost estimation to Costs tab

In CostDashboardComponent, add a pre-flight cost estimation section:

Step 2: Wire routing decisions to Costs > Routing tab

Call vittoApi.getRoutingDecisions() and display the decision audit trail:

Step 3: Wire extended audit trail

In ObservatoryAuditComponent, replace the 50-entry limit with pagination:

Step 4: Wire MCP usage history to Costs > AI Infrastructure

Call vittoApi.getMcpUsageHistory() and display as a trend chart in the AI Infrastructure sub-tab.

Step 5: Commit

git add libs/libs-ai/vitto-hub/src/lib/components/cost-dashboard/ libs/libs-ai/vitto-hub/src/lib/components/observatory-panel/observatory-audit*
git commit -m "feat: wire cost estimation, routing decisions, extended audit, and MCP usage history"

Task 22: Final Verification

Step 1: Build check

Run: npx nx affected --target=build --base=main --head=HEAD --dry-run 2>&1 | head -30

Verify no build errors in affected libraries.

Step 2: Verify all routes load

Start the dev server and navigate to:

Step 3: Verify MCP tools

Test MCP actions:

stats({ action: "alerts" })
stats({ action: "costs", days: 7 })
intelligence({ action: "trust" })

Step 4: Commit any final fixes

git commit -m "chore: final verification and cleanup for observatory redesign"

Summary

Task Part Description Files Modified
1 Bug Fix setInterval → RxJS timer 1
2 Bug Fix VittoMonitorService ref-counting 1
3 Bug Fix Sandbox tab (3 fixes) 4
4 Bug Fix System dashboard (3 fixes) 2
5 Bug Fix Failure chains + specialization 3
6 Bug Fix Type all VittoApiService responses ~8
7 Backend LLM cost persistence 4
8 Backend Unified alert pipeline 4
9 Frontend LLM charts + alerts section 4
10 Wire OrchestrationClientService signals 2
11 Wire VittoMonitorComponent features 2
12 Wire Observatory chat plugin 1
13 Wire GoGrabObservatoryComponent 2
14 Cleanup Remove overlap in Costs tab 1
15 Surface Enhance ObservatoryPanelComponent 3
16 Surface Add Operations routes to Vitto Hub 2+
17 Surface /observatory renders same shell 3
18 MCP stats.alerts action 1
19 MCP stats.costs action 1
20 MCP intelligence.trust action 1
21 Wire High-value unused endpoints 4
22 Verify Final build + route + MCP check 0

Total: 22 tasks, ~50 files touched, estimated 4-6 hours of focused execution.