From 28f6d3036841034c949bfdc1b58dcd8928aba0cc Mon Sep 17 00:00:00 2001 From: Maurice Date: Thu, 8 Oct 2026 12:00:25 +0200 Subject: [PATCH] docs(notifications): why the reminder backfill counts days in UTC The backfill in Migration20200101042100 marks reminders as sent by UTC date('now'), while the reworked job counts days in the app TZ, which read like a seam that sends twice or skips on upgrade day. It is not: the backfill stands in for the job before it, which matched start_date against UTC date('now') at 9 AM app time, so UTC is the clock those reminders went out by, and with one run per UTC day nothing it marks was left unsent. The only effect is its strict "<": the upgrade day's own reminders stay unmarked because the boot may precede that day's run, so a boot after it reminds such a trip once more. One extra reminder at most and never a lost one, so the shipped migration stays untouched and the reasoning now sits on the job. The todo claim's docstring writes its dedup bound as SQL. --- server/src/db/repositories/TodoItems.repository.ts | 2 +- .../src/nest/notifications/reminder-jobs.service.ts | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/server/src/db/repositories/TodoItems.repository.ts b/server/src/db/repositories/TodoItems.repository.ts index 3feae7474..7ad2b5368 100644 --- a/server/src/db/repositories/TodoItems.repository.ts +++ b/server/src/db/repositories/TodoItems.repository.ts @@ -245,7 +245,7 @@ export class TodoItemsRepository extends TrekRepository { /** * RJ5 (`reminder-jobs.service.ts#todoTick`, looped): claims the reminder * before it is sent. `UPDATE todo_items SET reminded_at = CURRENT_TIMESTAMP - * WHERE id = ? AND (reminded_at IS NULL OR reminded_at <= now - 20 h)`, the + * WHERE id = ? AND (reminded_at IS NULL OR reminded_at <= datetime(now, -20 hours))`, the * same dedup bound {@link listDueForReminder} selects by, so of two * processes running the tick only the one whose update lands sends. True * when this caller holds the claim. diff --git a/server/src/nest/notifications/reminder-jobs.service.ts b/server/src/nest/notifications/reminder-jobs.service.ts index f176f99fe..91d890926 100644 --- a/server/src/nest/notifications/reminder-jobs.service.ts +++ b/server/src/nest/notifications/reminder-jobs.service.ts @@ -96,6 +96,17 @@ export class ReminderJobsService implements OnApplicationBootstrap { * Each reminder is claimed before it is sent, so a second process running * the same tick skips it, and given back when the send fails, so the next * tick retries it until the trip starts. + * + * The upgrade backfill (`Migration20200101042100_…`) decides "already + * reminded" by UTC `date('now')` while this job counts days in the app TZ. + * That is deliberate, not a seam: the backfill stands in for the job before + * it, which matched `start_date = date('now', '+N days')` in UTC, so UTC is + * the clock those reminders went out by. Every UTC day had exactly one 9 AM + * run, so nothing the backfill marks was left unsent. Its strict `<` leaves + * the upgrade day's own reminders unmarked, because the boot may come before + * that day's run; when it comes after, such a trip is reminded once more on + * the first run after the upgrade. At most one extra reminder, never a lost + * one, which is why the migration stays as it shipped. */ async tripTick(): Promise { try {