feat(web): expose AwoooP run operator status chain
All checks were successful
CD Pipeline / tests (push) Successful in 1m31s
Code Review / ai-code-review (push) Successful in 13s
CD Pipeline / build-and-deploy (push) Successful in 3m34s
CD Pipeline / post-deploy-checks (push) Successful in 1m56s

This commit is contained in:
Your Name
2026-06-04 19:30:41 +08:00
parent 75c1b113d5
commit d99e7366b9
3 changed files with 130 additions and 8 deletions

View File

@@ -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": "已套用",

View File

@@ -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": "已套用",

View File

@@ -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 (
<div className="min-w-[190px]">
<span className={cn(
"inline-flex items-center gap-1.5 border px-2 py-1 text-xs font-semibold",
sourceFlowListClass(status)
)}>
<Icon className="h-3.5 w-3.5" aria-hidden="true" />
{t(`statuses.${status}`)}
</span>
<div className="min-w-[280px] max-w-[340px]">
<div className="flex flex-wrap gap-1.5">
<span className={cn(
"inline-flex items-center gap-1.5 border px-2 py-1 text-xs font-semibold",
sourceFlowListClass(status)
)}>
<Icon className="h-3.5 w-3.5" aria-hidden="true" />
{t(`statuses.${status}`)}
</span>
<span className={cn(
"inline-flex items-center gap-1.5 border px-2 py-1 text-xs font-semibold",
operatorFlowListClass(operatorStatus)
)}>
<OperatorIcon className="h-3.5 w-3.5" aria-hidden="true" />
{t(`operator.statuses.${operatorStatus}`)}
</span>
</div>
<p className="mt-1 truncate font-mono text-xs text-[#77736a]" title={detail}>
{detail}
</p>
{chain && (
<>
<p className="mt-1 truncate text-xs font-semibold text-[#141413]" title={shortValue(outcomeSummary)}>
{shortValue(outcomeSummary)}
</p>
<p className="mt-1 truncate font-mono text-xs text-[#9f2f25]" title={blockerLine}>
{blockerLine}
</p>
<div className="mt-1 grid gap-0.5 font-mono text-[11px] leading-4 text-[#5f5b52]">
<span className="truncate" title={mcpLine}>{mcpLine}</span>
<span className="truncate" title={ansibleLine}>{ansibleLine}</span>
<span className="truncate" title={kmLine}>{kmLine}</span>
</div>
</>
)}
</div>
);
}