diff --git a/apps/console/src/pages/system/ApprovalsInboxPage.tsx b/apps/console/src/pages/system/ApprovalsInboxPage.tsx index fb2cd57e22..bc370c91d2 100644 --- a/apps/console/src/pages/system/ApprovalsInboxPage.tsx +++ b/apps/console/src/pages/system/ApprovalsInboxPage.tsx @@ -686,11 +686,14 @@ export function ApprovalsInboxPage() { // so the hint never contradicts the buttons — it already reflects position/ // team approver resolution, which the client identity heuristic below can't. // The heuristic stays as a fallback for a backend that predates `viewer`. - // No actor-override branch: the admin "act as" escape hatch retired with the - // composer (cloud#861 enterprise act-as is the successor). + // framework#3424: a platform/tenant admin may OVERRIDE a stuck request (one + // routed to an unstaffed position) even holding no slot — `viewer.can_override` + // mirrors the server's decision authz, so the hint and the approve/reject/ + // reassign buttons (which OR in `can_override`) stay consistent. This is a + // privileged recovery path, not the retired per-request "act as" composer. const canApproveReject = useMemo(() => { if (!selected || selected.status !== 'pending') return false; - if (selected.viewer) return selected.viewer.can_act; + if (selected.viewer) return selected.viewer.can_act || !!selected.viewer.can_override; const pending = new Set(selected.pending_approvers || []); return identities.some(id => pending.has(id)); }, [selected, identities]); diff --git a/apps/console/src/services/approvalsApi.ts b/apps/console/src/services/approvalsApi.ts index 850fee0ac7..86251d55d2 100644 --- a/apps/console/src/services/approvalsApi.ts +++ b/apps/console/src/services/approvalsApi.ts @@ -83,6 +83,14 @@ export interface ApprovalRequestRow { can_act: boolean; /** The caller submitted this request. */ is_submitter: boolean; + /** + * framework#3424 — the caller is a platform/tenant admin who may OVERRIDE a + * stuck pending request (approve / reject / reassign it) despite holding no + * approver slot: the recovery path for an approval routed to an unstaffed + * position, which would otherwise lock the record forever. Optional so a + * response from an older backend (no `can_override`) reads as `false`. + */ + can_override?: boolean; }; /** ADR-0044 revision round on this (run, node): absent/1 = first round. */ round?: number;