feat(notifications): add dev/test 10-minute rollover toggle and plugin spec

- Add dev-only "Use 10-minute rollover (testing)" toggle in Reminder
  Notifications (Account view). Visible only when not on prod API server
  (isNotProdServer). Toggle persists and reschedules reminder with
  rolloverIntervalMinutes when changed.
- Extend daily notification flow to pass optional rolloverIntervalMinutes
  to the plugin: NotificationService/NativeNotificationService options,
  PushNotificationPermission dialog options, first-time and edit flows.
- Add settings: reminderFastRolloverForTesting (Settings, AccountSettings,
  PlatformServiceMixin boolean mapping, migration 007).
- Centralize isNotProdServer(apiServer) in constants/app.ts; use in
  AccountViewView (toggle visibility), ImportAccountView, and TestView.
- Add docs/plugin-spec-rollover-interval-minutes.md for the plugin repo
  (configurable rollover interval and persistence after reboot).

Note: Daily notification plugin dependency is currently pointed at the
"rollover-interval" branch for testing this feature.
This commit is contained in:
Jose Olarte III
2026-03-03 21:31:07 +08:00
parent 96ae89bcfa
commit af63ab70e7
14 changed files with 302 additions and 11 deletions

View File

@@ -0,0 +1,148 @@
# Plugin spec: Configurable rollover interval (e.g. 10 minutes for testing)
**Date:** 2026-03-03
**Target repo:** daily-notification-plugin
**Consuming app:** crowd-funder-for-time-pwa (TimeSafari)
**Platforms:** iOS and Android
## Summary
The consuming app needs to support **rapid testing** of daily notification rollover on device. Today, after a notification fires, the plugin always schedules the next occurrence **24 hours** later. We need an **optional** parameter so the app can request a different interval (e.g. **10 minutes**) for dev/testing. When that parameter is present, the plugin must:
1. Use the given interval (in minutes) when scheduling the **next** occurrence after a notification fires (rollover).
2. **Persist** that interval with the schedule so that it survives **device reboot** and is used again when:
- Boot recovery reschedules alarms from stored data, and
- Any subsequent rollover runs (after the next notification fires).
If the interval is not persisted, then after a device restart the plugin would no longer know to use 10 minutes and would fall back to 24 hours; rapid testing after reboot would break. So persistence is a **required** part of this feature.
---
## API contract (app → plugin)
### Method: `scheduleDailyNotification` (or equivalent used for the apps daily reminder)
**Add an optional parameter:**
- **Name:** `rolloverIntervalMinutes` (or equivalent, e.g. `repeatIntervalMinutes`).
- **Type:** `number` (integer), optional.
- **Meaning:** When the scheduled notification fires, schedule the **next** occurrence this many **minutes** after the current trigger time (instead of 24 hours). When **absent** or not provided, behavior is unchanged: next occurrence is **24 hours** later (current behavior).
**Example (pseudocode):**
- App calls: `scheduleDailyNotification({ id, time, title, body, ..., rolloverIntervalMinutes: 10 })`.
- Plugin stores the schedule **including** `rolloverIntervalMinutes: 10`.
- When the notification fires, plugin computes next trigger = current trigger + 10 minutes (instead of + 24 hours), and schedules that.
- When the device reboots, boot recovery loads the schedule, sees `rolloverIntervalMinutes: 10`, and uses it when (a) computing the next run time for reschedule and (b) any future rollover after the next fire.
**Example (normal production, no param):**
- App calls: `scheduleDailyNotification({ id, time, title, body })` (no `rolloverIntervalMinutes`).
- Plugin stores the schedule with no interval (or default 24h).
- Rollover and boot recovery behave as today: next occurrence 24 hours later.
---
## Persistence requirement (critical for device restart)
The rollover interval must be **stored with the schedule** in the plugins persistent storage (e.g. Room on Android, UserDefaults/DB on iOS), not only kept in memory. Concretely:
1. **When the app calls `scheduleDailyNotification` with `rolloverIntervalMinutes`:**
- Persist that value in the same place you persist the rest of the schedule (e.g. Schedule entity, or equivalent table/row that is read on boot and on rollover).
2. **When computing the next occurrence (rollover path, after a notification fires):**
- Read the stored `rolloverIntervalMinutes` for that schedule.
- If present and > 0: next trigger = current trigger + `rolloverIntervalMinutes` minutes.
- If absent or 0: next trigger = current trigger + 24 hours (existing behavior).
3. **When boot recovery runs (after device restart):**
- Load schedules from persistent storage (including the stored `rolloverIntervalMinutes`).
- When rescheduling each alarm, use the stored interval to compute the next run time (same logic as rollover: if interval is set, use it; otherwise 24 hours).
- When the next notification fires after reboot, the rollover path will again read the same stored value, so the 10-minute (or whatever) interval continues to apply.
4. **When the app calls `scheduleDailyNotification` without `rolloverIntervalMinutes` (or to turn off fast rollover):**
- Overwrite the stored schedule so that the interval is cleared or set to default (24h). Subsequent rollovers and boot recovery then use 24 hours again.
**Why this matters:** Without persisting the interval, a device restart would lose the “10 minutes” setting; rollover and boot recovery would have no way to know to use 10 minutes and would default to 24 hours. Rapid testing after reboot would not work.
---
## Platform-specific notes
### Android
- **Storage:** Add `rollover_interval_minutes` (or equivalent) to the Schedule entity (or wherever the apps reminder schedule is stored) and persist it when handling `scheduleDailyNotification`. Use it in:
- Rollover path (e.g. when scheduling next alarm after notification fires).
- Boot recovery path (when rebuilding alarms from DB after `BOOT_COMPLETED`).
- **Next trigger:** Current trigger time + `rolloverIntervalMinutes` minutes (using `Calendar` or equivalent so DST/timezone is handled correctly; same care as for 24h rollover).
### iOS
- **Storage:** Add the same field to whatever persistent structure holds the schedule (e.g. the same place that stores time, title, body, id). Persist it when the app calls the schedule method.
- **Rollover:** In `scheduleNextNotification()` (or equivalent), read the stored interval; if set, use `Calendar.date(byAdding: .minute, value: rolloverIntervalMinutes, to: currentDate)` (or equivalent) instead of adding 24 hours.
- **App launch / recovery:** If the plugin has any path that restores or reschedules after app launch or system events, use the stored interval there as well so behavior is consistent.
---
## Edge cases and defaults
- **Parameter absent:** Do not change current behavior. Next occurrence = 24 hours later.
- **Parameter = 0 or negative:** Treat as “use default”; same as absent (24 hours).
- **Parameter > 0 (e.g. 10):** Next occurrence = current trigger + that many minutes.
- **Existing schedules (created before this feature):** No stored interval → treat as 24 hours. No migration required beyond “missing field = default”.
---
## App-side behavior (for context)
- The app will only pass `rolloverIntervalMinutes` when a **dev-only** setting is enabled (e.g. “Use 10-minute rollover for testing” in the Reminder Notifications section). Production users will not set it.
- The app will pass it on every `scheduleDailyNotification` call when the user has that setting on (first-time enable and edit). When the user turns the setting off, the app will call `scheduleDailyNotification` without the parameter (so the plugin can persist “no interval” / 24h).
---
## Verification (plugin repo)
1. **Rollover with interval:** Schedule with `rolloverIntervalMinutes: 10`. Trigger the notification (or wait). Confirm the **next** scheduled time is ~10 minutes after the current trigger (not 24 hours). Let it fire again; confirm the following occurrence is again ~10 minutes later.
2. **Persistence:** Schedule with `rolloverIntervalMinutes: 10`, then **restart the device** (do not open the app). After boot, confirm (via logs or next fire) that the rescheduled alarm uses the 10-minute interval (e.g. next fire is 10 minutes after the last stored trigger, not 24 hours). After that notification fires, confirm the **next** rollover is still 10 minutes later.
3. **Default:** Schedule without `rolloverIntervalMinutes`. Confirm next occurrence is 24 hours later. Reboot; confirm boot recovery still uses 24 hours.
4. **Turn off:** Schedule with 10 minutes, then have the app call `scheduleDailyNotification` again with the same id/time but **no** `rolloverIntervalMinutes`. Confirm stored interval is cleared and next rollover is 24 hours.
---
## Short summary for plugin maintainers
- **New optional parameter:** `rolloverIntervalMinutes?: number` on the schedule method used for the apps daily reminder.
- **When set (e.g. 10):** After a notification fires, schedule the next occurrence in that many **minutes** instead of 24 hours.
- **Must persist:** Store the value with the schedule in the plugins DB/storage. Use it in **rollover** and in **boot recovery** so that after a device restart the same interval is used. Without persistence, the feature would not work after reboot.
- **When absent:** Behavior unchanged (24-hour rollover). No migration needed for existing schedules.
---
## For Cursor (plugin repo) — actionable handoff
Use this section when implementing this feature in the **daily-notification-plugin** repo (e.g. with Cursor). You can paste or @-mention this doc as context.
**Goal:** Support an optional `rolloverIntervalMinutes` (or equivalent) on the daily reminder schedule API. When provided (e.g. `10`), schedule the next occurrence that many minutes after the current trigger instead of 24 hours. **Persist this value** with the schedule so that rollover and boot recovery both use it; after a device restart, the same interval must still apply.
**Concrete tasks:**
1. **API:** In the plugin interface used by the app (e.g. `scheduleDailyNotification`), add an optional parameter `rolloverIntervalMinutes?: number`. Document that when absent, next occurrence is 24 hours (current behavior).
2. **Storage (Android):** In the Schedule entity (or equivalent), add a column/field for the rollover interval (e.g. `rollover_interval_minutes` nullable Int). When handling `scheduleDailyNotification`, persist the value if present; if absent, store null or 0 to mean “24 hours”.
3. **Storage (iOS):** Add the same field to the persistent structure that holds the reminder schedule. Persist it when the app calls the schedule method.
4. **Rollover (both platforms):** In the code that runs when a scheduled notification fires and schedules the next occurrence:
- Read the stored `rolloverIntervalMinutes` for that schedule.
- If present and > 0: next trigger = current trigger + that many minutes (using Calendar/date APIs that respect timezone/DST).
- Else: next trigger = current trigger + 24 hours (existing behavior).
- Persist the same interval on the new schedule record so the next rollover still uses it.
5. **Boot recovery (both platforms):** In the path that runs after device reboot and reschedules from stored data:
- Load the stored `rolloverIntervalMinutes` with each schedule.
- When computing “next run time” for reschedule, use the same logic: if interval set, current trigger + that many minutes; else + 24 hours.
- Do not rely on in-memory state; always read from persisted storage so behavior is correct after restart.
6. **Clearing the interval:** When the app calls `scheduleDailyNotification` without `rolloverIntervalMinutes` (e.g. user turned off “fast rollover” in the app), overwrite the stored schedule so the interval field is null/0. Subsequent rollovers and boot recovery then use 24 hours.
7. **Tests:** Add or extend tests for: (a) rollover with 10 minutes, (b) boot recovery with stored 10-minute interval, (c) default 24h when parameter absent or cleared.