fix(web): clarify incident flow stage on dashboard
All checks were successful
Code Review / ai-code-review (push) Successful in 11s
CD Pipeline / tests (push) Successful in 3m57s
CD Pipeline / build-and-deploy (push) Successful in 3m42s
CD Pipeline / post-deploy-checks (push) Successful in 1m49s

This commit is contained in:
Your Name
2026-05-20 11:43:23 +08:00
parent 1faaaf8fbc
commit 0c1f126479
3 changed files with 95 additions and 6 deletions

View File

@@ -417,6 +417,19 @@
"signalCount": "Signals",
"statusLabel": "Status",
"aiProposal": "AI Proposal",
"aiProposalPreview": "AI Proposal: {action}",
"flowCurrentLabel": "Current stage",
"flowNextLabel": "Next step",
"flowComplete": "Complete",
"flowStages": {
"alert": "Alert received",
"detection": "AI detection",
"analysis": "AI analysis",
"proposal": "Proposal generated",
"approval": "Waiting approval",
"execution": "Repair execution",
"resolved": "Complete"
},
"processingTimeline": "Processing Timeline",
"timelineLoading": "Loading processing timeline...",
"timelineEvents": "Event Details",

View File

@@ -418,6 +418,19 @@
"signalCount": "信號數",
"statusLabel": "狀態",
"aiProposal": "AI 提案",
"aiProposalPreview": "AI 提案:{action}",
"flowCurrentLabel": "目前階段",
"flowNextLabel": "下一步",
"flowComplete": "已完成",
"flowStages": {
"alert": "告警收到",
"detection": "AI 偵測",
"analysis": "AI 分析",
"proposal": "提案生成",
"approval": "等待授權",
"execution": "執行修復",
"resolved": "完成"
},
"processingTimeline": "處理歷程",
"timelineLoading": "載入處理歷程...",
"timelineEvents": "事件明細",

View File

@@ -46,6 +46,16 @@ const SEV_CONFIG = {
P3: { barColor: '#22C55E', label: 'P3', labelBg: 'rgba(34,197,94,0.1)', labelColor: '#16a34a' },
} as const
const FLOW_STAGE_ORDER: FlowStage[] = [
'alert',
'detection',
'analysis',
'proposal',
'approval',
'execution',
'resolved',
]
/** 根據 incident + decision evidence 對應 FlowStage */
function toFlowStage(status: string, severity: string, decision?: DecisionInfo | null): FlowStage {
const normalizedStatus = status.toLowerCase()
@@ -66,6 +76,13 @@ function toFlowStage(status: string, severity: string, decision?: DecisionInfo |
return severity === 'P0' ? 'alert' : 'detection'
}
function nextFlowStage(stage: FlowStage, isResolved: boolean): FlowStage | null {
if (isResolved) return null
const index = FLOW_STAGE_ORDER.indexOf(stage)
if (index < 0 || index >= FLOW_STAGE_ORDER.length - 1) return null
return FLOW_STAGE_ORDER[index + 1]
}
/** 格式化持續時間 */
function formatDuration(createdAt: string | undefined): string {
if (!createdAt) return '--'
@@ -217,6 +234,16 @@ export function IncidentCard({ incident, decision, onApprovalChange }: IncidentC
const sevCfg = SEV_CONFIG[sev] ?? SEV_CONFIG.P3
const flowStage = toFlowStage(incidentStatus, incident.severity, decision)
const isResolved = incidentStatus === 'resolved' || incidentStatus === 'closed'
const nextStage = nextFlowStage(flowStage, isResolved)
const flowStageLabels: Record<FlowStage, string> = {
alert: t('flowStages.alert'),
detection: t('flowStages.detection'),
analysis: t('flowStages.analysis'),
proposal: t('flowStages.proposal'),
approval: t('flowStages.approval'),
execution: t('flowStages.execution'),
resolved: t('flowStages.resolved'),
}
const serviceName = incident.affected_services?.[0] ?? '--'
const duration = formatDuration(incident.created_at)
@@ -315,9 +342,9 @@ export function IncidentCard({ incident, decision, onApprovalChange }: IncidentC
</span>
)
case 'approved':
return <span style={{ fontSize: 12, color: '#22C55E', fontWeight: 700 }}> {t('approved')}</span>
return <span style={{ fontSize: 12, color: '#22C55E', fontWeight: 700 }}>{t('approved')}</span>
case 'rejected':
return <span style={{ fontSize: 12, color: '#cc2200', fontWeight: 700 }}> {t('rejected')}</span>
return <span style={{ fontSize: 12, color: '#cc2200', fontWeight: 700 }}>{t('rejected')}</span>
case 'error':
case 'timeout':
return (
@@ -352,7 +379,7 @@ export function IncidentCard({ incident, decision, onApprovalChange }: IncidentC
}}
title={isAnalyzing ? t('analyzing') : decisionAction || t('authorizeExecution')}
>
{t('authorize')}
{t('authorize')}
</button>
<button
onClick={handleReject}
@@ -369,9 +396,20 @@ export function IncidentCard({ incident, decision, onApprovalChange }: IncidentC
fontFamily: 'inherit',
}}
>
{t('reject')}
{t('reject')}
</button>
{isAnalyzing && <span style={{ fontSize: 12, color: '#F59E0B' }}></span>}
{isAnalyzing && (
<span
aria-label={t('analyzing')}
style={{
width: 8,
height: 8,
borderRadius: '50%',
background: '#F59E0B',
display: 'inline-block',
}}
/>
)}
</div>
)
}
@@ -425,6 +463,31 @@ export function IncidentCard({ incident, decision, onApprovalChange }: IncidentC
{/* 流程狀態圖 */}
<FlowPipeline activeStage={flowStage} isResolved={isResolved} severity={sev} />
<div
data-testid="incident-flow-summary"
style={{
margin: '0 14px 8px',
padding: '7px 9px',
border: '0.5px solid #e0ddd4',
borderRadius: 6,
background: '#fbfaf6',
display: 'flex',
alignItems: 'center',
gap: 10,
flexWrap: 'wrap',
fontSize: 11,
color: '#555550',
}}
>
<span>
{t('flowCurrentLabel')}: <strong style={{ color: sevCfg.labelColor }}>{flowStageLabels[flowStage]}</strong>
</span>
<span style={{ color: '#b0ad9f' }}>/</span>
<span>
{t('flowNextLabel')}: <strong style={{ color: '#141413' }}>{nextStage ? flowStageLabels[nextStage] : t('flowComplete')}</strong>
</span>
</div>
{/* Impact 指標列 */}
<div style={{
margin: '0 14px 8px',
@@ -646,7 +709,7 @@ export function IncidentCard({ incident, decision, onApprovalChange }: IncidentC
fontFamily: 'inherit',
}}
>
<span> {t('aiProposal')}: {decisionAction.slice(0, 50)}{decisionAction.length > 50 ? '...' : ''}</span>
<span>{t('aiProposalPreview', { action: compactTimelineText(decisionAction, '') })}</span>
</button>
{aiExpanded && (
<div style={{