diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 9124a5d0..99ec5034 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -3683,6 +3683,25 @@ "learningValue": "KM {km}; AutoRepair {autoRepair}; Ops {ops}", "learningDetail": "verification={verification}; next={nextStep}" }, + "drilldown": { + "title": "單一 Incident 處理流程", + "step": "{step}. {label}", + "signal": "來源接收", + "signalDetail": "inbound={inbound}; outbound={outbound}; source={status}; reason={reason}", + "investigation": "MCP 調查", + "investigationValue": "success {success}/{total}", + "investigationDetail": "tools={tools}; failed={failed}; blocked={blocked}", + "playbook": "PlayBook / Ansible", + "playbookDetail": "candidates={candidates}; check/apply={check}/{apply}; approval={approval}", + "execution": "執行結果", + "executionValue": "{executor} / {status}", + "executionDetail": "operation={operation}; rc={rc}; mode={mode}", + "learning": "KM / Learning", + "learningValue": "KM {km}; autoRepair {autoRepair}", + "learningDetail": "verification={verification}; next={nextStep}", + "handoff": "人工 / 下一步", + "handoffDetail": "reason={reason}; next={nextAction}" + }, "source": { "status": "來源關聯", "verification": "狀態鏈驗證", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 9124a5d0..99ec5034 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -3683,6 +3683,25 @@ "learningValue": "KM {km}; AutoRepair {autoRepair}; Ops {ops}", "learningDetail": "verification={verification}; next={nextStep}" }, + "drilldown": { + "title": "單一 Incident 處理流程", + "step": "{step}. {label}", + "signal": "來源接收", + "signalDetail": "inbound={inbound}; outbound={outbound}; source={status}; reason={reason}", + "investigation": "MCP 調查", + "investigationValue": "success {success}/{total}", + "investigationDetail": "tools={tools}; failed={failed}; blocked={blocked}", + "playbook": "PlayBook / Ansible", + "playbookDetail": "candidates={candidates}; check/apply={check}/{apply}; approval={approval}", + "execution": "執行結果", + "executionValue": "{executor} / {status}", + "executionDetail": "operation={operation}; rc={rc}; mode={mode}", + "learning": "KM / Learning", + "learningValue": "KM {km}; autoRepair {autoRepair}", + "learningDetail": "verification={verification}; next={nextStep}", + "handoff": "人工 / 下一步", + "handoffDetail": "reason={reason}; next={nextAction}" + }, "source": { "status": "來源關聯", "verification": "狀態鏈驗證", diff --git a/apps/web/src/components/awooop/status-chain.tsx b/apps/web/src/components/awooop/status-chain.tsx index d1f8bf52..d5dc02e8 100644 --- a/apps/web/src/components/awooop/status-chain.tsx +++ b/apps/web/src/components/awooop/status-chain.tsx @@ -286,6 +286,8 @@ export function AwoooPStatusChainPanel({ const sourceStatus = String(sourceCorrelation?.status ?? "missing"); const sourceVerificationStatus = String(sourceCorrelation?.verification_status ?? sourceStatus); const sourceMissingReason = String(sourceCorrelation?.missing_reason ?? ""); + const sourceStatusLabel = sourceStatusLabels[sourceStatus] ?? valueOrEmpty(sourceStatus, emptyLabel); + const sourceReasonLabel = sourceReasonLabels[sourceMissingReason] ?? valueOrEmpty(sourceMissingReason, emptyLabel); const topAppliedLink = sourceCorrelation?.top_candidates?.find( (item) => item.link_state === "applied" ); @@ -356,6 +358,11 @@ export function AwoooPStatusChainPanel({ const mcpGateway = chain.mcp?.gateway ?? {}; const legacyMcp = chain.mcp?.legacy ?? {}; const topTool = chain.mcp?.top_tools?.[0]; + const topToolNames = chain.mcp?.top_tools + ?.slice(0, 3) + .map((item) => item.tool_name) + .filter(Boolean) + .join(" / ") || emptyLabel; const execution = chain.execution ?? {}; const ansible = execution.ansible ?? {}; const candidatePlaybook = ansible.candidate_playbooks?.[0]; @@ -399,14 +406,14 @@ export function AwoooPStatusChainPanel({ tone: sourceToolchainTone, label: t("toolchain.source"), value: t("toolchain.sourceValue", { - status: sourceStatusLabels[sourceStatus] ?? valueOrEmpty(sourceStatus, emptyLabel), + status: sourceStatusLabel, direct: directRefTotal, candidate: candidateTotal, applied: appliedLinkTotal, }), detail: t("toolchain.sourceDetail", { providers: sourceProviderSummary || emptyLabel, - reason: sourceReasonLabels[sourceMissingReason] ?? valueOrEmpty(sourceMissingReason, emptyLabel), + reason: sourceReasonLabel, }), }, { @@ -465,6 +472,95 @@ export function AwoooPStatusChainPanel({ }), }, ]; + const drilldownItems = [ + { + key: "signal", + Icon: Link2, + tone: (sourceCorrelation ? (providerIngressReady ? "success" : "warning") : "neutral") as SourceFlowTone, + title: t("drilldown.signal"), + value: valueOrEmpty(chain.source_id ?? chain.incident_ids?.[0], emptyLabel), + detail: t("drilldown.signalDetail", { + inbound: chain.source_refs?.inbound_total ?? 0, + outbound: chain.source_refs?.outbound_total ?? 0, + status: sourceStatusLabel, + reason: sourceReasonLabel, + }), + }, + { + key: "investigation", + Icon: RadioTower, + tone: (mcpGatewayTotal > 0 + ? (mcpGatewayProblemTotal > 0 ? "warning" : "success") + : "neutral") as SourceFlowTone, + title: t("drilldown.investigation"), + value: t("drilldown.investigationValue", { + success: mcpGateway.success ?? 0, + total: mcpGatewayTotal, + }), + detail: t("drilldown.investigationDetail", { + tools: topToolNames, + failed: mcpGateway.failed ?? 0, + blocked: mcpGateway.blocked ?? 0, + }), + }, + { + key: "playbook", + Icon: Wrench, + tone: (ansible.considered || (ansible.candidate_count ?? 0) > 0 + ? (ansible.applied || (ansible.apply_total ?? 0) > 0 ? "success" : "warning") + : "neutral") as SourceFlowTone, + title: t("drilldown.playbook"), + value: selectedPlaybook, + detail: t("drilldown.playbookDetail", { + candidates: ansible.candidate_count ?? 0, + check: ansible.check_mode_total ?? 0, + apply: ansible.apply_total ?? 0, + approval: valueOrEmpty(ansible.approval_source, emptyLabel), + }), + }, + { + key: "execution", + Icon: Activity, + tone: (executionTotal > 0 + ? (String(execution.latest_status ?? "").toLowerCase().includes("fail") ? "blocked" : "success") + : "neutral") as SourceFlowTone, + title: t("drilldown.execution"), + value: t("drilldown.executionValue", { + executor: execution.latest_executor ?? emptyLabel, + status: execution.latest_status ?? emptyLabel, + }), + detail: t("drilldown.executionDetail", { + operation: execution.latest_operation_type ?? emptyLabel, + rc: valueOrEmpty(ansible.latest_returncode, emptyLabel), + mode: valueOrEmpty(ansible.latest_execution_mode, emptyLabel), + }), + }, + { + key: "learning", + Icon: BookOpenCheck, + tone: ((evidence.knowledge_entries ?? 0) > 0 ? "success" : "neutral") as SourceFlowTone, + title: t("drilldown.learning"), + value: t("drilldown.learningValue", { + km: evidence.knowledge_entries ?? 0, + autoRepair: evidence.auto_repair_records ?? 0, + }), + detail: t("drilldown.learningDetail", { + verification: valueOrEmpty(chain.verification, emptyLabel), + nextStep: valueOrEmpty(chain.next_step, emptyLabel), + }), + }, + { + key: "handoff", + Icon: chain.needs_human ? TriangleAlert : CheckCircle2, + tone: (chain.needs_human ? "blocked" : "success") as SourceFlowTone, + title: t("drilldown.handoff"), + value: chain.needs_human ? t("human.yes") : t("human.no"), + detail: t("drilldown.handoffDetail", { + reason: valueOrEmpty(outcome?.human_action_reason, emptyLabel), + nextAction: valueOrEmpty(outcome?.next_action ?? chain.next_step, emptyLabel), + }), + }, + ]; return (
@@ -663,6 +759,39 @@ export function AwoooPStatusChainPanel({ )} + {!compact && ( +
+
+

{t("drilldown.title")}

+
+
+ {drilldownItems.map((item, index) => ( +
+
+ + +
+

+ {t("drilldown.step", { step: index + 1, label: item.title })} +

+

+ {item.value} +

+
+
+

+ {item.detail} +

+
+ ))} +
+
+ )} + {blockers.length > 0 && (
{t("blockers")}{" "}