Repository navigation
feat(reminders): server-side meal reminders — Cron → first Edge Function → Pushover (007) - #27
Merged
Merged
Conversation
…ion → Pushover (007) Closes #15. Covers SRS §3.6.1 (REQ-MR-001..005) and REQ-UX-010; enforces INV-PL-010/011. Database (0010, pgTAP ×31): - meal_reminders: one per (owner, slot) — time, optional pre-alert, ISO days-of-week (normalized by trigger), notification type, and the owner's IANA time zone (resolves the LocalTime-vs-UTC-cron gap in the domain docs) - reminder_deliveries: occurrence-unique dedup ledger, written only by service_role (no client write grants — deliberately sidesteps the #14 SECURITY DEFINER guard pattern; owners can SELECT their history) - due_meal_reminders(): all timezone/window/day/dedup math in SQL where pgTAP proves it; joins the day's still-PLANNED meal for personalized copy; EXECUTE revoked from anon/authenticated - pg_cron every minute → invoke_send_meal_reminders() → pg_net POST; no-ops until Vault is configured, so every environment migrates cleanly Edge Function (the project's first — bootstraps supabase/functions/): - send-meal-reminders: claim occurrence via the dedup key → Pushover → record SENT/FAILED; missed nudges are dropped, never delivered late - gated by a fail-closed x-cron-secret shared secret compared constant-time (verify_jwt off: the anon key is a public, valid JWT and gates nothing) - guilt-free copy per design/screens/mobile-reminder.html Client (design/screens/mobile-reminder-setup.html): - /reminders "Gentle nudges" screen — per-slot time, pre-alert, and toggle; batch upsert stamped with the device time zone - entry points: desktop sidebar "Nudges", Today-header bell (mobile) - src/lib/reminders/ service/store/types per the established feature anatomy Tests: 31 pgTAP + 6 unit + 4 e2e, all suites green (275 unit / 29 e2e total).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #15.
Feature 007 — the last unbuilt P1 item. Server-side meal reminders: pg_cron → the project's first Edge Function → Pushover, plus the "Gentle nudges" setup screen from
design/screens/mobile-reminder-setup.html. Covers SRS §3.6.1 (REQ-MR-001..005) and REQ-UX-010; enforces INV-PL-010/011.Database —
0010_meal_reminders.sqlmeal_reminders— one per (owner, slot): localreminder_time, optionalpre_alert_minutes(REQ-MR-002), ISOdays_of_week(trigger-normalized; INV-PL-011 as CHECK),notification_type(PUSH/SILENT/NONE), and the owner's IANAtimezone. The domain docs store a LocalTime while Cron runs UTC — the zone column (captured from the device at save time) closes that gap, with all conversion done by Postgres.reminder_deliveries— occurrence-unique dedup ledger (UNIQUE (reminder_id, kind, local_date)). Written only by service_role: authenticated/anon get no write grants and no write policy, so no SECURITY DEFINER ownership guard is needed — deliberately avoiding the SECURITY DEFINER ownership guards block service-role inserts that carry references (auth.uid() is NULL) #14 pattern that blocks service-role inserts (auth.uid()NULL). Owners can SELECT their own history. Failures stay visible as FAILED rows; nothing is retried or delivered late (REQ-MR-005).due_meal_reminders()— every timezone/day-of-week/window/dedup decision lives in SQL where pgTAP proves it; the RPC also joins the day's still-PLANNED meal for personalized copy. EXECUTE revoked from anon/authenticated.invoke_send_meal_reminders()→ pg_net POST. No-ops until the Vault secrets exist, so fresh/local environments migrate cleanly and CI is unaffected.Edge Function —
send-meal-reminders(bootstrapssupabase/functions/)Thin dispatch loop: due occurrences → claim via the dedup key (
upsert … ignoreDuplicates, safe under overlapping runs) → Pushover → mark SENT/FAILED. Copy is guilt-free perdesign/screens/mobile-reminder.html("No rush — just a gentle nudge.").Caller gate (from supabase-rls-reviewer feedback):
verify_jwtis off — the anon key is a valid, public JWT, so a JWT check gates nothing. Instead the function requires anx-cron-secretshared secret (Vault ↔ function env), compared constant-time, fail-closed (401 without it). Verified locally: no header → 401, wrong secret → 401, anon JWT → 401, correct secret → 200.Setup (per environment) is documented in
supabase/functions/README.md:CRON_SHARED_SECRET+ Pushover token/user key as function secrets,project_url+send_meal_reminders_secretin Vault.Client
/reminders— the Gentle nudges screen: per-slot time input, pre-alert select, toggle; batch upsert on (owner, slot) stamped with the device time zone. Svelte 5 runes, design tokens, feature-folder anatomy matchingsrc/lib/log/.Verification
reminders_rls.test.sql): RLS isolation both directions, schema invariants (INV-PL-010/011, tz validity, pre-alert bounds, slot uniqueness, day normalization), client write-denial on the ledger, server-only RPC, timezone math (New York + Tokyo), window edges (never late), day-of-week, suppression (disabled/SILENT), dedup, cron job existence, invoker no-op.types.spec.ts) — mapping/merge/defaults. e2e ×4 (reminders.e2e.ts) — defaults render, save persists all slots + timezone, edits round-trip a reload, bell/Maybe-later navigation.check,lint,buildclean.due: 0.Notes / follow-ups
PUSHOVER_USER_KEYenv — fine while auth is anonymous-only; a per-user key column is the obvious extension when real auth lands.SLOT_WINDOWSinsrc/lib/log/slots.ts) still uses its fixed windows; personalizing it frommeal_remindersis a natural follow-up.