feat(web): 視覺化治理自動化盤點首屏
Some checks failed
Code Review / ai-code-review (push) Successful in 43s
CD Pipeline / tests (push) Successful in 1m45s
CD Pipeline / build-and-deploy (push) Successful in 7m20s
CD Pipeline / post-deploy-checks (push) Has been cancelled

This commit is contained in:
Your Name
2026-06-18 10:46:06 +08:00
parent e06a741d13
commit 271152054f
3 changed files with 460 additions and 0 deletions

View File

@@ -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": "只讀決策支援",

View File

@@ -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": "只讀決策支援",

View File

@@ -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 (
<div style={{
position: 'relative',
padding: 12,
border: `0.5px solid ${color}45`,
borderRadius: 7,
background: '#fff',
display: 'grid',
gridTemplateColumns: '34px minmax(0, 1fr)',
gap: 10,
alignItems: 'start',
minWidth: 0,
}}>
<div style={{
width: 34,
height: 34,
borderRadius: 8,
border: `0.5px solid ${color}55`,
background: `${color}12`,
color,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}>
{icon}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 5, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, minWidth: 0 }}>
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 12, fontWeight: 700, color: '#141413', overflowWrap: 'anywhere' }}>
{label}
</span>
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color, fontWeight: 800, whiteSpace: 'nowrap' }}>
{index}
</span>
</div>
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 24, fontWeight: 750, color, lineHeight: 1 }}>
{value}
</span>
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#6d6a61', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
{detail}
</span>
</div>
</div>
)
}
function GateMatrixRow({
label,
value,
detail,
tone = 'neutral',
}: {
label: string
value: string
detail: string
tone?: 'ok' | 'warn' | 'danger' | 'neutral'
}) {
const color = toneColor(tone)
return (
<div style={{
display: 'grid',
gridTemplateColumns: 'minmax(0, 1fr) 64px',
gap: 10,
alignItems: 'center',
padding: '9px 0',
borderBottom: '0.5px solid #eee9dd',
minWidth: 0,
}}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, minWidth: 0 }}>
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 12, fontWeight: 700, color: '#141413', overflowWrap: 'anywhere' }}>
{label}
</span>
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
{detail}
</span>
</div>
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 13, fontWeight: 800, color, textAlign: 'right', whiteSpace: 'nowrap' }}>
{value}
</span>
</div>
)
}
export function AutomationInventoryTab() {
const t = useTranslations('governance.automationInventory')
const [snapshot, setSnapshot] = useState<AiAgentAutomationInventorySnapshot | null>(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: <BellRing size={16} />,
},
{
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: <Fingerprint size={16} />,
},
{
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: <ClipboardCheck size={16} />,
},
{
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: <Lock size={16} />,
},
{
key: 'verifier',
label: t('visualOps.stages.verifier.label'),
value: `${resultCaptureReleaseReadbacks}`,
detail: t('visualOps.stages.verifier.detail', { blocked: resultCaptureReleaseReadbackBlocked }),
tone: resultCaptureReleaseReadbackBlocked > 0 ? 'warn' : 'ok',
icon: <ShieldCheck size={16} />,
},
{
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: <BookOpenCheck size={16} />,
},
]
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() {
]}
/>
<GlassCard variant="subtle" padding="md">
<div style={{ display: 'flex', flexDirection: 'column', gap: 14, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap' }}>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 10, minWidth: 0 }}>
<div style={{
width: 38,
height: 38,
borderRadius: 8,
border: '0.5px solid #2563eb40',
background: 'rgba(37,99,235,0.08)',
color: '#2563eb',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
}}>
<Gauge size={18} />
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 5, minWidth: 0 }}>
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 18, fontWeight: 760, color: '#141413', lineHeight: 1.15, overflowWrap: 'anywhere' }}>
{t('visualOps.title')}
</span>
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 11, color: '#5c5a55', lineHeight: 1.55, overflowWrap: 'anywhere' }}>
{t('visualOps.subtitle', {
current: snapshot.program_status.current_task_id,
next: snapshot.program_status.next_task_id,
overall: backlogProgressPercent,
support: decisionSupportCoverage,
})}
</span>
</div>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'flex-end', gap: 6, minWidth: 0 }}>
<Chip value={t('visualOps.badges.readOnly')} muted />
<Chip value={t('visualOps.badges.runtimeGate', { value: resultCaptureReleaseReadbackLiveWrites })} />
<Chip value={t('visualOps.badges.approval', { value: explicitApprovalTaskCount })} muted={explicitApprovalTaskCount === 0} />
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1.35fr) minmax(280px, 0.65fr)', gap: 12 }} className="automation-inventory-visual-grid">
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10, flexWrap: 'wrap' }}>
<SmallLabel>{t('visualOps.flowTitle')}</SmallLabel>
<Chip value={t('visualOps.flowBadge', { stages: visualFlowStages.length })} muted />
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 10 }} className="automation-inventory-stage-grid">
{visualFlowStages.map((stage, index) => (
<FlowStageTile
key={stage.key}
index={String(index + 1).padStart(2, '0')}
label={stage.label}
value={stage.value}
detail={stage.detail}
icon={stage.icon}
tone={stage.tone}
/>
))}
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, minWidth: 0 }}>
<div style={{ padding: 12, border: '0.5px solid #e0ddd4', borderRadius: 7, background: '#fff', display: 'flex', flexDirection: 'column', gap: 4, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10, flexWrap: 'wrap' }}>
<SmallLabel>{t('visualOps.gateTitle')}</SmallLabel>
<Chip value={t('visualOps.weighted', { value: decisionSupportCoverage })} muted />
</div>
{visualGateRows.map(row => (
<GateMatrixRow
key={row.key}
label={row.label}
value={row.value}
detail={row.detail}
tone={row.tone}
/>
))}
</div>
<div style={{ padding: 12, border: `0.5px solid ${toneColor(decisionCoverageTone)}55`, borderRadius: 7, background: '#faf9f3', display: 'flex', flexDirection: 'column', gap: 9, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10 }}>
<SmallLabel>{t('visualOps.focusTitle')}</SmallLabel>
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 18, fontWeight: 800, color: toneColor(decisionCoverageTone), whiteSpace: 'nowrap' }}>
{decisionSupportCoverage}%
</span>
</div>
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#6d6a61', lineHeight: 1.55, overflowWrap: 'anywhere' }}>
{t('visualOps.focusDetail', {
runner: giteaRunnerActions,
evidence: runtimeLiveMissing,
candidate: candidateDryRunNeedsReview,
approval: explicitApprovalTaskCount,
})}
</span>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 8 }} className="automation-inventory-visual-factor-grid">
<MiniBar
label={t('decisionSupport.factors.taskCompletion')}
value={completedTaskCoverage}
detail={t('decisionSupport.details.taskCompletion', { done: completedTasks, total: snapshot.tasks.length })}
tone={completedTaskCoverage >= 70 ? 'ok' : completedTaskCoverage >= 45 ? 'warn' : 'danger'}
/>
<MiniBar
label={t('decisionSupport.factors.runtimeBinding')}
value={runtimeBindingCoverage}
detail={t('decisionSupport.details.runtimeBinding', {
bound: runtimeBoundComponents,
total: runtimeSurfaceComponentTotal,
})}
tone={runtimeBindingCoverage >= 80 ? 'ok' : runtimeBindingCoverage >= 50 ? 'warn' : 'danger'}
/>
</div>
</div>
</div>
</div>
</div>
</GlassCard>
<GlassCard variant="subtle" padding="md">
<div style={{ display: 'flex', flexDirection: 'column', gap: 14, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap' }}>
@@ -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,