feat(bot): link roll-call embeds to the mission page with a summary
The embed title links to the mission page and an 'Open the op' field carries the URL; the mission summary is shown under the title, truncated to 300 characters with an ellipsis. The attendance hash gains an embed schema version so live roll-calls re-render exactly once.
This commit is contained in:
parent
efae94c8a9
commit
4fad7cd259
1 changed files with 21 additions and 2 deletions
|
|
@ -64,8 +64,22 @@ const operationTypeLabel = (type: Mission["operationType"]): string => {
|
||||||
/**
|
/**
|
||||||
* Deterministic fingerprint of the attendance state and scheduled start time
|
* Deterministic fingerprint of the attendance state and scheduled start time
|
||||||
* so reconcile can skip embeds that haven't changed (anti-loop), while still
|
* 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 => {
|
export const attendanceHash = (docs: MissionAttendance[], startDateTime?: string): string => {
|
||||||
const rows = docs
|
const rows = docs
|
||||||
.map((doc) => {
|
.map((doc) => {
|
||||||
|
|
@ -73,7 +87,7 @@ export const attendanceHash = (docs: MissionAttendance[], startDateTime?: string
|
||||||
return [userId, doc.response] as const;
|
return [userId, doc.response] as const;
|
||||||
})
|
})
|
||||||
.sort((a, b) => a[0] - b[0] || a[1].localeCompare(b[1]));
|
.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 (
|
export const fetchAttendanceDocs = async (
|
||||||
|
|
@ -172,11 +186,15 @@ export const renderAttendanceEmbed = (
|
||||||
summary: MissionAttendanceSummary,
|
summary: MissionAttendanceSummary,
|
||||||
): EmbedBuilder => {
|
): EmbedBuilder => {
|
||||||
const startUnix = Math.floor(new Date(mission.classification.startDateTime).getTime() / 1000);
|
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 names = (users: User[]): string => users.map(memberName).join("\n").slice(0, 1024) || "-";
|
||||||
|
const summaryText = renderSummaryForEmbed(mission.summary);
|
||||||
return new EmbedBuilder()
|
return new EmbedBuilder()
|
||||||
.setTitle(missionTitle(mission))
|
.setTitle(missionTitle(mission))
|
||||||
|
.setURL(missionUrl)
|
||||||
.setDescription(
|
.setDescription(
|
||||||
[
|
[
|
||||||
|
...(summaryText ? [summaryText, ""] : []),
|
||||||
`**Operation Type:** ${operationTypeLabel(mission.operationType)}`,
|
`**Operation Type:** ${operationTypeLabel(mission.operationType)}`,
|
||||||
`**Starts:** <t:${startUnix}:F> (<t:${startUnix}:R>)`,
|
`**Starts:** <t:${startUnix}:F> (<t:${startUnix}:R>)`,
|
||||||
`**Max Players:** ${mission.missionRoles.maxPlayers}`,
|
`**Max Players:** ${mission.missionRoles.maxPlayers}`,
|
||||||
|
|
@ -191,6 +209,7 @@ export const renderAttendanceEmbed = (
|
||||||
inline: true,
|
inline: true,
|
||||||
},
|
},
|
||||||
{ name: `❌ No (${summary.counts.no})`, value: names(summary.no), inline: true },
|
{ name: `❌ No (${summary.counts.no})`, value: names(summary.no), inline: true },
|
||||||
|
{ name: "Open the op", value: missionUrl },
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue