feat(governance): surface adr100 slo states
This commit is contained in:
@@ -31,11 +31,32 @@ const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? ''
|
||||
// =============================================================================
|
||||
|
||||
interface SloApiResponse {
|
||||
metrics?: {
|
||||
metrics?: Array<{
|
||||
name: SloMetric['name']
|
||||
value: number | null
|
||||
threshold: number
|
||||
direction: 'above' | 'below'
|
||||
sample_count: number
|
||||
violated: boolean
|
||||
}> | {
|
||||
decision_accuracy?: { current: number; target: number; status: string; sparkline?: number[] }
|
||||
km_growth_rate?: { current: number; target: number; status: string; sparkline?: number[] }
|
||||
mcp_call_diversity?: { current: number; target: number; status: string; sparkline?: number[] }
|
||||
}
|
||||
adr100?: {
|
||||
overall_status?: string
|
||||
overall_compliance?: number | null
|
||||
metrics?: Array<{
|
||||
name: SloMetric['name']
|
||||
value: number | null
|
||||
target: number
|
||||
status: 'ok' | 'warning' | 'violated' | 'skipped_low_volume' | 'no_data' | 'error'
|
||||
unit: 'percent' | 'count'
|
||||
sample_count?: number | null
|
||||
window?: string
|
||||
reason?: string | null
|
||||
}>
|
||||
}
|
||||
overall_compliance?: number
|
||||
computed_at?: string
|
||||
}
|
||||
@@ -51,15 +72,55 @@ interface SummaryApiResponse {
|
||||
// =============================================================================
|
||||
|
||||
function mapStatus(s: string): SloMetric['status'] {
|
||||
if (s === 'healthy') return 'healthy'
|
||||
if (s === 'healthy' || s === 'ok') return 'healthy'
|
||||
if (s === 'warning') return 'warning'
|
||||
if (s === 'skipped_low_volume') return 'syncing'
|
||||
if (s === 'no_data') return 'idle'
|
||||
return 'critical'
|
||||
}
|
||||
|
||||
function buildMetrics(api: SloApiResponse): SloMetric[] {
|
||||
const adr100Metrics = api.adr100?.metrics
|
||||
if (adr100Metrics?.length) {
|
||||
const order: SloMetric['name'][] = ['autonomy_rate', 'decision_accuracy', 'confidence_calibration', 'km_growth_rate']
|
||||
const byName = new Map(adr100Metrics.map(metric => [metric.name, metric]))
|
||||
const built: SloMetric[] = []
|
||||
order.forEach(name => {
|
||||
const entry = byName.get(name)
|
||||
if (!entry) return
|
||||
built.push({
|
||||
name,
|
||||
current: entry.value ?? null,
|
||||
target: entry.target,
|
||||
status: mapStatus(entry.status),
|
||||
state: entry.status,
|
||||
unit: entry.unit === 'count' ? 'count' : '%',
|
||||
sparkline: [],
|
||||
sampleCount: entry.sample_count ?? null,
|
||||
window: entry.window,
|
||||
reason: entry.reason ?? null,
|
||||
})
|
||||
})
|
||||
return built
|
||||
}
|
||||
|
||||
if (Array.isArray(api.metrics)) {
|
||||
return api.metrics.map(entry => ({
|
||||
name: entry.name,
|
||||
current: entry.value,
|
||||
target: entry.threshold,
|
||||
status: entry.value == null ? 'syncing' : entry.violated ? 'critical' : 'healthy',
|
||||
state: entry.value == null ? 'skipped_low_volume' : entry.violated ? 'violated' : 'ok',
|
||||
unit: '%',
|
||||
sparkline: [],
|
||||
sampleCount: entry.sample_count,
|
||||
}))
|
||||
}
|
||||
|
||||
const m = api.metrics ?? {}
|
||||
const names: Array<SloMetric['name']> = ['decision_accuracy', 'km_growth_rate', 'mcp_call_diversity']
|
||||
return names.map(name => {
|
||||
if (Array.isArray(m)) return []
|
||||
const names: Array<'decision_accuracy' | 'km_growth_rate' | 'mcp_call_diversity'> = ['decision_accuracy', 'km_growth_rate', 'mcp_call_diversity']
|
||||
return names.map((name): SloMetric => {
|
||||
const entry = m[name]
|
||||
return {
|
||||
name,
|
||||
@@ -111,7 +172,7 @@ export function SloTab() {
|
||||
}, [])
|
||||
|
||||
const metrics = sloData ? buildMetrics(sloData) : []
|
||||
const compliance = sloData?.overall_compliance ?? null
|
||||
const compliance = sloData?.adr100?.overall_compliance ?? sloData?.overall_compliance ?? null
|
||||
|
||||
const chartData: ViolationDataPoint[] = summaryData?.data ?? []
|
||||
const eventTypes: string[] = summaryData?.event_types ?? []
|
||||
@@ -169,7 +230,7 @@ export function SloTab() {
|
||||
className="slo-kpi-grid"
|
||||
>
|
||||
{sloLoading
|
||||
? [0, 1, 2].map(i => <SloKpiCard key={i} metric={{ name: 'decision_accuracy', current: null, target: 0.9, status: 'warning' }} loading />)
|
||||
? [0, 1, 2, 3].map(i => <SloKpiCard key={i} metric={{ name: 'decision_accuracy', current: null, target: 0.9, status: 'warning' }} loading />)
|
||||
: metrics.map(m => <SloKpiCard key={m.name} metric={m} />)
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -24,12 +24,24 @@ import { useTranslations } from 'next-intl'
|
||||
// =============================================================================
|
||||
|
||||
export interface SloMetric {
|
||||
name: 'decision_accuracy' | 'km_growth_rate' | 'mcp_call_diversity'
|
||||
name:
|
||||
| 'autonomy_rate'
|
||||
| 'decision_accuracy'
|
||||
| 'confidence_calibration'
|
||||
| 'km_growth_rate'
|
||||
| 'mcp_call_diversity'
|
||||
| 'auto_execute_success_rate'
|
||||
| 'human_override_rate'
|
||||
| 'verifier_false_neg_rate'
|
||||
current: number | null
|
||||
target: number
|
||||
status: 'healthy' | 'warning' | 'critical'
|
||||
unit?: string
|
||||
status: 'healthy' | 'warning' | 'critical' | 'idle' | 'syncing'
|
||||
state?: 'ok' | 'warning' | 'violated' | 'skipped_low_volume' | 'no_data' | 'error' | 'partial'
|
||||
unit?: '%' | 'count'
|
||||
sparkline?: number[] // 7 points, most recent last
|
||||
sampleCount?: number | null
|
||||
window?: string
|
||||
reason?: string | null
|
||||
}
|
||||
|
||||
interface SloKpiCardProps {
|
||||
@@ -45,6 +57,22 @@ const statusColor: Record<SloMetric['status'], string> = {
|
||||
healthy: '#22C55E',
|
||||
warning: '#F59E0B',
|
||||
critical: '#FF3300',
|
||||
idle: '#87867f',
|
||||
syncing: '#3B82F6',
|
||||
}
|
||||
|
||||
function formatCompactNumber(value: number): string {
|
||||
if (value >= 100) return value.toFixed(0)
|
||||
if (value >= 10) return value.toFixed(1)
|
||||
return value.toFixed(2)
|
||||
}
|
||||
|
||||
function reasonKey(reason?: string | null): string {
|
||||
if (!reason) return 'none'
|
||||
if (reason === 'denominator_below_minimum_events') return 'denominator_below_minimum_events'
|
||||
if (reason === 'prometheus_nan_or_inf') return 'prometheus_nan_or_inf'
|
||||
if (reason === 'prometheus_empty_result_metric_not_emitted') return 'prometheus_empty_result_metric_not_emitted'
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -73,21 +101,24 @@ export function SloKpiCard({ metric, loading = false }: SloKpiCardProps) {
|
||||
if (loading) return <KpiSkeleton />
|
||||
|
||||
const color = statusColor[metric.status]
|
||||
const orbStatus: StatusType = metric.status === 'healthy' ? 'healthy'
|
||||
: metric.status === 'warning' ? 'warning'
|
||||
: 'critical'
|
||||
const orbStatus: StatusType = metric.status
|
||||
|
||||
const formattedValue = metric.current == null
|
||||
? '--'
|
||||
: metric.unit === '%'
|
||||
? `${(metric.current * 100).toFixed(1)}%`
|
||||
: metric.current.toFixed(2)
|
||||
: metric.current.toFixed(0)
|
||||
|
||||
const formattedTarget = metric.unit === '%'
|
||||
? `${(metric.target * 100).toFixed(0)}%`
|
||||
: metric.target.toFixed(2)
|
||||
: metric.target.toFixed(0)
|
||||
|
||||
const sparkData = (metric.sparkline ?? Array(7).fill(0)).map((v, i) => ({ i, v }))
|
||||
const stateLabel = metric.state ? t(`state.${metric.state}`) : ''
|
||||
const reasonLabel = metric.reason ? t(`reason.${reasonKey(metric.reason)}`) : null
|
||||
const sampleLabel = metric.sampleCount == null
|
||||
? null
|
||||
: t('sampleCount', { count: formatCompactNumber(metric.sampleCount) })
|
||||
|
||||
return (
|
||||
<GlassCard variant="elevated" padding="md" className="min-w-0 flex-1">
|
||||
@@ -114,35 +145,46 @@ export function SloKpiCard({ metric, loading = false }: SloKpiCardProps) {
|
||||
color,
|
||||
lineHeight: 1,
|
||||
marginBottom: 4,
|
||||
letterSpacing: '-0.5px',
|
||||
letterSpacing: 0,
|
||||
}}>
|
||||
{formattedValue}
|
||||
</div>
|
||||
|
||||
{/* Target + sparkline row */}
|
||||
<div style={{ display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between' }}>
|
||||
<span style={{
|
||||
fontFamily: "'DM Mono', monospace",
|
||||
fontSize: 10,
|
||||
color: '#87867f',
|
||||
}}>
|
||||
{t('target')} {formattedTarget}
|
||||
</span>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{/* Target + sparkline row */}
|
||||
<div style={{ display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', gap: 8 }}>
|
||||
<span style={{
|
||||
fontFamily: "'DM Mono', monospace",
|
||||
fontSize: 10,
|
||||
color: '#87867f',
|
||||
}}>
|
||||
{t('target')} {formattedTarget}
|
||||
</span>
|
||||
|
||||
{/* Sparkline 80×24px */}
|
||||
<div style={{ width: 80, height: 24 }} aria-label={t('sparkline')}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={sparkData} margin={{ top: 2, right: 0, bottom: 2, left: 0 }}>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="v"
|
||||
stroke={color}
|
||||
strokeWidth={1.5}
|
||||
dot={false}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
{/* Sparkline 80×24px */}
|
||||
<div style={{ width: 80, height: 24, flexShrink: 0 }} aria-label={t('sparkline')}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={sparkData} margin={{ top: 2, right: 0, bottom: 2, left: 0 }}>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="v"
|
||||
stroke={color}
|
||||
strokeWidth={1.5}
|
||||
dot={false}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 3, minHeight: 28 }}>
|
||||
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color }}>
|
||||
{stateLabel}
|
||||
</span>
|
||||
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 9, color: '#87867f', lineHeight: 1.35 }}>
|
||||
{reasonLabel ?? sampleLabel ?? (metric.window ? t('window', { window: metric.window }) : '')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
|
||||
Reference in New Issue
Block a user