diff --git a/src/bot/services/missionEmbeds.ts b/src/bot/services/missionEmbeds.ts index 6d77353..b34cec9 100644 --- a/src/bot/services/missionEmbeds.ts +++ b/src/bot/services/missionEmbeds.ts @@ -64,8 +64,22 @@ const operationTypeLabel = (type: Mission["operationType"]): string => { /** * Deterministic fingerprint of the attendance state and scheduled start time * so reconcile can skip embeds that haven't changed (anti-loop), while still - * detecting mission date changes. + * detecting mission date changes. The schema version forces one re-render of + * already-live roll-calls when the embed layout changes (v2 added the + * mission-page link); bump it again for future layout-only changes. */ +const EMBED_SCHEMA_VERSION = 3; + +const SUMMARY_MAX_CHARS = 300; + +const renderSummaryForEmbed = (summary: string | null | undefined): string => { + if (!summary?.trim()) return ""; + const text = summary.trim(); + return text.length > SUMMARY_MAX_CHARS + ? `${text.slice(0, SUMMARY_MAX_CHARS).trimEnd()}...` + : text; +}; + export const attendanceHash = (docs: MissionAttendance[], startDateTime?: string): string => { const rows = docs .map((doc) => { @@ -73,7 +87,7 @@ export const attendanceHash = (docs: MissionAttendance[], startDateTime?: string return [userId, doc.response] as const; }) .sort((a, b) => a[0] - b[0] || a[1].localeCompare(b[1])); - return JSON.stringify({ rows, startDateTime: startDateTime ?? null }); + return JSON.stringify({ rows, startDateTime: startDateTime ?? null, v: EMBED_SCHEMA_VERSION }); }; export const fetchAttendanceDocs = async ( @@ -172,11 +186,15 @@ export const renderAttendanceEmbed = ( summary: MissionAttendanceSummary, ): EmbedBuilder => { const startUnix = Math.floor(new Date(mission.classification.startDateTime).getTime() / 1000); + const missionUrl = `${botConfig.appUrl}/intelligence/missions/${mission.id}`; const names = (users: User[]): string => users.map(memberName).join("\n").slice(0, 1024) || "-"; + const summaryText = renderSummaryForEmbed(mission.summary); return new EmbedBuilder() .setTitle(missionTitle(mission)) + .setURL(missionUrl) .setDescription( [ + ...(summaryText ? [summaryText, ""] : []), `**Operation Type:** ${operationTypeLabel(mission.operationType)}`, `**Starts:** ()`, `**Max Players:** ${mission.missionRoles.maxPlayers}`, @@ -191,6 +209,7 @@ export const renderAttendanceEmbed = ( inline: true, }, { name: `❌ No (${summary.counts.no})`, value: names(summary.no), inline: true }, + { name: "Open the op", value: missionUrl }, ); };