import { Component, Input } from '@angular/core'; import { VspBadge, type BadgeTone } from './badge.component'; import { VspIcon } from './icon.component'; import { VspBlockSkeleton, VspBlockEmpty } from './block-state.component'; /** A titled section wrapping its content in a Vespera `card`. */ @Component({ selector: 'vsp-block', template: `
@if (title) {
{{ title }}
@if (desc) {
{{ desc }}
}
}
`, }) export class VspBlock { @Input() title?: string; @Input() desc?: string; } /* ===================== System status ===================== */ export type ServiceStatus = 'operational' | 'degraded' | 'maintenance' | 'down'; export interface Service { name: string; status: ServiceStatus; uptime: number; } const STATUS_TONE: Record = { operational: 'pos', degraded: 'warn', maintenance: 'info', down: 'neg', }; const DEFAULT_SERVICES: Service[] = [ { name: 'API gateway', status: 'operational', uptime: 99.98 }, { name: 'Database', status: 'operational', uptime: 99.95 }, { name: 'Webhooks', status: 'degraded', uptime: 98.2 }, { name: 'Auth service', status: 'operational', uptime: 100 }, { name: 'Billing', status: 'maintenance', uptime: 99.8 }, ]; /** Live service health with 30-day uptime bars. */ @Component({ selector: 'vsp-system-status-block', imports: [VspBlock, VspBadge, VspBlockSkeleton, VspBlockEmpty], template: `
{{ allOk ? 'All systems operational' : 'Partial degradation' }}
Updated 30s ago
@if (loading) { } @else if (services.length === 0) {
@if (!emptyWrap.children.length) { } } @else {
@for (s of services; track s.name) {
{{ s.name }}
@for (i of bars; track i) { }
{{ s.uptime }}% {{ s.status }}
}
}
`, }) export class VspSystemStatusBlock { @Input() services: Service[] = DEFAULT_SERVICES; @Input() loading = false; bars = Array.from({ length: 44 }, (_, i) => i); get allOk(): boolean { return this.services.every((s) => s.status === 'operational'); } get accent(): string { return this.allOk ? 'var(--success)' : 'var(--warning)'; } tone(s: ServiceStatus): BadgeTone { return STATUS_TONE[s]; } isBad(s: Service, i: number): boolean { return ( (s.status === 'degraded' && i > 38 && i < 42) || (s.status === 'maintenance' && i === 43) ); } barBg(s: Service, i: number): string { if (!this.isBad(s, i)) return 'color-mix(in oklab, var(--success) 70%, transparent)'; return s.status === 'degraded' ? 'var(--warning)' : 'var(--accent)'; } } /* ===================== Audit log ===================== */ export interface AuditEntry { who: string; action: string; tag: string; time: string; /** Icon name (see `vsp-icon`). */ icon: string; } const DEFAULT_AUDIT: AuditEntry[] = [ { who: 'Avery Quinn', action: 'updated billing settings', tag: 'Settings', time: '2 min ago', icon: 'settings', }, { who: 'Maya Okafor', action: 'upgraded to Enterprise', tag: 'Billing', time: '38 min ago', icon: 'arrowUp', }, { who: 'System', action: 'rotated production API key', tag: 'Security', time: '1 hr ago', icon: 'shield', }, { who: 'Leo Vega', action: 'invited 4 members', tag: 'Team', time: '3 hr ago', icon: 'users' }, { who: 'Billing', action: 'flagged failed payment ยท Cobalt', tag: 'Billing', time: '5 hr ago', icon: 'bell', }, ]; /** A chronological trail of privileged actions, as a timeline. */ @Component({ selector: 'vsp-audit-log-block', imports: [VspBlock, VspBadge, VspIcon, VspBlockSkeleton, VspBlockEmpty], template: `
Recent activity
@if (loading) { } @else if (entries.length === 0) {
@if (!emptyWrap.children.length) { } } @else {
@for (e of entries; track i; let i = $index) {
@if (i < entries.length - 1) { }
{{ e.who }} {{ e.action }}
{{ e.tag }} {{ e.time }}
}
}
`, }) export class VspAuditLogBlock { @Input() entries: AuditEntry[] = DEFAULT_AUDIT; @Input() loading = false; }