Приём (Ingest) стал быстрым: синхронно только парс magnet, синтез контекста из полей ссылки, атомарный дедуп и запись загрузки в новое состояние `catched` — ответ клиенту сразу. Медленный вывод имени (LLM) и добавление в qBittorrent вынесены в асинхронный шаг машины состояний, который двигает worker. - store: состояние `catched` (нетерминальное, активная группа); атомарный переход PromoteCatched (catched → downloading + display_name) с гардом state='catched' (ре-валидация после сетевых вызовов вне блокировки) - ingest: убраны namer/qbt из пути приёма; пишем `catched`, отвечаем сразу - worker.processCatched: вне w.mu выводит имя и qbt.Add, под w.mu — короткий переход; сбой add оставляет catched (ретрай тиком); предохранитель catch_timeout → failed(qbit_add)+notify; catched исключён из проверок пропажи - config: worker.catch_timeout (дефолт 10m) - веб-UI: бейдж catched, активная группа, самозавершающийся htmx-поллинг карточки/страницы до перехода в downloading; Telegram-текст без сырого catched - OpenSpec: дельты ingest/download-tracking/web-ui влиты в спеки, change заархивирован Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
78 lines
5.6 KiB
Markdown
78 lines
5.6 KiB
Markdown
## 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/задаче
|