M6 1908a: recurring reminders + complete/snooze (backend, no web-push)
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 6s
CI & Build / Python tests (push) Successful in 10s
CI & Build / Build & push image (push) Successful in 32s

Per operator: skip Web Push; build the rest of reminder delivery. This is
the client-agnostic half — the model + logic that foreground/native
delivery drives.

- notes.recurrence (migration 0022): daily/weekly/monthly/yearly or null.
  update_note accepts it (cleared when the reminder is cleared); rides
  export/import + sync push. Serialized on the note.
- Pure next_occurrence(remind_at, recurrence, after): the next fire strictly
  after `after`, rolling past missed occurrences; _add_months clamps the day
  to the target month (Jan 31 → Feb 28).
- POST /api/notes/<id>/reminder/complete — a recurring reminder advances to
  its next occurrence; a one-off clears. POST .../reminder/snooze {minutes}
  → remind_at = now + minutes (1 min .. 30 days).

No VAPID / push-subscription / service-worker — foreground + native delivery
land in the UI commit and the native clients.

Tests (DB-free): normalize_recurrence; next_occurrence (daily/weekly/
monthly-clamp/skip-missed/yearly/none); complete + snooze auth-guards.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
This commit is contained in:
2026-07-23 11:53:07 -04:00
co-authored by Claude Opus 4.8
parent 5c045aed63
commit 882d4206aa
5 changed files with 175 additions and 2 deletions
+2
View File
@@ -34,6 +34,7 @@ from .notes import (
_serialize_notes,
derive_display_title,
normalize_color,
normalize_recurrence,
)
bp = Blueprint("sync", __name__, url_prefix="/api/sync")
@@ -178,6 +179,7 @@ def _assign_note_fields(note: Note, ch: dict) -> None:
else:
note.deleted_at = None
note.remind_at = _parse_client_dt(ch.get("remind_at"))
note.recurrence = normalize_recurrence(ch.get("recurrence"))
if isinstance(ch.get("position"), int):
note.position = ch["position"]