From a68456f649aa99d461786d051f5903d3c942c487 Mon Sep 17 00:00:00 2001 From: Rachel Moore Date: Sun, 16 Aug 2026 12:26:45 -0400 Subject: [PATCH 1/3] Document Multi-Day Event attendance design (ADR-0011) Multi-Day Events now get real Attendance Tracking via per-day Multi-Day Child Events, reusing existing All-Day machinery unmodified, rather than the deferred/no-attendance state CONTEXT.md previously described. Ref: FRC2713/hawk-bot#32 Co-Authored-By: Claude Sonnet 5 --- CONTEXT.md | 10 +++++++--- ...iday-events-decompose-into-per-day-child-events.md | 11 +++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 docs/adr/0011-multiday-events-decompose-into-per-day-child-events.md diff --git a/CONTEXT.md b/CONTEXT.md index 6f12d7e..cafb63a 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -21,15 +21,19 @@ Classifies an Event by its time shape, and determines the Check-in Post timing r - **Hourly** — fixed start and end time on a single day (e.g. 10am–2pm). - **All-Day** — a full-day calendar entry where start date = end date. -- **Multi-Day** — a full-day calendar entry spanning a start date and a later end date (e.g. a competition). Deferred — not handled by v1. +- **Multi-Day** — a full-day calendar entry spanning a start date and a later end date (e.g. a competition). Gets no Event Check-in Post or attendance of its own — instead it generates a Multi-Day Child Event per day, each carrying real Attendance Tracking. See Multi-Day Child Event. _Avoid_: "event type" +**Multi-Day Child Event**: +A synthetic, ordinary All-Day Event that Hawk Bot generates automatically for each calendar day of a Multi-Day Event's span, once that Multi-Day Event's Calendar Role is `team_meeting` — a 3-day regional generates three. This is the entire mechanism by which a multi-day competition gets real Attendance Tracking: each Multi-Day Child Event gets its own Event Check-in Post, its own Reaction Cutoff, its own Event Attendance Report — every mechanic identical to any other All-Day Event, with no new attendance logic anywhere. The Multi-Day Event itself (the "parent") is untouched by this — it still syncs, still gets a Meeting Type, and still appears exactly once in the Weekly Summary Post with its full date range; a Multi-Day Child Event never appears there itself, so the same competition is never listed more than once. If the parent's date span changes on a later sync, Multi-Day Child Events are added or marked removed to match, the same way an Event is normally created or removed — a Multi-Day Child Event whose attendance is already finalized is never retroactively altered by this. See ADR-0011. +_Avoid_: "sub-event", "day instance" (suggests calendar recurrence, which this isn't) + **Event Check-in Post**: The single Slack message posted before an Event that both reminds the team of it and serves as the surface people react to for attendance. Includes the Event's title, date/time, location, description, and a short reminder of what each reaction means. Posted to the announcements channel with an `@channel` mention. The bot pre-populates it with 👍, a Clock Reaction, and ❌ so a team member can just click an existing reaction rather than pick their own emoji from scratch; picking something else entirely still works and is read the same way. The bot deletes it at the Reaction Cutoff, which finalizes attendance for that Event. _Avoid_: pre-event reminder post, check-in post (as if separate from the reminder) **Check-in Post Timing**: -How long before an Event its Check-in Post goes out. Set per Meeting Type (not per individual Event) by Hawk Bot admins. Starting defaults: Hourly posts 4 hours before the scheduled start; All-Day posts at 4pm the day before. +How long before an Event its Check-in Post goes out. Set per Meeting Type (not per individual Event) by Hawk Bot admins. Starting defaults: Hourly posts 4 hours before the scheduled start; All-Day posts at 4pm the day before. A Multi-Day Event's Check-in Posts (one per Multi-Day Child Event) all post together — admin-configurable days before the Multi-Day Event's first day (default 2), at the same time-of-day as the All-Day default — rather than each child posting the night before its own day, so the team can mark a whole competition's attendance in one sitting before they're busy at the venue. **Calendar Change Handling**: What happens when the Team Meeting Calendar changes after an Event's Check-in Post has already gone out. The original post is never deleted or reposted for an edit — the Event record behind it is simply updated (including a duration change), and any reactions already collected carry forward unchanged. What the bot does depends on the kind of change: @@ -74,7 +78,7 @@ No link out to another surface — the detail lives entirely inside the thread r Replaces "Slack workspace Owner/Admin" as the authorization model for every admin-gated capability (`/hawkbot config`, `/hawkbot event create`, the season CSV export, and the Reaction Cutoff Verification failure DM) — full replacement, not an additional either/or check. Defined by membership in a dedicated Slack User Group — not Slack's built-in Admins group — so a team can grant bot-admin duties (mentors, team leads) without handing out full Slack workspace administration. An admin configures it by handle (e.g. `hawkbot-admins`); the bot resolves that to the group's Slack-internal id once, at set-time, and checks membership against the id from then on. Slack workspace Owners always retain HawkBot Admin rights regardless of the group's state, as a bootstrap/lockout safety net — otherwise an empty or unconfigured group would mean nobody could run the command needed to fix it. Group membership is cached (same 5-minute-TTL shape as the old per-user admin cache), rather than looked up per check. **Weekly Summary Post**: -A single Slack message posted to the announcements channel at an admin-configurable day/time (default Sunday, noon), listing every Event scheduled for the 7 days starting the day after that post — a rolling window, not aligned to any fixed Monday–Sunday calendar week (see ADR-0011) — a full planning digest, including Hourly, All-Day, and Multi-Day Events alike, regardless of whether an Event already has (or will soon have) its own Event Check-in Post. Only one Weekly Summary Post exists at a time: the previous one is deleted before the new one is posted. This is the first place Multi-Day Events are tracked as Events at all — nothing about their attendance mechanics changes; they're still deferred — this just makes them visible for planning. +A single Slack message posted to the announcements channel at an admin-configurable day/time (default Sunday, noon), listing every Event scheduled for the 7 days starting the day after that post — a rolling window, not aligned to any fixed Monday–Sunday calendar week (see ADR-0011) — a full planning digest, including Hourly, All-Day, and Multi-Day Events alike, regardless of whether an Event already has (or will soon have) its own Event Check-in Post. Only one Weekly Summary Post exists at a time: the previous one is deleted before the new one is posted. A Multi-Day Event appears here exactly once, as its own date-range line — its Multi-Day Child Events are never listed individually, so the same competition is never shown more than once. _Avoid_: "this week" or "calendar week" implying Monday–Sunday alignment — "This Week" is the Slack-facing label, but the underlying window floats with whatever day the post lands on (see ADR-0011). **Weekly Summary Change Reflection**: diff --git a/docs/adr/0011-multiday-events-decompose-into-per-day-child-events.md b/docs/adr/0011-multiday-events-decompose-into-per-day-child-events.md new file mode 100644 index 0000000..9569f38 --- /dev/null +++ b/docs/adr/0011-multiday-events-decompose-into-per-day-child-events.md @@ -0,0 +1,11 @@ +# Multi-Day Event attendance is per-day Child Events, not one combined post + +A Multi-Day Event gets Attendance Tracking by generating a Multi-Day Child Event — an ordinary, synthetic All-Day Event — for each calendar day of its span. Each child gets its own Event Check-in Post, Reaction Cutoff, and Event Attendance Report, running through 100% of the existing All-Day machinery unmodified. The Multi-Day Event itself is untouched: it still syncs and still appears once in the Weekly Summary Post with its full date range, exactly as it does today; it simply never gets a Check-in Post or attendance of its own. See `CONTEXT.md`, Multi-Day Child Event. + +The alternative considered first — one combined Check-in Post per competition, with a numbered-emoji reaction (1️⃣, 2️⃣, …) per day — was designed in detail before being set aside. It would have required a new day dimension on the `attendance` table (today's schema is strictly one row per `(event_id, user_id)`, with no way to represent "attended day 1 and 3 but not 2"), a new emoji vocabulary with a hard cap on supported days, an extension to `reactionCutoff()` (which today only ever looks at an Event's `startsAt`, never `endsAt`) to handle a multi-day span correctly, and a carve-out from ADR-0003's "👍 is the sole attendance signal" rule, since a single combined post has no single boolean signal to key off of. The per-day decomposition needs none of that — every one of those problems either doesn't apply or is already solved by existing single-day code. + +**The trade being made**: this gives up the "one interaction per person per competition" goal that motivated the feature in the first place. A 3-day competition now produces three separate Check-in Posts instead of one. What keeps that from reintroducing the original problem — competition days already being busy — is that Check-in Post *timing* is decoupled from Reaction Cutoff *timing*: every child's Check-in Post is posted together, an admin-configurable number of days before the competition's first day (default 2, see `checkin_offset_multiday_days`), but each child's Reaction Cutoff still lands independently at the end of its own day. Posting is batched for convenience; attendance-locking isn't, so a schedule change mid-competition can still register on a later day. + +**Consequence**: one new nullable column, `events.multiday_parent_id`, linking a Multi-Day Child Event back to its parent — used both to exclude children from the Weekly Summary listing and to reconcile the child set when a competition's date span changes on a later sync (adding or removing children to match, never retroactively altering an already-finalized child). One new setting, `checkin_offset_multiday_days`. No other change to the attendance domain model. + +Worth revisiting if three (or four) near-simultaneous, separately-`@channel`-tagged Check-in Posts turn out to feel spammy in practice, or if mentors want one consolidated Event Attendance Report per competition rather than one per day — both were explicitly deferred, not ruled out, since either can likely be layered on without touching this decomposition. From 20b3705677627463a358bc55e732bf1ab5497d71 Mon Sep 17 00:00:00 2001 From: Rachel Moore Date: Wed, 19 Aug 2026 12:11:33 -0400 Subject: [PATCH 2/3] Renumber ADR to 0011->0012 to avoid a numbering collision main merged an unrelated ADR-0011 (Weekly Summary rolling windows) and ADR-0010 (config display) while this branch sat unmerged, so this branch's own ADR-0011 (Multi-Day Event child decomposition) now collides with a real ADR-0011 on main. Renumbered to the next free slot and fixed the one CONTEXT.md citation. Co-Authored-By: Claude Sonnet 5 --- CONTEXT.md | 2 +- ...0012-multiday-events-decompose-into-per-day-child-events.md} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename docs/adr/{0011-multiday-events-decompose-into-per-day-child-events.md => 0012-multiday-events-decompose-into-per-day-child-events.md} (100%) diff --git a/CONTEXT.md b/CONTEXT.md index cafb63a..cfe5e69 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -25,7 +25,7 @@ Classifies an Event by its time shape, and determines the Check-in Post timing r _Avoid_: "event type" **Multi-Day Child Event**: -A synthetic, ordinary All-Day Event that Hawk Bot generates automatically for each calendar day of a Multi-Day Event's span, once that Multi-Day Event's Calendar Role is `team_meeting` — a 3-day regional generates three. This is the entire mechanism by which a multi-day competition gets real Attendance Tracking: each Multi-Day Child Event gets its own Event Check-in Post, its own Reaction Cutoff, its own Event Attendance Report — every mechanic identical to any other All-Day Event, with no new attendance logic anywhere. The Multi-Day Event itself (the "parent") is untouched by this — it still syncs, still gets a Meeting Type, and still appears exactly once in the Weekly Summary Post with its full date range; a Multi-Day Child Event never appears there itself, so the same competition is never listed more than once. If the parent's date span changes on a later sync, Multi-Day Child Events are added or marked removed to match, the same way an Event is normally created or removed — a Multi-Day Child Event whose attendance is already finalized is never retroactively altered by this. See ADR-0011. +A synthetic, ordinary All-Day Event that Hawk Bot generates automatically for each calendar day of a Multi-Day Event's span, once that Multi-Day Event's Calendar Role is `team_meeting` — a 3-day regional generates three. This is the entire mechanism by which a multi-day competition gets real Attendance Tracking: each Multi-Day Child Event gets its own Event Check-in Post, its own Reaction Cutoff, its own Event Attendance Report — every mechanic identical to any other All-Day Event, with no new attendance logic anywhere. The Multi-Day Event itself (the "parent") is untouched by this — it still syncs, still gets a Meeting Type, and still appears exactly once in the Weekly Summary Post with its full date range; a Multi-Day Child Event never appears there itself, so the same competition is never listed more than once. If the parent's date span changes on a later sync, Multi-Day Child Events are added or marked removed to match, the same way an Event is normally created or removed — a Multi-Day Child Event whose attendance is already finalized is never retroactively altered by this. See ADR-0012. _Avoid_: "sub-event", "day instance" (suggests calendar recurrence, which this isn't) **Event Check-in Post**: diff --git a/docs/adr/0011-multiday-events-decompose-into-per-day-child-events.md b/docs/adr/0012-multiday-events-decompose-into-per-day-child-events.md similarity index 100% rename from docs/adr/0011-multiday-events-decompose-into-per-day-child-events.md rename to docs/adr/0012-multiday-events-decompose-into-per-day-child-events.md From ad600a078e33007cc135d20f78feb5d2ff54196e Mon Sep 17 00:00:00 2001 From: Rachel Moore Date: Wed, 19 Aug 2026 12:35:12 -0400 Subject: [PATCH 3/3] Run Prettier over the renumbered ADR Co-Authored-By: Claude Sonnet 5 --- .../0012-multiday-events-decompose-into-per-day-child-events.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/0012-multiday-events-decompose-into-per-day-child-events.md b/docs/adr/0012-multiday-events-decompose-into-per-day-child-events.md index 9569f38..37b44a5 100644 --- a/docs/adr/0012-multiday-events-decompose-into-per-day-child-events.md +++ b/docs/adr/0012-multiday-events-decompose-into-per-day-child-events.md @@ -4,7 +4,7 @@ A Multi-Day Event gets Attendance Tracking by generating a Multi-Day Child Event The alternative considered first — one combined Check-in Post per competition, with a numbered-emoji reaction (1️⃣, 2️⃣, …) per day — was designed in detail before being set aside. It would have required a new day dimension on the `attendance` table (today's schema is strictly one row per `(event_id, user_id)`, with no way to represent "attended day 1 and 3 but not 2"), a new emoji vocabulary with a hard cap on supported days, an extension to `reactionCutoff()` (which today only ever looks at an Event's `startsAt`, never `endsAt`) to handle a multi-day span correctly, and a carve-out from ADR-0003's "👍 is the sole attendance signal" rule, since a single combined post has no single boolean signal to key off of. The per-day decomposition needs none of that — every one of those problems either doesn't apply or is already solved by existing single-day code. -**The trade being made**: this gives up the "one interaction per person per competition" goal that motivated the feature in the first place. A 3-day competition now produces three separate Check-in Posts instead of one. What keeps that from reintroducing the original problem — competition days already being busy — is that Check-in Post *timing* is decoupled from Reaction Cutoff *timing*: every child's Check-in Post is posted together, an admin-configurable number of days before the competition's first day (default 2, see `checkin_offset_multiday_days`), but each child's Reaction Cutoff still lands independently at the end of its own day. Posting is batched for convenience; attendance-locking isn't, so a schedule change mid-competition can still register on a later day. +**The trade being made**: this gives up the "one interaction per person per competition" goal that motivated the feature in the first place. A 3-day competition now produces three separate Check-in Posts instead of one. What keeps that from reintroducing the original problem — competition days already being busy — is that Check-in Post _timing_ is decoupled from Reaction Cutoff _timing_: every child's Check-in Post is posted together, an admin-configurable number of days before the competition's first day (default 2, see `checkin_offset_multiday_days`), but each child's Reaction Cutoff still lands independently at the end of its own day. Posting is batched for convenience; attendance-locking isn't, so a schedule change mid-competition can still register on a later day. **Consequence**: one new nullable column, `events.multiday_parent_id`, linking a Multi-Day Child Event back to its parent — used both to exclude children from the Weekly Summary listing and to reconcile the child set when a competition's date span changes on a later sync (adding or removing children to match, never retroactively altering an already-finalized child). One new setting, `checkin_offset_multiday_days`. No other change to the attendance domain model.