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.
- Tasks 1-6: Bug fixes (stabilize existing code)
- Tasks 7-9: Backend new features (LLM persistence, alerts, new endpoints)
- Tasks 10-14: Wire unwired frontend code
- Tasks 15-17: Unified surface (route consolidation)
- Tasks 18-20: MCP convergence
- Task 21: Wire unused backend endpoints to observatory tabs
- Task 22: Final verification
Task 1: Fix setInterval in Approvals Component
Files:
- Modify:
libs/libs-ai/vitto-hub/src/lib/components/observatory-panel/observatory-approvals.component.ts:77
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:
- Modify:
libs/libs-ai/gograb-ai-lib/src/lib/services/vitto-monitor.service.ts:168-231
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:
- Modify:
apps/gograb-ai/src/app/pages/observatory/sandbox-tab/sandbox-tab.component.ts:26 - Modify:
apps/gograb-ai/src/app/pages/observatory/sandbox-tab/sandbox-tab.component.html:80-105 - Modify:
APIs/backend-ai-api/src/app/infrastructure/controllers/observatory.controller.ts:72-118 - Modify:
.env.local
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:
- Modify:
libs/libs-ai/gograb-ai-lib/src/lib/components/observatory-dashboard/observatory-dashboard.component.ts:75 - Modify:
libs/libs-ai/gograb-ai-lib/src/lib/components/observatory-dashboard/observatory-dashboard.component.html
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:
- Per-user cost table from
d.llmSpend.byUser - Backend health details from
b.detailsin the health card expansion
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:
- Modify:
libs/libs-ai/vitto-hub/src/lib/components/observatory-panel/failure-chains/failure-chains.component.ts - Modify:
libs/libs-ai/vitto-hub/src/lib/components/observatory-panel/failure-chains/failure-chains.component.html - Modify:
libs/libs-ai/vitto-hub/src/lib/components/observatory-panel/specialization-matrix/specialization-matrix.component.ts
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:
- Change
patterns = signal<any[]>([])topatterns = signal<SharedPattern[]>([])with proper interface - Join both HTTP calls with
forkJoinfor single loading cycle - Single
isLoadingsignal wraps both calls
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:
- Modify:
libs/libs-ai/vitto-core/src/lib/services/vitto-api.service.ts(canonical) - Create:
libs/libs-ai/vitto-core/src/lib/types/observatory-dtos.ts
Step 1: Create shared DTO types
Create observatory-dtos.ts with typed response interfaces for:
ApprovalListResponse { approvals: ToolApproval[] }SecurityAuditResponse { entries: AuditEntry[] }CostStatsResponse { stats: CostStats }RoutingStatsResponse { stats: RoutingStats }TrustScoresResponse { scores: TrustScore[], stats: TrustStats }FailureChainsResponse { chains: FailureChain[] }SpecializationsResponse { agents: AgentSpec[] }SharedPatternsResponse { patterns: SharedPattern[] }
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:
- Create:
APIs/backend-ai-api/src/app/schemas/llm-usage.schema.ts - Modify:
APIs/backend-ai-api/src/app/infrastructure/services/llm-cost-tracker.service.ts - Modify:
APIs/backend-ai-api/src/app/infrastructure/controllers/observatory.controller.ts - Modify:
APIs/backend-ai-api/src/app/infrastructure/infrastructure.module.ts
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:
- Create:
APIs/backend-ai-api/src/app/schemas/alert.schema.ts - Create:
APIs/backend-ai-api/src/app/infrastructure/services/alert-aggregator.service.ts - Modify:
APIs/backend-ai-api/src/app/infrastructure/controllers/observatory.controller.ts - Modify:
APIs/backend-ai-api/src/app/infrastructure/infrastructure.module.ts
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:
- Modify:
libs/libs-ai/gograb-ai-lib/src/lib/services/observatory-data.service.ts - Modify:
libs/libs-ai/gograb-ai-lib/src/lib/components/observatory-dashboard/observatory-dashboard.component.ts - Modify:
libs/libs-ai/gograb-ai-lib/src/lib/components/observatory-dashboard/observatory-dashboard.component.html - Modify:
libs/libs-ai/gograb-ai-lib/src/lib/components/observatory-dashboard/observatory-dashboard.component.scss
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:
- New signals:
llmHistory,alerts - Fetch on init alongside dashboard poll
- Simple SVG bar chart for 30-day cost trend (one
<rect>per day, height proportional to cost) - Alert list with severity badges and resolve button
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:
- Modify:
libs/libs-ai/gograb-ai-lib/src/lib/components/agent-observatory/agent-observatory.component.html - Modify:
libs/libs-ai/gograb-ai-lib/src/lib/components/agent-observatory/agent-observatory.component.scss
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:
- Modify:
libs/libs-ai/gograb-ai-lib/src/lib/components/agent-observatory/agent-observatory.component.html:120-144,338-372 - Modify:
libs/libs-ai/gograb-ai-lib/src/lib/components/vitto-monitor/vitto-monitor.component.ts(add @deprecated)
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:
- Modify:
libs/libs-ai/vitto-chat/src/lib/plugins/plugin-presets.ts:63-113
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:
- Modify:
apps/gograb-space/src/app/components/layout/main-layout/main-layout.component.ts:56 - Modify:
apps/gograb-space/src/app/components/layout/main-layout/main-layout.component.html
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:
- Modify:
libs/libs-ai/vitto-hub/src/lib/components/cost-dashboard/cost-dashboard.component.html
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:
- Decisions count (keep)
- Patterns count (keep)
- Conventions count (keep)
- Connected agents (keep)
- Circuit breaker status (keep)
- Remove: Generic backend health cards (MongoDB status, Backend status — these are in System tab)
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:
- Modify:
libs/libs-ai/vitto-hub/src/lib/components/observatory-panel/observatory-panel.component.ts - Modify:
libs/libs-ai/vitto-hub/src/lib/components/observatory-panel/observatory-panel.component.html - Modify:
libs/libs-ai/vitto-hub/src/lib/components/observatory-panel/observatory-panel.component.scss
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:
- Modify:
libs/libs-ai/vitto-hub/src/lib/routes/vitto-hub.routes.ts:263-321,521-564
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:
- Modify:
apps/gograb-ai/src/app/app.routes.ts:83-105 - Modify:
apps/gograb-ai/src/app/pages/observatory/observatory-page.component.ts - Modify:
apps/vitto-chrome-extension/sidepanel/sidepanel.js:202-204
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:
- Modify:
MCP/mcp-gograb-ai/src/tools/gateways/stats-gateway.ts
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:
- Modify:
MCP/mcp-gograb-ai/src/tools/gateways/stats-gateway.ts
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:
- Modify:
MCP/mcp-gograb-ai/src/tools/gateways/intelligence-gateway.ts
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:
- Modify:
libs/libs-ai/vitto-hub/src/lib/components/cost-dashboard/cost-dashboard.component.ts - Modify:
libs/libs-ai/vitto-hub/src/lib/components/cost-dashboard/cost-dashboard.component.html - Modify:
libs/libs-ai/vitto-hub/src/lib/components/observatory-panel/observatory-audit.component.ts - Modify:
libs/libs-ai/vitto-hub/src/lib/components/observatory-panel/observatory-audit.component.html
Step 1: Wire cost estimation to Costs tab
In CostDashboardComponent, add a pre-flight cost estimation section:
- Add input field for task description
- Call
vittoApi.estimateCost({ summary, complexity })on submit - Display estimated cost before task execution
Step 2: Wire routing decisions to Costs > Routing tab
Call vittoApi.getRoutingDecisions() and display the decision audit trail:
- Provider chosen, alternative considered, reason, timestamp
Step 3: Wire extended audit trail
In ObservatoryAuditComponent, replace the 50-entry limit with pagination:
- Call
vittoApi.getAuditTrail({ limit: 50, offset: page * 50, toolName: filter }) - Add "Load More" button
- Add date range filter using
getComplianceReport(startDate, endDate)
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:
/observatory/agents— verify agent monitor loads/observatory/system— verify system dashboard with charts and alerts/observatory/sandbox— verify dynamic server info/vitto/observatory/approvals— verify approvals tab/vitto/observatory/audit— verify audit with pagination/vitto/observatory/costs— verify all 4 sub-tabs/vitto/observatory/trust— verify trust dashboard/vitto/observatory/failure-chains— verify filter works/vitto/observatory/specialization— verify typed data
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.