up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
This commit is contained in:
116
src/Web/StellaOps.Web/src/app/core/api/aoc.client.ts
Normal file
116
src/Web/StellaOps.Web/src/app/core/api/aoc.client.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { Observable, of, delay } from 'rxjs';
|
||||
import { AppConfigService } from '../config/app-config.service';
|
||||
import {
|
||||
AocMetrics,
|
||||
AocVerificationRequest,
|
||||
AocVerificationResult,
|
||||
} from './aoc.models';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AocClient {
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly config = inject(AppConfigService);
|
||||
|
||||
/**
|
||||
* Gets AOC metrics for the dashboard.
|
||||
*/
|
||||
getMetrics(tenantId: string, windowMinutes = 1440): Observable<AocMetrics> {
|
||||
// TODO: Replace with real API call when available
|
||||
// return this.http.get<AocMetrics>(
|
||||
// this.config.apiBaseUrl + '/aoc/metrics',
|
||||
// { params: { tenantId, windowMinutes: windowMinutes.toString() } }
|
||||
// );
|
||||
|
||||
// Mock data for development
|
||||
return of(this.getMockMetrics()).pipe(delay(300));
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers verification of documents within a time window.
|
||||
*/
|
||||
verify(request: AocVerificationRequest): Observable<AocVerificationResult> {
|
||||
// TODO: Replace with real API call when available
|
||||
// return this.http.post<AocVerificationResult>(
|
||||
// this.config.apiBaseUrl + '/aoc/verify',
|
||||
// request
|
||||
// );
|
||||
|
||||
// Mock verification result
|
||||
return of(this.getMockVerificationResult()).pipe(delay(500));
|
||||
}
|
||||
|
||||
private getMockMetrics(): AocMetrics {
|
||||
const now = new Date();
|
||||
const dayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
|
||||
return {
|
||||
passCount: 12847,
|
||||
failCount: 23,
|
||||
totalCount: 12870,
|
||||
passRate: 99.82,
|
||||
recentViolations: [
|
||||
{
|
||||
code: 'AOC-PROV-001',
|
||||
description: 'Missing provenance attestation',
|
||||
count: 12,
|
||||
severity: 'high',
|
||||
lastSeen: new Date(now.getTime() - 15 * 60 * 1000).toISOString(),
|
||||
},
|
||||
{
|
||||
code: 'AOC-DIGEST-002',
|
||||
description: 'Digest mismatch in manifest',
|
||||
count: 7,
|
||||
severity: 'critical',
|
||||
lastSeen: new Date(now.getTime() - 45 * 60 * 1000).toISOString(),
|
||||
},
|
||||
{
|
||||
code: 'AOC-SCHEMA-003',
|
||||
description: 'Schema validation failed',
|
||||
count: 4,
|
||||
severity: 'medium',
|
||||
lastSeen: new Date(now.getTime() - 2 * 60 * 60 * 1000).toISOString(),
|
||||
},
|
||||
],
|
||||
ingestThroughput: {
|
||||
docsPerMinute: 8.9,
|
||||
avgLatencyMs: 145,
|
||||
p95LatencyMs: 312,
|
||||
queueDepth: 3,
|
||||
errorRate: 0.18,
|
||||
},
|
||||
timeWindow: {
|
||||
start: dayAgo.toISOString(),
|
||||
end: now.toISOString(),
|
||||
durationMinutes: 1440,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private getMockVerificationResult(): AocVerificationResult {
|
||||
const verifyId = 'verify-' + Date.now().toString();
|
||||
return {
|
||||
verificationId: verifyId,
|
||||
status: 'passed',
|
||||
checkedCount: 1523,
|
||||
passedCount: 1520,
|
||||
failedCount: 3,
|
||||
violations: [
|
||||
{
|
||||
documentId: 'doc-abc123',
|
||||
violationCode: 'AOC-PROV-001',
|
||||
field: 'attestation.provenance',
|
||||
expected: 'present',
|
||||
actual: 'missing',
|
||||
provenance: {
|
||||
sourceId: 'source-registry-1',
|
||||
ingestedAt: new Date().toISOString(),
|
||||
digest: 'sha256:abc123...',
|
||||
},
|
||||
},
|
||||
],
|
||||
completedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
74
src/Web/StellaOps.Web/src/app/core/api/aoc.models.ts
Normal file
74
src/Web/StellaOps.Web/src/app/core/api/aoc.models.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* AOC (Authorization of Containers) models for dashboard metrics.
|
||||
*/
|
||||
|
||||
export interface AocMetrics {
|
||||
/** Pass/fail counts for the time window */
|
||||
passCount: number;
|
||||
failCount: number;
|
||||
totalCount: number;
|
||||
passRate: number;
|
||||
|
||||
/** Recent violations grouped by code */
|
||||
recentViolations: AocViolationSummary[];
|
||||
|
||||
/** Ingest throughput metrics */
|
||||
ingestThroughput: AocIngestThroughput;
|
||||
|
||||
/** Time window for these metrics */
|
||||
timeWindow: {
|
||||
start: string;
|
||||
end: string;
|
||||
durationMinutes: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AocViolationSummary {
|
||||
code: string;
|
||||
description: string;
|
||||
count: number;
|
||||
severity: 'critical' | 'high' | 'medium' | 'low';
|
||||
lastSeen: string;
|
||||
}
|
||||
|
||||
export interface AocIngestThroughput {
|
||||
/** Documents processed per minute */
|
||||
docsPerMinute: number;
|
||||
/** Average processing latency in milliseconds */
|
||||
avgLatencyMs: number;
|
||||
/** P95 latency in milliseconds */
|
||||
p95LatencyMs: number;
|
||||
/** Current queue depth */
|
||||
queueDepth: number;
|
||||
/** Error rate percentage */
|
||||
errorRate: number;
|
||||
}
|
||||
|
||||
export interface AocVerificationRequest {
|
||||
tenantId: string;
|
||||
since?: string;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface AocVerificationResult {
|
||||
verificationId: string;
|
||||
status: 'passed' | 'failed' | 'partial';
|
||||
checkedCount: number;
|
||||
passedCount: number;
|
||||
failedCount: number;
|
||||
violations: AocViolationDetail[];
|
||||
completedAt: string;
|
||||
}
|
||||
|
||||
export interface AocViolationDetail {
|
||||
documentId: string;
|
||||
violationCode: string;
|
||||
field?: string;
|
||||
expected?: string;
|
||||
actual?: string;
|
||||
provenance?: {
|
||||
sourceId: string;
|
||||
ingestedAt: string;
|
||||
digest: string;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
OnInit,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { AocClient } from '../../core/api/aoc.client';
|
||||
import {
|
||||
AocMetrics,
|
||||
AocViolationSummary,
|
||||
AocVerificationResult,
|
||||
} from '../../core/api/aoc.models';
|
||||
|
||||
@Component({
|
||||
selector: 'app-sources-dashboard',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
templateUrl: './sources-dashboard.component.html',
|
||||
styleUrls: ['./sources-dashboard.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class SourcesDashboardComponent implements OnInit {
|
||||
private readonly aocClient = inject(AocClient);
|
||||
|
||||
readonly loading = signal(true);
|
||||
readonly error = signal<string | null>(null);
|
||||
readonly metrics = signal<AocMetrics | null>(null);
|
||||
readonly verifying = signal(false);
|
||||
readonly verificationResult = signal<AocVerificationResult | null>(null);
|
||||
|
||||
readonly passRate = computed(() => {
|
||||
const m = this.metrics();
|
||||
return m ? m.passRate.toFixed(2) : '0.00';
|
||||
});
|
||||
|
||||
readonly passRateClass = computed(() => {
|
||||
const m = this.metrics();
|
||||
if (!m) return 'neutral';
|
||||
if (m.passRate >= 99.5) return 'excellent';
|
||||
if (m.passRate >= 95) return 'good';
|
||||
if (m.passRate >= 90) return 'warning';
|
||||
return 'critical';
|
||||
});
|
||||
|
||||
readonly throughputStatus = computed(() => {
|
||||
const m = this.metrics();
|
||||
if (!m) return 'neutral';
|
||||
if (m.ingestThroughput.queueDepth > 100) return 'critical';
|
||||
if (m.ingestThroughput.queueDepth > 50) return 'warning';
|
||||
return 'good';
|
||||
});
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadMetrics();
|
||||
}
|
||||
|
||||
loadMetrics(): void {
|
||||
this.loading.set(true);
|
||||
this.error.set(null);
|
||||
|
||||
this.aocClient.getMetrics('default').subscribe({
|
||||
next: (metrics) => {
|
||||
this.metrics.set(metrics);
|
||||
this.loading.set(false);
|
||||
},
|
||||
error: (err) => {
|
||||
this.error.set('Failed to load AOC metrics');
|
||||
this.loading.set(false);
|
||||
console.error('AOC metrics error:', err);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onVerifyLast24h(): void {
|
||||
this.verifying.set(true);
|
||||
this.verificationResult.set(null);
|
||||
|
||||
const since = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
|
||||
this.aocClient.verify({ tenantId: 'default', since }).subscribe({
|
||||
next: (result) => {
|
||||
this.verificationResult.set(result);
|
||||
this.verifying.set(false);
|
||||
},
|
||||
error: (err) => {
|
||||
this.verifying.set(false);
|
||||
console.error('AOC verification error:', err);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getSeverityClass(severity: AocViolationSummary['severity']): string {
|
||||
return 'severity-' + severity;
|
||||
}
|
||||
|
||||
formatRelativeTime(isoDate: string): string {
|
||||
const date = new Date(isoDate);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
|
||||
if (diffMins < 1) return 'just now';
|
||||
if (diffMins < 60) return diffMins + 'm ago';
|
||||
const diffHours = Math.floor(diffMins / 60);
|
||||
if (diffHours < 24) return diffHours + 'h ago';
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
return diffDays + 'd ago';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user