Files
chore/.github/specs/archive/feat-notify-ending-chore.md
Ryan Kegel eb775ba7d8
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 3m0s
feat: Implement routines feature with CRUD operations and child assignment
- Add backend routines management with add, get, update, delete, and list functionalities.
- Create models for Routine, RoutineItem, RoutineSchedule, and RoutineExtension.
- Develop event types for routine confirmation and modification.
- Implement frontend components for routine assignment, confirmation dialog, and routine management views.
- Add unit tests for routine API and integration tests for routine CRUD flow.
- Create end-to-end test plan for routines feature covering parent and child interactions.
2026-05-05 09:08:19 -04:00

9.7 KiB

Feature: Push notification for chores that are going to expire in the next hour.

Overview

When a scheduled chore is going to expire within the next hour and it has not be marked completed or pending, a push notification should go out to tell the user that the scheduled chore needs to be finished soon.

Goal: When a chore is about to expire (within the hour) send a push notification to the user and allow the user to acknowlege it.

User Story: As a parent, I should receive a notification when a scheduled chore is going to expire soon. When I click on the notification, I should be brought to the page with the chore.

Rules:

  1. If there are multiple chores that are going to expire, present them in the push notification as well.
  2. The notification should show which child the chore belongs to.
  3. If a chore is scheduled to expire between 9:00pm to 9:15pm, the notification for it should go out at 8:00pm. That should give the earlier hour expiration plenty of notice.

Questions:

  1. If there are multiple chores in the push, should we say there are multiple chores ending next hour or should we list each by name?
  2. Should we section the notification if there are multiple children? [ex. Child1 chores: ... Child2 chores: ...]
  3. Should we reuse one of the other hourly schedulers or create a new one? should we refactor and have one hourly scheduler that does multiple operations?

Data Model Changes

Backend Model

No new models required. The feature reuses existing models:

  • ChoreSchedule — provides schedule mode, day configs, deadline times, and enabled state
  • PendingConfirmation — queried to determine if a chore is already pending or approved (either status means the child has acted on it; no notification is needed)

Frontend Model

No changes to TypeScript interfaces.


Backend Implementation

backend/utils/schedule_utils.py (new)

A Python port of the three core functions from frontend/vue-app/src/common/scheduleUtils.ts. The frontend is the authoritative source for schedule math; this file mirrors that logic server-side so the scheduler can determine which chores are active and when they expire without making HTTP calls or duplicating logic ad-hoc.

  • interval_hits_today(anchor_date, interval_days, local_date) — returns True if an interval-mode schedule fires on the given local date
  • is_scheduled_today(schedule_dict, local_date) — returns True for days-mode (weekday match) or interval-mode (anchor + interval math); paused schedules always return True
  • get_due_time_today(schedule_dict, local_date) — returns (hour, minute) or None (None means Anytime or paused — no expiry)

Note on weekday convention: the app uses JS/Python convention where Sunday = 0 and Saturday = 6 (same as DayConfig.day). Python's date.weekday() returns Monday = 0, so a conversion of (weekday + 1) % 7 is applied when comparing.

backend/utils/chore_expiry_notification_scheduler.py (new)

  • get_expiring_chores_for_user(user_id, tz_str, now_dt) — loads all children for the user, iterates their chore tasks, checks for an active schedule with a deadline falling in (now, now + 75 min], and excludes any chore that already has a PendingConfirmation record (any status). Returns a list of dicts with child and task metadata.
  • _build_push_payload(expiring) — constructs the push payload. Single chore: title names the chore and deep-link fields are set for direct navigation. Multiple chores: generic title, child_id and entity_id are null, and the body lists chores grouped by child name (e.g. Alex: Clean Room, Make Bed\nSam: Trash).
  • send_chore_expiry_notifications_for_user(user_id, tz_str) — calls get_expiring_chores_for_user, builds the payload, and calls send_push_to_user.
  • run_chore_expiry_check(app) — skips the e2e environment, loops all verified users, skips users with push_notifications_enabled=False, and calls send_chore_expiry_notifications_for_user for each eligible user.
  • start_chore_expiry_notification_scheduler(app) — registers an APScheduler cron job at minute=0 (top of every hour), consistent with the existing schedulers.

Push payload type: chore_expiring_soon. The absence of approve_token and deny_token fields is intentional and is what triggers the service worker to suppress action buttons.

backend/main.py (modified)

Import added and start_chore_expiry_notification_scheduler(app) called after the existing schedulers.

