Пока пусто — следующий блок: Блок 2 «AI-ввод».
Блок 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-Control5 мин, 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-middlewareBotиз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); ключи Redisrl: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); тикер:TelegramRetryAfter→lease_until = now + retry_after(реальный 429 и «занято» лимитера),Forbidden→failed+ чат неактивен;/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(defaultclosed),ALLOWED_TELEGRAM_IDS;AccessMiddlewareотвечает постороннему в личке егоtelegram_idне чаще раза в час. - Обработка блокировки/разблокировки бота (
my_chat_member→users.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 «Фундамент»: пустой бот живёт на 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.