diff --git a/artifacts/api-server/src/lib/executive-meeting-scheduler.ts b/artifacts/api-server/src/lib/executive-meeting-scheduler.ts index 8a45d09c..7a6b0410 100644 --- a/artifacts/api-server/src/lib/executive-meeting-scheduler.ts +++ b/artifacts/api-server/src/lib/executive-meeting-scheduler.ts @@ -20,6 +20,29 @@ function pad2(n: number): string { return String(n).padStart(2, "0"); } +/** + * Reduce a rich-text snippet (Tiptap HTML) to plain text safe for a + * push-notification body. Meeting titles are stored as rich text so the + * scheduler can't just shove them into the payload — iOS shows the raw + * `
…
` markup verbatim on the lock + * screen otherwise. + */ +function stripHtmlToPlainText(input: string | null | undefined): string { + if (!input) return ""; + // Drop tags, collapse whitespace, decode the handful of named + // entities Tiptap emits. Anything more (full HTML parser) would + // pull a dependency for zero benefit on a 1-line notification. + const noTags = input.replace(/<[^>]*>/g, " "); + const decoded = noTags + .replace(/ /g, " ") + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, '"') + .replace(/'/g, "'"); + return decoded.replace(/\s+/g, " ").trim(); +} + function dateKey(d: Date): string { return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`; } @@ -125,8 +148,14 @@ async function tickOnce(): Promise