Backend Tests

  • get_expiring_chores_for_user: chore in 75-min window → included
  • get_expiring_chores_for_user: deadline in the past → excluded
  • get_expiring_chores_for_user: deadline beyond 75-min window → excluded
  • get_expiring_chores_for_user: Anytime schedule (no deadline) → excluded
  • get_expiring_chores_for_user: chore not scheduled today (wrong weekday) → excluded
  • get_expiring_chores_for_user: status=pending confirmation exists → excluded
  • get_expiring_chores_for_user: status=approved confirmation exists → excluded
  • get_expiring_chores_for_user: no schedule → excluded
  • get_expiring_chores_for_user: paused schedule (enabled=False) → excluded
  • get_expiring_chores_for_user: interval mode hits today → included
  • get_expiring_chores_for_user: interval mode misses today → excluded
  • get_expiring_chores_for_user: multiple children returns entries for both
  • _build_push_payload: single chore → named title, deep-link fields set
  • _build_push_payload: multiple chores → generic title, child_id/entity_id null
  • _build_push_payload: body groups chore names by child
  • _build_push_payload: no approve_token or reject_token in payload
  • send_chore_expiry_notifications_for_user: calls send_push_to_user with correct payload
  • send_chore_expiry_notifications_for_user: no push when no chores expiring
  • run_chore_expiry_check: skips when DB_ENV=e2e
  • run_chore_expiry_check: skips user with push_notifications_enabled=False
  • run_chore_expiry_check: skips unverified user
  • run_chore_expiry_check: sends push for eligible verified user with push enabled
  • schedule_utils: interval_hits_today — same day as anchor hits
  • schedule_utils: interval_hits_today — correct interval day hits
  • schedule_utils: interval_hits_today — non-interval day misses
  • schedule_utils: interval_hits_today — date before anchor misses
  • schedule_utils: interval_hits_today — empty anchor hits today
  • schedule_utils: is_scheduled_today — days mode matching weekday
  • schedule_utils: is_scheduled_today — days mode non-matching weekday
  • schedule_utils: is_scheduled_today — paused always true
  • schedule_utils: is_scheduled_today — interval mode hit
  • schedule_utils: is_scheduled_today — interval mode miss
  • schedule_utils: get_due_time_today — days mode returns (hour, minute)
  • schedule_utils: get_due_time_today — Anytime returns None
  • schedule_utils: get_due_time_today — wrong day returns None
  • schedule_utils: get_due_time_today — paused returns None
  • schedule_utils: get_due_time_today — interval mode returns (hour, minute)
  • schedule_utils: get_due_time_today — interval Anytime returns None

Frontend Implementation

frontend/vue-app/public/sw.js (modified)

In the push event handler, the actions array passed to showNotification is now conditionally set: when payload.type === 'chore_expiring_soon' the array is empty (no buttons); otherwise Approve and Reject buttons are shown as before. The notificationclick handler requires no changes — tapping the notification body already navigates to /parent/{child_id}?scrollTo={entity_id} when those fields are present, and falls back to /parent when they are null (multi-chore case).

Frontend Tests

No automated tests added. The sw.js change is a simple conditional and is covered by manual verification: triggering a chore_expiring_soon push and confirming no action buttons appear in the browser notification, and that tapping opens the correct URL.


Future Considerations

  • 75-minute window double-notification: A chore deadline that falls in the overlap of two consecutive 75-minute windows (e.g. 9:10 pm appears in both the 8:00 pm run's window and the 9:00 pm run's window) will produce two notifications. This is a known and accepted trade-off; no deduplication is applied. A future improvement could track notified chores per day in a lightweight DB table to prevent repeats.
  • Task extensions: TaskExtension sets an extension_date that allows a chore to carry over to another day. Extended chores are treated identically to regular scheduled chores — the schedule deadline time is used as-is. A future enhancement could optionally adjust the deadline time for extended chores.

Acceptance Criteria (Definition of Done)

Backend

  • backend/utils/schedule_utils.py created — Python port of schedule math from scheduleUtils.ts
  • backend/utils/chore_expiry_notification_scheduler.py created — hourly scheduler with 75-min look-ahead window
  • Chores with Anytime schedule (no deadline) are not notified
  • Chores with a pending or approved confirmation are not notified
  • Chores on paused schedules are not notified
  • Users with push_notifications_enabled=False are skipped
  • Unverified users are skipped
  • e2e environment is skipped
  • Scheduler registered in backend/main.py
  • All backend tests pass

Frontend

  • sw.js updated — chore_expiring_soon notifications show no Approve/Reject action buttons
  • Tapping a single-chore notification navigates to the child's chore page
  • Tapping a multi-chore notification navigates to /parent