## 1. Состояние catched

- [x] 1.1 Добавить `StateCatched State = "catched"` в `internal/store/download.go`;
  убедиться, что оно НЕ в `terminalStates` (нетерминальное = активное)
- [x] 1.2 Включить `StateCatched` в `statesInGroup(GroupActive)`; проверить
  поиск/фильтры списка
- [x] 1.3 Тест: `catched` активно для дедупа (`CreateDownloadIfNoActive` держит
  инвариант ≤1 активная на infohash с участием `catched`)

## 2. Быстрый приём (ingest)

- [x] 2.1 `Ingest`: писать `download` в `StateCatched` с пустым `DisplayName`;
  убрать из синхронного пути `namer.DeriveName` и `qbt.Add`; вернуть `Result`
  сразу после записи
- [x] 2.2 Убрать из `ingest.Service` ставшие ненужными зависимости/код:
  `Namer`, `QBittorrent`, `notifyFailed`/`SetFailureNotifier`, `qbit_add`-путь
  падения add (падение теперь у worker'а — п.4)
- [x] 2.3 Обновить тесты ingest: приём создаёт `catched`, не зовёт qBit/namer;
  дедуп по активной (в т.ч. `catched`); синтез контекста сохраняется как прежде
- [x] 2.4 Транспорты (`httpapi`, `tgbot`): ответ по `Result` корректен для
  `catched` (без правок API); Telegram-текст не показывает сырое `catched`
  (дружелюбная формулировка «принято/добавляется»)

## 3. Асинхронный шаг добавления (worker)

- [x] 3.1 Внедрить в worker зависимости `Namer` и `qbt.Add`; добавить в
  интерфейс `worker.Store` новый метод записи имени (`SetDisplayName`)
- [x] 3.2 Шаг в поллинг-цикле, сеть ВНЕ `w.mu`: под замком снять
  `ListDownloadsByState(StateCatched)`; вне замка для каждой — разобрать
  `d.SourceRef` (`magnet.Parse`) для dn-hint, вывести имя (`namer.DeriveName`),
  вызвать `qbt.Add(urls=d.SourceRef, category, savepath, rename)`; снова под
  замком — ре-валидировать `state==catched` и записать переход `catched →
  downloading` + `display_name`
- [x] 3.3 Сбой `add`: оставить в `catched` (повтор на следующем тике), не уводить
  в терминальное по единичному сбою; логировать (ext.* уже логирует клиент)
- [x] 3.4 Тесты worker: `catched → downloading` при успехе (rename передан,
  display_name сохранён, сетевые вызовы вне замка); транзиентный сбой оставляет
  `catched` и повторяет; отмена во время add (ре-валидация отбрасывает переход)

## 4. Предохранитель catch_timeout

- [x] 4.1 Конфиг `catch_timeout` (+ дефолт, валидация на старте) — по образцу
  `magnet_timeout`; документация конфига
- [x] 4.2 В поллинг-цикле: `catched` старше `catch_timeout` (от создания) →
  `failed` (`error_code` `qbit_add`) + уведомление автора
- [x] 4.3 Тест: `catched` за таймаутом → `failed` + notify

## 5. Исключение catched из проверок пропажи

- [x] 5.1 Убедиться (и закрепить тестом), что поллинг активных и сверка
  рассинхрона (`state-reconciliation`) не трактуют `catched` как пропажу/
  orphaned
- [x] 5.2 Тест на гонку discover ↔ шаг добавления: дубль по infohash не заводится

## 6. Веб-UI

- [x] 6.1 Человекочитаемая подпись бейджа для `catched` (`badgeLabel`), чтобы не
  показывать сырое `catched`; `catched` в активной группе списка
- [x] 6.2 Заголовок при пустом `display_name` (фолбек) и отсутствие секции
  раздачи/живого прогресса для `catched` без ошибок
- [x] 6.3 Самозавершающийся htmx-поллинг карточки/страницы в `catched`: пока
  `catched` — опрашивает фрагмент; по переходе в `downloading` показывает
  прогресс/имя без перезагрузки и завершает поллинг фазы (правка live-рендера
  для не-`downloading` активной фазы)
- [x] 6.4 Тест httpapi/шаблонов: карточка/страница `catched` рендерится;
  фрагмент-поллинг отдаётся для `catched` и завершается после перехода

## 7. Проверка

- [x] 7.1 `task test` и `task lint` зелёные
- [x] 7.2 `openspec validate fast-catch-ingest --strict` проходит
- [x] 7.3 Прогон вручную/через verify: приём отвечает быстро (без LLM в пути),
  загрузка проходит `catched → downloading`, карточка обновляется без
  перезагрузки
- [x] 7.4 Рантбук отката: снять застрявшие `catched` (`UPDATE download SET
  state='failed', error_code='qbit_add' WHERE state='catched'`) — зафиксировать
  в описании change/задаче
