# Напоминалки: пауза и часовой пояс — дизайн Дата: 2026-09-05 ## Проблема Фича «Напоминалки» (см. [2026-09-02-admin-reminders-design.md](2026-09-02-admin-reminders-design.md)) уже работает, но не хватает двух вещей: 1. Нельзя временно отключить напоминание без удаления (например, аренду продлили раньше срока, но напоминание пригодится в следующий раз). 2. Дата/время вводятся и хранятся в серверном времени без учёта того, что настраивающий напоминание админ может физически находиться в другом часовом поясе — время срабатывания может «уехать» на несколько часов от того, что админ имел в виду. ## Часовой пояс ### Хранение Новая таблица SQLite `admin_settings`: ```sql 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`) ```python def server_utc_offset_hours() -> float: """Собственное смещение сервера от UTC в часах (для конвертации между часовым поясом админа и серверным временем хранения).""" def convert_time(dt: datetime, from_offset: float, to_offset: float) -> datetime: """Переводит наивный datetime из одного часового пояса (смещение от UTC в часах) в другой.""" ``` ## Пауза ### Хранение В таблицу `reminders` добавляется колонка: ```sql 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](../../../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](../../../reminders.py) — `server_utc_offset_hours()`, `convert_time()`. - [bot.py](../../../bot.py) — гейт часового пояса перед разделом «Напоминалки», состояние `ADMIN_REMINDER_TZ_SETUP`, конвертация в `ADMIN_REMINDER_DATE`, пересчёт для отображения в `_send_reminders_admin`, команда `часовой пояс <±N>`, кнопка и состояние `ADMIN_REMINDER_PAUSE_PICK`. - [keyboards.py](../../../keyboards.py) — кнопка «⏸/▶ Пауза» в `get_reminders_admin_keyboard`. ## Тестирование - `tests/test_reminders.py`: `server_utc_offset_hours` (проверка типа/разумного диапазона −12..14), `convert_time` для нескольких смещений и границы суток. - `tests/test_database.py`: CRUD `admin_settings`; `paused` — установка, фильтрация в `get_due_reminders`, попадание в `get_active_reminders`. - `tests/test_flow.py`: гейт часового пояса при первом входе в раздел (список не показывается, пока пояс не задан); конвертация введённой даты в серверное время при известном смещении; пауза/возобновление через мастер меняет фактическую отправку (`_check_due_reminders` не шлёт напоминание на паузе, шлёт после возобновления).