fix: 'Pause sending by' is off by default everywhere

The optional 'Pause sending by' deadline was defaulting to 18 (= 6 PM)
in three places:
  - reminders.delivery_window_end_hour schema default (NOT NULL DEFAULT 18)
  - createReminderAction / editScheduleAction fallback when the field
    is missing on the input
  - the Zod refine validator's secondary fallback

Net effect: any reminder created before this change has 18 in the DB,
so the edit form's checkbox flips ON automatically (the wizard treats
'value !== undefined && value !== 24' as 'opted in'). The wizard's
own create flow always sends 24 explicitly when the box is unchecked
— but legacy / direct API payloads + the schema default for older rows
don't carry that intent through.

Switch every default to 24 (the off-sentinel the wizard already uses)
so the optional toggle stays off until the operator ticks it. New
migration 0012 also backfills existing rows from 18 → 24 so editing
old reminders no longer auto-checks 'Pause sending by'.

Tests in when-form-deadline.test.tsx already lock in the UI contract
(off when initialDeliveryEndHour is undefined or 24, on for any other
value). No assertion changes needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
yiekheng 2026-05-10 19:30:09 +08:00
parent 5c48e0e85f
commit e800882d15
5 changed files with 1170 additions and 92 deletions

View File

@ -271,7 +271,7 @@ const createReminderSchema = z
path: ["messages"], path: ["messages"],
}, },
) )
.refine((d) => (d.deliveryWindowStartHour ?? 6) < (d.deliveryWindowEndHour ?? 18), { .refine((d) => (d.deliveryWindowStartHour ?? 6) < (d.deliveryWindowEndHour ?? 24), {
message: "Delivery window start must be earlier than end", message: "Delivery window start must be earlier than end",
path: ["deliveryWindowStartHour"], path: ["deliveryWindowStartHour"],
}); });
@ -328,7 +328,11 @@ export async function createReminderAction(
timezone, timezone,
} = parsed.data; } = parsed.data;
const deliveryWindowStartHour = parsed.data.deliveryWindowStartHour ?? 6; const deliveryWindowStartHour = parsed.data.deliveryWindowStartHour ?? 6;
const deliveryWindowEndHour = parsed.data.deliveryWindowEndHour ?? 18; // 24 = "no deadline" (off). The wizard sends 24 explicitly when the
// operator hasn't ticked the optional "Pause sending by" checkbox;
// fall back to 24 here so legacy payloads / direct API calls don't
// accidentally enable the deadline at 6pm.
const deliveryWindowEndHour = parsed.data.deliveryWindowEndHour ?? 24;
const parts = resolveMessageParts(parsed.data); const parts = resolveMessageParts(parsed.data);
const op = await getSeededOperator(); const op = await getSeededOperator();
@ -442,7 +446,11 @@ export async function updateReminderAction(
timezone, timezone,
} = parsed.data; } = parsed.data;
const deliveryWindowStartHour = parsed.data.deliveryWindowStartHour ?? 6; const deliveryWindowStartHour = parsed.data.deliveryWindowStartHour ?? 6;
const deliveryWindowEndHour = parsed.data.deliveryWindowEndHour ?? 18; // 24 = "no deadline" (off). The wizard sends 24 explicitly when the
// operator hasn't ticked the optional "Pause sending by" checkbox;
// fall back to 24 here so legacy payloads / direct API calls don't
// accidentally enable the deadline at 6pm.
const deliveryWindowEndHour = parsed.data.deliveryWindowEndHour ?? 24;
const parts = resolveMessageParts(parsed.data); const parts = resolveMessageParts(parsed.data);
const op = await getSeededOperator(); const op = await getSeededOperator();

View File

@ -0,0 +1,10 @@
-- Switch the default to 24 ("no deadline" sentinel) so newly-created
-- reminders are off-by-default for the optional "Pause sending by"
-- toggle, matching the wizard's UX contract.
ALTER TABLE "reminders" ALTER COLUMN "delivery_window_end_hour" SET DEFAULT 24;
-- Existing rows still hold the old default (18). Treat those as
-- "schema-default, never opted in by the operator" and clear them to
-- 24 so editing an old reminder doesn't auto-check the deadline box.
-- Operators who actually wanted a 6pm deadline can re-enable it from
-- the edit form.
UPDATE "reminders" SET "delivery_window_end_hour" = 24 WHERE "delivery_window_end_hour" = 18;

