feat(web): surface awooop agent evidence chain
All checks were successful
CD Pipeline / tests (push) Successful in 1m22s
Code Review / ai-code-review (push) Successful in 12s
CD Pipeline / build-and-deploy (push) Successful in 3m28s
CD Pipeline / post-deploy-checks (push) Successful in 1m18s

This commit is contained in:
Your Name
2026-05-25 15:57:56 +08:00
parent 683984dc47
commit 48a31ea2b9
4 changed files with 214 additions and 1 deletions

View File

@@ -1,6 +1,6 @@
"use client";
import { Activity, CheckCircle2, Link2, RadioTower, Route, ShieldAlert, TriangleAlert } from "lucide-react";
import { Activity, BookOpenCheck, CheckCircle2, Link2, RadioTower, Route, ShieldAlert, TriangleAlert, Wrench } from "lucide-react";
import { useTranslations } from "next-intl";
import { cn } from "@/lib/utils";
@@ -316,6 +316,110 @@ export function AwoooPStatusChainPanel({
}),
},
] : [];
const mcpGateway = chain.mcp?.gateway ?? {};
const legacyMcp = chain.mcp?.legacy ?? {};
const topTool = chain.mcp?.top_tools?.[0];
const execution = chain.execution ?? {};
const ansible = execution.ansible ?? {};
const candidatePlaybook = ansible.candidate_playbooks?.[0];
const selectedPlaybook = execution.playbook_paths?.[0]
?? execution.playbook_ids?.[0]
?? ansible.latest_playbook_path
?? candidatePlaybook?.playbook_path
?? candidatePlaybook?.catalog_id
?? emptyLabel;
const mcpGatewayTotal = mcpGateway.total ?? 0;
const mcpGatewayProblemTotal = (mcpGateway.failed ?? 0) + (mcpGateway.blocked ?? 0);
const executionTotal = execution.operation_total ?? 0;
const sourceToolchainTone: SourceFlowTone = sourceCorrelation
? (sourceLinkVerified ? "success" : (sourceVerificationBlocked ? "blocked" : "warning"))
: "neutral";
const toolchainItems = [
{
key: "mcp",
Icon: RadioTower,
tone: (mcpGatewayTotal > 0
? (mcpGatewayProblemTotal > 0 ? "warning" : "success")
: "neutral") as SourceFlowTone,
label: t("toolchain.mcp"),
value: t("toolchain.mcpValue", {
success: mcpGateway.success ?? 0,
total: mcpGatewayTotal,
failed: mcpGateway.failed ?? 0,
blocked: mcpGateway.blocked ?? 0,
}),
detail: t("toolchain.mcpDetail", {
topTool: topTool?.tool_name ?? emptyLabel,
firstClass: mcpGateway.first_class_total ?? 0,
legacy: legacyMcp.total ?? 0,
policy: mcpGateway.policy_enforced_total ?? 0,
}),
},
{
key: "source",
Icon: Link2,
tone: sourceToolchainTone,
label: t("toolchain.source"),
value: t("toolchain.sourceValue", {
status: sourceStatusLabels[sourceStatus] ?? valueOrEmpty(sourceStatus, emptyLabel),
direct: directRefTotal,
candidate: candidateTotal,
applied: appliedLinkTotal,
}),
detail: t("toolchain.sourceDetail", {
providers: sourceProviderSummary || emptyLabel,
}),
},
{
key: "execution",
Icon: Activity,
tone: (executionTotal > 0
? (String(execution.latest_status ?? "").toLowerCase().includes("fail") ? "blocked" : "success")
: "neutral") as SourceFlowTone,
label: t("toolchain.execution"),
value: t("toolchain.executionValue", {
executor: execution.latest_executor ?? emptyLabel,
status: execution.latest_status ?? emptyLabel,
}),
detail: t("toolchain.executionDetail", {
operation: execution.latest_operation_type ?? emptyLabel,
action: execution.latest_action ?? emptyLabel,
ops: executionTotal,
}),
},
{
key: "playbook",
Icon: Wrench,
tone: (ansible.considered || (ansible.candidate_count ?? 0) > 0
? (ansible.latest_status === "applied" ? "success" : "warning")
: "neutral") as SourceFlowTone,
label: t("toolchain.playbook"),
value: selectedPlaybook,
detail: t("toolchain.playbookDetail", {
ansible: boolValue(ansible.considered, emptyLabel),
candidates: ansible.candidate_count ?? 0,
checkMode: valueOrEmpty(ansible.latest_check_mode, emptyLabel),
status: ansible.latest_status ?? emptyLabel,
}),
},
{
key: "learning",
Icon: BookOpenCheck,
tone: ((evidence.knowledge_entries ?? 0) > 0
? ((evidence.auto_repair_records ?? 0) > 0 ? "success" : "warning")
: "neutral") as SourceFlowTone,
label: t("toolchain.learning"),
value: t("toolchain.learningValue", {
km: evidence.knowledge_entries ?? 0,
autoRepair: evidence.auto_repair_records ?? 0,
ops: evidence.operation_records ?? 0,
}),
detail: t("toolchain.learningDetail", {
verification: valueOrEmpty(chain.verification, emptyLabel),
nextStep: valueOrEmpty(chain.next_step, emptyLabel),
}),
},
];
return (
<section className={cn("border border-[#e0ddd4] bg-white", className)}>
@@ -388,6 +492,33 @@ export function AwoooPStatusChainPanel({
</div>
</div>
<div className="border-t border-[#e0ddd4] bg-[#faf9f3] px-4 py-2">
<p className="text-xs font-semibold text-[#141413]">{t("toolchain.title")}</p>
</div>
<div className={cn("grid gap-px bg-[#e0ddd4]", compact ? "md:grid-cols-2" : "md:grid-cols-5")}>
{toolchainItems.map((item) => (
<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]">{item.label}</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>
{sourceCorrelation && (
<div>
<div className={cn("grid gap-px bg-[#e0ddd4]", compact ? "grid-cols-1" : "md:grid-cols-3")}>