Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
9.6 KiB
Напоминалки: пауза и часовой пояс — дизайн
Дата: 2026-09-05
Проблема
Фича «Напоминалки» (см. 2026-09-02-admin-reminders-design.md) уже работает, но не хватает двух вещей:
- Нельзя временно отключить напоминание без удаления (например, аренду продлили раньше срока, но напоминание пригодится в следующий раз).
- Дата/время вводятся и хранятся в серверном времени без учёта того, что настраивающий напоминание админ может физически находиться в другом часовом поясе — время срабатывания может «уехать» на несколько часов от того, что админ имел в виду.
Часовой пояс
Хранение
Новая таблица SQLite admin_settings:
CREATE TABLE IF NOT EXISTS admin_settings (
user_id INTEGER PRIMARY KEY,
tz_offset INTEGER NOT NULL, -- смещение от UTC в часах, напр. 3 для Москвы
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
Диапазон значений: −12..+14 (реальные часовые пояса Земли).
Гейт при входе в раздел
Любое обращение к разделу «⏰ Напоминалки» (просмотр списка, «➕ Добавить»,
«🗑 Удалить», «⏸/▶ Пауза») сначала проверяет db.get_admin_tz_offset(user_id).
Если пояс не задан — бот переводит админа в состояние ADMIN_REMINDER_TZ_SETUP
и просит ввести смещение («Ваш часовой пояс относительно UTC, например +3 —
Москва, +5 — Екатеринбург, 0 — UTC»). После сохранения бот показывает то, за
чем админ изначально пришёл (список напоминаний) — без необходимости повторно
жать исходную кнопку.
Пояс можно сменить позже в любой момент командой часовой пояс <±N>.
Конвертация при создании (вход → сервер)
Дата/время, введённые автором в шаге ADMIN_REMINDER_DATE, интерпретируются
как его локальное время и переводятся в серверное перед сохранением:
server_dt = admin_dt + timedelta(hours=(server_utc_offset - author_tz_offset))
где server_utc_offset = datetime.now().astimezone().utcoffset().total_seconds() / 3600
(собственное смещение сервера от UTC, вычисляется на лету — часовой пояс сервера
нигде явно не настроен и может быть любым).
remind_at/next_at в БД как хранили серверное время, так и хранят —
планировщик (_check_due_reminders) и расчёт периодичности
(compute_next_occurrence) не меняются вообще.
Отображение в общем списке (сервер → зритель)
Список напоминаний общий для всех админов. Время каждой строки пересчитывается
под часовой пояс того, кто сейчас смотрит список (используя его собственный
tz_offset, который уже гарантированно задан благодаря гейту):
viewer_dt = server_dt + timedelta(hours=(viewer_tz_offset - server_utc_offset))
Рядом с временем — пометка (UTC+N). В тексте раздела — короткая приписка:
«ℹ️ Чтобы время в списке отображалось верно у всех, каждому нужно один раз
зайти сюда и указать свой часовой пояс».
Важно: срабатывание напоминания НЕ зависит от того, установили ли получатели свой часовой пояс — момент отправки уже зафиксирован в серверном времени при создании. Часовой пояс получателей влияет только на то, как ОНИ видят время в списке, когда сами зайдут в раздел.
Общие функции (в reminders.py)
def server_utc_offset_hours() -> float:
"""Собственное смещение сервера от UTC в часах (для конвертации между
часовым поясом админа и серверным временем хранения)."""
def convert_time(dt: datetime, from_offset: float, to_offset: float) -> datetime:
"""Переводит наивный datetime из одного часового пояса (смещение от UTC
в часах) в другой."""
Пауза
Хранение
В таблицу reminders добавляется колонка:
ALTER TABLE reminders ADD COLUMN paused INTEGER NOT NULL DEFAULT 0
(миграция через try/except sqlite3.OperationalError, как другие миграции в
database.py, чтобы не ломать уже существующую БД).
Логика
get_due_reminders добавляет условие AND paused = 0 — напоминание на паузе
не попадает в выборку планировщика, но остаётся в get_active_reminders для
отображения и управления.
Новые методы DatabaseManager: set_reminder_paused(reminder_id: int, paused: bool).
Возобновление не требует пересчёта дат — просто снимает флаг paused.
Если время уже прошло, пока напоминание было на паузе, срабатывает то же
правило, что и при простое бота (уже реализовано в Task 5): разовое — пришлёт
один раз при ближайшей проверке и деактивируется; периодическое — пришлёт раз
и само пересчитает следующее срабатывание в будущее (compute_next_occurrence
уже умеет пропускать пропущенные периоды). Никакой новой логики для этого не
требуется.
UI
Одна кнопка «⏸/▶ Пауза» в разделе «Напоминалки» (кнопка добавляется в
get_reminders_admin_keyboard). Открывает список выбора (как удаление, через
get_pick_keyboard), где подпись каждого пункта отражает текущее состояние:
- активное →
"⏸ Поставить на паузу: <текст>" - на паузе →
"▶ Возобновить: <текст>"
Выбор пункта переключает флаг на противоположный. Новое состояние мастера:
ADMIN_REMINDER_PAUSE_PICK.
В списке напоминаний (_send_reminders_admin) у напоминаний на паузе —
пометка ⏸ (на паузе) перед текстом.
Изменяемые файлы
- database.py — таблица
admin_settings+ CRUD (get_admin_tz_offset,set_admin_tz_offset); колонкаpausedвreminders+set_reminder_paused; фильтрpaused = 0вget_due_reminders. - reminders.py —
server_utc_offset_hours(),convert_time(). - bot.py — гейт часового пояса перед разделом «Напоминалки»,
состояние
ADMIN_REMINDER_TZ_SETUP, конвертация вADMIN_REMINDER_DATE, пересчёт для отображения в_send_reminders_admin, командачасовой пояс <±N>, кнопка и состояниеADMIN_REMINDER_PAUSE_PICK. - keyboards.py — кнопка «⏸/▶ Пауза» в
get_reminders_admin_keyboard.
Тестирование
tests/test_reminders.py:server_utc_offset_hours(проверка типа/разумного диапазона −12..14),convert_timeдля нескольких смещений и границы суток.tests/test_database.py: CRUDadmin_settings;paused— установка, фильтрация вget_due_reminders, попадание вget_active_reminders.tests/test_flow.py: гейт часового пояса при первом входе в раздел (список не показывается, пока пояс не задан); конвертация введённой даты в серверное время при известном смещении; пауза/возобновление через мастер меняет фактическую отправку (_check_due_remindersне шлёт напоминание на паузе, шлёт после возобновления).