File diff suppressed because it is too large Load Diff

View File

@ -1,90 +1,97 @@
{ {
"version": "7", "version": "7",
"dialect": "postgresql", "dialect": "postgresql",
"entries": [ "entries": [
{ {
"idx": 0, "idx": 0,
"version": "7", "version": "7",
"when": 1778311164225, "when": 1778311164225,
"tag": "0000_conscious_tarantula", "tag": "0000_conscious_tarantula",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 1, "idx": 1,
"version": "7", "version": "7",
"when": 1778320434707, "when": 1778320434707,
"tag": "0001_smart_vertigo", "tag": "0001_smart_vertigo",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 2, "idx": 2,
"version": "7", "version": "7",
"when": 1778338808600, "when": 1778338808600,
"tag": "0002_left_jimmy_woo", "tag": "0002_left_jimmy_woo",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 3, "idx": 3,
"version": "7", "version": "7",
"when": 1778343712901, "when": 1778343712901,
"tag": "0003_messy_bruce_banner", "tag": "0003_messy_bruce_banner",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 4, "idx": 4,
"version": "7", "version": "7",
"when": 1778345543406, "when": 1778345543406,
"tag": "0004_next_prowler", "tag": "0004_next_prowler",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 5, "idx": 5,
"version": "7", "version": "7",
"when": 1778347437350, "when": 1778347437350,
"tag": "0005_flippant_joystick", "tag": "0005_flippant_joystick",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 6, "idx": 6,
"version": "7", "version": "7",
"when": 1778385559051, "when": 1778385559051,
"tag": "0006_adorable_nehzno", "tag": "0006_adorable_nehzno",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 7, "idx": 7,
"version": "7", "version": "7",
"when": 1778386591494, "when": 1778386591494,
"tag": "0007_overconfident_menace", "tag": "0007_overconfident_menace",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 8, "idx": 8,
"version": "7", "version": "7",
"when": 1778395584234, "when": 1778395584234,
"tag": "0008_greedy_matthew_murdock", "tag": "0008_greedy_matthew_murdock",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 9, "idx": 9,
"version": "7", "version": "7",
"when": 1778464000000, "when": 1778464000000,
"tag": "0009_rename_ended_to_inactive", "tag": "0009_rename_ended_to_inactive",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 10, "idx": 10,
"version": "7", "version": "7",
"when": 1778464001000, "when": 1778464001000,
"tag": "0010_fancy_wolf_cub", "tag": "0010_fancy_wolf_cub",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 11, "idx": 11,
"version": "7", "version": "7",
"when": 1778464002000, "when": 1778464002000,
"tag": "0011_premium_grandmaster", "tag": "0011_premium_grandmaster",
"breakpoints": true "breakpoints": true
} },
] {
"idx": 12,
"version": "7",
"when": 1778412502601,
"tag": "0012_lucky_masked_marvel",
"breakpoints": true
}
]
} }

View File

@ -92,8 +92,11 @@ export const reminders = pgTable("reminders", {
// Delivery window (operator timezone). End hour is enforced at runtime // Delivery window (operator timezone). End hour is enforced at runtime
// by fire-reminder when window enforcement lands; start hour is documented // by fire-reminder when window enforcement lands; start hour is documented
// here but not gated in v1. // here but not gated in v1.
// 24 is the "no deadline" sentinel — it's the off-by-default state so a
// reminder created without the operator explicitly opting into "Pause
// sending by" stays unbounded.
deliveryWindowStartHour: integer("delivery_window_start_hour").notNull().default(6), deliveryWindowStartHour: integer("delivery_window_start_hour").notNull().default(6),
deliveryWindowEndHour: integer("delivery_window_end_hour").notNull().default(18), deliveryWindowEndHour: integer("delivery_window_end_hour").notNull().default(24),
}); });
export const reminderTargets = pgTable( export const reminderTargets = pgTable(