Files
cameleer-server/ui/src/pages/DashboardTab/dashboard-utils.ts
hsiegeln 213aa86c47 feat: progressive drill-down dashboard with RED metrics and SLA compliance (#94)
Three-level dashboard driven by sidebar selection:
- L1 (no selection): all-apps overview with health table, per-app charts
- L2 (app selected): route performance table, error velocity, top errors
- L3 (route selected): processor table, latency heatmap data, bottleneck KPI

Backend: 3 new endpoints (timeseries/by-app, timeseries/by-route, errors/top),
per-app SLA settings (app_settings table, V12 migration), exact SLA compliance
from executions hypertable, error velocity with acceleration detection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 23:29:20 +02:00

71 lines
2.4 KiB
TypeScript

import type { AppSettings } from '../../api/queries/dashboard';
export type HealthStatus = 'success' | 'warning' | 'error';
const DEFAULT_SETTINGS: Pick<AppSettings, 'healthErrorWarn' | 'healthErrorCrit' | 'healthSlaWarn' | 'healthSlaCrit'> = {
healthErrorWarn: 1.0,
healthErrorCrit: 5.0,
healthSlaWarn: 99.0,
healthSlaCrit: 95.0,
};
export function computeHealthDot(
errorRate: number,
slaCompliance: number,
settings?: Partial<AppSettings> | null,
): HealthStatus {
const s = { ...DEFAULT_SETTINGS, ...settings };
const errorPct = errorRate * 100;
if (errorPct > s.healthErrorCrit || slaCompliance < s.healthSlaCrit) return 'error';
if (errorPct > s.healthErrorWarn || slaCompliance < s.healthSlaWarn) return 'warning';
return 'success';
}
export function formatThroughput(count: number, windowSeconds: number): string {
if (windowSeconds <= 0) return '0/s';
const tps = count / windowSeconds;
if (tps >= 1000) return `${(tps / 1000).toFixed(1)}k/s`;
if (tps >= 1) return `${tps.toFixed(0)}/s`;
return `${tps.toFixed(2)}/s`;
}
export function formatSlaCompliance(pct: number): string {
if (pct < 0) return '—';
return `${pct.toFixed(1)}%`;
}
export function trendIndicator(current: number, previous: number): { label: string; direction: 'up' | 'down' | 'flat' } {
if (previous === 0) return { label: '—', direction: 'flat' };
const delta = ((current - previous) / previous) * 100;
if (Math.abs(delta) < 0.5) return { label: '—', direction: 'flat' };
return {
label: `${delta > 0 ? '+' : ''}${delta.toFixed(1)}%`,
direction: delta > 0 ? 'up' : 'down',
};
}
export function trendArrow(trend: 'accelerating' | 'stable' | 'decelerating'): string {
switch (trend) {
case 'accelerating': return '\u25B2';
case 'decelerating': return '\u25BC';
default: return '\u2500\u2500';
}
}
export function formatDuration(ms: number): string {
if (ms < 1) return '<1ms';
if (ms < 1000) return `${Math.round(ms)}ms`;
return `${(ms / 1000).toFixed(2)}s`;
}
export function formatRelativeTime(isoString: string): string {
const diff = Date.now() - new Date(isoString).getTime();
const minutes = Math.floor(diff / 60_000);
if (minutes < 1) return 'just now';
if (minutes < 60) return `${minutes} min ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours} hr ago`;
return `${Math.floor(hours / 24)} d ago`;
}