feat(web): 視覺化治理自動化盤點首屏
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user