From 7b81d9ab83bf7a8978c7341810ac544b99b14c7f Mon Sep 17 00:00:00 2001 From: nitin <142569587+ehconitin@users.noreply.github.com> Date: Tue, 28 Jul 2026 19:38:39 +0530 Subject: [PATCH] Fix call-recorder REC badge rendering as empty boxes (#23415) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What was wrong The bot camera image draws a "REC" pill on top of the workspace logo. The label used an SVG `` element, and sharp resolves SVG text through the host's fonts. The runtimes that execute app logic functions ship no fonts, so every character fell back to an empty box: the badge showed "▯▯▯" instead of "REC" in real meetings. It looked fine locally because dev machines have fonts. ## The fix Draw the label as vector outlines instead of text. "REC" is outlined once from Inter SemiBold and stored as an SVG path constant, so the badge renders the same on any host with no font lookup. The pill width is derived from its contents instead of hardcoded, and tests fail if `` or `font-family` ever comes back. CleanShot 2026-07-28 at 19 16 46 1 -- the bug on prod 2 -- how it looks when its not bugged on prod 3 -- this branches changes Review in cubic --- .../constants/recall-bot-image-config.ts | 4 -- .../recording-status-badge-config.ts | 32 +++++++++++ .../__tests__/build-bot-image.util.test.ts | 33 +++++++++++ .../build-recording-status-badge.util.test.ts | 55 +++++++++++++++++++ .../domain/build-bot-image.util.ts | 32 ++--------- .../build-recording-status-badge.util.ts | 31 +++++++++++ 6 files changed, 155 insertions(+), 32 deletions(-) create mode 100644 packages/twenty-apps/public/call-recorder/src/logic-functions/constants/recording-status-badge-config.ts create mode 100644 packages/twenty-apps/public/call-recorder/src/logic-functions/domain/__tests__/build-recording-status-badge.util.test.ts create mode 100644 packages/twenty-apps/public/call-recorder/src/logic-functions/domain/build-recording-status-badge.util.ts diff --git a/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/recall-bot-image-config.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/recall-bot-image-config.ts index 81a74534de..4e93838800 100644 --- a/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/recall-bot-image-config.ts +++ b/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/recall-bot-image-config.ts @@ -8,7 +8,3 @@ export const RECALL_BOT_IMAGE_JPEG_QUALITY = 90; export const RECALL_BOT_IMAGE_MIN_JPEG_QUALITY = 40; // Recall rejects images above 1.3MB; stay just under it. export const RECALL_BOT_IMAGE_MAX_BYTES = 1_300_000; -export const RECALL_BOT_IMAGE_RECORDING_STATUS_BADGE_WIDTH = 132; -export const RECALL_BOT_IMAGE_RECORDING_STATUS_BADGE_HEIGHT = 48; -export const RECALL_BOT_IMAGE_RECORDING_STATUS_BADGE_INSET = 56; -export const RECALL_BOT_IMAGE_RECORDING_STATUS_BADGE_DOT_DIAMETER = 12; diff --git a/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/recording-status-badge-config.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/recording-status-badge-config.ts new file mode 100644 index 0000000000..0ed5cc060d --- /dev/null +++ b/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/recording-status-badge-config.ts @@ -0,0 +1,32 @@ +export const RECORDING_STATUS_BADGE_HEIGHT = 48; +export const RECORDING_STATUS_BADGE_INSET = 56; +export const RECORDING_STATUS_BADGE_PADDING = 18; +export const RECORDING_STATUS_BADGE_DOT_DIAMETER = 12; +export const RECORDING_STATUS_BADGE_DOT_LABEL_GAP = 12; +export const RECORDING_STATUS_BADGE_BACKGROUND = '#feebec'; +export const RECORDING_STATUS_BADGE_BACKGROUND_OPACITY = 0.96; +export const RECORDING_STATUS_BADGE_COLOR = '#ce2c31'; +export const RECORDING_STATUS_BADGE_LABEL_CAP_HEIGHT = 14; + +// "REC" outlined from Inter SemiBold (14px cap height, 2px tracking); the app runtimes ship no fonts, so SVG renders as boxes. +export const RECORDING_STATUS_BADGE_LABEL_PATH = + 'M3.92 14L1.38 14L1.38 0L6.63 0Q8.24 0 9.34 0.56Q10.44 1.12 11.00 2.13Q11.57 3.14 11.57 4.48L11.57 4.48Q11.57 5.82 11.00 6.81Q10.42 7.80 9.32 8.34Q8.21 8.87 6.60 8.87L6.60 8.87L2.86 8.87L2.86 6.77L6.25 6.77Q7.20 6.77 7.80 6.50Q8.40 6.24 8.69 5.73Q8.98 5.22 8.98 4.48L8.98 4.48Q8.98 3.73 8.69 3.20Q8.39 2.67 7.79 2.40Q7.19 2.12 6.24 2.12L6.24 2.12L3.92 2.12L3.92 14Z' + + 'M5.84 7.66L8.61 7.66L12.08 14L9.25 14L5.84 7.66Z' + + 'M25.06 14L15.90 14L15.90 0L25.01 0L25.01 2.13L18.44 2.13L18.44 5.93L24.54 5.93L24.54 8.05L18.44 8.05L18.44 11.87L25.06 11.87L25.06 14Z' + + 'M41.58 4.72L41.58 4.72L39.03 4.72Q38.92 4.09 38.62 3.61Q38.33 3.12 37.89 2.78Q37.46 2.43 36.90 2.26Q36.34 2.08 35.70 2.08L35.70 2.08Q34.56 2.08 33.67 2.66Q32.79 3.23 32.29 4.32Q31.79 5.42 31.79 7L31.79 7Q31.79 8.61 32.30 9.70Q32.80 10.80 33.68 11.36Q34.56 11.92 35.69 11.92L35.69 11.92Q36.32 11.92 36.87 11.75Q37.42 11.58 37.86 11.25Q38.30 10.92 38.61 10.45Q38.91 9.97 39.03 9.35L39.03 9.35L41.58 9.37Q41.44 10.36 40.97 11.24Q40.49 12.11 39.72 12.78Q38.96 13.45 37.93 13.82Q36.91 14.19 35.66 14.19L35.66 14.19Q33.81 14.19 32.36 13.34Q30.91 12.48 30.08 10.87Q29.25 9.26 29.25 7L29.25 7Q29.25 4.74 30.09 3.13Q30.93 1.52 32.38 0.66Q33.83-0.19 35.66-0.19L35.66-0.19Q36.83-0.19 37.83 0.14Q38.84 0.46 39.62 1.10Q40.41 1.73 40.92 2.64Q41.43 3.55 41.58 4.72Z'; + +export const RECORDING_STATUS_BADGE_LABEL_WIDTH = 42.53; + +export const RECORDING_STATUS_BADGE_LABEL_LEFT = + RECORDING_STATUS_BADGE_PADDING + + RECORDING_STATUS_BADGE_DOT_DIAMETER + + RECORDING_STATUS_BADGE_DOT_LABEL_GAP; + +export const RECORDING_STATUS_BADGE_LABEL_TOP = + (RECORDING_STATUS_BADGE_HEIGHT - RECORDING_STATUS_BADGE_LABEL_CAP_HEIGHT) / 2; + +export const RECORDING_STATUS_BADGE_WIDTH = Math.round( + RECORDING_STATUS_BADGE_LABEL_LEFT + + RECORDING_STATUS_BADGE_LABEL_WIDTH + + RECORDING_STATUS_BADGE_PADDING, +); diff --git a/packages/twenty-apps/public/call-recorder/src/logic-functions/domain/__tests__/build-bot-image.util.test.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/domain/__tests__/build-bot-image.util.test.ts index 0af066ccf8..1afd4c057f 100644 --- a/packages/twenty-apps/public/call-recorder/src/logic-functions/domain/__tests__/build-bot-image.util.test.ts +++ b/packages/twenty-apps/public/call-recorder/src/logic-functions/domain/__tests__/build-bot-image.util.test.ts @@ -7,6 +7,11 @@ import { RECALL_BOT_IMAGE_MAX_BYTES, RECALL_BOT_IMAGE_WIDTH, } from 'src/logic-functions/constants/recall-bot-image-config'; +import { + RECORDING_STATUS_BADGE_HEIGHT, + RECORDING_STATUS_BADGE_INSET, + RECORDING_STATUS_BADGE_WIDTH, +} from 'src/logic-functions/constants/recording-status-badge-config'; const createLogo = (format: 'png' | 'gif' | 'jpeg'): Promise => { const image = sharp({ @@ -83,6 +88,34 @@ describe('buildBotImage', () => { expect(metadata.format).toBe('jpeg'); }); + it('composites the recording badge at the badge inset', async () => { + const logoBuffer = await createLogo('png'); + + const readBadgeRegion = async ( + withRecordingStatusBadge: boolean, + ): Promise => { + const base64Jpeg = await buildBotImage({ + logoBuffer, + background: '#ffffff', + withRecordingStatusBadge, + }); + + return sharp(Buffer.from(base64Jpeg as string, 'base64')) + .extract({ + top: RECORDING_STATUS_BADGE_INSET, + left: RECORDING_STATUS_BADGE_INSET, + width: RECORDING_STATUS_BADGE_WIDTH, + height: RECORDING_STATUS_BADGE_HEIGHT, + }) + .raw() + .toBuffer(); + }; + + expect(await readBadgeRegion(true)).not.toEqual( + await readBadgeRegion(false), + ); + }); + it('returns undefined when the source cannot be decoded', async () => { const result = await buildBotImage({ logoBuffer: Buffer.from('not-an-image'), diff --git a/packages/twenty-apps/public/call-recorder/src/logic-functions/domain/__tests__/build-recording-status-badge.util.test.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/domain/__tests__/build-recording-status-badge.util.test.ts new file mode 100644 index 0000000000..ac9a6bccb4 --- /dev/null +++ b/packages/twenty-apps/public/call-recorder/src/logic-functions/domain/__tests__/build-recording-status-badge.util.test.ts @@ -0,0 +1,55 @@ +import sharp from 'sharp'; +import { describe, expect, it } from 'vitest'; + +import { + RECORDING_STATUS_BADGE_HEIGHT, + RECORDING_STATUS_BADGE_LABEL_LEFT, + RECORDING_STATUS_BADGE_WIDTH, +} from 'src/logic-functions/constants/recording-status-badge-config'; +import { + buildRecordingStatusBadge, + buildRecordingStatusBadgeSvg, +} from 'src/logic-functions/domain/build-recording-status-badge.util'; + +describe('buildRecordingStatusBadge', () => { + it('draws the label without depending on any system font', () => { + const svg = buildRecordingStatusBadgeSvg(); + + expect(svg).not.toContain(' { + const metadata = await sharp( + await buildRecordingStatusBadge(sharp), + ).metadata(); + + expect(metadata.width).toBe(RECORDING_STATUS_BADGE_WIDTH); + expect(metadata.height).toBe(RECORDING_STATUS_BADGE_HEIGHT); + }); + + it('rasterizes label ink to the right of the dot', async () => { + const { data, info } = await sharp(await buildRecordingStatusBadge(sharp)) + .ensureAlpha() + .raw() + .toBuffer({ resolveWithObject: true }); + + let labelInkPixelCount = 0; + + for (let y = 0; y < info.height; y++) { + for (let x = RECORDING_STATUS_BADGE_LABEL_LEFT; x < info.width; x++) { + const offset = (y * info.width + x) * info.channels; + + if ( + data[offset] > 150 && + data[offset + 1] < 120 && + data[offset + 2] < 120 + ) { + labelInkPixelCount++; + } + } + } + + expect(labelInkPixelCount).toBeGreaterThan(100); + }); +}); diff --git a/packages/twenty-apps/public/call-recorder/src/logic-functions/domain/build-bot-image.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/domain/build-bot-image.util.ts index 0362de4d84..aba046e598 100644 --- a/packages/twenty-apps/public/call-recorder/src/logic-functions/domain/build-bot-image.util.ts +++ b/packages/twenty-apps/public/call-recorder/src/logic-functions/domain/build-bot-image.util.ts @@ -7,17 +7,12 @@ import { RECALL_BOT_IMAGE_LOGO_MAX_WIDTH, RECALL_BOT_IMAGE_MAX_BYTES, RECALL_BOT_IMAGE_MIN_JPEG_QUALITY, - RECALL_BOT_IMAGE_RECORDING_STATUS_BADGE_DOT_DIAMETER, - RECALL_BOT_IMAGE_RECORDING_STATUS_BADGE_HEIGHT, - RECALL_BOT_IMAGE_RECORDING_STATUS_BADGE_INSET, - RECALL_BOT_IMAGE_RECORDING_STATUS_BADGE_WIDTH, RECALL_BOT_IMAGE_WIDTH, } from 'src/logic-functions/constants/recall-bot-image-config'; +import { RECORDING_STATUS_BADGE_INSET } from 'src/logic-functions/constants/recording-status-badge-config'; +import { buildRecordingStatusBadge } from 'src/logic-functions/domain/build-recording-status-badge.util'; const JPEG_QUALITY_STEP = 10; -const RECORDING_STATUS_BADGE_BACKGROUND = '#feebec'; -const RECORDING_STATUS_BADGE_COLOR = '#ce2c31'; -const RECORDING_STATUS_BADGE_TEXT = 'REC'; type SharpFactory = typeof import('sharp'); @@ -50,8 +45,8 @@ export const buildBotImage = async ({ if (withRecordingStatusBadge) { composites.push({ input: await buildRecordingStatusBadge(sharp), - top: RECALL_BOT_IMAGE_RECORDING_STATUS_BADGE_INSET, - left: RECALL_BOT_IMAGE_RECORDING_STATUS_BADGE_INSET, + top: RECORDING_STATUS_BADGE_INSET, + left: RECORDING_STATUS_BADGE_INSET, }); } @@ -107,25 +102,6 @@ const composeJpeg = ({ .jpeg({ quality, mozjpeg: true }) .toBuffer(); -const buildRecordingStatusBadge = (sharp: SharpFactory): Promise => { - const badgeCornerRadius = RECALL_BOT_IMAGE_RECORDING_STATUS_BADGE_HEIGHT / 2; - const recordingDotCenter = RECALL_BOT_IMAGE_RECORDING_STATUS_BADGE_HEIGHT / 2; - const recordingDotRadius = - RECALL_BOT_IMAGE_RECORDING_STATUS_BADGE_DOT_DIAMETER / 2; - const recordingStatusTextBaseline = - RECALL_BOT_IMAGE_RECORDING_STATUS_BADGE_HEIGHT / 2 + 7; - - const svg = [ - ``, - ``, - ``, - `${RECORDING_STATUS_BADGE_TEXT}`, - '', - ].join(''); - - return sharp(Buffer.from(svg)).png().toBuffer(); -}; - const loadSharp = async (): Promise => { const sharpModule = await import('sharp'); diff --git a/packages/twenty-apps/public/call-recorder/src/logic-functions/domain/build-recording-status-badge.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/domain/build-recording-status-badge.util.ts new file mode 100644 index 0000000000..b173e31d01 --- /dev/null +++ b/packages/twenty-apps/public/call-recorder/src/logic-functions/domain/build-recording-status-badge.util.ts @@ -0,0 +1,31 @@ +import { + RECORDING_STATUS_BADGE_BACKGROUND, + RECORDING_STATUS_BADGE_BACKGROUND_OPACITY, + RECORDING_STATUS_BADGE_COLOR, + RECORDING_STATUS_BADGE_DOT_DIAMETER, + RECORDING_STATUS_BADGE_HEIGHT, + RECORDING_STATUS_BADGE_LABEL_LEFT, + RECORDING_STATUS_BADGE_LABEL_PATH, + RECORDING_STATUS_BADGE_LABEL_TOP, + RECORDING_STATUS_BADGE_PADDING, + RECORDING_STATUS_BADGE_WIDTH, +} from 'src/logic-functions/constants/recording-status-badge-config'; + +type SharpFactory = typeof import('sharp'); + +export const buildRecordingStatusBadgeSvg = (): string => { + const dotRadius = RECORDING_STATUS_BADGE_DOT_DIAMETER / 2; + + return [ + ``, + ``, + ``, + ``, + '', + ].join(''); +}; + +export const buildRecordingStatusBadge = ( + sharp: SharpFactory, +): Promise => + sharp(Buffer.from(buildRecordingStatusBadgeSvg())).png().toBuffer();