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

@@ -12060,8 +12060,8 @@
"detail": "需要 AI 補齊、retry、rollback 或 break-glass 的審批"
},
"handoff": {
"title": "接手",
"detail": "Gate 5、舊 HITL 證據與 AI 工作項補齊"
"title": "Action Packages",
"detail": "Gate 5 projections, historical evidence, and AI work item backfill"
},
"verifier": {
"title": "驗證",
@@ -12074,7 +12074,7 @@
"detail": "找出 learning_recorded、execution_failed、AI retry / rollback 或逾時的審批。",
"cta": "查看卡點",
"meta": {
"needsHuman": "需要 AI 補齊",
"controlledAction": "AI action package",
"executionFailed": "執行失敗 / 降級",
"learningRecorded": "卡在學習紀錄"
}
@@ -12096,7 +12096,7 @@
"meta": {
"gate5": "Gate 5 投影",
"legacy": "Legacy HITL",
"manual": "AI 補齊"
"controlledAction": "AI backfill"
}
},
"guardrail": {
@@ -12109,6 +12109,10 @@
"providerSwitch": "供應者切換"
}
}
},
"controlledProof": {
"title": "Low / Medium / High default to AI controlled apply",
"detail": "AI action packages {packages}; Gate 5 {gate5}; historical HITL evidence {legacy}. Only critical / break-glass goes to incident-grade authorization; the rest is completed by AI selectors, check-mode, rollback, and verifiers."
}
},
"badges": {
@@ -12159,7 +12163,7 @@
"openTickets": "Tickets",
"empty": "無",
"flowTitle": "處理流程",
"handoffTitle": "審批與 AI 受控接手",
"handoffTitle": "Approvals And AI Action Packages",
"timelineEmpty": "尚未取得 Incident timeline。",
"linkedExplanation": "此 Incident 已有受控決策 / timeline 關聯;若下方 AI 受控清單為空,代表它可能已完成、過期、拒絕,或已轉成 verifier / rollback / AI 補齊。",
"unlinkedExplanation": "目前沒有對應 批准 id這代表此 Incident不是等待批准的狀態應從 Work Items / Runs 追下一步。",
@@ -12167,12 +12171,16 @@
"yes": "需要 AI 補齊",
"no": "不需 AI 補齊"
},
"controlledAction": {
"yes": "AI action package pending",
"no": "AI action package clear"
},
"metrics": {
"approvals": "關聯審批",
"stage": "目前階段",
"repair": "修復狀態",
"verification": "驗證",
"handoff": "AI 受控接手"
"handoff": "AI Action Package"
},
"handoff": {
"approvalIds": "Approval IDs",

View File

@@ -12060,8 +12060,8 @@
"detail": "需要 AI 補齊、retry、rollback 或 break-glass 的審批"
},
"handoff": {
"title": "接手",
"detail": "Gate 5、舊 HITL 證據與 AI 工作項補齊"
"title": "處置包",
"detail": "Gate 5、歷史證據與 AI 工作項補齊"
},
"verifier": {
"title": "驗證",
@@ -12074,7 +12074,7 @@
"detail": "找出 learning_recorded、execution_failed、AI retry / rollback 或逾時的審批。",
"cta": "查看卡點",
"meta": {
"needsHuman": "需要 AI 補齊",
"controlledAction": "AI 處置包",
"executionFailed": "執行失敗 / 降級",
"learningRecorded": "卡在學習紀錄"
}
@@ -12096,7 +12096,7 @@
"meta": {
"gate5": "Gate 5 投影",
"legacy": "Legacy HITL",
"manual": "AI 補齊"
"controlledAction": "AI 補齊"
}
},
"guardrail": {
@@ -12109,6 +12109,10 @@
"providerSwitch": "供應者切換"
}
}
},
"controlledProof": {
"title": "低 / 中 / 高風險預設 AI controlled apply",
"detail": "AI 處置包 {packages}Gate 5 {gate5};既有 HITL 歷史證據 {legacy}。critical / break-glass 才進事故級授權,其餘由 AI 補齊 selector、check-mode、rollback 與 verifier。"
}
},
"badges": {
@@ -12159,7 +12163,7 @@
"openTickets": "Tickets",
"empty": "無",
"flowTitle": "處理流程",
"handoffTitle": "審批與 AI 受控接手",
"handoffTitle": "審批與 AI 處置包",
"timelineEmpty": "尚未取得 Incident timeline。",
"linkedExplanation": "此 Incident 已有受控決策 / timeline 關聯;若下方 AI 受控清單為空,代表它可能已完成、過期、拒絕,或已轉成 verifier / rollback / AI 補齊。",
"unlinkedExplanation": "目前沒有對應 批准 id這代表此 Incident不是等待批准的狀態應從 Work Items / Runs 追下一步。",
@@ -12167,12 +12171,16 @@
"yes": "需要 AI 補齊",
"no": "不需 AI 補齊"
},
"controlledAction": {
"yes": "AI 處置包待補齊",
"no": "AI 處置包已清空"
},
"metrics": {
"approvals": "關聯審批",
"stage": "目前階段",
"repair": "修復狀態",
"verification": "驗證",
"handoff": "AI 受控接手"
"handoff": "AI 處置包"
},
"handoff": {
"approvalIds": "Approval IDs",

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>