From 4ee3998f03100a63e56d8a9982be6850a65e8e20 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 1 Jun 2026 11:52:47 +0800 Subject: [PATCH] feat(web): visualize awooop automation flow --- apps/web/messages/en.json | 14 + apps/web/messages/zh-TW.json | 14 + .../web/src/app/[locale]/awooop/runs/page.tsx | 414 ++++++++++++------ 3 files changed, 309 insertions(+), 133 deletions(-) diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 4c406aa4..b7e4782a 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -4181,11 +4181,24 @@ "claimReady": "可以宣稱全自動修復", "claimBlocked": "不可宣稱全自動修復", "claimReason": "原因:{reason}", + "mapTitle": "Automation Flow Map", + "mapSubtitle": "依照真實 truth-chain gate,把告警到 Operator 可見性切成 8 個節點。", + "statusCount": "{status} {count}", + "attentionTitle": "優先瓶頸", + "attentionAction": "下一步:{action}", + "heatmapTitle": "Gate Evidence Heatmap", + "bottleneckTitle": "目前瓶頸", "coverage": "{percent}% 通過", "counts": "pass {passed} / warn {warning} / miss {missing} / fail {failed}", "nextAction": "下一步:{action}", "example": "例:{incidentId} / {verdict}", "sourceStatuses": "來源 Gate:{statuses}", + "heatmap": { + "pass": "Pass", + "warn": "Warn", + "miss": "Miss", + "fail": "Fail" + }, "statuses": { "passed": "Passed", "warning": "Warning", @@ -4204,6 +4217,7 @@ "verifiedRepairDetail": "只有修復成功且完成事後驗證才計入。", "blockedGates": "Blocked Gate", "blockedGatesDetail": "有 missing / failed 就不能宣稱完整自動化。", + "readiness": "流程健康度", "warningGates": "Warning Gate", "warningGatesDetail": "仍可觀測,但需要補脈絡或人工判斷。" }, diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 4c406aa4..ca6040ee 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -4181,11 +4181,24 @@ "claimReady": "可以宣稱全自動修復", "claimBlocked": "不可宣稱全自動修復", "claimReason": "原因:{reason}", + "mapTitle": "Automation Flow Map", + "mapSubtitle": "依照真實 truth-chain gate,把告警到可見性的流程畫成 8 個節點。", + "statusCount": "{status} {count}", + "attentionTitle": "優先處理", + "attentionAction": "下一步:{action}", + "heatmapTitle": "Gate Evidence Heatmap", + "bottleneckTitle": "目前瓶頸", "coverage": "{percent}% 通過", "counts": "pass {passed} / warn {warning} / miss {missing} / fail {failed}", "nextAction": "下一步:{action}", "example": "例:{incidentId} / {verdict}", "sourceStatuses": "來源 Gate:{statuses}", + "heatmap": { + "pass": "Pass", + "warn": "Warn", + "miss": "Miss", + "fail": "Fail" + }, "statuses": { "passed": "Passed", "warning": "Warning", @@ -4204,6 +4217,7 @@ "verifiedRepairDetail": "只有修復成功且完成事後驗證才計入。", "blockedGates": "Blocked Gate", "blockedGatesDetail": "有 missing / failed 就不能宣稱完整自動化。", + "readiness": "流程健康度", "warningGates": "Warning Gate", "warningGatesDetail": "仍可觀測,但需要補脈絡或人工判斷。" }, diff --git a/apps/web/src/app/[locale]/awooop/runs/page.tsx b/apps/web/src/app/[locale]/awooop/runs/page.tsx index 3b808a39..5f0e578c 100644 --- a/apps/web/src/app/[locale]/awooop/runs/page.tsx +++ b/apps/web/src/app/[locale]/awooop/runs/page.tsx @@ -2869,6 +2869,27 @@ function automationFlowStatusClass(status?: AutomationFlowStatus | null) { return "border-[#d8d3c7] bg-[#faf9f3] text-[#5f5b52]"; } +function automationFlowStatusFill(status?: AutomationFlowStatus | null) { + if (status === "passed") return "bg-[#4f9d5f]"; + if (status === "warning") return "bg-[#c58a24]"; + if (status === "blocked") return "bg-[#c65145]"; + return "bg-[#b8b2a7]"; +} + +function automationFlowStatusSurface(status?: AutomationFlowStatus | null) { + if (status === "passed") return "border-[#9bc7a4] bg-[#f4fbf5]"; + if (status === "warning") return "border-[#d9b36f] bg-[#fff8ea]"; + if (status === "blocked") return "border-[#e2a29b] bg-[#fff5f3]"; + return "border-[#d8d3c7] bg-[#faf9f3]"; +} + +function automationFlowStatusText(status?: AutomationFlowStatus | null) { + if (status === "passed") return "text-[#17602a]"; + if (status === "warning") return "text-[#8a5a08]"; + if (status === "blocked") return "text-[#9f2f25]"; + return "text-[#5f5b52]"; +} + function automationFlowStatusLabelKey(status?: AutomationFlowStatus | null) { if ( status === "passed" || @@ -2911,6 +2932,34 @@ function AutomationFlowGatePanel({ const claimReasonLabel = claimReasonKey ? t(`claimReasons.${claimReasonKey}` as never) : claimReason; + const gates = flow?.gates ?? []; + const readiness = gates.length + ? Math.round(gates.reduce((acc, gate) => acc + (gate.passed_percent ?? 0), 0) / gates.length) + : 0; + const statusCounts = gates.reduce( + (acc, gate) => { + if (gate.status === "passed") acc.passed += 1; + else if (gate.status === "warning") acc.warning += 1; + else if (gate.status === "blocked") acc.blocked += 1; + return acc; + }, + { passed: 0, warning: 0, blocked: 0 } + ); + const blockedGates = gates.filter((gate) => gate.status === "blocked"); + const warningGates = gates.filter((gate) => gate.status === "warning"); + const firstAttentionGate = blockedGates[0] ?? warningGates[0] ?? null; + const attentionGateLabelKey = firstAttentionGate + ? AUTOMATION_FLOW_GATE_LABEL_KEYS[firstAttentionGate.gate] + : null; + const attentionActionKey = firstAttentionGate?.next_action + ? AUTOMATION_FLOW_ACTION_KEYS[firstAttentionGate.next_action] + : null; + const attentionGateLabel = attentionGateLabelKey + ? t(`gates.${attentionGateLabelKey}` as never) + : firstAttentionGate?.gate ?? "--"; + const attentionActionLabel = attentionActionKey + ? t(`actions.${attentionActionKey}` as never) + : firstAttentionGate?.next_action ?? "--"; return (
@@ -2950,146 +2999,245 @@ function AutomationFlowGatePanel({ ) : ( <> -
-
- {claimReady ? ( -
-

{t("claimReason", { reason: claimReasonLabel })}

-
- -
- {[ - { - label: t("metrics.evaluated"), - value: flow.evaluated_total, - detail: t("metrics.evaluatedDetail", { - incidents: summary.incident_total ?? 0, - }), - }, - { - label: t("metrics.verifiedRepair"), - value: summary.verified_auto_repair_total ?? 0, - detail: t("metrics.verifiedRepairDetail"), - }, - { - label: t("metrics.blockedGates"), - value: flow.blocked_gates?.length ?? 0, - detail: t("metrics.blockedGatesDetail"), - }, - { - label: t("metrics.warningGates"), - value: flow.warning_gates?.length ?? 0, - detail: t("metrics.warningGatesDetail"), - }, - ].map((item) => ( -
-

{item.label}

-

- {item.value} -

-

{item.detail}

-
- ))} -
- -
- {flow.gates.map((gate) => { - const StatusIcon = automationFlowStatusIcon(gate.status); - const gateLabelKey = AUTOMATION_FLOW_GATE_LABEL_KEYS[gate.gate]; - const gateLabel = gateLabelKey - ? t(`gates.${gateLabelKey}` as never) - : gate.gate; - const actionKey = gate.next_action - ? AUTOMATION_FLOW_ACTION_KEYS[gate.next_action] - : null; - const actionLabel = actionKey - ? t(`actions.${actionKey}` as never) - : gate.next_action ?? "--"; - const passedPercent = Math.max( - 0, - Math.min(100, Number.isFinite(gate.passed_percent) ? gate.passed_percent : 0) - ); - const example = gate.examples?.[0]; - const sourceStatuses = Object.entries(example?.source_statuses ?? {}) - .map(([key, value]) => `${key}:${value}`) - .join(", "); - - return ( -
-
-
-

{gateLabel}

-

- {gate.quality_gates?.join(" / ") || gate.gate} -

-
- - -
-
-
-
-

- {t("coverage", { percent: passedPercent.toFixed(1) })} +

+
+
+
+

+ {t("mapTitle")}

- {t("counts", { - passed: gate.passed_total ?? 0, - warning: gate.warning_total ?? 0, - missing: gate.missing_total ?? 0, - failed: gate.failed_total ?? 0, - })} + {t("mapSubtitle")}

-

- {t("nextAction", { action: actionLabel })} -

- {example ? ( -
-

- {t("example", { - incidentId: example.incident_id ?? "--", - verdict: example.verdict ?? "--", - })} +

+
+ {(["passed", "warning", "blocked"] as const).map((status) => ( + + + {t("statusCount", { + status: t(automationFlowStatusLabelKey(status) as never), + count: statusCounts[status], + })} + + ))} +
+
+ +
+
+ {gates.map((gate, index) => { + const StatusIcon = automationFlowStatusIcon(gate.status); + const gateLabelKey = AUTOMATION_FLOW_GATE_LABEL_KEYS[gate.gate]; + const gateLabel = gateLabelKey + ? t(`gates.${gateLabelKey}` as never) + : gate.gate; + const passedPercent = Math.max( + 0, + Math.min(100, Number.isFinite(gate.passed_percent) ? gate.passed_percent : 0) + ); + return ( +
+ {index < gates.length - 1 ? ( +
+
+
+ +
+
+
+ {claimReady ? ( +
+

{t("claimReason", { reason: claimReasonLabel })}

+
+ +
+ {[ + { label: t("metrics.evaluated"), value: flow.evaluated_total }, + { label: t("metrics.verifiedRepair"), value: summary.verified_auto_repair_total ?? 0 }, + { label: t("metrics.blockedGates"), value: blockedGates.length }, + { label: t("metrics.readiness"), value: `${readiness}%` }, + ].map((item) => ( +
+

{item.label}

+

{item.value}

+
+ ))} +
+ +
+

{t("attentionTitle")}

+

{attentionGateLabel}

+

+ {t("attentionAction", { action: attentionActionLabel })} +

+
+
+
+ +
+
+
+

+ {t("heatmapTitle")} +

+
+ {t("heatmap.pass")} + {t("heatmap.warn")} + {t("heatmap.miss")} + {t("heatmap.fail")} +
+
+
+ {gates.map((gate) => { + const gateLabelKey = AUTOMATION_FLOW_GATE_LABEL_KEYS[gate.gate]; + const gateLabel = gateLabelKey + ? t(`gates.${gateLabelKey}` as never) + : gate.gate; + const total = Math.max(1, gate.evaluated_total ?? 1); + const segments = [ + { key: "pass", value: gate.passed_total ?? 0, className: "bg-[#4f9d5f]" }, + { key: "warn", value: gate.warning_total ?? 0, className: "bg-[#c58a24]" }, + { key: "miss", value: gate.missing_total ?? 0, className: "bg-[#c65145]" }, + { key: "fail", value: gate.failed_total ?? 0, className: "bg-[#7d2018]" }, + ]; + return ( +
+
+ + {gateLabel} +
+
+ {segments.map((segment) => ( +
+ ))} +
+
+ {gate.passed_total ?? 0} + {gate.warning_total ?? 0} + {gate.missing_total ?? 0} + {gate.failed_total ?? 0} +
+
+ ); + })} +
+
+ +
+

+ {t("bottleneckTitle")} +

+
+ {(blockedGates.length ? blockedGates : warningGates).slice(0, 4).map((gate) => { + const gateLabelKey = AUTOMATION_FLOW_GATE_LABEL_KEYS[gate.gate]; + const gateLabel = gateLabelKey + ? t(`gates.${gateLabelKey}` as never) + : gate.gate; + const actionKey = gate.next_action + ? AUTOMATION_FLOW_ACTION_KEYS[gate.next_action] + : null; + const actionLabel = actionKey + ? t(`actions.${actionKey}` as never) + : gate.next_action ?? "--"; + const example = gate.examples?.[0]; + return ( +
+
+

{gateLabel}

+ + {t(automationFlowStatusLabelKey(gate.status) as never)} + +
+

+ {t("nextAction", { action: actionLabel })}

- {sourceStatuses ? ( -

- {t("sourceStatuses", { statuses: sourceStatuses })} + {example ? ( +

+ {example.incident_id ?? "--"} / {example.verdict ?? "--"}

) : null}
- ) : null} -
- ); - })} + ); + })} +
+ )}