From b3058d47eab2582e26ea2ff58cd2f6b88cc903a8 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Fri, 17 Jul 2026 12:07:19 +0100 Subject: [PATCH] fix(frontend): surface the refusal reason when a container update plan fails A refused update plan showed only 'Action execution is unavailable'; the actionable explanation (agent disconnected, agent too old, stale inventory) arrives in the API error's details.reason and was dropped. Prefer it in the failure badge and tooltip. Seen in #1564 where the toast gave the reporter nothing to act on and Actions showed no record (refusal is pre-audit). --- .../__tests__/ContainerUpdateBadge.test.tsx | 27 +++++++++++++++++++ .../shared/containerUpdateBadgeModel.ts | 14 ++++++++++ .../shared/useContainerUpdateButtonState.ts | 3 ++- 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/frontend-modern/src/components/shared/__tests__/ContainerUpdateBadge.test.tsx b/frontend-modern/src/components/shared/__tests__/ContainerUpdateBadge.test.tsx index 677964049b..f3fa1fa91e 100644 --- a/frontend-modern/src/components/shared/__tests__/ContainerUpdateBadge.test.tsx +++ b/frontend-modern/src/components/shared/__tests__/ContainerUpdateBadge.test.tsx @@ -7,6 +7,7 @@ import { ContainerUpdateBadge, UpdateButton, } from '@/components/shared/ContainerUpdateBadge'; +import { getUpdatePlanErrorMessage } from '@/components/shared/containerUpdateBadgeModel'; vi.mock('@/api/monitoring', () => ({ MonitoringAPI: { @@ -123,3 +124,29 @@ describe('ContainerUpdateBadge', () => { expect(screen.queryByRole('button')).not.toBeInTheDocument(); }); }); + +describe('getUpdatePlanErrorMessage', () => { + it('prefers the availability refusal reason over the generic message', () => { + const error = Object.assign(new Error('Action execution is unavailable'), { + details: { + reasonCode: 'operation_receipt_unsupported', + reason: 'The Pulse agent on this host is still on an older version.', + }, + }); + + expect(getUpdatePlanErrorMessage(error)).toBe( + 'The Pulse agent on this host is still on an older version.', + ); + }); + + it('falls back to the error message when no reason detail exists', () => { + expect(getUpdatePlanErrorMessage(new Error('Pulse refused the update plan.'))).toBe( + 'Pulse refused the update plan.', + ); + }); + + it('falls back to a default when the error is empty', () => { + expect(getUpdatePlanErrorMessage(new Error(''))).toBe('Failed to plan the update'); + expect(getUpdatePlanErrorMessage(null)).toBe('Failed to plan the update'); + }); +}); diff --git a/frontend-modern/src/components/shared/containerUpdateBadgeModel.ts b/frontend-modern/src/components/shared/containerUpdateBadgeModel.ts index 8e1dff7935..5fb7e3cdc1 100644 --- a/frontend-modern/src/components/shared/containerUpdateBadgeModel.ts +++ b/frontend-modern/src/components/shared/containerUpdateBadgeModel.ts @@ -146,3 +146,17 @@ export function getUpdateButtonTooltip(options: { return `Click to update\nCurrent: ${current}...\nLatest: ${latest}...`; } } + +/** + * Pick the user-facing message for a failed update plan request. + * + * Availability refusals carry the actionable explanation (agent disconnected, + * agent too old, stale inventory) in the API error's details.reason; the + * top-level message is just "Action execution is unavailable". + */ +export function getUpdatePlanErrorMessage(error: unknown): string { + const apiError = error as (Error & { details?: Record }) | null; + return ( + apiError?.details?.reason?.trim() || apiError?.message?.trim() || 'Failed to plan the update' + ); +} diff --git a/frontend-modern/src/components/shared/useContainerUpdateButtonState.ts b/frontend-modern/src/components/shared/useContainerUpdateButtonState.ts index 29e330d1a4..a08f8511d2 100644 --- a/frontend-modern/src/components/shared/useContainerUpdateButtonState.ts +++ b/frontend-modern/src/components/shared/useContainerUpdateButtonState.ts @@ -13,6 +13,7 @@ import type { ActionDetailResponse } from '@/types/actionAudit'; import { getUpdateButtonLabel, getUpdateButtonTooltip, + getUpdatePlanErrorMessage, hasContainerUpdate, hasContainerUpdateCurrent, hasContainerUpdateError, @@ -113,7 +114,7 @@ export function useContainerUpdateButtonState(props: UpdateButtonProps) { setReviewDetail(await ResourceActionsAPI.getAction(plan.actionId)); setLocalState('idle'); } catch (error) { - const message = (error as Error).message || 'Failed to plan the update'; + const message = getUpdatePlanErrorMessage(error); setErrorMessage(message); setLocalState('idle'); markContainerUpdateError(props.agentId, props.containerId, message);