Choosing What Wakes You Up
A fire chief in San Diego County does not want a 3 AM phone call about a low-urgency alert in Humboldt. Notification settings are how each user draws that line: which places, how urgent, what hours, which channels. This lesson covers the form that captures those choices, the bit-twiddling in the data model, and two verified traps in the save path that you should know before touching this code.
The four questions
NotificationsForm (in components/molecules/Notifications/NotificationSettingsForm/) asks:
- Where? — one or more locations, duplicates rejected by a Zod
refine - How urgent? — a minimum urgency level
- When? — day-of-week + time-range windows, or all-day
- How? — email, SMS, and/or phone call
Validation is react-hook-form + Zod; each question must have at least one answer before
submit. Saving posts to /api/notifications/subscribe; the backend then matches future alerts
against the stored subscription and only notifies when everything lines up.
The bitmask data model
Three of the four questions are encoded as bit flags, and the encodings are worth internalizing:
Channels are a straightforward bitmask: Email = 1, SMS = 2, PhoneCall = 4 — email+SMS
is 3. Zero (None) fails validation.
Days of week are likewise one bit per day; daysOfWeek: 0 means "no days" and is rejected.
Urgency is the sneaky one. The values are cumulative thresholds, not independent flags:
High = 4 // notify only for High
Medium = 6 // 4|2 — Medium and above
Low = 7 // 4|2|1 — everything
Choosing "Low" doesn't mean "only low alerts" — it means low or worse, i.e. everything. The
helpers subscriptionReturnStringToEnum / normalizeSubscriptionUrgency in
models/Misc/Notifications.ts translate between this scheme and display strings. Keep the
cumulative semantics in mind when reading matching logic anywhere.
Times travel as HHmm strings; the form works in dayjs objects and converts on the way out,
parsing back with dayjs(tf.startTime, 'HHmm') when prefilling.
Going deeper: two traps in the save path (verified in source)
Pause/resume has its own endpoint and mutation (/api/notifications/pause). But onSubmit
builds its payload with a hard-coded isPaused: false — regardless of the form state or the
existing subscription. A paused user who edits any setting and hits save is un-paused without
being told. If you're working in this form, this line of NotificationsForm.tsx is the first
thing to reconcile with the pause feature.
The schema carries bufferWidthMiles per location, and the prefill maps existing values in —
but onSubmit maps every location to bufferWidthMiles: 0, and new locations are created with
0 too. Whatever buffer a user believes they configured, the backend receives zero. Treat the
buffer UI as decorative until this is fixed.
Wrapping up
Four questions, three bitmasks (one of them cumulative), HHmm strings on the wire — and a
save path with two verified surprises: it un-pauses, and it zeroes buffers. Read onSubmit in
NotificationsForm.tsx before believing anything else about what gets persisted.
That completes the features chapter's alert side. The receiving end of these notifications — badges, sounds, and the AI modal — lives in AI alerts and the activity log.