diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 9a4b1b1af..c423282ad 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -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 寫回狀態", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 9a4b1b1af..c423282ad 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -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 寫回狀態", diff --git a/apps/web/src/app/[locale]/governance/tabs/slo-tab.tsx b/apps/web/src/app/[locale]/governance/tabs/slo-tab.tsx index bc4778665..c604d054a 100644 --- a/apps/web/src/app/[locale]/governance/tabs/slo-tab.tsx +++ b/apps/web/src/app/[locale]/governance/tabs/slo-tab.tsx @@ -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 ( + +
+
+
+
+ +
+ {t('title')} +
+
+
+ {t('subtitle', { window: summary.window_days ?? 7 })} +
+
+
+ {t(`status.${statusKey}`)} +
+
+ +
+
+
+
+ {rateLabel} +
+
+ {t('threshold', { target: thresholdLabel })} +
+
+
+
+
+
+
+ {t('windowSummary', { success, failed, total })} +
+
+ +
+ {statItems.map(item => { + const Icon = item.icon + return ( +
+
+ 0 ? '#FF3300' : tone, flexShrink: 0 }} /> +
+ {t(`stat.${item.key}`)} +
+
+
+ {item.value} +
+
+ {item.detail} +
+
+ ) + })} +
+
+ + {groups.length > 0 && ( +
+
+ {t('groupsTitle')} +
+
+ {groups.slice(0, 5).map(group => { + const closureKey = closureStatusKey(group.closure_status) + const closureTone = closureKey === 'open_failure_source' ? '#FF3300' : '#22C55E' + return ( +
+
+
+ {compactLabel(group.alertname)} +
+
+ {t('groupCount', { count: group.count ?? 0 })} +
+
+
+
+ {compactLabel(group.playbook_id)} +
+
+ {compactLabel(group.playbook_name)} +
+
+
+ {t(`closure.${closureKey}`)} +
+
+ {group.recommended_action || group.closure_label || '--'} +
+
+ ) + })} +
+
+ )} + +
+ + {t(`nextAction.${nextActionKey}`)} +
+
+ + ) +} + function VerificationCoveragePanel({ coverage }: { coverage?: Adr100VerificationCoverage }) { const t = useTranslations('governance.slo.coverage') const [actionState, setActionState] = useState>({}) @@ -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() { }
+ {!sloLoading && } + {!sloLoading && } {/* 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; } }