fix(web): 在 Runs 顯示自動化資產沉澱
This commit is contained in:
@@ -9464,6 +9464,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"automationAssetLedger": {
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"runs": {
|
||||
"automationFlow": {
|
||||
"title": "AI 自動化流程 Gate",
|
||||
|
||||
@@ -9464,6 +9464,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"automationAssetLedger": {
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"runs": {
|
||||
"automationFlow": {
|
||||
"title": "AI 自動化流程 Gate",
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
AwoooPStatusChainPanel,
|
||||
type AwoooPStatusChain,
|
||||
} from "@/components/awooop/status-chain";
|
||||
import { AwoooPAutomationAssetLedger } from "@/components/awooop/automation-asset-ledger";
|
||||
import type { IncidentTimelineResponse } from "@/lib/api-client";
|
||||
import {
|
||||
publicAgentText,
|
||||
@@ -291,95 +292,6 @@ 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,
|
||||
@@ -681,7 +593,10 @@ function ApprovalRow({ approval }: { approval: Approval }) {
|
||||
<ApprovalSourceFlowCell chain={approval.awooop_status_chain} />
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<ApprovalAutomationAssetLedger approval={approval} />
|
||||
<AwoooPAutomationAssetLedger
|
||||
chain={approval.awooop_status_chain}
|
||||
remediationSummary={approval.remediation_summary}
|
||||
/>
|
||||
</td>
|
||||
<td className="min-w-[280px] px-4 py-3">
|
||||
<AwoooPStatusChainPanel chain={approval.awooop_status_chain} compact />
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
AwoooPStatusChainPanel,
|
||||
type AwoooPStatusChain,
|
||||
} from "@/components/awooop/status-chain";
|
||||
import { AwoooPAutomationAssetLedger } from "@/components/awooop/automation-asset-ledger";
|
||||
import { useIncidentStatusChains } from "@/hooks/useIncidentStatusChains";
|
||||
import {
|
||||
Activity,
|
||||
@@ -1939,6 +1940,12 @@ function RunRow({
|
||||
loading={sourceFlowLoading}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<AwoooPAutomationAssetLedger
|
||||
chain={statusChain}
|
||||
remediationSummary={run.remediation_summary}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<ShadowBadge isShadow={run.is_shadow} />
|
||||
</td>
|
||||
@@ -3960,6 +3967,7 @@ function AiRouteStatusPanel({
|
||||
|
||||
export default function RunsPage() {
|
||||
const tEvidence = useTranslations("awooop.listEvidence");
|
||||
const tAssetLedger = useTranslations("awooop.automationAssetLedger");
|
||||
const tCallback = useTranslations("awooop.callbackReply");
|
||||
const [runs, setRuns] = useState<Run[]>([]);
|
||||
const [groupedEvents, setGroupedEvents] = useState<PlatformEvent[]>([]);
|
||||
@@ -4564,6 +4572,9 @@ export default function RunsPage() {
|
||||
<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">
|
||||
{tAssetLedger("column")}
|
||||
</th>
|
||||
<th className="text-left px-4 py-3 text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||
Shadow
|
||||
</th>
|
||||
@@ -4579,7 +4590,7 @@ export default function RunsPage() {
|
||||
{loading ? (
|
||||
Array.from({ length: 8 }).map((_, i) => (
|
||||
<tr key={i} className="border-b border-border">
|
||||
{Array.from({ length: 12 }).map((_, j) => (
|
||||
{Array.from({ length: 13 }).map((_, j) => (
|
||||
<td key={j} className="px-4 py-3">
|
||||
<div className="h-5 bg-muted animate-pulse rounded w-20" />
|
||||
</td>
|
||||
@@ -4588,7 +4599,7 @@ export default function RunsPage() {
|
||||
))
|
||||
) : runs.length === 0 && !error ? (
|
||||
<tr>
|
||||
<td colSpan={12} className="px-4 py-16 text-center">
|
||||
<td colSpan={13} className="px-4 py-16 text-center">
|
||||
<Activity className="w-10 h-10 text-muted-foreground/30 mx-auto mb-3" aria-hidden="true" />
|
||||
<p className="text-sm text-muted-foreground">尚無 Run 資料</p>
|
||||
</td>
|
||||
|
||||
117
apps/web/src/components/awooop/automation-asset-ledger.tsx
Normal file
117
apps/web/src/components/awooop/automation-asset-ledger.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import type { AwoooPStatusChain } from "@/components/awooop/status-chain";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type AutomationAssetTone = "ready" | "pending" | "blocked";
|
||||
|
||||
export interface AutomationAssetRemediationSummary {
|
||||
has_mcp_investigation?: boolean | null;
|
||||
latest_route?: string | null;
|
||||
}
|
||||
|
||||
function assetToneClass(tone: AutomationAssetTone) {
|
||||
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) : "--";
|
||||
}
|
||||
|
||||
export function AwoooPAutomationAssetLedger({
|
||||
chain,
|
||||
remediationSummary,
|
||||
className,
|
||||
}: {
|
||||
chain?: AwoooPStatusChain | null;
|
||||
remediationSummary?: AutomationAssetRemediationSummary | null;
|
||||
className?: string;
|
||||
}) {
|
||||
const t = useTranslations("awooop.automationAssetLedger");
|
||||
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(remediationSummary?.latest_route ?? chain?.evidence?.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"
|
||||
: remediationSummary?.has_mcp_investigation || (chain?.mcp?.gateway?.total ?? 0) > 0
|
||||
? "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: AutomationAssetTone; value: string }>;
|
||||
const readyCount = items.filter((item) => item.tone === "ready").length;
|
||||
const blockedCount = items.filter((item) => item.tone === "blocked").length;
|
||||
|
||||
return (
|
||||
<div className={cn("min-w-[260px] border border-[#e0ddd4] bg-white p-2", className)}>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user