diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json
index 9ac442a6..5de75a7f 100644
--- a/apps/web/messages/en.json
+++ b/apps/web/messages/en.json
@@ -3524,6 +3524,69 @@
"cd_login_banner_observed_os_only": "僅 CD banner 觀察"
}
},
+ "visualOps": {
+ "title": "自動化戰情視覺總覽",
+ "subtitle": "{current} → {next};自動化 {overall}%;決策支援 {support}%。先看流程與 Gate,再下鑽證據。",
+ "flowTitle": "AI 自動化流程",
+ "flowBadge": "{stages} 階段",
+ "gateTitle": "Gate / 阻塞矩陣",
+ "weighted": "決策支援 {value}%",
+ "focusTitle": "本輪優先焦點",
+ "focusDetail": "先補 Runner {runner}、Live 證據 {evidence}、修復候選複核 {candidate}、人工批准任務 {approval};未通過前不開 runtime gate。",
+ "badges": {
+ "readOnly": "只讀營運台",
+ "runtimeGate": "正式寫入 {value}",
+ "approval": "需批准 {value}"
+ },
+ "stages": {
+ "intake": {
+ "label": "告警 / 服務收件",
+ "detail": "健康目標中 {actions} 個仍需處置。"
+ },
+ "evidence": {
+ "label": "MCP / 來源證據",
+ "detail": "仍缺 {missing} 個 live evidence 綁定。"
+ },
+ "candidate": {
+ "label": "修復候選",
+ "detail": "{review} 個待 owner 複核;{blocked} 個被 allowlist / policy 阻擋。"
+ },
+ "approval": {
+ "label": "人工 Gate",
+ "detail": "共 {total} 個任務邊界,未批准不會執行。"
+ },
+ "verifier": {
+ "label": "執行讀回 / Verifier",
+ "detail": "{blocked} 個 release readback 仍阻擋。"
+ },
+ "learning": {
+ "label": "KM / PlayBook 學習",
+ "detail": "{gates} 個 learning gate 等 owner review。"
+ }
+ },
+ "gates": {
+ "notification": {
+ "label": "Failure-only 通知",
+ "detail": "成功降噪 {quiet};需處置 {action};立即升級 {escalation}。"
+ },
+ "repairCandidate": {
+ "label": "修復候選完整度",
+ "detail": "待 owner review {review};verifier plan {verifier}。"
+ },
+ "approval": {
+ "label": "批准邊界",
+ "detail": "{blocked} 個批准項仍是明確 false。"
+ },
+ "execution": {
+ "label": "正式執行寫入",
+ "detail": "讀回 {readback};阻擋 {blocked};0 代表尚未開 live write。"
+ },
+ "learning": {
+ "label": "PlayBook / KM 回寫",
+ "detail": "候選 {candidates};learning gate {gates}。"
+ }
+ }
+ },
"overview": {
"title": "決策指揮摘要",
"mode": "只讀決策支援",
diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json
index 9ac442a6..5de75a7f 100644
--- a/apps/web/messages/zh-TW.json
+++ b/apps/web/messages/zh-TW.json
@@ -3524,6 +3524,69 @@
"cd_login_banner_observed_os_only": "僅 CD banner 觀察"
}
},
+ "visualOps": {
+ "title": "自動化戰情視覺總覽",
+ "subtitle": "{current} → {next};自動化 {overall}%;決策支援 {support}%。先看流程與 Gate,再下鑽證據。",
+ "flowTitle": "AI 自動化流程",
+ "flowBadge": "{stages} 階段",
+ "gateTitle": "Gate / 阻塞矩陣",
+ "weighted": "決策支援 {value}%",
+ "focusTitle": "本輪優先焦點",
+ "focusDetail": "先補 Runner {runner}、Live 證據 {evidence}、修復候選複核 {candidate}、人工批准任務 {approval};未通過前不開 runtime gate。",
+ "badges": {
+ "readOnly": "只讀營運台",
+ "runtimeGate": "正式寫入 {value}",
+ "approval": "需批准 {value}"
+ },
+ "stages": {
+ "intake": {
+ "label": "告警 / 服務收件",
+ "detail": "健康目標中 {actions} 個仍需處置。"
+ },
+ "evidence": {
+ "label": "MCP / 來源證據",
+ "detail": "仍缺 {missing} 個 live evidence 綁定。"
+ },
+ "candidate": {
+ "label": "修復候選",
+ "detail": "{review} 個待 owner 複核;{blocked} 個被 allowlist / policy 阻擋。"
+ },
+ "approval": {
+ "label": "人工 Gate",
+ "detail": "共 {total} 個任務邊界,未批准不會執行。"
+ },
+ "verifier": {
+ "label": "執行讀回 / Verifier",
+ "detail": "{blocked} 個 release readback 仍阻擋。"
+ },
+ "learning": {
+ "label": "KM / PlayBook 學習",
+ "detail": "{gates} 個 learning gate 等 owner review。"
+ }
+ },
+ "gates": {
+ "notification": {
+ "label": "Failure-only 通知",
+ "detail": "成功降噪 {quiet};需處置 {action};立即升級 {escalation}。"
+ },
+ "repairCandidate": {
+ "label": "修復候選完整度",
+ "detail": "待 owner review {review};verifier plan {verifier}。"
+ },
+ "approval": {
+ "label": "批准邊界",
+ "detail": "{blocked} 個批准項仍是明確 false。"
+ },
+ "execution": {
+ "label": "正式執行寫入",
+ "detail": "讀回 {readback};阻擋 {blocked};0 代表尚未開 live write。"
+ },
+ "learning": {
+ "label": "PlayBook / KM 回寫",
+ "detail": "候選 {candidates};learning gate {gates}。"
+ }
+ }
+ },
"overview": {
"title": "決策指揮摘要",
"mode": "只讀決策支援",
diff --git a/apps/web/src/app/[locale]/governance/tabs/automation-inventory-tab.tsx b/apps/web/src/app/[locale]/governance/tabs/automation-inventory-tab.tsx
index fc0e3270..a4b27367 100644
--- a/apps/web/src/app/[locale]/governance/tabs/automation-inventory-tab.tsx
+++ b/apps/web/src/app/[locale]/governance/tabs/automation-inventory-tab.tsx
@@ -454,6 +454,105 @@ function SummaryTile({
)
}
+function FlowStageTile({
+ index,
+ label,
+ value,
+ detail,
+ icon,
+ tone = 'neutral',
+}: {
+ index: string
+ label: string
+ value: string
+ detail: string
+ icon: ReactNode
+ tone?: 'ok' | 'warn' | 'danger' | 'neutral'
+}) {
+ const color = toneColor(tone)
+ return (
+
+
+ {icon}
+
+
+
+
+ {label}
+
+
+ {index}
+
+
+
+ {value}
+
+
+ {detail}
+
+
+
+ )
+}
+
+function GateMatrixRow({
+ label,
+ value,
+ detail,
+ tone = 'neutral',
+}: {
+ label: string
+ value: string
+ detail: string
+ tone?: 'ok' | 'warn' | 'danger' | 'neutral'
+}) {
+ const color = toneColor(tone)
+ return (
+
+
+
+ {label}
+
+
+ {detail}
+
+
+
+ {value}
+
+
+ )
+}
+
export function AutomationInventoryTab() {
const t = useTranslations('governance.automationInventory')
const [snapshot, setSnapshot] = useState(null)
@@ -2155,6 +2254,7 @@ export function AutomationInventoryTab() {
const runtimeSecrets = runtimeSurface?.rollups.secret_surface_ids.length ?? 0
const runtimeLiveMissing = runtimeSurface?.rollups.live_check_missing_surface_ids.length ?? 0
const runtimeBoundComponents = runtimeSurface?.rollups.source_components_with_runtime_binding ?? 0
+ const runtimeSurfaceComponentTotal = runtimeSurface?.rollups.total_source_components ?? 0
const giteaRunnerActions = giteaHealth.rollups.workflow_ids_requiring_runner_attestation.length
const giteaQuietPolicies = giteaHealth.rollups.notification_contracts_quiet_success_count
const observabilityActions = observabilityMatrix.rollups.surface_ids_requiring_action.length
@@ -3813,6 +3913,122 @@ export function AutomationInventoryTab() {
const blockedApprovals = Object.entries(snapshot.approval_boundaries)
.filter(([, allowed]) => allowed === false)
.map(([key]) => key)
+ const visualFlowStages: Array<{
+ key: string
+ label: string
+ value: string
+ detail: string
+ tone: 'ok' | 'warn' | 'danger' | 'neutral'
+ icon: ReactNode
+ }> = [
+ {
+ key: 'intake',
+ label: t('visualOps.stages.intake.label'),
+ value: String(serviceHealthGapMatrix.rollups.total_targets),
+ detail: t('visualOps.stages.intake.detail', { actions: serviceHealthActions }),
+ tone: serviceHealthActions > 0 ? 'warn' : 'ok',
+ icon: ,
+ },
+ {
+ key: 'evidence',
+ label: t('visualOps.stages.evidence.label'),
+ value: `${runtimeBoundComponents}/${runtimeSurfaceComponentTotal}`,
+ detail: t('visualOps.stages.evidence.detail', { missing: runtimeLiveMissing }),
+ tone: runtimeLiveMissing > 0 ? 'warn' : 'ok',
+ icon: ,
+ },
+ {
+ key: 'candidate',
+ label: t('visualOps.stages.candidate.label'),
+ value: `${candidateDryRunEvidenceCount}/${candidateDryRunCount}`,
+ detail: t('visualOps.stages.candidate.detail', {
+ review: candidateDryRunNeedsReview,
+ blocked: candidateDryRunAllowlistBlocked + candidateDryRunPolicyBlocked,
+ }),
+ tone: candidateDryRunNeedsReview > 0 || candidateDryRunPolicyBlocked > 0 ? 'warn' : 'ok',
+ icon: ,
+ },
+ {
+ key: 'approval',
+ label: t('visualOps.stages.approval.label'),
+ value: String(explicitApprovalTaskCount),
+ detail: t('visualOps.stages.approval.detail', { total: taskBoundaryCount }),
+ tone: explicitApprovalTaskCount > 0 ? 'warn' : 'ok',
+ icon: ,
+ },
+ {
+ key: 'verifier',
+ label: t('visualOps.stages.verifier.label'),
+ value: `${resultCaptureReleaseReadbacks}`,
+ detail: t('visualOps.stages.verifier.detail', { blocked: resultCaptureReleaseReadbackBlocked }),
+ tone: resultCaptureReleaseReadbackBlocked > 0 ? 'warn' : 'ok',
+ icon: ,
+ },
+ {
+ key: 'learning',
+ label: t('visualOps.stages.learning.label'),
+ value: `${learningWritebackLiveWrites}/${learningWritebackLanes}`,
+ detail: t('visualOps.stages.learning.detail', { gates: learningWritebackApprovals }),
+ tone: learningWritebackApprovals > 0 ? 'warn' : 'ok',
+ icon: ,
+ },
+ ]
+ const visualGateRows: Array<{
+ key: string
+ label: string
+ value: string
+ detail: string
+ tone: 'ok' | 'warn' | 'danger' | 'neutral'
+ }> = [
+ {
+ key: 'notification',
+ label: t('visualOps.gates.notification.label'),
+ value: String(serviceHealthNotificationAllowedCount),
+ detail: t('visualOps.gates.notification.detail', {
+ quiet: serviceHealthSuppressedSuccess,
+ action: serviceHealthActionRequiredRules,
+ escalation: serviceHealthImmediateEscalations,
+ }),
+ tone: serviceHealthNotificationAllowedCount === 0 ? 'ok' : 'danger',
+ },
+ {
+ key: 'repairCandidate',
+ label: t('visualOps.gates.repairCandidate.label'),
+ value: `${candidateDryRunEvidenceCount}/${candidateDryRunCount}`,
+ detail: t('visualOps.gates.repairCandidate.detail', {
+ review: candidateDryRunNeedsReview,
+ verifier: candidateDryRunVerifierPlans,
+ }),
+ tone: candidateDryRunEvidenceCount < candidateDryRunCount ? 'warn' : 'ok',
+ },
+ {
+ key: 'approval',
+ label: t('visualOps.gates.approval.label'),
+ value: `${explicitApprovalTaskCount}/${taskBoundaryCount}`,
+ detail: t('visualOps.gates.approval.detail', { blocked: blockedApprovals.length }),
+ tone: explicitApprovalTaskCount > 0 ? 'warn' : 'ok',
+ },
+ {
+ key: 'execution',
+ label: t('visualOps.gates.execution.label'),
+ value: String(resultCaptureReleaseReadbackLiveWrites),
+ detail: t('visualOps.gates.execution.detail', {
+ readback: resultCaptureReleaseReadbacks,
+ blocked: resultCaptureReleaseReadbackBlocked,
+ }),
+ tone: resultCaptureReleaseReadbackLiveWrites === 0 ? 'ok' : 'danger',
+ },
+ {
+ key: 'learning',
+ label: t('visualOps.gates.learning.label'),
+ value: String(matchedPlaybookUpdated),
+ detail: t('visualOps.gates.learning.detail', {
+ candidates: matchedPlaybookCandidates,
+ gates: matchedPlaybookGates,
+ }),
+ tone: matchedPlaybookGates > 0 ? 'warn' : 'ok',
+ },
+ ]
const statusLabel = (value: string) => {
try {
@@ -4016,6 +4232,121 @@ export function AutomationInventoryTab() {
]}
/>
+
+
+
+
+
+
+
+
+
+ {t('visualOps.title')}
+
+
+ {t('visualOps.subtitle', {
+ current: snapshot.program_status.current_task_id,
+ next: snapshot.program_status.next_task_id,
+ overall: backlogProgressPercent,
+ support: decisionSupportCoverage,
+ })}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {t('visualOps.flowTitle')}
+
+
+
+ {visualFlowStages.map((stage, index) => (
+
+ ))}
+
+
+
+
+
+
+ {t('visualOps.gateTitle')}
+
+
+ {visualGateRows.map(row => (
+
+ ))}
+
+
+
+
+ {t('visualOps.focusTitle')}
+
+ {decisionSupportCoverage}%
+
+
+
+ {t('visualOps.focusDetail', {
+ runner: giteaRunnerActions,
+ evidence: runtimeLiveMissing,
+ candidate: candidateDryRunNeedsReview,
+ approval: explicitApprovalTaskCount,
+ })}
+
+
+ = 70 ? 'ok' : completedTaskCoverage >= 45 ? 'warn' : 'danger'}
+ />
+ = 80 ? 'ok' : runtimeBindingCoverage >= 50 ? 'warn' : 'danger'}
+ />
+
+
+
+
+
+
+
@@ -15387,6 +15718,9 @@ export function AutomationInventoryTab() {
}
@media (max-width: 900px) {
+ .automation-inventory-visual-grid,
+ .automation-inventory-stage-grid,
+ .automation-inventory-visual-factor-grid,
.automation-inventory-kpi-grid,
.automation-inventory-command-grid,
.automation-inventory-summary-grid,