Add reminder notifications to schedules

- ScheduleDocument/Schedule gain optional reminderMinutes (minutes from
  midnight; decode-compatible, no schema bump)
- New Workout form gets a Reminder section (toggle + time picker, hidden
  for Now; footer warns when notifications are denied)
- ReminderPlanner (pure, tested) derives notification triggers: daily
  repeating, per-weekday repeating for fixed days, one-shot for once
- ReminderScheduler resyncs pending requests from the cache on every
  change, multiplexed onto onCacheChanged after the watch push; asks
  notification permission only once a reminder actually exists
This commit is contained in:
2026-07-11 12:32:08 -04:00
parent df07e0a043
commit 5ec5de9295
10 changed files with 440 additions and 3 deletions
@@ -31,6 +31,7 @@ struct ScheduleDocumentTests {
goal: GoalKind.strength.rawValue,
recurrence: ScheduleRecurrence.fixedDays.rawValue,
weekdays: [2, 5],
reminderMinutes: 7 * 60 + 30,
order: 3,
createdAt: Self.created,
updatedAt: Self.updated
@@ -45,6 +46,7 @@ struct ScheduleDocumentTests {
#expect(decoded.isReadable)
#expect(decoded.goalKind == .strength)
#expect(decoded.recurrenceEnum == .fixedDays)
#expect(decoded.reminderMinutes == 450)
}
/// The nil-heavy cases round-trip too: a `.daily` schedule with no goal and no
@@ -70,6 +72,7 @@ struct ScheduleDocumentTests {
#expect(!json.contains("goal"))
#expect(!json.contains("weekdays"))
#expect(!json.contains("\"date\""))
#expect(!json.contains("reminderMinutes"))
let decoded = try DocumentCoder.decode(ScheduleDocument.self, from: data)
#expect(decoded == original)
@@ -77,6 +80,7 @@ struct ScheduleDocumentTests {
#expect(decoded.goalKind == nil)
#expect(decoded.weekdays == nil)
#expect(decoded.date == nil)
#expect(decoded.reminderMinutes == nil)
}
/// A `.once` schedule carries its one-off day and drops the recurring-only fields.
@@ -179,6 +183,7 @@ struct ScheduleDocumentTests {
goal: GoalKind.mobility.rawValue,
recurrence: ScheduleRecurrence.fixedDays.rawValue,
weekdays: [2, 4, 6],
reminderMinutes: 18 * 60 + 45,
order: 7,
createdAt: Self.created,
updatedAt: Self.updated
@@ -195,6 +200,7 @@ struct ScheduleDocumentTests {
#expect(entity.recurrenceRaw == original.recurrence)
#expect(entity.recurrenceEnum == .fixedDays)
#expect(entity.weekdays == [2, 4, 6])
#expect(entity.reminderMinutes == 1125)
#expect(entity.order == 7)
#expect(entity.jsonRelativePath == "Schedules/\(original.id).json")