feat(web): visualize auto execute slo diagnostics
Some checks failed
CD Pipeline / tests (push) Successful in 1m21s
Code Review / ai-code-review (push) Successful in 13s
CD Pipeline / build-and-deploy (push) Successful in 5m34s
CD Pipeline / post-deploy-checks (push) Failing after 32s

This commit is contained in:
Your Name
2026-06-01 18:09:47 +08:00
parent 900211406a
commit 7bdf5a7ce6
3 changed files with 363 additions and 1 deletions

View File

@@ -2191,6 +2191,45 @@
"title": "整體合規率",
"target": "目標 ≥ 95%"
},
"autoExecute": {
"title": "自動修復紅燈診斷",
"subtitle": "近 {window} 日 rolling window顯示紅燈來源、封口狀態與回綠預估。",
"threshold": "門檻 {target}",
"windowSummary": "成功 {success}、失敗 {failed}、總樣本 {total}",
"groupsTitle": "失敗群組與處理狀態",
"groupCount": "{count} 次",
"status": {
"green": "已回綠",
"sealed_waiting_window": "已封口,等待視窗",
"needs_investigation": "仍需調查",
"insufficient_diagnostics": "診斷不足",
"diagnostics_unavailable": "診斷不可用",
"unknown": "狀態待查"
},
"stat": {
"successRate": "成功率",
"successRateDetail": "{success}/{total} 目前樣本",
"sealedGroups": "已封口",
"sealedGroupsDetail": "未來同型失敗已加防線",
"openGroups": "待查",
"openGroupsDetail": "需要人工或 Agent 續查",
"greenAt": "回綠時間",
"greenAtDetail": "等待舊失敗自然滾出",
"greenAtDetailWithNeed": "或新增 {count} 次真實成功"
},
"closure": {
"sealed_by_mcp_grant": "MCP grant 已封口",
"sealed_by_external_site_guard": "外部站台防呆已封口",
"open_failure_source": "待查來源",
"unknown": "狀態待查"
},
"nextAction": {
"keep_monitoring": "持續監控,不需要人工介入。",
"observe_rolling_window_no_manual_restart": "目前已知失敗來源已封口;等待 rolling window 回綠,不要重啟服務或改寫歷史資料。",
"investigate_open_failure_groups": "仍有待查失敗群組;請反查 truth-chain、PlayBook 與 MCP 執行紀錄。",
"refresh_truth_chain_and_execution_logs": "重新整理 truth-chain 與 execution logs補齊診斷證據。"
}
},
"coverage": {
"title": "驗證覆蓋率",
"subtitle": "近 {window} 自動修復執行與 verifier 寫回狀態",

View File

@@ -2191,6 +2191,45 @@
"title": "整體合規率",
"target": "目標 ≥ 95%"
},
"autoExecute": {
"title": "自動修復紅燈診斷",
"subtitle": "近 {window} 日 rolling window顯示紅燈來源、封口狀態與回綠預估。",
"threshold": "門檻 {target}",
"windowSummary": "成功 {success}、失敗 {failed}、總樣本 {total}",
"groupsTitle": "失敗群組與處理狀態",
"groupCount": "{count} 次",
"status": {
"green": "已回綠",
"sealed_waiting_window": "已封口,等待視窗",
"needs_investigation": "仍需調查",
"insufficient_diagnostics": "診斷不足",
"diagnostics_unavailable": "診斷不可用",
"unknown": "狀態待查"
},
"stat": {
"successRate": "成功率",
"successRateDetail": "{success}/{total} 目前樣本",
"sealedGroups": "已封口",
"sealedGroupsDetail": "未來同型失敗已加防線",
"openGroups": "待查",
"openGroupsDetail": "需要人工或 Agent 續查",
"greenAt": "回綠時間",
"greenAtDetail": "等待舊失敗自然滾出",
"greenAtDetailWithNeed": "或新增 {count} 次真實成功"
},
"closure": {
"sealed_by_mcp_grant": "MCP grant 已封口",
"sealed_by_external_site_guard": "外部站台防呆已封口",
"open_failure_source": "待查來源",
"unknown": "狀態待查"
},
"nextAction": {
"keep_monitoring": "持續監控,不需要人工介入。",
"observe_rolling_window_no_manual_restart": "目前已知失敗來源已封口;等待 rolling window 回綠,不要重啟服務或改寫歷史資料。",
"investigate_open_failure_groups": "仍有待查失敗群組;請反查 truth-chain、PlayBook 與 MCP 執行紀錄。",
"refresh_truth_chain_and_execution_logs": "重新整理 truth-chain 與 execution logs補齊診斷證據。"
}
},
"coverage": {
"title": "驗證覆蓋率",
"subtitle": "近 {window} 自動修復執行與 verifier 寫回狀態",

View File

@@ -15,7 +15,7 @@
import { useEffect, useState } from 'react'
import { useTranslations } from 'next-intl'
import { ShieldCheck, AlertTriangle, PlayCircle, SearchCheck } from 'lucide-react'
import { ShieldCheck, AlertTriangle, PlayCircle, SearchCheck, CheckCircle2, Clock3, Wrench } from 'lucide-react'
import { SloKpiCard, type SloMetric } from '@/components/governance/slo-kpi-card'
import { SloViolationChart, type ViolationDataPoint } from '@/components/governance/slo-violation-chart'
import { GlassCard } from '@/components/ui/glass-card'
@@ -60,6 +60,42 @@ interface SloApiResponse {
}
overall_compliance?: number
computed_at?: string
diagnostics?: {
auto_execute_success_rate?: AutoExecuteDiagnostics
}
}
interface AutoExecuteDiagnostics {
schema_version?: string
status?: string
summary?: {
total?: number
success?: number
failed?: number
rate?: number | null
threshold?: number
window_days?: number
min_samples?: number
}
top_failure_groups?: AutoExecuteFailureGroup[]
sealed_failure_group_count?: number
open_failure_group_count?: number
immediate_successes_needed?: number
projected_green_at?: string | null
projection_reason?: string | null
next_action?: string | null
}
interface AutoExecuteFailureGroup {
alertname?: string | null
playbook_id?: string | null
playbook_name?: string | null
count?: number
closure_status?: string | null
closure_label?: string | null
recommended_action?: string | null
first_seen?: string | null
last_seen?: string | null
}
interface Adr100VerificationCoverage {
@@ -218,6 +254,37 @@ function formatPercent(value?: number | null): string {
return value == null ? '--' : `${(value * 100).toFixed(1)}%`
}
function autoExecuteStatusKey(value?: string | null): string {
if (value === 'green') return value
if (value === 'sealed_waiting_window') return value
if (value === 'needs_investigation') return value
if (value === 'insufficient_diagnostics') return value
if (value === 'diagnostics_unavailable') return value
return 'unknown'
}
function autoExecuteNextActionKey(value?: string | null): string {
if (value === 'keep_monitoring') return value
if (value === 'observe_rolling_window_no_manual_restart') return value
if (value === 'investigate_open_failure_groups') return value
if (value === 'refresh_truth_chain_and_execution_logs') return value
return 'refresh_truth_chain_and_execution_logs'
}
function closureStatusKey(value?: string | null): string {
if (value === 'sealed_by_mcp_grant') return value
if (value === 'sealed_by_external_site_guard') return value
if (value === 'open_failure_source') return value
return 'unknown'
}
function diagnosticTone(status?: string | null, openGroups = 0): string {
if (openGroups > 0 || status === 'needs_investigation') return '#FF3300'
if (status === 'green') return '#22C55E'
if (status === 'diagnostics_unavailable') return '#87867f'
return '#F59E0B'
}
function failureClassKey(value?: string | null): string {
if (value === 'unsupported_action_scheme') return value
if (value === 'verifier_missing_promql') return value
@@ -292,6 +359,18 @@ function formatShortDateTime(value?: string | null): string {
})
}
function formatShortDate(value?: string | null): string {
if (!value) return '--'
const date = new Date(value)
if (Number.isNaN(date.getTime())) return '--'
return date.toLocaleString(undefined, {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
})
}
function historyRouteLabel(summary: RemediationHistoryWorkItemSummary): string {
return compactLabel(
[summary.latest_agent_id, summary.latest_tool_name]
@@ -355,6 +434,205 @@ function buildMetrics(api: SloApiResponse): SloMetric[] {
})
}
function AutoExecuteDiagnosticsPanel({ diagnostics }: { diagnostics?: AutoExecuteDiagnostics }) {
const t = useTranslations('governance.slo.autoExecute')
const summary = diagnostics?.summary
if (!summary) return null
const rate = summary.rate ?? null
const threshold = summary.threshold ?? 0.85
const total = summary.total ?? 0
const success = summary.success ?? 0
const failed = summary.failed ?? Math.max(total - success, 0)
const sealedGroups = diagnostics?.sealed_failure_group_count ?? 0
const openGroups = diagnostics?.open_failure_group_count ?? 0
const tone = diagnosticTone(diagnostics?.status, openGroups)
const progress = Math.max(0, Math.min(100, (rate ?? 0) * 100))
const thresholdPosition = Math.max(0, Math.min(100, threshold * 100))
const groups = diagnostics?.top_failure_groups ?? []
const statusKey = autoExecuteStatusKey(diagnostics?.status)
const nextActionKey = autoExecuteNextActionKey(diagnostics?.next_action)
const projectedAt = formatShortDate(diagnostics?.projected_green_at)
const rateLabel = rate == null ? '--' : `${(rate * 100).toFixed(1)}%`
const thresholdLabel = `${(threshold * 100).toFixed(0)}%`
const statItems = [
{ key: 'successRate', icon: CheckCircle2, value: rateLabel, detail: t('stat.successRateDetail', { success, total }) },
{ key: 'sealedGroups', icon: Wrench, value: String(sealedGroups), detail: t('stat.sealedGroupsDetail') },
{ key: 'openGroups', icon: AlertTriangle, value: String(openGroups), detail: t('stat.openGroupsDetail') },
{
key: 'greenAt',
icon: Clock3,
value: projectedAt,
detail: diagnostics?.immediate_successes_needed
? t('stat.greenAtDetailWithNeed', { count: diagnostics.immediate_successes_needed })
: t('stat.greenAtDetail'),
},
]
return (
<GlassCard variant="subtle" padding="md">
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 12 }}>
<div style={{ minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<AlertTriangle size={15} style={{ color: tone, flexShrink: 0 }} />
<div style={{ fontFamily: 'Syne, sans-serif', fontSize: 13, fontWeight: 700, color: '#141413' }}>
{t('title')}
</div>
</div>
<div style={{ marginTop: 5, fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f', lineHeight: 1.45 }}>
{t('subtitle', { window: summary.window_days ?? 7 })}
</div>
</div>
<div style={{
flexShrink: 0,
minHeight: 26,
display: 'inline-flex',
alignItems: 'center',
padding: '4px 8px',
borderRadius: 6,
border: `0.5px solid ${tone}40`,
background: `${tone}12`,
fontFamily: "'DM Mono', monospace",
fontSize: 10,
color: tone,
}}>
{t(`status.${statusKey}`)}
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(220px, 1.1fr) minmax(260px, 1.8fr)', gap: 14 }} className="slo-auto-diagnostics-grid">
<div style={{ minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 10, marginBottom: 8 }}>
<div style={{ fontFamily: 'Syne, sans-serif', fontSize: 30, fontWeight: 700, color: tone, letterSpacing: 0, lineHeight: 1 }}>
{rateLabel}
</div>
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f' }}>
{t('threshold', { target: thresholdLabel })}
</div>
</div>
<div style={{
position: 'relative',
height: 10,
borderRadius: 5,
background: 'rgba(20,20,19,0.08)',
overflow: 'hidden',
}}>
<div style={{ width: `${progress}%`, height: '100%', background: tone, borderRadius: 5 }} />
<div style={{
position: 'absolute',
left: `${thresholdPosition}%`,
top: 0,
bottom: 0,
width: 2,
background: '#141413',
opacity: 0.45,
}} />
</div>
<div style={{ marginTop: 7, fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f', lineHeight: 1.45 }}>
{t('windowSummary', { success, failed, total })}
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 8 }} className="slo-auto-stat-grid">
{statItems.map(item => {
const Icon = item.icon
return (
<div key={item.key} style={{
minWidth: 0,
padding: '8px 9px',
borderRadius: 6,
border: '0.5px solid rgba(20,20,19,0.08)',
background: 'rgba(255,255,255,0.58)',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 5 }}>
<Icon size={13} style={{ color: item.key === 'openGroups' && openGroups > 0 ? '#FF3300' : tone, flexShrink: 0 }} />
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 9, color: '#87867f' }}>
{t(`stat.${item.key}`)}
</div>
</div>
<div style={{ fontFamily: 'Syne, sans-serif', fontSize: 18, fontWeight: 700, color: '#141413', letterSpacing: 0 }}>
{item.value}
</div>
<div style={{ marginTop: 2, fontFamily: "'DM Mono', monospace", fontSize: 9, color: '#87867f', lineHeight: 1.35 }}>
{item.detail}
</div>
</div>
)
})}
</div>
</div>
{groups.length > 0 && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<div style={{ fontFamily: 'Syne, sans-serif', fontSize: 11, fontWeight: 700, color: '#141413' }}>
{t('groupsTitle')}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 7 }}>
{groups.slice(0, 5).map(group => {
const closureKey = closureStatusKey(group.closure_status)
const closureTone = closureKey === 'open_failure_source' ? '#FF3300' : '#22C55E'
return (
<div key={`${group.alertname}-${group.playbook_id}-${closureKey}`} style={{
display: 'grid',
gridTemplateColumns: 'minmax(130px, 0.75fr) minmax(180px, 1fr) minmax(150px, 0.8fr) minmax(150px, 1fr)',
gap: 10,
alignItems: 'center',
minWidth: 0,
padding: '7px 0',
borderTop: '0.5px solid rgba(20,20,19,0.08)',
}} className="slo-auto-group-row">
<div style={{ minWidth: 0 }}>
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#141413', overflowWrap: 'anywhere' }}>
{compactLabel(group.alertname)}
</div>
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 9, color: '#87867f', marginTop: 2 }}>
{t('groupCount', { count: group.count ?? 0 })}
</div>
</div>
<div style={{ minWidth: 0 }}>
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#141413', overflowWrap: 'anywhere' }}>
{compactLabel(group.playbook_id)}
</div>
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 9, color: '#87867f', marginTop: 2, overflowWrap: 'anywhere' }}>
{compactLabel(group.playbook_name)}
</div>
</div>
<div style={{
width: 'fit-content',
minHeight: 24,
display: 'inline-flex',
alignItems: 'center',
padding: '3px 7px',
borderRadius: 6,
border: `0.5px solid ${closureTone}40`,
background: `${closureTone}12`,
fontFamily: "'DM Mono', monospace",
fontSize: 9,
color: closureTone,
}}>
{t(`closure.${closureKey}`)}
</div>
<div style={{ minWidth: 0, fontFamily: "'DM Mono', monospace", fontSize: 9, color: '#87867f', lineHeight: 1.4, overflowWrap: 'anywhere' }}>
{group.recommended_action || group.closure_label || '--'}
</div>
</div>
)
})}
</div>
</div>
)}
<div style={{ display: 'flex', alignItems: 'center', gap: 7, fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#4b5563', lineHeight: 1.45 }}>
<SearchCheck size={13} style={{ color: tone, flexShrink: 0 }} />
<span>{t(`nextAction.${nextActionKey}`)}</span>
</div>
</div>
</GlassCard>
)
}
function VerificationCoveragePanel({ coverage }: { coverage?: Adr100VerificationCoverage }) {
const t = useTranslations('governance.slo.coverage')
const [actionState, setActionState] = useState<Record<string, RemediationActionState>>({})
@@ -721,6 +999,7 @@ export function SloTab() {
const metrics = sloData ? buildMetrics(sloData) : []
const compliance = sloData?.adr100?.overall_compliance ?? sloData?.overall_compliance ?? null
const verificationCoverage = sloData?.adr100?.verification_coverage
const autoExecuteDiagnostics = sloData?.diagnostics?.auto_execute_success_rate
const chartData: ViolationDataPoint[] = summaryData?.data ?? []
const eventTypes: string[] = summaryData?.event_types ?? []
@@ -783,6 +1062,8 @@ export function SloTab() {
}
</div>
{!sloLoading && <AutoExecuteDiagnosticsPanel diagnostics={autoExecuteDiagnostics} />}
{!sloLoading && <VerificationCoveragePanel coverage={verificationCoverage} />}
{/* Violation timeline chart */}
@@ -799,6 +1080,9 @@ export function SloTab() {
@media (max-width: 640px) {
.slo-kpi-grid > * { flex: 0 0 100%; min-width: 0; }
.slo-coverage-grid { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; }
.slo-auto-diagnostics-grid { grid-template-columns: 1fr !important; }
.slo-auto-stat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; }
.slo-auto-group-row { grid-template-columns: 1fr !important; }
.slo-finding-row { grid-template-columns: 1fr !important; }
.slo-remediation-row { grid-template-columns: 1fr !important; }
}