# notifications Specification ## Purpose Уведомление автора загрузки о значимых событиях: падения (`failed`/`stuck`, включая приёмный `qbit_add` мимо поллинга) с дебаунсом повторов, приглашение в `review` и готовность, рассинхрон (`orphaned`/`target_missing`). Единое место доставки пингов, над которым транспорты (Telegram и др.) — тонкие адаптеры. ## Requirements ### Requirement: Уведомление о падении загрузки Любой переход загрузки в `failed`/`stuck` система SHALL сопровождать уведомлением автора загрузки через настроенный механизм (`notifier`), чтобы падение не оставалось незамеченным. Это SHALL включать приёмное падение `qbit_add` (не удалось добавить раздачу в qBittorrent), которое идёт мимо поллинг-цикла worker. #### Scenario: Уведомление при падении приёма - **GIVEN** приём загрузки, где добавление в qBittorrent не удалось - **WHEN** загрузка помечается `failed` с `error_code` `qbit_add` - **THEN** автор загрузки получает уведомление о падении ### Requirement: Дебаунс повторных падений Повторные падения одной задачи в пределах окна дебаунса система SHALL уведомлять лишь один раз, чтобы мерцающий stalled-торрент (`stuck` ↔ `downloading`) не спамил автора. #### Scenario: Мерцающий stalled не спамит - **GIVEN** задача, многократно переходящая `stuck` ↔ `downloading` в пределах окна дебаунса - **WHEN** происходят повторные падения - **THEN** уведомление отправляется один раз за окно ### Requirement: Пинг о входе в review и готовности При переходе загрузки в `review` система SHALL пинговать автора (сообщение в Telegram / бейдж в вебе) — пользователя зовут, а не он опрашивает. После успешного применения (готовность) система SHALL показывать, что создано. #### Scenario: Пинг при входе в review - **GIVEN** загрузка переходит в `review` - **WHEN** происходит переход - **THEN** автор получает пинг с приглашением подтвердить раскладку ### Requirement: Уведомление о рассинхроне При переходе задачи в `orphaned` или `target_missing` система SHALL уведомлять автора загрузки через настроенный механизм уведомлений (`notifier`), чтобы рассинхрон не оставался незамеченным. #### Scenario: Уведомление при потере источника - **WHEN** задача переходит в `orphaned` - **THEN** автор загрузки получает уведомление о рассинхроне ### Requirement: Download id в уведомлениях моноширинным для tap-to-copy Уведомления, содержащие download id, система SHALL отображать id моноширинным блоком (в Telegram — ``), чтобы в клиенте работало tap-to-copy: id можно скопировать одним касанием для перехода на `/download/{id}` или диагностики по логам. Визуальный префикс (`#` / `download_id=`) SHALL оставаться вне моноширинного блока, чтобы копировался чистый id без лишних символов. #### Scenario: Id карточки review копируется одним касанием - **GIVEN** уведомление о входе загрузки в `review` с download id - **WHEN** бот рендерит сообщение - **THEN** download id выводится моноширинным блоком (tap-to-copy), а префикс `#` остаётся обычным текстом вне блока #### Scenario: Id в сообщении об ошибке копируется - **GIVEN** сообщение об отказе операции с `download_id` - **WHEN** бот рендерит сообщение - **THEN** значение download id выводится моноширинным блоком для копирования ### Requirement: Экранирование внешнего текста при форматированных уведомлениях При включённом форматировании исходящих сообщений (parse mode) система MUST экранировать все внешние/недоверенные фрагменты перед вставкой в размеченное сообщение: display name, распознанное название, источник/контекст, целевой путь, причины распознавания, provider, код и текст ошибки. Это защищает от того, что спецсимволы разметки сломают сообщение или что разметка будет инъектирована из недоверенного источника (инвариант «выход LLM недоверенный»). Секреты (токены/ключи/пароли) MUST NOT попадать в текст уведомлений и логи. #### Scenario: Спецсимволы в названии не ломают разметку - **GIVEN** уведомление, где display name или распознанное название содержит символы разметки (`<`, `>`, `&`) - **WHEN** бот рендерит форматированное сообщение - **THEN** эти символы экранируются, сообщение доставляется корректно, а разметка из недоверенного текста не интерпретируется #### Scenario: Внешний путь и причины экранируются - **GIVEN** уведомление с целевым путём плана и причинами распознавания - **WHEN** бот рендерит форматированное сообщение - **THEN** символы разметки в пути и причинах экранируются перед вставкой