diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 9ea34f37..60007ee1 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -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 證據", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 9ea34f37..60007ee1 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -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 證據", diff --git a/apps/web/src/app/[locale]/awooop/work-items/page.tsx b/apps/web/src/app/[locale]/awooop/work-items/page.tsx index b395caaa..64bebba4 100644 --- a/apps/web/src/app/[locale]/awooop/work-items/page.tsx +++ b/apps/web/src/app/[locale]/awooop/work-items/page.tsx @@ -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({ + ) : closureAvailable ? ( +
{t("promotion.bridge.metrics.readiness")}
+{closurePercent}%
+{t("promotion.bridge.metrics.ready")}
++ {t("promotion.bridge.readyValue", { + ready: closureReady, + total: closureTotal, + blocked: closureBlocked, + })} +
+{t("promotion.bridge.metrics.status")}
++ {closureReadiness?.status ?? "--"} +
+{t("promotion.metrics.runtime")}
++ {t("promotion.runtimeClosed")} +
+{t("promotion.bridge.title")}
++ {t("promotion.bridge.detail", { + reason: closureReadiness?.blocked_reason ?? promotion?.reason ?? "repair_candidate_promotion_contract_not_found", + })} +
++ {gate.key ?? "--"} +
++ {gate.asset_id ?? gate.detail ?? "--"} +
+{t("promotion.bridge.ownerFields")}
+