notify: event reminders with a per-event lead time #103

Merged
nalum merged 1 commit from feat/event-reminders into main 2026-08-16 19:44:37 +00:00
Owner

Stacked on #102 (merge #101 → #102 → this; Forgejo retargets down the chain). Closes #98 — the last of the reminders-audit tickers, and the first to fire on exact instants.

The gap. An appointment, a birthday or an EVENT_TYPE_DEADLINE on the calendar never reminded anyone before it happened. PR #90's kinds fire at creation and cancellation; the day itself passed in silence.

Per-event lead time. Each event can carry reminder_lead_minutes (0 = no reminder, the default — reminders are the creator's choice, not ambient noise). EVENT_REMINDER rings at start minus lead. Per the design decisions on the issue: a deadline-type event with a reminder also rings a day ahead of its start, and birthday reminders broadcast to the whole family — a birthday with one member on it defeats its purpose. Other events ring their members only. The board is speaking, so recipients are their own actor, like the meal reminder.

Events gain a stored time zone. Requests always carried time_zone transiently for span validation; the ticker has no request. The event now keeps the zone whenever a create or update names one (the ItemRepeat.time_zone precedent), and reminder projection plus message rendering run in it — "“Reminder Test” starts at 2026-08-16 16:46" comes out in Europe/Dublin, not UTC.

The ticker. Third consumer of the #97 seam. Each scan projects occurrences through domain.ExpandEventOccurrences (45-day lookahead), skips slots owned by child rows (an override speaks for itself; a tombstoned slot is silent), and fires every nudge whose window is open: fireAt ≤ now < start — after the start, silence, which is also the catch-up bound. Claims are a compare-and-set token "<slot>|<phase>" on Event.reminder_last_sent — bookkeeping without a mutation entry, like the other tickers. The ahead phase is superseded once the lead fire is due, so a fired lead can never let the day-ahead nudge ring again behind it.

Surfaces. The event form on web and Android gains a "Remind everyone going?" choice row (no / 15 min / an hour / a day before), saved through create and update on both. The Android occurrence cache learns the lead (schema v14, drop-and-rebuild as ever) so the edit sheet prefills honestly instead of silently zeroing a stored lead. Usual kind plumbing throughout: proto enum, notify method, en+ga messages, webhook decorator, web push-preference checkbox + PUSH_KINDS, Android channel.

make check and the Android unit tests are green; the ticker has specs for the lead window, the claim, the missing-lead silence, the deadline day-ahead phase and the next-wake computation, and the lead reminder was verified against the live deploy end to end. On-device Android verification pending, as with #102.

Stacked on #102 (merge #101 → #102 → this; Forgejo retargets down the chain). Closes #98 — the last of the reminders-audit tickers, and the first to fire on exact instants. **The gap.** An appointment, a birthday or an `EVENT_TYPE_DEADLINE` on the calendar never reminded anyone before it happened. PR #90's kinds fire at creation and cancellation; the day itself passed in silence. **Per-event lead time.** Each event can carry `reminder_lead_minutes` (0 = no reminder, the default — reminders are the creator's choice, not ambient noise). `EVENT_REMINDER` rings at start minus lead. Per the design decisions on the issue: a deadline-type event with a reminder also rings a day ahead of its start, and birthday reminders broadcast to the whole family — a birthday with one member on it defeats its purpose. Other events ring their members only. The board is speaking, so recipients are their own actor, like the meal reminder. **Events gain a stored time zone.** Requests always carried `time_zone` transiently for span validation; the ticker has no request. The event now keeps the zone whenever a create or update names one (the `ItemRepeat.time_zone` precedent), and reminder projection plus message rendering run in it — "“Reminder Test” starts at 2026-08-16 16:46" comes out in Europe/Dublin, not UTC. **The ticker.** Third consumer of the #97 seam. Each scan projects occurrences through `domain.ExpandEventOccurrences` (45-day lookahead), skips slots owned by child rows (an override speaks for itself; a tombstoned slot is silent), and fires every nudge whose window is open: `fireAt ≤ now < start` — after the start, silence, which is also the catch-up bound. Claims are a compare-and-set token `"<slot>|<phase>"` on `Event.reminder_last_sent` — bookkeeping without a mutation entry, like the other tickers. The ahead phase is superseded once the lead fire is due, so a fired lead can never let the day-ahead nudge ring again behind it. **Surfaces.** The event form on web and Android gains a "Remind everyone going?" choice row (no / 15 min / an hour / a day before), saved through create and update on both. The Android occurrence cache learns the lead (schema v14, drop-and-rebuild as ever) so the edit sheet prefills honestly instead of silently zeroing a stored lead. Usual kind plumbing throughout: proto enum, notify method, en+ga messages, webhook decorator, web push-preference checkbox + `PUSH_KINDS`, Android channel. `make check` and the Android unit tests are green; the ticker has specs for the lead window, the claim, the missing-lead silence, the deadline day-ahead phase and the next-wake computation, and the lead reminder was verified against the live deploy end to end. On-device Android verification pending, as with #102.
feat(notify): event reminders with a per-event lead time
Some checks failed
check / commits (pull_request) Successful in 6s
check / go (pull_request) Successful in 7m58s
check / web (pull_request) Successful in 11m11s
android / build (pull_request) Successful in 33m36s
check / report (pull_request) Successful in 5s
android / report (pull_request) Successful in 3s
android / report (push) Has been cancelled
check / go (push) Has been cancelled
check / report (push) Has been cancelled
check / web (push) Has been cancelled
tag / tag (push) Has been cancelled
check / commits (push) Has been cancelled
android / build (push) Has been cancelled
2b2c5e8ba8
An appointment, a birthday or an EVENT_TYPE_DEADLINE on the calendar
never reminded anyone before it happened — the day itself passed in
silence. Each event can now carry a reminder lead: EVENT_REMINDER
rings the members at start minus lead, and a deadline-type event also
rings a day ahead. Birthday reminders broadcast to the whole family —
a birthday with one member on it defeats its purpose. The third
consumer of the reminder ticker seam, firing on exact instants; the
claim is a compare-and-set token (slot and phase) on the event row,
bookkeeping without a mutation entry. Events gain a stored time zone
(captured from the setting request, the ItemRepeat precedent) so
projection and rendering have a wall clock. Closes #98.

Test report

Suite Tests Result Skipped
Unit 1354 ✅ pass 1
Integration 83 ✅ pass —

Coverage: 28.0%

Updated by the check workflow · commit 2b2c5e8ba8

<!-- ci-test-report --> ## Test report | Suite | Tests | Result | Skipped | | --- | --: | --- | --: | | Unit | 1354 | ✅ pass | 1 | | Integration | 83 | ✅ pass | — | **Coverage:** 28.0% <sub>Updated by the check workflow · commit 2b2c5e8ba85870b9bb66add2fb85ae9feedceab9</sub>

Android test report

Suite Tests Result Skipped
Unit (debug) 31 ✅ pass 0

Coverage: 2.4% of lines

Updated by the android workflow · commit 2b2c5e8ba8

<!-- android-test-report --> ## Android test report | Suite | Tests | Result | Skipped | | --- | --: | --- | --: | | Unit (debug) | 31 | ✅ pass | 0 | **Coverage:** 2.4% of lines <sub>Updated by the android workflow · commit 2b2c5e8ba85870b9bb66add2fb85ae9feedceab9</sub>
nalum changed target branch from feat/android-parity-and-ci to main 2026-08-16 19:44:31 +00:00
nalum merged commit 2b2c5e8ba8 into main 2026-08-16 19:44:37 +00:00
nalum deleted branch feat/event-reminders 2026-08-16 19:44:37 +00:00
Sign in to join this conversation.
No reviewers
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
eagraiclainne/app!103
No description provided.