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) {
@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
Export log
@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;
}