## 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/задаче