Wire up per-task reminder notifications (before due time) #8

Closed
opened 2026-09-05 09:33:49 -06:00 by Agent · 1 comment
Owner

Currently the only proactive notification is notifications.tasks.send_daily_task_email, a once-a-day digest (6-7 AM local time) listing tasks due today and overdue tasks. There is no per-task notification at/near a task's actual due time.

Requested triggers (per user, 2026-09-05):

  1. Reminder before due - X minutes before due_date/due_time, per-user configurable. The schema already has this half-built: User.default_reminder_minutes (users/models.py) is a saved, user-editable preference (profile page: "Default Reminder" - 5/15/30/60/1440 min before) but nothing ever reads it.
  2. Notification at the due time itself - when due_date+due_time arrives.
  3. Notification when a task becomes overdue - a one-time notification fired at the moment a task crosses from "due" into "overdue" (i.e. due time has passed and status is still pending/in_progress), not repeated on every check.

Delivery channel choice: the user should be able to choose whether each of these fires as a push notification, an email, or both. The User model already has independent email_notifications and push_notifications booleans (users/models.py) - reuse these (both can already be enabled simultaneously) rather than adding new fields, unless it turns out these new per-task reminders need to be configurable separately from the existing daily-digest email toggle (e.g. someone wants the daily digest by email only, but instant due/overdue alerts by push only) - that would need its own reminder-specific channel setting(s) in the profile UI (templates/users/profile.html) distinct from the current single email/push toggle pair.

The ScheduledReminder model (notifications/models.py) has task, remind_at, is_sent, sent_at - built for exactly this kind of purpose - but nothing ever creates rows in it, and there's no Celery task or beat-schedule entry that processes them.

Implementation sketch:

  1. Create ScheduledReminder row(s) whenever a task's due_date/due_time is set or changed:
    • one at due_date/due_time - default_reminder_minutes (skip if the user's setting is 0/"No reminder", or if there's no due_time - needs a decision on date-only due dates)
    • one at due_date/due_time itself (the "due now" notification)
    • one at due_date/due_time (or shortly after) for the "now overdue" notification - functionally this fires once the due moment passes, so it may be implementable as the same scheduled check but distinct wording/type, or a follow-up check some short interval later confirming the task is still not completed before sending
  2. Add a Celery task (registered in config/celery.py's beat_schedule, running frequently, e.g. every 1-5 minutes) that finds ScheduledReminder rows where remind_at <= now and is_sent=False, sends email and/or push per the user's chosen channel(s) for these reminders (reusing send_web_push_to_user from notifications/tasks.py for the push side), then marks them is_sent=True.
  3. Clean up/cancel stale/pending reminders when a task's due date changes, is completed, or is deleted, so a reminder doesn't fire for a task that's no longer due (or already done).
  4. Decide behavior for recurring tasks - each new recurrence instance needs its own set of ScheduledReminder rows.

Raised during investigation of a user question: "Should I be getting a notification when tasks are due or before they are due?" - answer today is no, only the daily digest.

Currently the only proactive notification is `notifications.tasks.send_daily_task_email`, a once-a-day digest (6-7 AM local time) listing tasks due today and overdue tasks. There is no per-task notification at/near a task's actual due time. Requested triggers (per user, 2026-09-05): 1. **Reminder before due** - X minutes before `due_date`/`due_time`, per-user configurable. The schema already has this half-built: `User.default_reminder_minutes` (`users/models.py`) is a saved, user-editable preference (profile page: "Default Reminder" - 5/15/30/60/1440 min before) but nothing ever reads it. 2. **Notification at the due time itself** - when `due_date`+`due_time` arrives. 3. **Notification when a task becomes overdue** - a one-time notification fired at the moment a task crosses from "due" into "overdue" (i.e. due time has passed and status is still pending/in_progress), not repeated on every check. **Delivery channel choice**: the user should be able to choose whether each of these fires as a push notification, an email, or both. The `User` model already has independent `email_notifications` and `push_notifications` booleans (`users/models.py`) - reuse these (both can already be enabled simultaneously) rather than adding new fields, unless it turns out these new per-task reminders need to be configurable separately from the existing daily-digest email toggle (e.g. someone wants the daily digest by email only, but instant due/overdue alerts by push only) - that would need its own reminder-specific channel setting(s) in the profile UI (`templates/users/profile.html`) distinct from the current single email/push toggle pair. The `ScheduledReminder` model (`notifications/models.py`) has `task`, `remind_at`, `is_sent`, `sent_at` - built for exactly this kind of purpose - but nothing ever creates rows in it, and there's no Celery task or beat-schedule entry that processes them. Implementation sketch: 1. Create `ScheduledReminder` row(s) whenever a task's `due_date`/`due_time` is set or changed: - one at `due_date/due_time - default_reminder_minutes` (skip if the user's setting is 0/"No reminder", or if there's no `due_time` - needs a decision on date-only due dates) - one at `due_date/due_time` itself (the "due now" notification) - one at `due_date/due_time` (or shortly after) for the "now overdue" notification - functionally this fires once the due moment passes, so it may be implementable as the same scheduled check but distinct wording/type, or a follow-up check some short interval later confirming the task is still not completed before sending 2. Add a Celery task (registered in `config/celery.py`'s `beat_schedule`, running frequently, e.g. every 1-5 minutes) that finds `ScheduledReminder` rows where `remind_at <= now` and `is_sent=False`, sends email and/or push per the user's chosen channel(s) for these reminders (reusing `send_web_push_to_user` from `notifications/tasks.py` for the push side), then marks them `is_sent=True`. 3. Clean up/cancel stale/pending reminders when a task's due date changes, is completed, or is deleted, so a reminder doesn't fire for a task that's no longer due (or already done). 4. Decide behavior for recurring tasks - each new recurrence instance needs its own set of `ScheduledReminder` rows. Raised during investigation of a user question: "Should I be getting a notification when tasks are due or before they are due?" - answer today is no, only the daily digest.
Agent added the Kind/Feature label 2026-09-05 09:34:09 -06:00
Author
Owner

Confirmed working in production: before-due, due-now, and overdue reminders all fired correctly across email, web push, and mobile push, including after a couple of live-testing refinements (delayed the overdue notification by an hour so it does not arrive alongside due-now, and made changing the Default Reminder profile setting retroactively reschedule already-due-dated tasks).

Shipped in commits 5359bf2, f92ce7a (plus the unrelated a27fcc4 crash fix for task edits that this testing surfaced along the way).

Closing as complete.

Confirmed working in production: before-due, due-now, and overdue reminders all fired correctly across email, web push, and mobile push, including after a couple of live-testing refinements (delayed the overdue notification by an hour so it does not arrive alongside due-now, and made changing the Default Reminder profile setting retroactively reschedule already-due-dated tasks). Shipped in commits 5359bf2, f92ce7a (plus the unrelated a27fcc4 crash fix for task edits that this testing surfaced along the way). Closing as complete.
Agent closed this issue 2026-09-05 13:18:03 -06:00
Sign in to join this conversation.