From 607fc291e9cfe0eeb0f91f3ef9505e5fc3214426 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 1 Jun 2026 00:14:43 +0800 Subject: [PATCH] fix(web): clarify alert operator handoff --- apps/web/messages/en.json | 30 ++++++ apps/web/messages/zh-TW.json | 30 ++++++ apps/web/src/app/[locale]/alerts/page.tsx | 116 +++++++++++++++++++++- 3 files changed, 172 insertions(+), 4 deletions(-) diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 8b753274..73a44afa 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -1237,6 +1237,36 @@ "sourceDetail": "direct {direct} / candidate {candidate} / applied {applied};原因 {reason}", "needsHumanYes": "需要", "needsHumanNo": "不需要", + "stateLabels": { + "verificationDegradedManualRequired": "驗證退化,需人工確認" + }, + "nextActionLabels": { + "manualVerifyOrRepair": "人工確認修復狀態;需要時重新送審修復" + }, + "reasonLabels": { + "incidentOpenAfterSuccessfulExecution": "自動執行已完成,但 Incident 仍開啟" + }, + "sourceReasonLabels": { + "providerHeartbeatPresentButNoIncidentMatch": "Sentry / SigNoz 有新鮮心跳,但沒有匹配到此 Incident" + }, + "handoff": { + "eyebrow": "現在要做", + "titleManual": "需要人工接手確認", + "titleAutomated": "自動鏈路已完成,持續觀察", + "titleUnknown": "等待 truth-chain 資料", + "actionManualVerifyOrRepair": "到 AwoooP Work Items / Approvals 確認執行證據;若服務仍異常,再重新送審修復,不要直接重啟或靜默關閉。", + "actionNoManual": "目前不需要人工介入;保留真相鏈與 Run history 供稽核追蹤。", + "actionUnknown": "尚未拿到完整狀態,先等 status-chain 載入完成。", + "ownerLabel": "主責", + "ownerSre": "SRE owner / AwoooP operator", + "ownerAutomation": "AI 自動化鏈路", + "entryLabel": "處理入口", + "entryManual": "Work Items / Approvals / Runs", + "entryReadOnly": "Runs / History", + "reasonLabel": "原因", + "boundaryLabel": "邊界", + "boundary": "只讀追蹤,不觸發修復" + }, "repeatStates": { "duplicate": "最新入站重複", "related": "同指紋重複", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 8b753274..73a44afa 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -1237,6 +1237,36 @@ "sourceDetail": "direct {direct} / candidate {candidate} / applied {applied};原因 {reason}", "needsHumanYes": "需要", "needsHumanNo": "不需要", + "stateLabels": { + "verificationDegradedManualRequired": "驗證退化,需人工確認" + }, + "nextActionLabels": { + "manualVerifyOrRepair": "人工確認修復狀態;需要時重新送審修復" + }, + "reasonLabels": { + "incidentOpenAfterSuccessfulExecution": "自動執行已完成,但 Incident 仍開啟" + }, + "sourceReasonLabels": { + "providerHeartbeatPresentButNoIncidentMatch": "Sentry / SigNoz 有新鮮心跳,但沒有匹配到此 Incident" + }, + "handoff": { + "eyebrow": "現在要做", + "titleManual": "需要人工接手確認", + "titleAutomated": "自動鏈路已完成,持續觀察", + "titleUnknown": "等待 truth-chain 資料", + "actionManualVerifyOrRepair": "到 AwoooP Work Items / Approvals 確認執行證據;若服務仍異常,再重新送審修復,不要直接重啟或靜默關閉。", + "actionNoManual": "目前不需要人工介入;保留真相鏈與 Run history 供稽核追蹤。", + "actionUnknown": "尚未拿到完整狀態,先等 status-chain 載入完成。", + "ownerLabel": "主責", + "ownerSre": "SRE owner / AwoooP operator", + "ownerAutomation": "AI 自動化鏈路", + "entryLabel": "處理入口", + "entryManual": "Work Items / Approvals / Runs", + "entryReadOnly": "Runs / History", + "reasonLabel": "原因", + "boundaryLabel": "邊界", + "boundary": "只讀追蹤,不觸發修復" + }, "repeatStates": { "duplicate": "最新入站重複", "related": "同指紋重複", diff --git a/apps/web/src/app/[locale]/alerts/page.tsx b/apps/web/src/app/[locale]/alerts/page.tsx index 3485365e..9a8f48fe 100644 --- a/apps/web/src/app/[locale]/alerts/page.tsx +++ b/apps/web/src/app/[locale]/alerts/page.tsx @@ -200,6 +200,26 @@ function FocusIncidentEvidencePanel({ const sourceStatusLabel = statusLabels[sourceStatus] ?? valueOrEmpty(sourceStatus, emptyLabel) const mcpGateway = chain?.mcp?.gateway const ansible = chain?.execution?.ansible + const outcomeState = String(chain?.operator_outcome?.state ?? '') + const nextAction = String(chain?.operator_outcome?.next_action ?? chain?.next_step ?? '') + const humanReason = String(chain?.operator_outcome?.human_action_reason ?? '') + const sourceReasonCode = String(sourceCorrelation?.missing_reason ?? '') + const outcomeStateLabels: Record = { + verification_degraded_manual_required: t('operatorFlow.stateLabels.verificationDegradedManualRequired'), + } + const nextActionLabels: Record = { + manual_verify_or_repair: t('operatorFlow.nextActionLabels.manualVerifyOrRepair'), + } + const humanReasonLabels: Record = { + incident_open_after_successful_execution: t('operatorFlow.reasonLabels.incidentOpenAfterSuccessfulExecution'), + } + const sourceReasonLabels: Record = { + provider_heartbeat_present_but_no_incident_match: t('operatorFlow.sourceReasonLabels.providerHeartbeatPresentButNoIncidentMatch'), + } + const outcomeStateLabel = outcomeStateLabels[outcomeState] ?? valueOrEmpty(outcomeState, emptyLabel) + const nextActionLabel = nextActionLabels[nextAction] ?? valueOrEmpty(nextAction, emptyLabel) + const humanReasonLabel = humanReasonLabels[humanReason] ?? valueOrEmpty(humanReason, emptyLabel) + const sourceReasonLabel = sourceReasonLabels[sourceReasonCode] ?? valueOrEmpty(sourceReasonCode, emptyLabel) const relatedIncidentIds = chain?.source_refs?.refs?.incident_ids ?? [] const fingerprint = chain?.source_refs?.refs?.fingerprints?.[0] ?? emptyLabel const latestInbound = chain?.source_refs?.latest_inbound @@ -219,8 +239,49 @@ function FocusIncidentEvidencePanel({ const latestOutboundLabel = latestOutbound?.sent_at ? formatTimestamp(latestOutbound.sent_at, locale, emptyLabel) : emptyLabel - const sourceReason = valueOrEmpty(sourceCorrelation?.missing_reason, emptyLabel) const needsHumanLabel = chain?.needs_human ? t('operatorFlow.needsHumanYes') : t('operatorFlow.needsHumanNo') + const handoffTone = !chain ? 'gray' : chain.needs_human ? 'red' : 'green' + const handoffTitle = chain?.needs_human + ? t('operatorFlow.handoff.titleManual') + : chain + ? t('operatorFlow.handoff.titleAutomated') + : t('operatorFlow.handoff.titleUnknown') + const handoffAction = !chain + ? t('operatorFlow.handoff.actionUnknown') + : chain.needs_human + ? t('operatorFlow.handoff.actionManualVerifyOrRepair') + : t('operatorFlow.handoff.actionNoManual') + const handoffOwner = chain?.needs_human + ? t('operatorFlow.handoff.ownerSre') + : chain + ? t('operatorFlow.handoff.ownerAutomation') + : emptyLabel + const handoffEntry = chain?.needs_human + ? t('operatorFlow.handoff.entryManual') + : chain + ? t('operatorFlow.handoff.entryReadOnly') + : emptyLabel + const handoffReason = chain?.needs_human ? humanReasonLabel : valueOrEmpty(chain?.verdict, emptyLabel) + const handoffLinks = [ + { + key: 'workItems', + label: t('links.workItems'), + href: `/awooop/work-items?project_id=${encodedProjectId}&incident_id=${encodedIncidentId}` as never, + Icon: ListChecks, + }, + { + key: 'approvals', + label: t('links.approvals'), + href: `/awooop/approvals?project_id=${encodedProjectId}&incident_id=${encodedIncidentId}` as never, + Icon: ShieldCheck, + }, + { + key: 'runs', + label: t('links.runs'), + href: `/awooop/runs?project_id=${encodedProjectId}&incident_id=${encodedIncidentId}` as never, + Icon: Activity, + }, + ] const operatorCards = [ { key: 'repeat', @@ -259,8 +320,8 @@ function FocusIncidentEvidencePanel({ title: t('operatorFlow.aiTitle'), value: valueOrEmpty(chain?.operator_outcome?.summary_zh ?? chain?.verdict, emptyLabel), detail: t('operatorFlow.aiDetail', { - state: valueOrEmpty(chain?.operator_outcome?.state, emptyLabel), - nextStep: valueOrEmpty(chain?.operator_outcome?.next_action ?? chain?.next_step, emptyLabel), + state: outcomeStateLabel, + nextStep: nextActionLabel, needsHuman: needsHumanLabel, }), tone: chain?.needs_human ? 'red' : chain ? 'green' : 'gray', @@ -275,7 +336,7 @@ function FocusIncidentEvidencePanel({ direct: sourceCorrelation?.direct_ref_total ?? 0, candidate: sourceCorrelation?.candidate_total ?? 0, applied: sourceCorrelation?.applied_link_total ?? 0, - reason: sourceReason, + reason: sourceReasonLabel, }), tone: sourceStatus === 'linked' ? 'green' : sourceStatus === 'missing' ? 'red' : 'amber', testId: 'alerts-source-state', @@ -395,6 +456,53 @@ function FocusIncidentEvidencePanel({

{t('operatorFlow.title')}

{t('operatorFlow.subtitle')}

+
+
+
+

{t('operatorFlow.handoff.eyebrow')}

+

{handoffTitle}

+

{handoffAction}

+
+
+ {handoffLinks.map(({ key, label, href, Icon }) => ( + +
+
+
+
+

{t('operatorFlow.handoff.ownerLabel')}

+

{handoffOwner}

+
+
+

{t('operatorFlow.handoff.entryLabel')}

+

{handoffEntry}

+
+
+

{t('operatorFlow.handoff.reasonLabel')}

+

{handoffReason}

+
+
+

{t('operatorFlow.handoff.boundaryLabel')}

+

{t('operatorFlow.handoff.boundary')}

+
+
+
{operatorCards.map(({ key, Icon, title, value, detail, tone, testId }) => (