Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions supabase/functions/deadline-digest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
7 changes: 5 additions & 2 deletions supabase/functions/deadline-digest/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,11 @@ Deno.serve(async (req) => {
for (const profile of profiles ?? []) {
const settings = (profile.settings ?? {}) as Record<string, unknown>
const prefs = (settings.preferences ?? {}) as Record<string, unknown>
// 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
Expand Down
18 changes: 17 additions & 1 deletion web/src/lib/preferences.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>) =>
Expand Down Expand Up @@ -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)
})
})
7 changes: 6 additions & 1 deletion web/src/lib/preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,12 @@ export function enabledDeadlineModules(prefs: UserPreferences): string[] {
}

export const DEFAULT_PREFERENCES: Required<UserPreferences> = {
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,
Expand Down
Loading