diff --git a/docs/superpowers/specs/2026-09-02-admin-reminders-design.md b/docs/superpowers/specs/2026-09-02-admin-reminders-design.md new file mode 100644 index 0000000..05283cd --- /dev/null +++ b/docs/superpowers/specs/2026-09-02-admin-reminders-design.md @@ -0,0 +1,121 @@ +# Настраиваемые напоминалки для админов — дизайн + +Дата: 2026-09-02 + +## Проблема + +Админам нужно, чтобы бот сам напоминал о разовых или периодических событиях (например, +об окончании аренды домена/сервера): дата и время, текст, кому из админов отправить, +периодичность (разово / каждую неделю / каждый месяц / каждый год). + +## Права доступа + +Создавать, просматривать и удалять напоминания может **любой админ** (из `ADMIN_IDS` +или добавленный в рантайме, см. `_is_admin` в [bot.py](../../../bot.py)). Список +напоминаний — общий для всех админов (как общая доска задач), отдельного разграничения +«моё / чужое» нет. + +## Хранение данных + +Новая таблица SQLite `reminders` (создаётся в `Database._init_db()` в +[database.py](../../../database.py), по образцу существующих `admins`/`custom_faq`): + +```sql +CREATE TABLE IF NOT EXISTS reminders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + text TEXT NOT NULL, + remind_at TEXT NOT NULL, -- "YYYY-MM-DD HH:MM:SS", исходное (якорное) время + next_at TEXT NOT NULL, -- "YYYY-MM-DD HH:MM:SS", следующее срабатывание + recurrence TEXT NOT NULL, -- 'once' | 'weekly' | 'monthly' | 'yearly' + recipients TEXT NOT NULL, -- JSON: список vk_id, либо строка "all" + created_by INTEGER NOT NULL, + created_at TEXT NOT NULL, + active INTEGER NOT NULL DEFAULT 1 +); +``` + +`remind_at` — неизменное якорное время первого срабатывания (день месяца/год для +пересчёта периодов), `next_at` — то, что реально проверяет планировщик и что двигается +вперёд после каждой отправки. + +CRUD-методы в `Database` (по аналогии с `add_admin`/`remove_admin`/`get_admins`): +`add_reminder(...)`, `get_active_reminders()`, `get_due_reminders(now_str)`, +`update_reminder_next_at(id, next_at)`, `deactivate_reminder(id)`, `delete_reminder(id)`. + +## Планировщик + +Отдельный фоновый поток `threading.Thread(target=self._reminders_loop, daemon=True)`, +запускается в `run()` рядом с уже существующими фоновыми потоками (прогрев медиа, +рассылка). Цикл: + +``` +while True: + now = datetime.now() # локальное серверное время, как везде в проекте (без TZ-логики) + for r in db.get_due_reminders(now): + отправить текст всем recipients (self.send_message) + if r.recurrence == 'once': + db.deactivate_reminder(r.id) + else: + next_at = compute_next(r.remind_at, r.recurrence, after=now) + db.update_reminder_next_at(r.id, next_at) + time.sleep(60) +``` + +`compute_next` считает **следующее срабатывание строго после now**, отталкиваясь от +якорной даты `remind_at` (а не от предыдущего `next_at`) — так периодичность не +дрейфует из-за клампинга коротких месяцев: + +- `weekly`: `remind_at + 7*N дней`, ближайшее значение `> now`; +- `monthly`: тот же день месяца, что и в `remind_at`; если в целевом месяце такого дня + нет — последний день этого месяца (31.01 → 28/29.02 → 31.03, а не 28.03); +- `yearly`: тот же день/месяц; 29 февраля в невисокосный год → 28 февраля. + +**Пропуск при простое бота**: если бот был выключен и время срабатывания уже прошло — +при первом же тике после старта напоминание досылается один раз, после чего сразу +пересчитывается следующее *будущее* срабатывание (не досылается «пачка» пропущенных +периодов). + +## UI — мастер в админ-панели + +Новая кнопка «⏰ Напоминалки» в `get_admin_keyboard` ([keyboards.py](../../../keyboards.py)). + +**Список** (`_send_reminders_admin`): для каждого активного напоминания — id, дата +следующего срабатывания, периодичность, текст (обрезанный превью), получатели. +Плюс инструкция: «напоминалка добавить» / «напоминалка удалить ». + +**Добавление** — пошаговый мастер состояний (по образцу `ADMIN_FAQ_ADD` в +[bot.py](../../../bot.py)): + +1. `ADMIN_REMINDER_DATE` — ввод `ДД.ММ.ГГГГ ЧЧ:ММ`, валидация формата и что дата не в + прошлом; +2. `ADMIN_REMINDER_TEXT` — текст напоминания; +3. `ADMIN_REMINDER_RECIPIENTS` — нумерованный список текущих админов + пункт + «0 — все админы»; можно ввести несколько номеров через запятую (например «1,3»); +4. `ADMIN_REMINDER_PERIOD` — кнопки «Разово / Каждую неделю / Каждый месяц / Каждый год»; +5. подтверждение и запись в БД (`add_reminder`), возврат к списку напоминаний. + +Отмена мастера на любом шаге — существующим паттерном («отмена»/кнопка отмены → +`get_cancel_admin_keyboard`). + +**Удаление** — по id, текстовой командой «напоминалка удалить », как удаление +админа (`db.delete_reminder`). + +**Сознательное упрощение (YAGNI)**: редактирования существующего напоминания нет — +только добавить/удалить, как сейчас устроено управление списком админов. Чтобы +поправить напоминание — удалить и создать заново. + +## Изменяемые файлы + +- [database.py](../../../database.py) — таблица `reminders` + CRUD-методы. +- [bot.py](../../../bot.py) — мастер `ADMIN_REMINDER_*`, обработчики команд, фоновый + поток-планировщик, запуск потока в `run()`. +- [keyboards.py](../../../keyboards.py) — кнопка «⏰ Напоминалки» в админ-клавиатуре, + клавиатура выбора периодичности. + +## Тестирование + +Юнит-тесты в `tests/` (по образцу `tests/test_database.py`): +- CRUD напоминаний в БД; +- расчёт `compute_next` для всех 3 периодичностей, включая крайние случаи (31 число, + 29 февраля); +- логика «пропущенного» напоминания при простое (одна отправка + пересчёт в будущее).