From f6d42399d48cb256c53f96f9fc1afd41298921fd Mon Sep 17 00:00:00 2001 From: Z8MB1E Date: Fri, 28 Aug 2026 00:20:24 -0400 Subject: [PATCH] fix(bot): refresh roll-call date changes --- src/bot/events/interactionCreate.ts | 5 +++- src/bot/services/missionEmbeds.ts | 38 +++++++++++++++++------------ 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/bot/events/interactionCreate.ts b/src/bot/events/interactionCreate.ts index 0f73547..0b097bf 100644 --- a/src/bot/events/interactionCreate.ts +++ b/src/bot/events/interactionCreate.ts @@ -66,7 +66,10 @@ export const handleButtonInteraction = async ( await setMissionAttendance(payload, { missionId, userId: user.id, response }); const summary = await getMissionAttendance(payload, missionId); - const hash = attendanceHash(await fetchAttendanceDocs(payload, missionId)); + const hash = attendanceHash( + await fetchAttendanceDocs(payload, missionId), + mission.classification.startDateTime, + ); await interaction.message.edit({ embeds: [renderAttendanceEmbed(mission, summary)], diff --git a/src/bot/services/missionEmbeds.ts b/src/bot/services/missionEmbeds.ts index b6dcf3e..14671e9 100644 --- a/src/bot/services/missionEmbeds.ts +++ b/src/bot/services/missionEmbeds.ts @@ -28,12 +28,6 @@ const missionTitle = (mission: Mission): string => const memberName = (user: User): string => user.displayName ?? user.username; -const isStillFuture = (mission: Mission): boolean => { - const start = new Date(mission.classification.startDateTime).getTime(); - const end = start + mission.classification.estimatedDuration * 60_000; - return end > Date.now(); -}; - /** * Roll call is sent once: as soon as the mission is Ready, or three days * before a Scheduled mission, whichever happens first. @@ -59,17 +53,18 @@ const operationTypeLabel = (type: Mission["operationType"]): string => { }; /** - * Deterministic fingerprint of the attendance state so reconcile can skip - * embeds that haven't changed (anti-loop). + * 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. */ -export const attendanceHash = (docs: MissionAttendance[]): string => { +export const attendanceHash = (docs: MissionAttendance[], startDateTime?: string): string => { const rows = docs .map((doc) => { const userId = typeof doc.user === "object" ? doc.user.id : doc.user; return [userId, doc.response] as const; }) .sort((a, b) => a[0] - b[0] || a[1].localeCompare(b[1])); - return JSON.stringify(rows); + return JSON.stringify({ rows, startDateTime: startDateTime ?? null }); }; export const fetchAttendanceDocs = async ( @@ -97,17 +92,23 @@ export const getEligibleMissions = async (payload: Payload): Promise limit: MAX_ELIGIBLE_MISSIONS, where: { and: [ - { "classification.startDateTime": { greater_than: new Date().toISOString() } }, { "ownershipAndStatus.visibility": { equals: "unit" } }, { or: [ - { "ownershipAndStatus.status": { equals: "Ready" } }, { and: [ + { "classification.startDateTime": { greater_than: new Date().toISOString() } }, + { "ownershipAndStatus.status": { equals: "Ready" } }, + ], + }, + { + and: [ + { "classification.startDateTime": { greater_than: new Date().toISOString() } }, { "ownershipAndStatus.status": { equals: "Scheduled" } }, { "classification.startDateTime": { less_than_equal: rollCallCutoff } }, ], }, + { discordMessageId: { exists: true } }, ], }, ], @@ -200,7 +201,10 @@ const postEmbed = async ( mission: Mission, ): Promise => { const summary = await getMissionAttendance(payload, mission.id); - const hash = attendanceHash(await fetchAttendanceDocs(payload, mission.id)); + const hash = attendanceHash( + await fetchAttendanceDocs(payload, mission.id), + mission.classification.startDateTime, + ); const channel = await getOpsChannel(client, opsChannelId); const message = await channel.send({ embeds: [renderAttendanceEmbed(mission, summary)], @@ -257,7 +261,10 @@ const manageMission = async ( return; } - const hash = attendanceHash(await fetchAttendanceDocs(payload, mission.id)); + const hash = attendanceHash( + await fetchAttendanceDocs(payload, mission.id), + mission.classification.startDateTime, + ); if (hash === mission.discordAttendanceHash) { return; } @@ -297,7 +304,6 @@ export const reconcileMission = async ( if ( mission.ownershipAndStatus.visibility !== "unit" || !["Ready", "Scheduled"].includes(mission.ownershipAndStatus.status) || - !isStillFuture(mission) || (!isRollCallDue(mission) && !mission.discordMessageId) ) { return; @@ -353,7 +359,7 @@ export const reconcile = async (client: Client, payload: Payload): Promise } const eligible = (await getEligibleMissions(payload)).filter( - (mission) => isStillFuture(mission) && (isRollCallDue(mission) || !!mission.discordMessageId), + (mission) => isRollCallDue(mission) || !!mission.discordMessageId, ); for (const mission of eligible) { try {