fix(web): show approvals controlled automation proof
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 43s
E2E Health Check / e2e-health (push) Failing after 29s
CD Pipeline / build-and-deploy (push) Successful in 6m2s
CD Pipeline / post-deploy-checks (push) Successful in 1m41s

This commit is contained in:
Your Name
2026-07-01 23:59:35 +08:00
parent 1433f151f6
commit ca19e7ee44
3 changed files with 59 additions and 22 deletions

View File

@@ -366,7 +366,7 @@ function chainValues(approval: Approval): string[] {
.map(lowerValue);
}
function approvalNeedsHuman(approval: Approval): boolean {
function approvalNeedsControlledActionPackage(approval: Approval): boolean {
const chain = approval.awooop_status_chain;
const outcome = chain?.operator_outcome;
return Boolean(
@@ -1057,7 +1057,7 @@ function ApprovalDecisionRail({
}) {
const t = useTranslations("awooop.approvals.decisionRail");
const projectQuery = encodeURIComponent(projectId);
const needsHuman = approvals.filter(approvalNeedsHuman);
const controlledActionPackages = approvals.filter(approvalNeedsControlledActionPackage);
const executionFailed = approvals.filter(approvalHasExecutionFailure);
const learningRecorded = approvals.filter(approvalHasLearningRecorded);
const expired = approvals.filter(approvalIsExpired);
@@ -1071,10 +1071,10 @@ function ApprovalDecisionRail({
const noEvidence = approvals.filter(
(approval) => normalizeRemediationStatus(approval.remediation_summary) === "no_evidence"
);
const firstStuck = firstApproval([needsHuman, executionFailed, learningRecorded, expired]);
const stuckCount = uniqueApprovalCount([needsHuman, executionFailed, learningRecorded, expired]);
const firstStuck = firstApproval([controlledActionPackages, executionFailed, learningRecorded, expired]);
const stuckCount = uniqueApprovalCount([controlledActionPackages, executionFailed, learningRecorded, expired]);
const evidenceCount = uniqueApprovalCount([mcpObserved, readOnly, noEvidence]);
const handoffCount = uniqueApprovalCount([needsHuman, gate5]) + legacyApprovals.length;
const handoffCount = uniqueApprovalCount([controlledActionPackages, gate5]) + legacyApprovals.length;
const hasLoadIssue = Boolean(error || legacyError);
const conclusionKey = stuckCount > 0
? "blocked"
@@ -1091,7 +1091,7 @@ function ApprovalDecisionRail({
icon: TriangleAlert,
href: firstStuck ? approvalHref(firstStuck) : `/awooop/approvals?project_id=${projectQuery}`,
meta: {
needsHuman: needsHuman.length,
controlledAction: controlledActionPackages.length,
executionFailed: executionFailed.length,
learningRecorded: learningRecorded.length,
},
@@ -1117,7 +1117,7 @@ function ApprovalDecisionRail({
meta: {
gate5: gate5.length,
legacy: legacyApprovals.length,
manual: needsHuman.length,
controlledAction: controlledActionPackages.length,
},
},
{
@@ -1136,7 +1136,7 @@ function ApprovalDecisionRail({
const flow = [
{ key: "request", value: approvals.length + legacyApprovals.length },
{ key: "evidence", value: mcpObserved.length + readOnly.length },
{ key: "decision", value: needsHuman.length + expired.length },
{ key: "decision", value: controlledActionPackages.length + expired.length },
{ key: "handoff", value: gate5.length + legacyApprovals.length },
{ key: "verifier", value: executionFailed.length },
];
@@ -1213,6 +1213,22 @@ function ApprovalDecisionRail({
<p className="mt-3 border border-[#d9b36f] bg-white px-3 py-2 text-xs leading-5 text-[#8a5a08]">
{t("boundary")}
</p>
<div
data-testid="approvals-controlled-automation-proof"
className="mt-3 flex items-start gap-3 border border-[#9bc7a4] bg-[#f0faf2] px-3 py-3 text-xs leading-5 text-[#17602a]"
>
<ShieldCheck className="mt-0.5 h-4 w-4 shrink-0" aria-hidden="true" />
<div className="min-w-0">
<p className="font-semibold text-[#17602a]">{t("controlledProof.title")}</p>
<p className="mt-1 text-[#255f33]">
{t("controlledProof.detail", {
packages: controlledActionPackages.length,
gate5: gate5.length,
legacy: legacyApprovals.length,
})}
</p>
</div>
</div>
</div>
</div>
@@ -1719,7 +1735,9 @@ function FocusedIncidentApprovalPanel({
const topMcpTool = chain?.mcp?.top_tools?.[0]?.tool_name ?? "--";
const ansible = chain?.execution?.ansible;
const outcome = chain?.operator_outcome;
const needsHuman = chain?.needs_human ?? outcome?.needs_human ?? false;
const needsControlledActionPackage = Boolean(
chain?.needs_human || outcome?.needs_human || outcome?.human_action_required
);
const title = timeline?.title ?? chain?.source_id ?? incidentId;
const sourceCorrelation = chain?.source_refs?.correlation;
@@ -1789,7 +1807,10 @@ function FocusedIncidentApprovalPanel({
[t("metrics.stage"), chain?.current_stage ?? "--"],
[t("metrics.repair"), chain?.repair_state ?? "--"],
[t("metrics.verification"), verifier?.status ?? chain?.verification ?? "--"],
[t("metrics.handoff"), needsHuman ? t("needsHuman.yes") : t("needsHuman.no")],
[
t("metrics.handoff"),
needsControlledActionPackage ? t("controlledAction.yes") : t("controlledAction.no"),
],
].map(([label, value]) => (
<div key={label} className="min-w-0 bg-white px-4 py-3">
<p className="text-xs font-semibold text-[#77736a]">{label}</p>