fix(web): 在審批佇列顯示資產沉澱矩陣
Some checks failed
CD Pipeline / build-and-deploy (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
Code Review / ai-code-review (push) Has been cancelled
CD Pipeline / tests (push) Has been cancelled

This commit is contained in:
Your Name
2026-06-18 15:20:31 +08:00
parent 8548892f59
commit dafe534259
3 changed files with 132 additions and 1 deletions

View File

@@ -9718,6 +9718,24 @@
"gate5Projection": "Gate 5 投影",
"executorHandoffPending": "等待 executor handoff"
},
"assetLedger": {
"column": "資產沉澱",
"title": "資產沉澱",
"summary": "完成 {ready} / 卡點 {blocked}",
"boundary": "只讀推導;不代表已寫入 KM、更新 PlayBook trust、套用腳本 / 排程或執行 verifier。",
"items": {
"km": "KM",
"playbook": "PlayBook",
"script": "腳本",
"schedule": "排程",
"verifier": "Verifier"
},
"values": {
"ready": "ready",
"pending": "pending",
"blocked": "blocked"
}
},
"columns": {
"runId": "執行 ID",
"projectId": "專案 ID",

View File

@@ -9718,6 +9718,24 @@
"gate5Projection": "Gate 5 投影",
"executorHandoffPending": "等待 executor handoff"
},
"assetLedger": {
"column": "資產沉澱",
"title": "資產沉澱",
"summary": "完成 {ready} / 卡點 {blocked}",
"boundary": "只讀推導;不代表已寫入 KM、更新 PlayBook trust、套用腳本 / 排程或執行 verifier。",
"items": {
"km": "KM",
"playbook": "PlayBook",
"script": "腳本",
"schedule": "排程",
"verifier": "Verifier"
},
"values": {
"ready": "ready",
"pending": "pending",
"blocked": "blocked"
}
},
"columns": {
"runId": "執行 ID",
"projectId": "專案 ID",

View File

@@ -291,6 +291,95 @@ function RemediationEvidenceCell({ summary }: { summary?: RemediationSummary | n
);
}
type ApprovalAssetTone = "ready" | "pending" | "blocked";
function assetToneClass(tone: ApprovalAssetTone) {
if (tone === "ready") return "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]";
if (tone === "blocked") return "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]";
return "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]";
}
function countLabel(value: number) {
return value > 0 ? String(value) : "--";
}
function ApprovalAutomationAssetLedger({ approval }: { approval: Approval }) {
const t = useTranslations("awooop.approvals.assetLedger");
const chain = approval.awooop_status_chain;
const summary = approval.remediation_summary;
const kmCount = chain?.evidence?.knowledge_entries ?? 0;
const playbookCount = (chain?.execution?.playbook_ids?.length ?? 0) + (chain?.execution?.playbook_paths?.length ?? 0);
const ansible = chain?.execution?.ansible;
const scriptCount = (ansible?.candidate_count ?? 0) + (ansible?.check_mode_total ?? 0) + (ansible?.apply_total ?? 0);
const correlation = chain?.source_refs?.correlation;
const scheduleCount = (correlation?.provider_event_total ?? 0)
+ (correlation?.direct_ref_total ?? 0)
+ (correlation?.candidate_total ?? 0)
+ (correlation?.applied_link_total ?? 0);
const verification = String(chain?.verification ?? "").toLowerCase();
const verifierReady = verification.includes("verified") || verification.includes("success");
const verifierBlocked = verification.includes("degraded") || verification.includes("fail");
const route = String(summary?.latest_route ?? "");
const noRepairCandidate = route.includes("fallback") || route.includes("no_action") || chain?.blockers?.some((item) => item.includes("playbook") || item.includes("candidate"));
const items = [
{
key: "km",
tone: kmCount > 0 ? "ready" : chain?.needs_human ? "blocked" : "pending",
value: countLabel(kmCount),
},
{
key: "playbook",
tone: playbookCount > 0 ? "ready" : noRepairCandidate ? "blocked" : "pending",
value: countLabel(playbookCount),
},
{
key: "script",
tone: scriptCount > 0 ? "ready" : summary?.has_mcp_investigation ? "pending" : "blocked",
value: countLabel(scriptCount),
},
{
key: "schedule",
tone: scheduleCount > 0 ? "ready" : "pending",
value: countLabel(scheduleCount),
},
{
key: "verifier",
tone: verifierReady ? "ready" : verifierBlocked ? "blocked" : "pending",
value: verifierReady ? t("values.ready") : verifierBlocked ? t("values.blocked") : t("values.pending"),
},
] satisfies Array<{ key: string; tone: ApprovalAssetTone; value: string }>;
const readyCount = items.filter((item) => item.tone === "ready").length;
const blockedCount = items.filter((item) => item.tone === "blocked").length;
return (
<div className="min-w-[260px] border border-[#e0ddd4] bg-white p-2">
<div className="flex flex-wrap items-center justify-between gap-2">
<span className="text-xs font-semibold text-[#141413]">{t("title")}</span>
<span className={cn(
"border px-2 py-0.5 font-mono text-[11px] font-semibold",
blockedCount > 0 ? "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]" : "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]"
)}>
{t("summary", { ready: readyCount, blocked: blockedCount })}
</span>
</div>
<div className="mt-2 grid grid-cols-2 gap-1.5">
{items.map((item) => (
<div key={item.key} className={cn("min-w-0 border px-2 py-1", assetToneClass(item.tone))}>
<div className="flex items-center justify-between gap-2">
<span className="truncate text-[11px] font-semibold">{t(`items.${item.key}` as never)}</span>
<span className="font-mono text-[11px] font-semibold">{item.value}</span>
</div>
</div>
))}
</div>
<p className="mt-2 text-[11px] leading-5 text-[#77736a]">
{t("boundary")}
</p>
</div>
);
}
function LegacyHitlBacklogPanel({
approvals,
loading,
@@ -591,6 +680,9 @@ function ApprovalRow({ approval }: { approval: Approval }) {
<td className="px-4 py-3">
<ApprovalSourceFlowCell chain={approval.awooop_status_chain} />
</td>
<td className="px-4 py-3">
<ApprovalAutomationAssetLedger approval={approval} />
</td>
<td className="min-w-[280px] px-4 py-3">
<AwoooPStatusChainPanel chain={approval.awooop_status_chain} compact />
</td>
@@ -1519,6 +1611,9 @@ export default function ApprovalsPage() {
<th className="text-left px-4 py-3 text-xs font-medium text-muted-foreground uppercase tracking-wider">
{tEvidence("sourceFlow.column")}
</th>
<th className="text-left px-4 py-3 text-xs font-medium text-muted-foreground uppercase tracking-wider">
{t("assetLedger.column")}
</th>
<th className="text-left px-4 py-3 text-xs font-medium text-muted-foreground uppercase tracking-wider">
{tStatusChain("title")}
</th>
@@ -1534,7 +1629,7 @@ export default function ApprovalsPage() {
{loading ? (
Array.from({ length: 5 }).map((_, i) => (
<tr key={i} className="border-b border-border">
{Array.from({ length: 9 }).map((_, j) => (
{Array.from({ length: 10 }).map((_, j) => (
<td key={j} className="px-4 py-3">
<div className="h-5 bg-muted animate-pulse rounded w-20" />
</td>