Family Assist

Changelog

Формат — Keep a Changelog, версии — SemVer. До публичного запуска мажор 0.

Unreleasedещё не выпущено

Пока пусто — следующий блок: Блок 2 «AI-ввод».

0.2.0

Блок 1 «Ядро без AI»: полноценный планировщик без единого вызова LLM — онбординг, разделы, задачи, календарь с повторами, напоминания и доставка с лимитером и тихими часами; плюс страница changelog на своём домене. Шаг 1.1 на сервере с 2026-09-03, шаги 1.2–1.6 и страница — одним деплоем 2026-09-04; gate шагов 1.2–1.6 на сервере — RUNBOOK §6–§10.


Страница changelog (план): релизы видны в браузере — без второго приложения и без переноса DNS.

Added

  • GET / и GET /changelog процесса bot: CHANGELOG.md → HTML (app/changelog.py, markdown-it-py: CommonMark, таблицы, зачёркивание; сырой HTML экранируется, относительные ссылки — текстом). Макет из структуры Keep a Changelog: шапка с навигацией по релизам, карточка на релиз с версией и датой по-русски (#v0.1.0 / #unreleased), цветные теги Added/Changed/Schema/…, тёмная тема, мобильная вёрстка, noindex; ETag и 304, Cache-Control 5 мин, CSP без скриптов; CHANGELOG.md копируется в образ.
  • Сканер find_sensitive (токены, URL с паролями, IP, telegram_id, телефоны, имена ботов, e-mail, хосты Timeweb, *_TOKEN=…, ключи) — в tests/test_changelog.py и на старте процесса: находка = страница скрыта целиком (503), в /health"changelog": "withheld".
  • RUNBOOK §1.9: домен family.0xtechdir.ru — A-запись в AdminVPS, привязка в App Platform, Let's Encrypt.

Шаг 1.6 «Доставка» (план): лимитер отправки в Redis, общий для bot и scheduler, 429 и retry в одном месте, тихие часы, реакция на блокировку бота. Реализован 2026-09-04, gate — после деплоя вместе с 1.2–1.5 (RUNBOOK §10).

Added

  • delivery/limiter.py: Limiter (Lua по времени Redis: окно чата — GCRA с резервированием слота, 1 msg/sec и burst 3, группа chat_id < 0 — 20 msg/min; окно бота — корзины по секундам, 25 msg/sec; штраф чату после 429; счётчики) и RateLimitMiddleware — session-middleware Bot из create_bot(settings, health, redis, keys): ожидание слота, TelegramRetryAfter без вызова Bot API при ожидании дольше 10 с, повтор после 429 с retry_after ≤ 10 с, один повтор после ошибки соединения и 5xx, таймаут запроса без повтора; Redis недоступен — fail-open. Таймаут send_text/edit_text — 10 с.
  • Тихие часы: core/quiet_hours.py (is_quiet, set_quiet_hours, clear_quiet_hours; начало 20:00–01:00, конец 05:00–10:00, диапазон через полночь), /settings → строка «Тихие часы: …» и экран «🌙 Тихие часы» (sp:qs:<час>, sp:qe:<час>, sp:qoff:0); тикер шлёт напоминания без звука (disable_notification), лог reminder.sent(silent=True), TickStats.silent.
  • core/users.mark_chat_inactive: TelegramForbiddenError при отправке напоминания → chats.is_active=false, users.is_blocked=true (лог chat.deactivated); остальные строки чата тикер гасит как chat_inactive.
  • errors-роутер: ветка TelegramRetryAfter — лог handler.throttled без ответа.
  • /health: блок delivery (waited_total, busy_total, retry_after_total, retries_total); ключи Redis rl:global, rl:chat:<id>, delivery:<field>.
  • Тесты: Limiter с фиксированным now_ms (окна чата и группы, корзины бота, штраф, fail-open), middleware через FakeSession с заглушкой sleep (общее окно двух Bot, busy, 429, сеть/5xx/таймаут), тихие часы (core, тикер, /settings), mark_chat_inactive, handler.throttled, quiet_kb.

Changed

  • create_bot(settings, health, redis, keys); тикер: TelegramRetryAfterlease_until = now + retry_after (реальный 429 и «занято» лимитера), Forbiddenfailed + чат неактивен; /help и описание /settings в меню упоминают тихие часы.

Fixed

  • Недельная повестка подписывала каждый день «Сегодня, …» (week_screen звал day_title(day, today=day)); теперь week_day_title — «Сегодня» только у сегодняшнего, остальные «Сб, 5 сен». Найдено e2e-прогоном локального бота 2026-09-04.

Шаг 1.5 «Напоминания» (план): напоминания приходят вовремя и по одному разу без единого вызова LLM. Реализован 2026-09-04, gate — после деплоя вместе с 1.2–1.4 (RUNBOOK §9).

Added

  • Напоминания о событиях: за reminder_min минут до начала (по умолчанию 15, у всё-дневных — утром в день события), пикер «🔔 Напоминание» в карточке (нет · в момент начала · 5 · 15 · 30 мин · 1 ч · 2 ч · 1 день; для всё-дневных — утром · накануне в 19:00); строка «Напоминание: …» в карточке. Напоминания о задачах со сроком: за default_reminder_min до срока со временем, утром в день срока без времени; строка «Напомню: …» в карточке задачи.
  • Сообщение-напоминание: «🔔 Через 15 мин — …» с «📆 Открыть» / «✅ Выполнено» и «⏰ +10 мин», «⏰ +1 ч» (отложить — rm:snooze); несколько напоминаний в один чат за тик — одно сообщение «🔔 Напоминания» с кнопкой на строку.
  • /settings: строка «Напоминания: …», пикеры «🔔 За сколько напоминать» (в момент · 5 · 15 · 30 мин · 1 ч) и «🌅 Утреннее время» (06:00–11:00); задачи перепланируются, события остаются на своём смещении.
  • core/reminders.py: dedup_key = цель + момент, планирование sync_event/sync_task/ sync_all в горизонте 14 дней, оживление отменённого ключа, захват claim_due (FOR UPDATE SKIP LOCKED, аренда 5 мин, attempts + 1), проверка цели resolve (stale/expired/chat_inactive), retry с backoff 1/3 мин и retry_after, failed после трёх попыток, snooze, настройки; storage/repositories/reminders.py.
  • scheduler: джобы reminders_tick (раз в минуту; захват коммитится до отправки, статусы — вторым коммитом) и reminders_sync (02:10 UTC и при старте); процесс держит свой Bot; алерт админу о failed не чаще раза в 10 минут; метрики тикера в Redis и /health (reminders.last_tick_at, last_tick_age_sec, last_late_s, sent_total, failed_total).
  • bot/watchdog.py: алерт админу, когда heartbeat планировщика старше 5 минут (grace 5 мин после старта, один алерт в час), «✅ Планировщик снова работает».
  • Тесты: core/reminders (планирование, мутации целей, оживление ключа, захват и аренда, retry, resolve, snooze, ночная синхронизация), тикер через FakeSession (отправка и статусы, группа, retry/429/Forbidden/failed с алертом, stale/expired/повторный захват), поток в чате (пикер события, snooze, /settings, карточка задачи), watchdog.

Changed

  • create_event подставляет reminder_min из настроек; reschedule_event, set_recurrence, set_until, move_occurrence, cancel_occurrence, restore_occurrence, delete_event, set_reminder, set_due, complete_task, reopen_task, delete_task гасят и перепланируют напоминания цели; move_occurrence/cancel_occurrence/restore_occurrence/ set_due/reopen_task принимают now.
  • Карточка события: ряд «🔔 Напоминание · 🗑 Удалить»; /settings — четыре кнопки (SettingsPickCb для пикеров, формат st:tz прежний); /help, fallback, «Готово» и «С возвращением» говорят о напоминаниях; описание /settings в меню — «Часовой пояс и напоминания»; FakeSession.fail_next принимает kwargs (retry_after).

Schema

  • 0007_events_reminder_min: events.reminder_min integer (backfill 15 / −540), ix_reminders_target_pending (target_type, target_id) where status = 'pending'.

Шаг 1.4 «Календарь» (план): события, серии и повестка без единого вызова LLM. Реализован 2026-09-04, gate — после деплоя вместе с 1.2 и 1.3 (RUNBOOK §8).

Added

  • /event <что и когда> — событие одной строкой: «завтра 15:00 Зубной», «пт 10-12 Встреча», «каждый вторник 19:00 Тренировка», «Зубной у Маши, завтра в 15»; без даты бот спрашивает «Когда?», /event без текста и «➕ Событие» — «Что и когда?». Раздел — активный или personal, пояс — пользователя.
  • /agenda [когда] — день (сегодня по умолчанию; «завтра», «пт», «12.09») и неделя («неделя», кнопка «📆 Неделя»): «весь день» первыми, затем по времени, кнопка на событие, навигация по дням и неделям, фильтр по активному разделу в SQL с заголовком режима, переключатель области «📂 Раздел: …» (тот же active_section_id).
  • Карточка экземпляра: у серии — «🕒 Перенести это» (текстом, только этот раз) и «❌ Отменить это» / «↩️ Вернуть»; у всех — «📅 Когда» (у серии — вся серия с этого момента, правило пересчитывается от нового дня), «🔁 Повтор» (нет · каждый день · по будням · каждую неделю · каждые 2 недели · каждый месяц · каждый год, «📅 До даты…» текстом), раздел (лог-сигнал event.section_changed), название, место, заметка, мягкое удаление с подтверждением.
  • core/dates.py: When, parse_when, split_when (диапазон «10-12» / «с 10 до 12», окончание «до 12», длительность «на 2 часа», повтор текстом, «до 31.12»), format_span, RRULE-пресеты (rrule_for, preset_of, describe_rrule, validate_rrule) и expand_rrule поверх python-dateutil с локальной арифметикой в поясе события; core/lang.py (plural); core/calendar.py и storage/repositories/events.py.
  • Джоб occurrences в scheduler (02:00 UTC и при старте): материализация всех живых серий до горизонта 90 дней, идемпотентно по слоту правила.
  • Тесты: грамматика «когда» и RRULE (в том числе DST на Europe/Berlin), core/calendar (экземпляры, повестка в SQL, перенос и отмена, правка серии, генератор), /agenda и /event через Dispatcher, джоб в расписании.

Changed

  • /agenda, /event в меню команд, в командах группы, в /help, fallback, приветствии и «Готово»; python-dateutil — в основных зависимостях, types-python-dateutil — в dev.
  • scope_filter (core/sections.py) — общий фильтр области для задач и событий; keyboards/common.scope_rows — общий переключатель области; Event.eager_defaults.

Schema

  • 0006_event_occurrences_original_starts_at: event_occurrences.original_starts_at timestamptz not null (слот правила, RECURRENCE-ID), unique (event_id, original_starts_at) вместо (event_id, starts_at).

Шаг 1.3 «Задачи» (план): планировщик задач без единого вызова LLM. Реализован 2026-09-04, gate — после деплоя вместе с 1.2 (RUNBOOK §7).

Added

  • /tasks — список задач в активном разделе: заголовок называет режим («все разделы» / «💼 Работа (с подразделами)»), группы «Просрочено / Сегодня / Завтра / На неделе / Позже / Без срока», по 10 на страницу, у строки кнопки «карточка» и ; вкладка выполненных с ↩️; переключатель области «📂 Раздел: …» (тот же active_section_id, что в /sections).
  • /task <текст> — задача сразу (первая строка — название, остальные — заметка), раздел — активный или personal; /task без текста и «➕ Новая задача» — ввод текстом.
  • Карточка задачи: срок кнопками (сегодня / завтра / послезавтра / через неделю / без срока) и текстом по грамматике core/dates.py («завтра», «пт», «в пятницу 15:00», «через 3 дня», «12.09», «12 сентября», «в 18:00», «завтра вечером», «-»), приоритет 🔴/обычный/🔵, раздел (перенос — лог-сигнал task.section_changed для автоматики), заметка, название, мягкое удаление с подтверждением.
  • core/dates.py (Due, parse_due, due_to_utc/due_from_utc, format_due, bucket_of) и core/tasks.py; storage/repositories/tasks.py с фильтром области в SQL.
  • Тесты: 40+ случаев грамматики, core/tasks (раздел всегда заполнен, фильтр в SQL, статусы, срок, приоритет, сигнал), /tasks через Dispatcher (создание, выполнение, срок, пикеры, область, пагинация, удаление, чужие и протухшие кнопки, состояния ввода).

Changed

  • /tasks, /task в меню команд, в командах группы, в /help, fallback, приветствии и «Готово»; edit_or_send возвращает bool (повторный тап той же вкладки — всплывашка).
  • require_aware и zone в core/timezones.py публичные; Task.eager_defaults.

Schema

  • 0005_tasks_due_has_time: tasks.due_has_time boolean not null default false — срок «до конца дня» (due_at — локальная полночь) против точного момента.

Шаг 1.2 «Разделы» (план): два уровня разделов и активный раздел. Реализован 2026-09-04, gate — после деплоя (RUNBOOK §6).

Added

  • /sections: дерево из трёх системных корней и пользовательских подразделов, переключение активного раздела (NULL = все разделы, корень = он и его подразделы), создание, переименование, значок из палитры 24 emoji, порядок ⬆⬇, удаление с подтверждением и переносом задач и событий в родителя. Лимит 8 подразделов на корень, названия ≤ 32 символов.
  • Системные корни (personal, work, family) неизменяемы целиком: правит только код и миграция (SectionNotEditable на любую попытку, в том числе крафтовым callback).
  • ActiveOnlyMiddleware — шлюз ACTIVE на роутеры команд: не-ACTIVE получает экран онбординга или всплывашку, хендлер не вызывается; проверка start_state ушла из хендлеров.
  • Контракт для 1.3/1.4 и блока 2 (STACK §3, §5.2): resolve_target_section, active_scope_ids для фильтра выдачи в SQL, формат дерева разделов в промпте и правило «неуверенность → корень».

Changed

  • Ревизия 0004: sections.parent_id, user_settings.active_section_id (ON DELETE SET NULL), партиал-индекс «один is_default на владельца» с защитным UPDATE перед созданием.
  • app/bot/texts и app/bot/keyboards стали пакетами, общие экраны и хелперы — app/bot/screens.py; /sections в меню команд и в списке команд группы; кнопка «📂 Разделы» на экране /settings.

Шаг 1.1 «Онбординг» (план): первый реальный контакт с ботом плюс инфраструктура хендлеров с БД. Задеплоен и проверен на сервере 2026-09-03 (RUNBOOK §5).

Added

  • Онбординг: согласие на обработку данных с версией текста (CONSENT_VERSION, повторное согласие при смене версии без повторного онбординга), выбор часового пояса кнопками (11 зон РФ) и текстом (IANA-ключ или город из CITY_TO_TZ, русские подписи TZ_TO_CITY), три раздела по умолчанию (personal, work, family), user_settings с серверными дефолтами.
  • /settings — смена часового пояса кнопкой или текстом; /help; меню команд (setMyCommands для личных чатов, with_retries).
  • Закрытый режим: BOT_ACCESS=closed|open (default closed), ALLOWED_TELEGRAM_IDS; AccessMiddleware отвечает постороннему в личке его telegram_id не чаще раза в час.
  • Обработка блокировки/разблокировки бота (my_chat_memberusers.is_blocked, chats.is_active) и добавления в группу («работаю только в личке» раз в час на чат, бот остаётся).
  • Fallback-ответ на любое сообщение в личке и на неизвестные кнопки (cb_unknown) — бот не молчит; errors-роутер: LockError, «query is too old», общая ветка с ответом.
  • DbSessionMiddleware (сессия и транзакция на хендлер, user в DI), LogContextMiddleware (update_id/telegram_id/chat_id в structlog), RedisEventIsolation с LOCK_KWARGS, CallbackAnswerMiddleware.
  • storage/repositories/ (chats, users, user_settings, sections) и сервисы core/ (users, consent, sections, timezones); delivery.edit_text с best_effort; tests/test_layering.py::test_no_direct_sending.
  • Тесты хендлеров без сети (tests/telegram_fake.py, фикстуры tg_bot, dp) и тесты core с БД в savepoint-режиме (db_conn, session_factory, session).

Changed

  • create_dispatcher(settings, redis, keys, session_factory, *, lock_kwargs=None); роутеры собираются функциями create_router(); runner передаёт session_factory и вызывает setup_commands после delete_webhook.
  • tzdata — в основных зависимостях (набор ключей dev = prod).
  • Правило 5 CLAUDE.md распространено на правку сообщений; служебные вызовы (answerCallbackQuery, setMyCommands, deleteWebhook, getMe) — исключение.
  • Термин «раздел» (вместо «контекст» из spec.md) закреплён в CLAUDE.md, STACK, ROADMAP.

Removed

  • Заглушка /start Блока 0 (app/bot/routers/start.py); /ping переехал в service.

Schema

  • 0003_users_consent: users.consent_version text null, users.consented_at timestamptz null.
0.1.0

Блок 0 «Фундамент»: пустой бот живёт на Timeweb App Platform, git push в main его обновляет, бэкап восстановлен в локальный контейнер.

Added

  • Блок 0 «Фундамент»: каркас app/{bot,core,ai,storage,scheduler,delivery}, конфиг из ENV (TELEGRAM_API_BASE, TELEGRAM_PROXY, libpq-DATABASE_URL), structlog.
  • /health в обоих процессах; HEALTHCHECK в Dockerfile (единственный механизм при Compose-деплое на Timeweb App Platform).
  • Дедупликация update_id через Redis (fail-open); состояние polling в /health.
  • Миграции на старте bot под pg_advisory_xact_lock; scheduler ждёт alembic head.
  • Схема данных: все таблицы STACK §3 с заделами V2, vector первой ревизией, HNSW.
  • Ночной pg_dump → S3 Timeweb из scheduler, heartbeat в Redis, scripts/restore_check.py для проверки восстановления в локальный контейнер.
  • docker-compose.yml для App Platform, Dockerfile (python 3.14 + pg_dump 18), GitLab CI (lint/test/build), локальная инфраструктура docker/compose.dev.yml.