Skip to main content

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:

  1. Where? — one or more locations, duplicates rejected by a Zod refine
  2. How urgent? — a minimum urgency level
  3. When? — day-of-week + time-range windows, or all-day
  4. 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)

Saving settings silently un-pauses you

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.

Buffer radii are always sent as zero

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.