feat(notifications): Phase C — Don't Disturb (quiet hours window)
User задаёт quiet window (e.g. 22:00–08:00 Europe/Moscow) — external channels (email + express) DROP'аются dispatcher'ом. In-app bell badge остаётся (юзер видит при следующем визите UI). Cross-midnight aware. ## Backend - Migration 0026: user_notification_settings table (user_id PK + quiet_hours_start/end LocalTime + tz) - Entity UserNotificationSettings + Repository (stock CRUD) - UserPreferencesGate extended: - Constructor takes settingsRepo - allows() checks isQuiet(userId) первым for external channels (EMAIL/EXPRESS/TELEGRAM) - isQuiet: load settings → if hasEffectiveQuietHours() → check current LocalTime в user's TZ - inWindow() helper: cross-midnight semantics (22:00–08:00 wraps midnight) - Invalid TZ → fallback к DEFAULT_TZ (Europe/Moscow) с WARN log - Test seam: setClockForTesting(Clock) для deterministic tests - MeNotificationsSettingsController: GET/PUT /api/v1/me/notifications/settings - HH:mm input parsing, IANA TZ validation, enabled=false clears window - Auth required (JWT sub claim) ## Frontend - NotificationSettings type в client.ts - useMyNotificationSettings query (60s stale) - useUpdateNotificationSettings mutation - QuietHoursSection в /me/notifications/preferences route: - Switch toggle + 2 time inputs + TZ select (12 RU/UTC zones) - Optimistic local state synced from query - Save button с pending/success/error states - i18n RU + EN ## Tests - 9 new tests UserPreferencesGateQuietHoursTest: - Same-day window match (inclusive start, exclusive end) - Cross-midnight window match (22:00–08:00) - No settings row → не блокирует - Quiet window active → email + express dropped - Outside window → email allowed - start == end → disabled (zero-duration treated as off) - Reviewer pool bypasses (always ON) - Invalid TZ → fallback default - Specific pref ON + quiet ON → quiet wins (drop) - NotificationsDispatcherTest constructor updated к new signature - Total notifications tests: 53 → 62, все green ## Design rationale (drop vs defer) Drop chosen за defer (queue + re-fire scheduler) потому что: - Defer needs persistent queue + scheduled retry — high complexity - Most notifications time-relevant (draft N hours ago less actionable) - Bell badge показывает full feed — ничего не «теряется» - Phase D summary digest можно build later если demand surfaces
This commit is contained in:
@@ -674,6 +674,17 @@ i18n
|
||||
'Telegram бот. Канал ещё не подключён — toggle сохраняется на будущее.',
|
||||
'prefs.comingSoon': 'Скоро',
|
||||
'prefs.saveError': 'Не удалось сохранить настройки. Попробуйте ещё раз.',
|
||||
'prefs.quietHours.title': 'Тихие часы',
|
||||
'prefs.quietHours.description':
|
||||
'В этом окне email и express не отправляются. Внутренние уведомления в шапке остаются.',
|
||||
'prefs.quietHours.toggle': 'Включить тихие часы',
|
||||
'prefs.quietHours.start': 'Начало',
|
||||
'prefs.quietHours.end': 'Конец',
|
||||
'prefs.quietHours.tz': 'Часовой пояс',
|
||||
'prefs.quietHours.save': 'Сохранить',
|
||||
'prefs.quietHours.saving': 'Сохраняю…',
|
||||
'prefs.quietHours.saved': 'Сохранено',
|
||||
'prefs.quietHours.saveError': 'Не удалось сохранить тихие часы.',
|
||||
'prefs.descriptionMatrix':
|
||||
'Per-event настройки доставки. Reviewer pool inbox остаётся ON независимо от ваших настроек.',
|
||||
'prefs.matrix.eventCol': 'Событие',
|
||||
@@ -1503,6 +1514,17 @@ i18n
|
||||
'Telegram bot. Channel not wired yet — toggle is stored for later.',
|
||||
'prefs.comingSoon': 'Soon',
|
||||
'prefs.saveError': 'Failed to save preferences. Please try again.',
|
||||
'prefs.quietHours.title': 'Quiet hours',
|
||||
'prefs.quietHours.description':
|
||||
'Email and express muted during this window. In-app notifications remain visible.',
|
||||
'prefs.quietHours.toggle': 'Enable quiet hours',
|
||||
'prefs.quietHours.start': 'From',
|
||||
'prefs.quietHours.end': 'To',
|
||||
'prefs.quietHours.tz': 'Timezone',
|
||||
'prefs.quietHours.save': 'Save',
|
||||
'prefs.quietHours.saving': 'Saving…',
|
||||
'prefs.quietHours.saved': 'Saved',
|
||||
'prefs.quietHours.saveError': 'Failed to save quiet hours.',
|
||||
'prefs.descriptionMatrix':
|
||||
'Per-event delivery preferences. Reviewer pool inbox stays ON regardless of your settings.',
|
||||
'prefs.matrix.eventCol': 'Event',
|
||||
|
||||
Reference in New Issue
Block a user