notify: moved events, added steps, and the deadline tickers #101

Merged
nalum merged 10 commits from feat/notify-kinds-and-reminders into main 2026-08-16 19:44:24 +00:00
Owner

Four backlog items from the #100 plan, shipped as one stack — the three notification kinds ride the PR #90 pattern, and the reminder-ticker seam lands under the job-deadline work that needed it.

EVENT_MOVED (closes #94). EventService/Update diffs the event's when — start, end, all-day, daily-times — inside the locked apply and rings the event's members only when that changed. Cosmetic edits (name, description, location, guests) stay silent. The new start is rendered in the request's zone: date and 24-hour clock, date alone for all-day events.

STEP_ADDED (closes #93). ItemService/Create with a parent_uid rings the parent job's owner when someone else adds the step. One banner per step, no dedup window, as decided — at family scale the noise is the feature. The actor guard sits in the handler, so an owner adding their own steps never reaches the notifier or the webhook decorator.

Reminder ticker seam (closes #97). The meal reminder's sleep/claim/fire loop moves to internal/notify/ticker.go: the seam owns the pacing (one-hour sleep cap so zone edits and DST transitions are absorbed, one-minute floor, retry pause on a zero next-fire) and schedules on arbitrary instants, per the #98 decision. The claim stays with each consumer — a locked compare-and-set on its own row, bookkeeping without a mutation entry. The meal reminder is the first consumer, behavior unchanged, existing specs untouched.

JOB_DUE + JOB_OVERDUE (closes #91, closes #99). The second ticker consumer scans open standalone jobs: the day-of nudge rings on the deadline's own local date (the RFC 3339 string carries its offset), and the overdue nag rings every day the job stays open after — separately mutable, so muting the nag keeps the day-of nudge. An owned job rings its owner; an unassigned job with a deadline rings everyone, so someone can pick it up. Firings are claimed per job per day on the new Item.due_reminder_sent field, the same bookkeeping shape as MealRota.reminder_last_sent.

Every kind carries the full plumbing: proto enum, notify method, en+ga messages, webhook decorator, web push-preference checkbox + PUSH_KINDS entry, Android channel + string catalogs. make check and the Android unit tests are green on every commit, and each behavior was verified against the live deploy (moved/cosmetic split, step-added both directions, due broadcast and overdue nag on a booted ticker).

Four backlog items from the #100 plan, shipped as one stack — the three notification kinds ride the PR #90 pattern, and the reminder-ticker seam lands under the job-deadline work that needed it. **EVENT_MOVED (closes #94).** `EventService/Update` diffs the event's *when* — start, end, all-day, daily-times — inside the locked apply and rings the event's members only when that changed. Cosmetic edits (name, description, location, guests) stay silent. The new start is rendered in the request's zone: date and 24-hour clock, date alone for all-day events. **STEP_ADDED (closes #93).** `ItemService/Create` with a `parent_uid` rings the parent job's owner when someone else adds the step. One banner per step, no dedup window, as decided — at family scale the noise is the feature. The actor guard sits in the handler, so an owner adding their own steps never reaches the notifier or the webhook decorator. **Reminder ticker seam (closes #97).** The meal reminder's sleep/claim/fire loop moves to `internal/notify/ticker.go`: the seam owns the pacing (one-hour sleep cap so zone edits and DST transitions are absorbed, one-minute floor, retry pause on a zero next-fire) and schedules on arbitrary instants, per the #98 decision. The claim stays with each consumer — a locked compare-and-set on its own row, bookkeeping without a mutation entry. The meal reminder is the first consumer, behavior unchanged, existing specs untouched. **JOB_DUE + JOB_OVERDUE (closes #91, closes #99).** The second ticker consumer scans open standalone jobs: the day-of nudge rings on the deadline's own local date (the RFC 3339 string carries its offset), and the overdue nag rings every day the job stays open after — separately mutable, so muting the nag keeps the day-of nudge. An owned job rings its owner; an unassigned job with a deadline rings everyone, so someone can pick it up. Firings are claimed per job per day on the new `Item.due_reminder_sent` field, the same bookkeeping shape as `MealRota.reminder_last_sent`. Every kind carries the full plumbing: proto enum, notify method, en+ga messages, webhook decorator, web push-preference checkbox + `PUSH_KINDS` entry, Android channel + string catalogs. `make check` and the Android unit tests are green on every commit, and each behavior was verified against the live deploy (moved/cosmetic split, step-added both directions, due broadcast and overdue nag on a booted ticker).
PR #90 left Update silent because a naive emission fires on every
tweak. EVENT_MOVED threads the needle: the update handler diffs the
event's when — start, end, all-day, daily-times — before and after
the apply, and only a change in those mints a notification to the
event's members (never the actor). Cosmetic edits stay silent.

Issue #94.
EVENT_MOVED joins the push-preference checkboxes and the synced kind
list — a kind missing from PUSH_KINDS is stripped from the stored
preference on the next settings write, so every new NotificationKind
must join.
One channel per kind, as ever — the phone's own per-channel switch
lines up with the new server kind.
A manager can attach steps (ADR-0025) to a job someone else owns; the
owner found out only by opening the job. STEP_ADDED rings the parent
job's owner when someone else adds a sub-task. One banner per step, no
dedup window — at family scale the noise is the feature (issue #93).
The new kind joins the push-preference checkboxes and the synced kind
list — a kind missing from PUSH_KINDS is stripped from the stored
preference on the next settings write.
One channel per kind, as ever — the phone's own per-channel switch
lines up with the new server kind.
The meal reminder's sleep/claim/fire loop is about to be copied for
job deadlines (#91), overdue nags (#99) and event reminders (#98) —
three hand-rolled copies is how restart-safety and catch-up subtleties
drift apart. The generic part (clock, next-fire computation, catch-up
bound, retry pacing) moves to internal/notify/ticker.go; the meal
reminder becomes its first consumer, behaviour unchanged. Fire
scheduling works on arbitrary instants, not a daily wall-clock hour,
because event reminders (#98) need per-event lead times (issue #97).
Jobs with deadlines never nudged anyone — the owner found out by
looking at the board. A second consumer of the reminder ticker seam
scans open standalone jobs: JOB_DUE rings on the deadline's own local
date, JOB_OVERDUE keeps ringing every day the job stays open after it
(naggy by design, and separately mutable). An owned job nudges its
owner; an unassigned job with a deadline nudges everyone, so someone
can pick it up. Firings are claimed per job per day with a locked
compare-and-set on Item.due_reminder_sent — bookkeeping, not history,
so no mutation entry (issues #91, #99).
Both join the push-preference checkboxes and the synced kind list —
overdue is its own checkbox so the daily nag can be muted without
losing the day-of nudge.
feat(android): channels for the due and overdue job kinds
Some checks failed
check / commits (pull_request) Successful in 7s
check / go (pull_request) Successful in 8m7s
check / report (pull_request) Successful in 4s
check / web (pull_request) Successful in 13m58s
android / build (pull_request) Successful in 34m6s
android / report (pull_request) Successful in 5s
android / report (push) Has been cancelled
check / report (push) Has been cancelled
check / web (push) Has been cancelled
tag / tag (push) Has been cancelled
android / build (push) Has been cancelled
check / go (push) Has been cancelled
check / commits (push) Has been cancelled
977c3681d6
One channel per kind, as ever — overdue separate so the daily nag has
its own switch.

Test report

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

Coverage: 27.8%

Updated by the check workflow · commit 977c3681d6

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

Android test report

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

Updated by the android workflow · commit 977c3681d6

<!-- android-test-report --> ## Android test report | Suite | Tests | Result | Skipped | | --- | --: | --- | --: | | Unit (debug) | 31 | ✅ pass | 0 | <sub>Updated by the android workflow · commit 977c3681d6dc0185f0c2333eb22dab3bb99e1532</sub>
nalum merged commit 977c3681d6 into main 2026-08-16 19:44:24 +00:00
nalum deleted branch feat/notify-kinds-and-reminders 2026-08-16 19:44:25 +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!101
No description provided.