feat(notifications): reminders for todos with upcoming due dates

Todos already support a due_date field but nothing notifies the user
when a deadline is approaching — you'd only remember if you happened
to look at the Lists tab. This wires a reminder into the existing
notification pipeline so due-date todos behave like trip-start
reminders.

Details:
- New `todo_due` event type alongside trip_reminder; all four channels
  (in-app, email, webhook, ntfy) supported and toggleable per user in
  Settings > Notifications.
- New daily scheduler task (9 AM local TZ) queries unchecked todos
  whose due_date is within the next 3 days. Each todo gets at most
  one reminder per 24 hours, tracked via a new todo_items.reminded_at
  column (migration 116).
- If the todo has an assigned user, only that user is reminded; if
  not, every member of the trip gets the notification.
- Strings added in all 15 UI languages and for all notification
  carriers.
- Gated by app_settings.notify_todo_due (default on) so admins can
  disable it globally.
This commit is contained in:
Maurice
2026-04-20 17:31:25 +02:00
parent 58c061e653
commit d7a71c0572
22 changed files with 154 additions and 1 deletions
@@ -25,6 +25,7 @@ const EVENT_LABEL_KEYS: Record<string, string> = {
trip_invite: 'settings.notifyTripInvite',
booking_change: 'settings.notifyBookingChange',
trip_reminder: 'settings.notifyTripReminder',
todo_due: 'settings.notifyTodoDue',
vacay_invite: 'settings.notifyVacayInvite',
photos_shared: 'settings.notifyPhotosShared',
collab_message: 'settings.notifyCollabMessage',
+3
View File
@@ -204,6 +204,7 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
'settings.notifyTripInvite': 'دعوات الرحلات',
'settings.notifyBookingChange': 'تغييرات الحجز',
'settings.notifyTripReminder': 'تذكيرات الرحلات',
'settings.notifyTodoDue': 'مهمة مستحقة',
'settings.notifyVacayInvite': 'دعوات دمج الإجازات',
'settings.notifyPhotosShared': 'صور مشتركة (Immich)',
'settings.notifyCollabMessage': 'رسائل الدردشة (Collab)',
@@ -1995,6 +1996,8 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
'notif.booking_change.text': '{actor} حدّث حجزاً في {trip}',
'notif.trip_reminder.title': 'تذكير بالرحلة',
'notif.trip_reminder.text': 'رحلتك {trip} تقترب!',
'notif.todo_due.title': 'مهمة مستحقة',
'notif.todo_due.text': '{todo} في {trip} مستحقة في {due}',
'notif.vacay_invite.title': 'دعوة دمج الإجازة',
'notif.vacay_invite.text': '{actor} يدعوك لدمج خطط الإجازة',
'notif.photos_shared.title': 'تمت مشاركة الصور',
+3
View File
@@ -199,6 +199,7 @@ const br: Record<string, string | { name: string; category: string }[]> = {
'settings.notifyTripInvite': 'Convites de viagem',
'settings.notifyBookingChange': 'Alterações de reserva',
'settings.notifyTripReminder': 'Lembretes de viagem',
'settings.notifyTodoDue': 'Tarefa com vencimento',
'settings.notifyVacayInvite': 'Convites de fusão Vacay',
'settings.notifyPhotosShared': 'Fotos compartilhadas (Immich)',
'settings.notifyCollabMessage': 'Mensagens de chat (Colab)',
@@ -1935,6 +1936,8 @@ const br: Record<string, string | { name: string; category: string }[]> = {
'notif.booking_change.text': '{actor} atualizou uma reserva em {trip}',
'notif.trip_reminder.title': 'Lembrete de viagem',
'notif.trip_reminder.text': 'Sua viagem {trip} está chegando!',
'notif.todo_due.title': 'Tarefa com vencimento',
'notif.todo_due.text': '{todo} em {trip} vence em {due}',
'notif.vacay_invite.title': 'Convite Vacay Fusion',
'notif.vacay_invite.text': '{actor} convidou você para fundir planos de férias',
'notif.photos_shared.title': 'Fotos compartilhadas',
+3
View File
@@ -200,6 +200,7 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
'settings.notifyTripInvite': 'Pozvánky na cesty',
'settings.notifyBookingChange': 'Změny rezervací',
'settings.notifyTripReminder': 'Připomínky cest',
'settings.notifyTodoDue': 'Úkol se blíží',
'settings.notifyVacayInvite': 'Pozvánky k propojení Vacay',
'settings.notifyPhotosShared': 'Sdílené fotky (Immich)',
'settings.notifyCollabMessage': 'Zprávy v chatu (Collab)',
@@ -1940,6 +1941,8 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
'notif.booking_change.text': '{actor} aktualizoval rezervaci v {trip}',
'notif.trip_reminder.title': 'Připomínka výletu',
'notif.trip_reminder.text': 'Váš výlet {trip} se blíží!',
'notif.todo_due.title': 'Úkol se blíží',
'notif.todo_due.text': '{todo} ve výletě {trip} má termín {due}',
'notif.vacay_invite.title': 'Pozvánka Vacay Fusion',
'notif.vacay_invite.text': '{actor} vás pozval ke spojení dovolenkových plánů',
'notif.photos_shared.title': 'Fotky sdíleny',
+3
View File
@@ -204,6 +204,7 @@ const de: Record<string, string | { name: string; category: string }[]> = {
'settings.notifyTripInvite': 'Trip-Einladungen',
'settings.notifyBookingChange': 'Buchungsänderungen',
'settings.notifyTripReminder': 'Trip-Erinnerungen',
'settings.notifyTodoDue': 'Aufgabe bald fällig',
'settings.notifyVacayInvite': 'Vacay Fusion-Einladungen',
'settings.notifyPhotosShared': 'Geteilte Fotos (Immich)',
'settings.notifyCollabMessage': 'Chat-Nachrichten (Collab)',
@@ -1945,6 +1946,8 @@ const de: Record<string, string | { name: string; category: string }[]> = {
'notif.booking_change.text': '{actor} hat eine Buchung in {trip} aktualisiert',
'notif.trip_reminder.title': 'Reiseerinnerung',
'notif.trip_reminder.text': 'Deine Reise {trip} steht bald an!',
'notif.todo_due.title': 'Aufgabe fällig',
'notif.todo_due.text': '{todo} in {trip} ist am {due} fällig',
'notif.vacay_invite.title': 'Vacay Fusion-Einladung',
'notif.vacay_invite.text': '{actor} hat dich zum Fusionieren von Urlaubsplänen eingeladen',
'notif.photos_shared.title': 'Fotos geteilt',
+3
View File
@@ -204,6 +204,7 @@ const en: Record<string, string | { name: string; category: string }[]> = {
'settings.notifyTripInvite': 'Trip invitations',
'settings.notifyBookingChange': 'Booking changes',
'settings.notifyTripReminder': 'Trip reminders',
'settings.notifyTodoDue': 'Todo due soon',
'settings.notifyVacayInvite': 'Vacay fusion invitations',
'settings.notifyPhotosShared': 'Shared photos (Immich)',
'settings.notifyCollabMessage': 'Chat messages (Collab)',
@@ -1948,6 +1949,8 @@ const en: Record<string, string | { name: string; category: string }[]> = {
'notif.booking_change.text': '{actor} updated a booking in {trip}',
'notif.trip_reminder.title': 'Trip Reminder',
'notif.trip_reminder.text': 'Your trip {trip} is coming up soon!',
'notif.todo_due.title': 'To-do due',
'notif.todo_due.text': '{todo} in {trip} is due on {due}',
'notif.vacay_invite.title': 'Vacay Fusion Invite',
'notif.vacay_invite.text': '{actor} invited you to fuse vacation plans',
'notif.photos_shared.title': 'Photos Shared',
+3
View File
@@ -200,6 +200,7 @@ const es: Record<string, string> = {
'settings.notifyTripInvite': 'Invitaciones de viaje',
'settings.notifyBookingChange': 'Cambios en reservas',
'settings.notifyTripReminder': 'Recordatorios de viaje',
'settings.notifyTodoDue': 'Tarea próxima',
'settings.notifyVacayInvite': 'Invitaciones de fusión Vacay',
'settings.notifyPhotosShared': 'Fotos compartidas (Immich)',
'settings.notifyCollabMessage': 'Mensajes de chat (Collab)',
@@ -1945,6 +1946,8 @@ const es: Record<string, string> = {
'notif.booking_change.text': '{actor} actualizó una reserva en {trip}',
'notif.trip_reminder.title': 'Recordatorio de viaje',
'notif.trip_reminder.text': '¡Tu viaje {trip} se acerca!',
'notif.todo_due.title': 'Tarea pendiente',
'notif.todo_due.text': '{todo} en {trip} vence el {due}',
'notif.vacay_invite.title': 'Invitación Vacay Fusion',
'notif.vacay_invite.text': '{actor} te invitó a fusionar planes de vacaciones',
'notif.photos_shared.title': 'Fotos compartidas',
+3
View File
@@ -199,6 +199,7 @@ const fr: Record<string, string> = {
'settings.notifyTripInvite': 'Invitations de voyage',
'settings.notifyBookingChange': 'Modifications de réservation',
'settings.notifyTripReminder': 'Rappels de voyage',
'settings.notifyTodoDue': 'Tâche à échéance',
'settings.notifyVacayInvite': 'Invitations de fusion Vacay',
'settings.notifyPhotosShared': 'Photos partagées (Immich)',
'settings.notifyCollabMessage': 'Messages de chat (Collab)',
@@ -1939,6 +1940,8 @@ const fr: Record<string, string> = {
'notif.booking_change.text': '{actor} a mis à jour une réservation dans {trip}',
'notif.trip_reminder.title': 'Rappel de voyage',
'notif.trip_reminder.text': 'Votre voyage {trip} approche !',
'notif.todo_due.title': 'Tâche à échéance',
'notif.todo_due.text': '{todo} dans {trip} est due le {due}',
'notif.vacay_invite.title': 'Invitation Vacay Fusion',
'notif.vacay_invite.text': '{actor} vous invite à fusionner les plans de vacances',
'notif.photos_shared.title': 'Photos partagées',
+3
View File
@@ -199,6 +199,7 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
'settings.notifyTripInvite': 'Utazási meghívók',
'settings.notifyBookingChange': 'Foglalási változások',
'settings.notifyTripReminder': 'Utazási emlékeztetők',
'settings.notifyTodoDue': 'Teendő esedékes',
'settings.notifyVacayInvite': 'Vacay összevonási meghívók',
'settings.notifyPhotosShared': 'Megosztott fotók (Immich)',
'settings.notifyCollabMessage': 'Csevegés üzenetek (Collab)',
@@ -1937,6 +1938,8 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
'notif.booking_change.text': '{actor} frissített egy foglalást a(z) {trip} utazásban',
'notif.trip_reminder.title': 'Utazás emlékeztető',
'notif.trip_reminder.text': 'A(z) {trip} utazás hamarosan kezdődik!',
'notif.todo_due.title': 'Teendő esedékes',
'notif.todo_due.text': '{todo} ({trip}) határideje: {due}',
'notif.vacay_invite.title': 'Vacay Fusion meghívó',
'notif.vacay_invite.text': '{actor} meghívott a nyaralási tervek összevonásához',
'notif.photos_shared.title': 'Fotók megosztva',
+3
View File
@@ -202,6 +202,7 @@ const id: Record<string, string | { name: string; category: string }[]> = {
'settings.notifyTripInvite': 'Undangan perjalanan',
'settings.notifyBookingChange': 'Perubahan pemesanan',
'settings.notifyTripReminder': 'Pengingat perjalanan',
'settings.notifyTodoDue': 'Tugas jatuh tempo',
'settings.notifyVacayInvite': 'Undangan Vacay fusion',
'settings.notifyPhotosShared': 'Foto dibagikan (Immich)',
'settings.notifyCollabMessage': 'Pesan chat (Collab)',
@@ -1946,6 +1947,8 @@ const id: Record<string, string | { name: string; category: string }[]> = {
'notif.booking_change.text': '{actor} memperbarui pemesanan di {trip}',
'notif.trip_reminder.title': 'Pengingat Perjalanan',
'notif.trip_reminder.text': 'Perjalananmu {trip} akan segera dimulai!',
'notif.todo_due.title': 'Tugas jatuh tempo',
'notif.todo_due.text': '{todo} di {trip} jatuh tempo pada {due}',
'notif.vacay_invite.title': 'Undangan Vacay Fusion',
'notif.vacay_invite.text': '{actor} mengundangmu untuk menggabungkan rencana liburan',
'notif.photos_shared.title': 'Foto Dibagikan',
+3
View File
@@ -199,6 +199,7 @@ const it: Record<string, string | { name: string; category: string }[]> = {
'settings.notifyTripInvite': 'Inviti di viaggio',
'settings.notifyBookingChange': 'Modifiche alle prenotazioni',
'settings.notifyTripReminder': 'Promemoria di viaggio',
'settings.notifyTodoDue': 'Attività in scadenza',
'settings.notifyVacayInvite': 'Inviti fusione Vacay',
'settings.notifyPhotosShared': 'Foto condivise (Immich)',
'settings.notifyCollabMessage': 'Messaggi chat (Collab)',
@@ -1940,6 +1941,8 @@ const it: Record<string, string | { name: string; category: string }[]> = {
'notif.booking_change.text': '{actor} ha aggiornato una prenotazione in {trip}',
'notif.trip_reminder.title': 'Promemoria viaggio',
'notif.trip_reminder.text': 'Il tuo viaggio {trip} si avvicina!',
'notif.todo_due.title': 'Attività in scadenza',
'notif.todo_due.text': '{todo} in {trip} scade il {due}',
'notif.vacay_invite.title': 'Invito Vacay Fusion',
'notif.vacay_invite.text': '{actor} ti ha invitato a fondere i piani vacanza',
'notif.photos_shared.title': 'Foto condivise',
+3
View File
@@ -199,6 +199,7 @@ const nl: Record<string, string> = {
'settings.notifyTripInvite': 'Reisuitnodigingen',
'settings.notifyBookingChange': 'Boekingswijzigingen',
'settings.notifyTripReminder': 'Reisherinneringen',
'settings.notifyTodoDue': 'Taak verloopt',
'settings.notifyVacayInvite': 'Vacay-fusieuitnodigingen',
'settings.notifyPhotosShared': 'Gedeelde foto\'s (Immich)',
'settings.notifyCollabMessage': 'Chatberichten (Collab)',
@@ -1939,6 +1940,8 @@ const nl: Record<string, string> = {
'notif.booking_change.text': '{actor} heeft een boeking bijgewerkt in {trip}',
'notif.trip_reminder.title': 'Reisherinnering',
'notif.trip_reminder.text': 'Je reis {trip} komt eraan!',
'notif.todo_due.title': 'Taak verloopt',
'notif.todo_due.text': '{todo} in {trip} verloopt op {due}',
'notif.vacay_invite.title': 'Vacay Fusion-uitnodiging',
'notif.vacay_invite.text': '{actor} nodigt je uit om vakantieplannen te fuseren',
'notif.photos_shared.title': 'Foto\'s gedeeld',
+3
View File
@@ -182,6 +182,7 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
'settings.notifyTripInvite': 'Zaproszenia do podróży',
'settings.notifyBookingChange': 'Zmiany w rezerwacjach',
'settings.notifyTripReminder': 'Przypomnienia o podróżach',
'settings.notifyTodoDue': 'Zadanie z terminem',
'settings.notifyVacayInvite': 'Zaproszenia do połączenia kalendarzy',
'settings.notifyPhotosShared': 'Udostępnione zdjęcia (Immich)',
'settings.notifyCollabMessage': 'Wiadomości czatu (Collab)',
@@ -1929,6 +1930,8 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
'notif.booking_change.text': '{actor} zaktualizował rezerwację w {trip}',
'notif.trip_reminder.title': 'Przypomnienie o podróży',
'notif.trip_reminder.text': 'Twoja podróż {trip} zbliża się!',
'notif.todo_due.title': 'Zadanie z terminem',
'notif.todo_due.text': '{todo} w {trip} — termin {due}',
'notif.vacay_invite.title': 'Zaproszenie Vacay Fusion',
'notif.vacay_invite.text': '{actor} zaprosił Cię do połączenia planów urlopowych',
'notif.photos_shared.title': 'Zdjęcia udostępnione',
+3
View File
@@ -199,6 +199,7 @@ const ru: Record<string, string> = {
'settings.notifyTripInvite': 'Приглашения в поездку',
'settings.notifyBookingChange': 'Изменения бронирований',
'settings.notifyTripReminder': 'Напоминания о поездке',
'settings.notifyTodoDue': 'Задача к сроку',
'settings.notifyVacayInvite': 'Приглашения слияния Vacay',
'settings.notifyPhotosShared': 'Общие фото (Immich)',
'settings.notifyCollabMessage': 'Сообщения чата (Collab)',
@@ -1936,6 +1937,8 @@ const ru: Record<string, string> = {
'notif.booking_change.text': '{actor} обновил бронирование в {trip}',
'notif.trip_reminder.title': 'Напоминание о поездке',
'notif.trip_reminder.text': 'Ваша поездка {trip} скоро начнётся!',
'notif.todo_due.title': 'Задача к сроку',
'notif.todo_due.text': '{todo} в {trip} — срок {due}',
'notif.vacay_invite.title': 'Приглашение Vacay Fusion',
'notif.vacay_invite.text': '{actor} приглашает вас объединить планы отпуска',
'notif.photos_shared.title': 'Фото опубликованы',
+3
View File
@@ -199,6 +199,7 @@ const zh: Record<string, string> = {
'settings.notifyTripInvite': '旅行邀请',
'settings.notifyBookingChange': '预订变更',
'settings.notifyTripReminder': '旅行提醒',
'settings.notifyTodoDue': '待办事项即将到期',
'settings.notifyVacayInvite': 'Vacay 融合邀请',
'settings.notifyPhotosShared': '共享照片 (Immich)',
'settings.notifyCollabMessage': '聊天消息 (Collab)',
@@ -1936,6 +1937,8 @@ const zh: Record<string, string> = {
'notif.booking_change.text': '{actor} 更新了 {trip} 中的预订',
'notif.trip_reminder.title': '旅行提醒',
'notif.trip_reminder.text': '您的旅行 {trip} 即将开始!',
'notif.todo_due.title': '待办事项即将到期',
'notif.todo_due.text': '{trip} 中的 {todo} 将于 {due} 到期',
'notif.vacay_invite.title': 'Vacay 融合邀请',
'notif.vacay_invite.text': '{actor} 邀请您合并假期计划',
'notif.photos_shared.title': '照片已分享',
+3
View File
@@ -199,6 +199,7 @@ const zhTw: Record<string, string> = {
'settings.notifyTripInvite': '旅行邀請',
'settings.notifyBookingChange': '預訂變更',
'settings.notifyTripReminder': '旅行提醒',
'settings.notifyTodoDue': '待辦事項即將到期',
'settings.notifyVacayInvite': 'Vacay 融合邀請',
'settings.notifyPhotosShared': '共享照片 (Immich)',
'settings.notifyCollabMessage': '聊天訊息 (Collab)',
@@ -2195,6 +2196,8 @@ const zhTw: Record<string, string> = {
'notif.booking_change.text': '{actor} 更新了 {trip} 中的預訂',
'notif.trip_reminder.title': '旅行提醒',
'notif.trip_reminder.text': '你的旅行 {trip} 即將開始!',
'notif.todo_due.title': '待辦事項即將到期',
'notif.todo_due.text': '{trip} 中的 {todo} 將於 {due} 到期',
'notif.vacay_invite.title': 'Vacay Fusion 邀請',
'notif.vacay_invite.text': '{actor} 邀請你合併假期計畫',
'notif.photos_shared.title': '照片已分享',