feat(web): enrich Runs apply gate handoff
Some checks failed
Code Review / ai-code-review (push) Successful in 15s
CD Pipeline / tests (push) Successful in 1m37s
CD Pipeline / build-and-deploy (push) Successful in 4m35s
CD Pipeline / post-deploy-checks (push) Successful in 1m43s
Ansible / Reboot Recovery Contract / validate (push) Has been cancelled

This commit is contained in:
Your Name
2026-06-25 22:27:10 +08:00
parent a6844ac1a0
commit 6ed461cf11
5 changed files with 143 additions and 7 deletions

View File

@@ -419,6 +419,10 @@ export function AwoooPStatusChainPanel({
const mcpGatewayTotal = mcpGateway.total ?? 0;
const mcpGatewayProblemTotal = (mcpGateway.failed ?? 0) + (mcpGateway.blocked ?? 0);
const executionTotal = execution.operation_total ?? 0;
const handoffCandidate = automationHandoff?.candidate;
const handoffAssets = automationHandoff?.asset_ids;
const ownerReviewChecklist = automationHandoff?.owner_review_checklist ?? [];
const forbiddenActions = automationHandoff?.forbidden_actions ?? [];
const sourceToolchainTone: SourceFlowTone = sourceCorrelation
? (sourceLinkVerified ? "success" : (sourceVerificationBlocked ? "blocked" : "warning"))
: "neutral";
@@ -516,6 +520,18 @@ export function AwoooPStatusChainPanel({
},
];
const handoffGates = automationHandoff?.gates ?? [];
const handoffCandidateCards = [
{ key: "catalog", label: t("applyGate.catalog"), value: handoffCandidate?.catalog_id },
{ key: "risk", label: t("applyGate.risk"), value: handoffCandidate?.risk_level },
{ key: "matchScore", label: t("applyGate.matchScore"), value: handoffCandidate?.match_score },
{ key: "checkPlaybook", label: t("applyGate.checkPlaybook"), value: handoffCandidate?.check_mode_playbook_path },
{ key: "applyPlaybook", label: t("applyGate.applyPlaybook"), value: handoffCandidate?.apply_playbook_path },
];
const handoffAssetCards = [
{ key: "dryRunAsset", label: t("applyGate.dryRunAsset"), value: handoffAssets?.dry_run },
{ key: "applyAsset", label: t("applyGate.applyAsset"), value: handoffAssets?.apply_candidate },
{ key: "verifierAsset", label: t("applyGate.verifierAsset"), value: handoffAssets?.verifier },
];
const handoffGateLabel = (key: string | null | undefined) => {
const labels: Record<string, string> = {
dry_run: t("applyGate.gates.dryRun"),
@@ -775,23 +791,77 @@ export function AwoooPStatusChainPanel({
<div className="grid gap-px bg-[#e0ddd4] md:grid-cols-3">
<div className="min-w-0 bg-white px-4 py-3">
<p className="text-xs font-semibold text-[#77736a]">{t("applyGate.workItem")}</p>
<p className="mt-2 truncate font-mono text-sm text-[#141413]" title={valueOrEmpty(automationHandoff.work_item_id, emptyLabel)}>
<p className="mt-2 break-all font-mono text-sm text-[#141413]" title={valueOrEmpty(automationHandoff.work_item_id, emptyLabel)}>
{valueOrEmpty(automationHandoff.work_item_id, emptyLabel)}
</p>
</div>
<div className="min-w-0 bg-white px-4 py-3">
<p className="text-xs font-semibold text-[#77736a]">{t("applyGate.applyCandidate")}</p>
<p className="mt-2 truncate font-mono text-sm text-[#141413]" title={valueOrEmpty(automationHandoff.candidate?.apply_playbook_path, emptyLabel)}>
{valueOrEmpty(automationHandoff.candidate?.apply_playbook_path, emptyLabel)}
<p className="text-xs font-semibold text-[#77736a]">{t("applyGate.ownerGate")}</p>
<p className="mt-2 break-words font-mono text-sm text-[#141413]" title={valueOrEmpty(automationHandoff.owner_review_gate, emptyLabel)}>
{valueOrEmpty(automationHandoff.owner_review_gate, emptyLabel)}
</p>
</div>
<div className="min-w-0 bg-white px-4 py-3">
<p className="text-xs font-semibold text-[#77736a]">{t("applyGate.verifier")}</p>
<p className="mt-2 truncate font-mono text-sm text-[#141413]" title={valueOrEmpty(automationHandoff.asset_ids?.verifier, emptyLabel)}>
{valueOrEmpty(automationHandoff.asset_ids?.verifier, emptyLabel)}
<p className="text-xs font-semibold text-[#77736a]">{t("applyGate.nextAction")}</p>
<p className="mt-2 break-words font-mono text-sm text-[#141413]" title={valueOrEmpty(automationHandoff.next_action, emptyLabel)}>
{valueOrEmpty(automationHandoff.next_action, emptyLabel)}
</p>
</div>
</div>
<div className="grid gap-px bg-[#e0ddd4] md:grid-cols-5">
{handoffCandidateCards.map((item) => (
<div key={item.key} className="min-w-0 bg-white px-4 py-3">
<p className="text-xs font-semibold text-[#77736a]">{item.label}</p>
<p className="mt-2 break-all font-mono text-sm text-[#141413]" title={valueOrEmpty(item.value, emptyLabel)}>
{valueOrEmpty(item.value, emptyLabel)}
</p>
</div>
))}
</div>
<div className="grid gap-px bg-[#e0ddd4] md:grid-cols-3">
{handoffAssetCards.map((item) => (
<div key={item.key} className="min-w-0 bg-white px-4 py-3">
<p className="text-xs font-semibold text-[#77736a]">{item.label}</p>
<p className="mt-2 break-all font-mono text-sm text-[#141413]" title={valueOrEmpty(item.value, emptyLabel)}>
{valueOrEmpty(item.value, emptyLabel)}
</p>
</div>
))}
</div>
<div className="grid gap-px bg-[#e0ddd4] lg:grid-cols-2">
<div className="min-w-0 bg-white px-4 py-3">
<div className="flex items-center justify-between gap-3">
<p className="text-xs font-semibold text-[#77736a]">{t("applyGate.checklistTitle")}</p>
<span className="border border-[#d8d3c7] bg-[#faf9f3] px-2 py-0.5 font-mono text-[11px] text-[#5f5b52]">
{ownerReviewChecklist.length}
</span>
</div>
<ul className="mt-3 space-y-2">
{(ownerReviewChecklist.length ? ownerReviewChecklist : [emptyLabel]).map((item, index) => (
<li key={`${item}-${index}`} className="flex min-w-0 items-start gap-2 text-xs leading-5 text-[#141413]">
<CheckCircle2 className="mt-0.5 h-3.5 w-3.5 shrink-0 text-[#17602a]" aria-hidden="true" />
<span className="min-w-0 break-words font-mono">{item}</span>
</li>
))}
</ul>
</div>
<div className="min-w-0 bg-white px-4 py-3">
<div className="flex items-center justify-between gap-3">
<p className="text-xs font-semibold text-[#77736a]">{t("applyGate.forbiddenTitle")}</p>
<span className="border border-[#e2a29b] bg-[#fff0ef] px-2 py-0.5 font-mono text-[11px] text-[#9f2f25]">
{forbiddenActions.length}
</span>
</div>
<ul className="mt-3 space-y-2">
{(forbiddenActions.length ? forbiddenActions : [emptyLabel]).map((item, index) => (
<li key={`${item}-${index}`} className="flex min-w-0 items-start gap-2 text-xs leading-5 text-[#141413]">
<TriangleAlert className="mt-0.5 h-3.5 w-3.5 shrink-0 text-[#9f2f25]" aria-hidden="true" />
<span className="min-w-0 break-words font-mono">{item}</span>
</li>
))}
</ul>
</div>
</div>
</div>
)}