diff --git a/meteor/server/api/client.ts b/meteor/server/api/client.ts index ba62dfb7be..10dd6dc7ca 100644 --- a/meteor/server/api/client.ts +++ b/meteor/server/api/client.ts @@ -30,6 +30,7 @@ import { } from '../security/check' import { UserActionsLog } from '../collections' import { executePeripheralDeviceFunctionWithCustomTimeout } from './peripheralDevice/executeFunction' +import { resolveActionResult } from './peripheralDevice' import { LeveledLogMethodFixed } from '@sofie-automation/corelib/dist/logging' import { assertConnectionHasOneOfPermissions } from '../security/auth' @@ -458,7 +459,7 @@ class ServerClientAPIClass extends MethodContextAPI implements NewClientAPI { actionId: string, payload?: Record ) { - return ServerClientAPI.callPeripheralDeviceFunctionOrAction( + const result = await ServerClientAPI.callPeripheralDeviceFunctionOrAction( this, context, deviceId, @@ -470,6 +471,7 @@ class ServerClientAPIClass extends MethodContextAPI implements NewClientAPI { actionId, payload ) + return resolveActionResult(deviceId, result) } async callBackgroundPeripheralDeviceFunction( deviceId: PeripheralDeviceId, diff --git a/meteor/server/api/peripheralDevice.ts b/meteor/server/api/peripheralDevice.ts index a49d777410..a037cd6386 100644 --- a/meteor/server/api/peripheralDevice.ts +++ b/meteor/server/api/peripheralDevice.ts @@ -75,7 +75,7 @@ import { assertConnectionHasOneOfPermissions } from '../security/auth' import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { getRootSubpath } from '../lib' import { evalBlueprint } from './blueprints/cache' -import { StudioBlueprintManifest } from '@sofie-automation/blueprints-integration' +import { StudioBlueprintManifest, TSR } from '@sofie-automation/blueprints-integration' import { StatusMessageResolver } from '@sofie-automation/corelib' import { interpollateTranslation } from '@sofie-automation/corelib/dist/TranslatableMessage' import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' @@ -189,6 +189,82 @@ async function resolveDeviceStatusDetails( } } +/** + * Resolve a TSR ActionExecutionResult using the Studio blueprint's deviceActionMessages. + * If the result has a structured `code` and `context`, and the blueprint defines a custom + * message template for that code, the `response` field is replaced with the resolved message. + * + * @param deviceId - The peripheral device ID (used to look up the studio and blueprint) + * @param result - The action execution result from TSR + * @returns The result with `response` resolved if a custom message was found + */ +export async function resolveActionResult( + deviceId: PeripheralDeviceId, + result: TSR.ActionExecutionResult +): Promise { + if (result.result === TSR.ActionExecutionResultCode.Ok) return result + if (!result.code) return result + + try { + const device = (await PeripheralDevices.findOneAsync(deviceId, { + projection: { name: 1, studioAndConfigId: 1 }, + })) as Pick | undefined + + if (!device?.studioAndConfigId?.studioId) return result + + const studio = (await Studios.findOneAsync(device.studioAndConfigId.studioId, { + projection: { blueprintId: 1 }, + })) as Pick | undefined + + if (!studio?.blueprintId) return result + + const blueprint = (await Blueprints.findOneAsync(studio.blueprintId, { + projection: { _id: 1, name: 1, code: 1 }, + })) as Pick | undefined + + if (!blueprint) return result + + const blueprintManifest = evalBlueprint(blueprint) as StudioBlueprintManifest + + if (!blueprintManifest.deviceActionMessages) return result + + const resolver = new StatusMessageResolver(blueprint._id, blueprintManifest.deviceActionMessages, undefined) + + // Use the existing TSR response as the fallback default message + const defaultMessage = result.response?.key ?? '' + + const resolved = resolver.getDeviceStatusMessage( + result.code, + { + ...(result.context ?? {}), + deviceName: device.name, + deviceId: unprotectString(deviceId), + }, + defaultMessage + ) + + if (resolved === null) { + // Message suppressed by blueprint + return result + } + + // resolved.key is either the custom blueprint message or the defaultMessage + if (resolved.key === defaultMessage) { + // No custom message found - keep original response unchanged + return result + } + + const interpolated = interpollateTranslation(resolved.key, resolved.args) + return { + ...result, + response: { key: interpolated }, + } + } catch (e) { + logger.error(`Error resolving device action messages: ${e}`) + return result + } +} + export namespace ServerPeripheralDeviceAPI { export async function initialize( context: MethodContext, diff --git a/meteor/yarn.lock b/meteor/yarn.lock index 17569f1a74..181b6062e4 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -2421,7 +2421,7 @@ __metadata: dependencies: "@mos-connection/model": "npm:^5.0.0-alpha.0" kairos-lib: "npm:^1.0.0" - timeline-state-resolver-types: "npm:10.0.0-nightly-main-20260601-094843-59ce43391.0" + timeline-state-resolver-types: "npm:10.0.0-nightly-main-20260602-133931-c0882da4d.0" tslib: "npm:^2.8.1" type-fest: "npm:^4.41.0" languageName: node @@ -12415,13 +12415,13 @@ __metadata: languageName: node linkType: hard -"timeline-state-resolver-types@npm:10.0.0-nightly-main-20260601-094843-59ce43391.0": - version: 10.0.0-nightly-main-20260601-094843-59ce43391.0 - resolution: "timeline-state-resolver-types@npm:10.0.0-nightly-main-20260601-094843-59ce43391.0" +"timeline-state-resolver-types@npm:10.0.0-nightly-main-20260602-133931-c0882da4d.0": + version: 10.0.0-nightly-main-20260602-133931-c0882da4d.0 + resolution: "timeline-state-resolver-types@npm:10.0.0-nightly-main-20260602-133931-c0882da4d.0" dependencies: kairos-lib: "npm:1.0.0" tslib: "npm:^2.8.1" - checksum: 10/87a33b766a94e809cdcd3517f695c4e171bbb3a451ad390adbfc2ca31be8ccff0a3531e81f0a548b683d9fea964252043837b89c6cd12c7d27d0f7e9205b65f2 + checksum: 10/8021f03e15a7b6f581ea4c5b77e809238a3e8120a7b0a8eed40d3a1afcb03749aaed33177b96a0c42078dcd19da08703f125ee3a1f28739bfcb9782344321bc8 languageName: node linkType: hard diff --git a/packages/blueprints-integration/src/api/studio.ts b/packages/blueprints-integration/src/api/studio.ts index 2fc47141b9..f52c380600 100644 --- a/packages/blueprints-integration/src/api/studio.ts +++ b/packages/blueprints-integration/src/api/studio.ts @@ -112,6 +112,31 @@ export interface StudioBlueprintManifest< */ deviceStatusMessages?: Record + /** + * Alternate device action error messages, to override the default messages from TSR devices. + * Keys are action error code strings from TSR devices (e.g., 'ACTION_HTTPSEND_REQUEST_FAILED'). + * + * Similar to deviceStatusMessages but applies to device action execution failures + * (e.g., HTTP Send failures, device restart failures) rather than ongoing status errors. + * + * Import action error codes from 'timeline-state-resolver-types' for type safety. + * Values can be: + * - String templates using {{variable}} syntax for interpolation with context values + * - Functions that receive DeviceStatusContext and return a custom message string + * - Empty string to suppress the message entirely (action result will show as generic error) + * + * @example + * ```typescript + * import { HttpSendActionErrorCode } from 'timeline-state-resolver-types' + * + * deviceActionMessages: { + * [HttpSendActionErrorCode.REQUEST_FAILED]: 'Failed to trigger graphics: {{errorMessage}}', + * [HttpSendActionErrorCode.MISSING_URL]: 'HTTP action not configured - missing URL', + * } + * ``` + */ + deviceActionMessages?: Record + /** Returns the items used to build the baseline (default state) of a studio, this is the baseline used when there's no active rundown */ getBaseline: (context: IStudioBaselineContext) => BlueprintResultStudioBaseline diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index 8a9468376c..9dc7011170 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -51,7 +51,7 @@ "dependencies": { "@mos-connection/model": "^5.0.0-alpha.0", "kairos-lib": "^1.0.0", - "timeline-state-resolver-types": "10.0.0-nightly-main-20260601-094843-59ce43391.0", + "timeline-state-resolver-types": "10.0.0-nightly-main-20260602-133931-c0882da4d.0", "tslib": "^2.8.1", "type-fest": "^4.41.0" }, diff --git a/packages/yarn.lock b/packages/yarn.lock index 2f4364a196..637690b89d 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -7142,7 +7142,7 @@ __metadata: dependencies: "@mos-connection/model": "npm:^5.0.0-alpha.0" kairos-lib: "npm:^1.0.0" - timeline-state-resolver-types: "npm:10.0.0-nightly-main-20260601-094843-59ce43391.0" + timeline-state-resolver-types: "npm:10.0.0-nightly-main-20260602-133931-c0882da4d.0" tslib: "npm:^2.8.1" type-fest: "npm:^4.41.0" languageName: unknown @@ -28341,6 +28341,16 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"timeline-state-resolver-types@npm:10.0.0-nightly-main-20260602-133931-c0882da4d.0": + version: 10.0.0-nightly-main-20260602-133931-c0882da4d.0 + resolution: "timeline-state-resolver-types@npm:10.0.0-nightly-main-20260602-133931-c0882da4d.0" + dependencies: + kairos-lib: "npm:1.0.0" + tslib: "npm:^2.8.1" + checksum: 10/8021f03e15a7b6f581ea4c5b77e809238a3e8120a7b0a8eed40d3a1afcb03749aaed33177b96a0c42078dcd19da08703f125ee3a1f28739bfcb9782344321bc8 + languageName: node + linkType: hard + "timeline-state-resolver@npm:10.0.0-nightly-main-20260601-094843-59ce43391.0": version: 10.0.0-nightly-main-20260601-094843-59ce43391.0 resolution: "timeline-state-resolver@npm:10.0.0-nightly-main-20260601-094843-59ce43391.0"