fix(web): add incident drilldown flow to status chain
Some checks failed
CD Pipeline / tests (push) Successful in 1m16s
Code Review / ai-code-review (push) Successful in 13s
CD Pipeline / build-and-deploy (push) Successful in 3m18s
CD Pipeline / post-deploy-checks (push) Has been cancelled

This commit is contained in:
Your Name
2026-05-31 17:23:41 +08:00
parent ccea510e87
commit 88f196a040
3 changed files with 169 additions and 2 deletions

View File

@@ -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": "狀態鏈驗證",

View File

@@ -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": "狀態鏈驗證",

View File

@@ -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 (
<section className={cn("border border-[#e0ddd4] bg-white", className)}>
@@ -663,6 +759,39 @@ export function AwoooPStatusChainPanel({
</div>
)}
{!compact && (
<div>
<div className="border-t border-[#e0ddd4] bg-[#faf9f3] px-4 py-2">
<p className="text-xs font-semibold text-[#141413]">{t("drilldown.title")}</p>
</div>
<div className="grid gap-px bg-[#e0ddd4] md:grid-cols-2 xl:grid-cols-3">
{drilldownItems.map((item, index) => (
<div key={item.key} className="min-w-0 bg-white px-4 py-3">
<div className="flex min-w-0 items-start gap-3">
<span className={cn(
"flex h-8 w-8 shrink-0 items-center justify-center border",
sourceFlowToneClass(item.tone)
)}>
<item.Icon className="h-4 w-4" aria-hidden="true" />
</span>
<div className="min-w-0">
<p className="text-xs font-semibold text-[#77736a]">
{t("drilldown.step", { step: index + 1, label: item.title })}
</p>
<p className="mt-1 truncate font-mono text-sm font-semibold text-[#141413]" title={item.value}>
{item.value}
</p>
</div>
</div>
<p className="mt-2 truncate font-mono text-xs text-[#5f5b52]" title={item.detail}>
{item.detail}
</p>
</div>
))}
</div>
</div>
)}
{blockers.length > 0 && (
<div className="border-t border-[#eee9dd] bg-[#fff7e8] px-4 py-3 text-xs leading-5 text-[#8a5a08]">
<span className="font-semibold">{t("blockers")}</span>{" "}