diff --git a/CONTEXT.md b/CONTEXT.md index 6f12d7e..cfe5e69 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-0012. +_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/0012-multiday-events-decompose-into-per-day-child-events.md b/docs/adr/0012-multiday-events-decompose-into-per-day-child-events.md new file mode 100644 index 0000000..37b44a5 --- /dev/null +++ b/docs/adr/0012-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.