fix(awooop): 顯示受控執行閘門卡點
This commit is contained in:
@@ -9652,6 +9652,17 @@
|
||||
"status": "合約狀態",
|
||||
"runtime": "執行邊界"
|
||||
},
|
||||
"bridge": {
|
||||
"title": "改看受控執行閘門",
|
||||
"detail": "這筆事件沒有 repair promotion contract,但已有 Apply Gate readiness;目前卡點:{reason}。",
|
||||
"readyValue": "已備 {ready}/{total} · 卡點 {blocked}",
|
||||
"ownerFields": "Owner release 必填欄位",
|
||||
"metrics": {
|
||||
"readiness": "閘門準備度",
|
||||
"ready": "完成 / 卡點",
|
||||
"status": "閘門狀態"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"target_selector": "Target selector",
|
||||
"mcp_evidence_refs": "MCP 證據",
|
||||
|
||||
@@ -9652,6 +9652,17 @@
|
||||
"status": "合約狀態",
|
||||
"runtime": "執行邊界"
|
||||
},
|
||||
"bridge": {
|
||||
"title": "改看受控執行閘門",
|
||||
"detail": "這筆事件沒有 repair promotion contract,但已有 Apply Gate readiness;目前卡點:{reason}。",
|
||||
"readyValue": "已備 {ready}/{total} · 卡點 {blocked}",
|
||||
"ownerFields": "Owner release 必填欄位",
|
||||
"metrics": {
|
||||
"readiness": "閘門準備度",
|
||||
"ready": "完成 / 卡點",
|
||||
"status": "閘門狀態"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"target_selector": "Target selector",
|
||||
"mcp_evidence_refs": "MCP 證據",
|
||||
|
||||
@@ -1121,6 +1121,14 @@ function promotionStatusTone(status: string | null | undefined): WorkStatus {
|
||||
return "blocked";
|
||||
}
|
||||
|
||||
function closureGateTone(status: string | null | undefined): WorkStatus {
|
||||
const normalized = String(status ?? "").toLowerCase();
|
||||
if (normalized === "passed" || normalized === "ready") return "live";
|
||||
if (normalized === "warning" || normalized.includes("candidate")) return "in_progress";
|
||||
if (normalized === "blocked" || normalized.startsWith("blocked")) return "blocked";
|
||||
return "watching";
|
||||
}
|
||||
|
||||
function buildAutomationBlockerLanes(
|
||||
telemetry: Telemetry,
|
||||
focusedDraft: RepairCandidateDraftFocus | null
|
||||
@@ -4195,6 +4203,17 @@ function RepairCandidateDraftPanel({
|
||||
)
|
||||
.filter((field): field is string => Boolean(field))
|
||||
.slice(0, 8);
|
||||
const closureReadiness = chain?.automation_handoff?.closure_readiness;
|
||||
const closureAvailable = Boolean(!promotionAvailable && closureReadiness);
|
||||
const closureReady = toCount(closureReadiness?.ready_count);
|
||||
const closureTotal = toCount(closureReadiness?.total_count);
|
||||
const closureBlocked = toCount(closureReadiness?.blocked_count);
|
||||
const closurePercent = toCount(closureReadiness?.completion_percent)
|
||||
|| (closureTotal > 0
|
||||
? Math.min(100, Math.round((closureReady / closureTotal) * 100))
|
||||
: 0);
|
||||
const closureGates = closureReadiness?.gates?.slice(0, 8) ?? [];
|
||||
const closureOwnerFields = closureReadiness?.required_owner_fields?.slice(0, 8) ?? [];
|
||||
|
||||
const runsHref = draft.incidentId
|
||||
? `/awooop/runs?project_id=${encodeURIComponent(draft.projectId)}&incident_id=${encodeURIComponent(draft.incidentId)}`
|
||||
@@ -4364,6 +4383,95 @@ function RepairCandidateDraftPanel({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : closureAvailable ? (
|
||||
<div className="p-3">
|
||||
<div className="grid gap-px bg-[#e0ddd4] md:grid-cols-4">
|
||||
<div className="min-w-0 bg-white px-3 py-2">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("promotion.bridge.metrics.readiness")}</p>
|
||||
<p className="mt-1 font-mono text-2xl font-semibold text-[#141413]">{closurePercent}%</p>
|
||||
</div>
|
||||
<div className="min-w-0 bg-white px-3 py-2">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("promotion.bridge.metrics.ready")}</p>
|
||||
<p className="mt-1 font-mono text-sm font-semibold text-[#141413]">
|
||||
{t("promotion.bridge.readyValue", {
|
||||
ready: closureReady,
|
||||
total: closureTotal,
|
||||
blocked: closureBlocked,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div className="min-w-0 bg-white px-3 py-2">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("promotion.bridge.metrics.status")}</p>
|
||||
<p className="mt-1 break-words font-mono text-xs font-semibold text-[#141413]">
|
||||
{closureReadiness?.status ?? "--"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="min-w-0 bg-white px-3 py-2">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("promotion.metrics.runtime")}</p>
|
||||
<p className="mt-1 font-mono text-xs font-semibold text-[#9f2f25]">
|
||||
{t("promotion.runtimeClosed")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 h-2 border border-[#d8d3c7] bg-white">
|
||||
<div
|
||||
className="h-full bg-[#d97757]"
|
||||
style={{ width: `${Math.min(100, Math.max(0, closurePercent))}%` }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 border border-[#d9b36f] bg-[#fff7e8] px-3 py-2">
|
||||
<p className="text-xs font-semibold text-[#8a5a08]">{t("promotion.bridge.title")}</p>
|
||||
<p className="mt-1 text-[11px] leading-5 text-[#5f5b52]">
|
||||
{t("promotion.bridge.detail", {
|
||||
reason: closureReadiness?.blocked_reason ?? promotion?.reason ?? "repair_candidate_promotion_contract_not_found",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 grid gap-2 md:grid-cols-4">
|
||||
{closureGates.map((gate) => {
|
||||
const tone = closureGateTone(gate.status);
|
||||
const Icon = statusConfig[tone].icon;
|
||||
return (
|
||||
<div key={`${gate.key}-${gate.asset_id ?? gate.detail ?? ""}`} className="min-w-0 border border-[#e0ddd4] bg-white px-3 py-2">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<p className="break-words text-xs font-semibold text-[#141413]">
|
||||
{gate.key ?? "--"}
|
||||
</p>
|
||||
<p className="mt-1 break-all font-mono text-[11px] leading-4 text-[#5f5b52]">
|
||||
{gate.asset_id ?? gate.detail ?? "--"}
|
||||
</p>
|
||||
</div>
|
||||
<span className={cn("inline-flex shrink-0 items-center gap-1 border px-1.5 py-0.5 font-mono text-[10px] font-semibold", statusConfig[tone].className)}>
|
||||
<Icon className="h-3 w-3" aria-hidden="true" />
|
||||
{gate.status ?? "--"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 border border-[#e2a29b] bg-[#fff0ef] px-3 py-2">
|
||||
<p className="text-xs font-semibold text-[#9f2f25]">{t("promotion.bridge.ownerFields")}</p>
|
||||
<div className="mt-2 flex flex-wrap gap-1.5">
|
||||
{closureOwnerFields.map((field) => (
|
||||
<span key={field} className="border border-[#e2a29b] bg-white px-2 py-0.5 font-mono text-[10px] font-semibold text-[#9f2f25]">
|
||||
{field}
|
||||
</span>
|
||||
))}
|
||||
{closureOwnerFields.length === 0 ? (
|
||||
<span className="border border-[#d8d3c7] bg-white px-2 py-0.5 font-mono text-[10px] font-semibold text-[#5f5b52]">
|
||||
{t("promotion.noBlockedFields")}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-3">
|
||||
<div className="border border-[#d9b36f] bg-[#fff7e8] px-3 py-2">
|
||||
|
||||
Reference in New Issue
Block a user