diff --git a/supabase/functions/deadline-digest/README.md b/supabase/functions/deadline-digest/README.md index b81e09a..0a30dc4 100644 --- a/supabase/functions/deadline-digest/README.md +++ b/supabase/functions/deadline-digest/README.md @@ -69,3 +69,14 @@ The cron job fires daily at 13:00 UTC. The function refuses to mail the same student twice within 20 hours, stamped in `profiles.settings.digest_last_sent_at` only after a successful send — so a provider outage retries tomorrow instead of skipping that student for the day. + +## Who gets mail + +Only students whose `preferences.email_reminders` is explicitly `true`. An +unset preference means no mail: the web app now defaults the switch off until +a provider is configured, and the two have to agree. Treating unset as "send" +would mail everyone who never opened Settings the moment a key was added. + +To make it the default for new accounts once mail is live, flip +`DEFAULT_PREFERENCES.email_reminders` in `web/src/lib/preferences.ts`. That +governs new users; anyone who already saved a preference keeps theirs. diff --git a/supabase/functions/deadline-digest/index.ts b/supabase/functions/deadline-digest/index.ts index ad1c864..7570ca3 100644 --- a/supabase/functions/deadline-digest/index.ts +++ b/supabase/functions/deadline-digest/index.ts @@ -185,8 +185,11 @@ Deno.serve(async (req) => { for (const profile of profiles ?? []) { const settings = (profile.settings ?? {}) as Record const prefs = (settings.preferences ?? {}) as Record - // Unset means on, matching DEFAULT_PREFERENCES in the web app. - if (prefs.email_reminders === false) { skipped++; continue } + // Only an explicit opt-in gets mail, matching DEFAULT_PREFERENCES in the + // web app. Unset used to mean "send", which would have mailed every + // student who never opened Settings the moment a provider key was added — + // people who had been shown the switch was on but never chose it. + if (prefs.email_reminders !== true) { skipped++; continue } if (typeof profile.email !== "string" || !profile.email) { skipped++; continue } const lastSent = (settings.digest_last_sent_at ?? null) as string | null diff --git a/web/src/lib/preferences.test.ts b/web/src/lib/preferences.test.ts index 7f23044..64fd4c9 100644 --- a/web/src/lib/preferences.test.ts +++ b/web/src/lib/preferences.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest' -import { resolveDeadlinePreferences, URGENT_WINDOWS } from './preferences' +import { resolveDeadlinePreferences, resolvePreferences, DEFAULT_PREFERENCES, URGENT_WINDOWS } from './preferences' import type { UserSettings } from '../types/user' const settings = (preferences: Record) => @@ -63,3 +63,19 @@ describe('resolveDeadlinePreferences', () => { expect(resolveDeadlinePreferences(settings({ deadline_modules: 'all' })).modules).toEqual([]) }) }) + +describe('email reminders are opt-in', () => { + it('is off for a student who has never opened Settings', () => { + // The digest function is a deliberate no-op without a mail provider, so + // defaulting this on told every new student they would be emailed before + // a deadline and then sent nothing. It also has to stay in step with + // deadline-digest, which mails only an explicit `true`: were they to + // disagree, adding a provider key would mail everyone who never chose it. + expect(DEFAULT_PREFERENCES.email_reminders).toBe(false) + }) + + it('keeps a student\'s own choice', () => { + expect(resolvePreferences(settings({ email_reminders: true })).email_reminders).toBe(true) + expect(resolvePreferences(settings({ email_reminders: false })).email_reminders).toBe(false) + }) +}) diff --git a/web/src/lib/preferences.ts b/web/src/lib/preferences.ts index 7cf3c8b..066359c 100644 --- a/web/src/lib/preferences.ts +++ b/web/src/lib/preferences.ts @@ -38,7 +38,12 @@ export function enabledDeadlineModules(prefs: UserPreferences): string[] { } export const DEFAULT_PREFERENCES: Required = { - email_reminders: true, + // Off until a mail provider is configured. The digest function is a + // deliberate no-op without EMAIL_PROVIDER_KEY / EMAIL_FROM, so defaulting + // this on tells every student who never opens Settings that they will be + // emailed before a deadline, and then sends nothing — the one promise a + // deadline tool most needs to keep. Flip it back once mail is live. + email_reminders: false, theme: 'light', timeline_show_completed: true, timeline_auto_advance: true,