fix(web): 在審批佇列顯示資產沉澱矩陣
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user