Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion nodes/ZOOM_RECAPS_TO_SLACK.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ The workflow has two ways in, and both feed one shared posting path:

1. **Webhook**: Zoom calls the workflow when a meeting's summary finishes, and the recap posts about 1 hour later.
Zoom rewrites a summary about 20 minutes after creating it, and the Gate only passes a summary that has been quiet for 15 minutes, so the 1 hour wait lets the rewrite land and the cooldown clear before the recap is fetched.
2. **Daily sweep**: a Schedule Trigger runs each morning, asks Zoom for the previous day's meeting summaries, and posts anything the webhook missed.
2. **Daily sweep**: a Schedule Trigger runs each morning and posts a meeting the webhook missed, as long as no later meeting in its series has posted yet.
Zoom's summary webhook sometimes doesn't fire, and the sweep covers those misses.

Both paths post only meetings listed in the Config table below.
Within a series, only an occurrence that started after the newest one already posted can post.
A series with nothing posted yet, such as one just added to Config, posts its newest ready occurrence.
Zoom's summary list ignores its date window and returns every summary back to 2024, while the Posted table starts in August 2026, so for a series that has posted, this rule is what stops an old, never-posted occurrence from going out as a new recap.

### Data tables

Expand Down
16 changes: 13 additions & 3 deletions src/zoom-recaps-to-slack/build-candidates.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Pick the one meeting to post this run: the newest UNPOSTED occurrence of any configured meeting
// (on a webhook run, of the triggering meeting only) whose summary is ready to post (see readyToPost). An occurrence with nothing to post - a room started
// (on a webhook run, of the triggering meeting only), newer than its series' latest posted occurrence, whose summary is ready to post (see readyToPost). An occurrence with nothing to post - a room started
// and stopped in seconds, a summary Zoom never generated, a failed fetch - is skipped, never allowed to
// block the occurrences behind it. Calendar guardrail: a meeting headed for a PUBLIC channel
// must match a scheduled public-calendar event (same Zoom meeting id + weekday/time) - this stops a
Expand Down Expand Up @@ -52,9 +52,19 @@ try { trigger = $('Verify Zoom').first().json; } catch (e) { trigger = null; }
const triggerId = trigger && trigger.meetingId ? String(trigger.meetingId) : '';
const triggerUuid = trigger && trigger.meetingUuid ? String(trigger.meetingUuid) : '';

// Every unposted occurrence of a configured meeting, newest first.
// The start of each meeting series' newest posted occurrence. Only a later occurrence can post;
// "How It Works" in nodes/ZOOM_RECAPS_TO_SLACK.md gives the reason.
const latestPosted = {};
for (const s of all) {
if (!posted.has(s.meeting_uuid)) continue;
const id = String(s.meeting_id), t = new Date(s.meeting_start_time).getTime();
if (!(latestPosted[id] >= t)) latestPosted[id] = t;
}

// Every unposted occurrence of a configured meeting newer than its series' latest post, newest first.
let eligible = all
.filter(s => config[String(s.meeting_id)] && !posted.has(s.meeting_uuid))
.filter(s => config[String(s.meeting_id)] && !posted.has(s.meeting_uuid)
&& !(new Date(s.meeting_start_time).getTime() <= latestPosted[String(s.meeting_id)]))
.filter(s => !triggerId || String(s.meeting_id) === triggerId)
.sort((a, b) => new Date(b.meeting_start_time) - new Date(a.meeting_start_time));
// Prefer the exact occurrence that triggered the webhook; if the list doesn't show it yet, fall back to
Expand Down
Loading