diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 357e5fc0..21a76e4b 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -3944,6 +3944,20 @@ "column": "來源流程", "notLinked": "尚未關聯 incident", "detail": "providers={providers}; d/c/a={direct}/{candidate}/{applied}", + "nextLine": "next={next}", + "blockedLine": "blocked={blocker}", + "mcpLine": "MCP {success}/{total}; failed={failed}; blocked={blocked}", + "ansibleLine": "Ansible candidates={candidates}; apply={applied}; reason={reason}", + "kmLine": "KM entries={count}", + "operator": { + "statuses": { + "needsHuman": "Needs human", + "failed": "Execution failed", + "verified": "Verified", + "executed": "Executed", + "waiting": "Pending" + } + }, "statuses": { "verified": "已驗證", "applied": "已套用", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 357e5fc0..a1d1a624 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -3944,6 +3944,20 @@ "column": "來源流程", "notLinked": "尚未關聯 incident", "detail": "providers={providers}; d/c/a={direct}/{candidate}/{applied}", + "nextLine": "next={next}", + "blockedLine": "blocked={blocker}", + "mcpLine": "MCP {success}/{total}; failed={failed}; blocked={blocked}", + "ansibleLine": "Ansible candidates={candidates}; apply={applied}; reason={reason}", + "kmLine": "KM entries={count}", + "operator": { + "statuses": { + "needsHuman": "需人工", + "failed": "執行失敗", + "verified": "已驗證", + "executed": "已執行", + "waiting": "待判讀" + } + }, "statuses": { "verified": "已驗證", "applied": "已套用", diff --git a/apps/web/src/app/[locale]/awooop/runs/page.tsx b/apps/web/src/app/[locale]/awooop/runs/page.tsx index 78242791..f0e63bb3 100644 --- a/apps/web/src/app/[locale]/awooop/runs/page.tsx +++ b/apps/web/src/app/[locale]/awooop/runs/page.tsx @@ -1693,6 +1693,7 @@ function IncidentIdsCell({ run }: { run: Run }) { } type SourceFlowListStatus = "verified" | "applied" | "evidence" | "provider" | "waiting" | "loading"; +type OperatorFlowListStatus = "needsHuman" | "failed" | "verified" | "executed" | "waiting"; function sourceFlowListStatus(chain?: AwoooPStatusChain | null): SourceFlowListStatus { const correlation = chain?.source_refs?.correlation; @@ -1716,6 +1717,45 @@ function sourceFlowListClass(status: SourceFlowListStatus) { return "border-[#d8d3c7] bg-[#faf9f3] text-[#5f5b52]"; } +function operatorFlowListStatus(chain?: AwoooPStatusChain | null): OperatorFlowListStatus { + if (!chain) return "waiting"; + const outcomeState = String(chain.operator_outcome?.state ?? "").toLowerCase(); + const completionStatus = String(chain.operator_outcome?.execution_result?.completion_status ?? "").toLowerCase(); + const commandStatus = String(chain.operator_outcome?.execution_result?.command_status ?? "").toLowerCase(); + const repairState = String(chain.repair_state ?? "").toLowerCase(); + const verification = String(chain.verification ?? "").toLowerCase(); + const stageStatus = String(chain.stage_status ?? "").toLowerCase(); + + if (chain.needs_human) return "needsHuman"; + if ( + outcomeState.includes("failed") || + completionStatus.includes("failed") || + commandStatus.includes("failed") || + stageStatus === "error" + ) { + return "failed"; + } + if (repairState === "auto_repaired_verified" || verification === "verified") { + return "verified"; + } + if (repairState === "executed" || completionStatus === "completed" || stageStatus === "success") { + return "executed"; + } + return "waiting"; +} + +function operatorFlowListClass(status: OperatorFlowListStatus) { + if (status === "verified") return "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]"; + if (status === "executed") return "border-[#9bb6d9] bg-[#eef5ff] text-[#1f5b9b]"; + if (status === "failed" || status === "needsHuman") return "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]"; + return "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]"; +} + +function shortValue(value?: string | number | boolean | null) { + if (value === null || value === undefined || value === "") return "--"; + return String(value); +} + function SourceFlowCell({ chain, hasIncident, @@ -1727,7 +1767,34 @@ function SourceFlowCell({ }) { const t = useTranslations("awooop.listEvidence.sourceFlow"); const status = loading && hasIncident ? "loading" : sourceFlowListStatus(chain); + const operatorStatus = operatorFlowListStatus(chain); const correlation = chain?.source_refs?.correlation; + const outcome = chain?.operator_outcome; + const executionResult = outcome?.execution_result; + const gateway = chain?.mcp?.gateway; + const ansible = chain?.execution?.ansible; + const firstBlocker = chain?.blockers?.[0]; + const nextAction = outcome?.next_action ?? chain?.next_step; + const outcomeSummary = outcome?.summary_zh + ?? executionResult?.summary_zh + ?? (chain ? `${shortValue(chain.current_stage)} / ${shortValue(chain.stage_status)}` : null); + const blockerLine = firstBlocker + ? t("blockedLine", { blocker: firstBlocker }) + : t("nextLine", { next: shortValue(nextAction) }); + const mcpLine = t("mcpLine", { + success: gateway?.success ?? 0, + total: gateway?.total ?? 0, + failed: gateway?.failed ?? 0, + blocked: gateway?.blocked ?? 0, + }); + const ansibleLine = t("ansibleLine", { + candidates: ansible?.candidate_count ?? 0, + applied: ansible?.apply_total ?? 0, + reason: shortValue(ansible?.not_used_reason), + }); + const kmLine = t("kmLine", { + count: chain?.evidence?.knowledge_entries ?? 0, + }); const detail = !hasIncident ? t("notLinked") : t("detail", { @@ -1737,19 +1804,46 @@ function SourceFlowCell({ applied: correlation?.applied_link_total ?? 0, }); const Icon = status === "verified" ? ShieldCheck : (status === "waiting" ? BellOff : SearchCheck); + const OperatorIcon = operatorStatus === "needsHuman" || operatorStatus === "failed" + ? TriangleAlert + : (operatorStatus === "verified" ? ShieldCheck : SearchCheck); return ( -
- - +
+
+ + + + +

{detail}

+ {chain && ( + <> +

+ {shortValue(outcomeSummary)} +

+

+ {blockerLine} +

+
+ {mcpLine} + {ansibleLine} + {kmLine} +
+ + )}
); }