## ADDED Requirements ### Requirement: Самообновление живой задачи Карточка списка и страница `/download/{id}` SHALL самообновляться, пока задача **наблюдаема**, и SHALL прекращать самообновление, как только она наблюдаемой быть перестала. Наблюдаемы все нетерминальные задачи, а из терминальных — те, которые фоновая сверка возвращает в поток сама: `failed`, `target_missing`, `orphaned`. Задача, которую с места двигает только человек (`done`, `cancelled`, `reverted`, `deleted`), наблюдаемой не является. Признак SHALL жить в домене рядом с признаком терминальности; второго перечня состояний веб-UI MUST NOT заводить. Самообновление SHALL приносить смену состояния целиком — бейдж статуса, заголовок, набор доступных действий и живые цифры, если они есть, — и MUST NOT сбрасывать клиентские фильтр, поиск и прокрутку. Смена, произошедшая без участия этого браузера (переход воркера, действие из Telegram, фоновая сверка), MUST становиться видимой тем же способом, пока задача наблюдаема: интерфейс не знает, кто изменил состояние. У одной поверхности SHALL быть **ровно один** источник самообновления. Вложенные живые регионы (прогресс качания в карточке, секция раздачи на странице) MUST NOT опрашивать сервер самостоятельно: своп корня уносит вложенный узел вместе с его поллером, поэтому два опроса на одну поверхность подменяют разметку друг друга и опрашивают одно и то же дважды. Интервал самообновления SHALL зависеть от того, несёт ли поверхность блок живых цифр качания: у поверхности с таким блоком интервал SHALL быть **строго меньше**, чем у поверхности без него. Числовые значения интервалов живут в документации проекта, не в спеке. Тик самообновления, не сумевший прочитать задачу (записи нет, хранилище отказало), SHALL отвечать успехом и фрагментом, который объясняет положение дел и **не несёт** самообновления: неуспешный ответ не заменяет разметку, поэтому поверхность осталась бы прежней, а опрос продолжался бы бесконечно. #### Scenario: Завершение качания видно без перезагрузки - **GIVEN** открыт список загрузок и в нём есть задача в `downloading` - **WHEN** qBittorrent довёл раздачу до конца и воркер увёл задачу в `recognizing` и дальше в `review` - **THEN** карточка без перезагрузки страницы показывает бейдж ревью и кнопку «Ревью →» - **AND** блок живого прогресса с неё исчезает #### Scenario: Переход, сделанный не из этого браузера - **GIVEN** открыт список загрузок и в нём есть задача в `review` - **WHEN** человек подтвердил план из Telegram и задача прошла `linking` в `done` - **THEN** карточка без перезагрузки страницы показывает бейдж `done` и действия терминальной задачи #### Scenario: Ненаблюдаемая задача не опрашивается - **WHEN** задача находится в `done`, `cancelled`, `reverted` или `deleted` - **THEN** её карточка и страница `/download/{id}` не несут самообновления, и фоновых запросов по ним не уходит #### Scenario: Задача, оживлённая сверкой, видна без перезагрузки - **GIVEN** открыт список, и в нём есть задача в `failed` (магнет не добрал метаданные за отведённое время) - **WHEN** источник ожил и фоновая сверка вернула задачу в `downloading` - **THEN** карточка без перезагрузки страницы показывает состояние качания #### Scenario: Один источник обновления на поверхность - **WHEN** отрисована карточка задачи в `downloading` или страница задачи, чья раздача сидирует - **THEN** самообновление объявлено ровно в одном месте поверхности, а вложенные живые регионы своего опроса не ведут #### Scenario: Быстрее обновляется то, где есть живые цифры - **WHEN** рядом отрисованы карточка задачи в `downloading` и карточка задачи в `review` - **THEN** объявленный интервал самообновления первой строго меньше, чем у второй #### Scenario: Тик, который не смог прочитать задачу - **GIVEN** открыта карточка наблюдаемой задачи - **WHEN** очередной тик самообновления не нашёл записи или получил отказ хранилища - **THEN** ответ успешен и несёт фрагмент с объяснением - **AND** фрагмент не несёт самообновления, поэтому опрос прекращается #### Scenario: Группа и фильтр списка пересчитываются навигацией - **GIVEN** открыт список и в нём есть задача в `downloading` - **WHEN** задача дошла до терминального состояния на глазах у смотрящего - **THEN** карточка показывает новое состояние и остаётся на своём месте в прежней группе списка - **AND** группа и фильтр пересчитываются при следующей навигации или перезагрузке — список целиком самообновлением не пересобирается ## MODIFIED Requirements ### Requirement: Отображение промежуточного состояния catched Веб-UI SHALL отображать состояние `catched` как штатную промежуточную фазу («поймано, добавляется в qBittorrent»): бейдж статуса загрузки SHALL иметь понятную человекочитаемую подпись для `catched` (а не сырое `catched`), а загрузка в `catched` SHALL относиться к **активной** группе списка. Пока отображаемое имя ещё не выведено (в `catched` `download.display_name` пуст), заголовок загрузки SHALL деградировать по существующему фолбеку (распознанное название или усечённый источник) — см. «Заголовок загрузки из имени раздачи». Секция раздачи/живого прогресса для `catched` SHALL корректно отсутствовать (раздачи в qBittorrent ещё нет), не создавая ошибок отображения. Самообновление карточки и страницы в `catched` — частный случай требования «Самообновление живой задачи»: `catched` нетерминален, поэтому интерфейс подхватывает переход в `downloading` (бейдж, выведенное имя, появившийся живой прогресс) без перезагрузки страницы. Отдельного правила самообновления для этой фазы веб-UI MUST NOT иметь: фаза перестала быть единственной, где интерфейс обновляется сам. #### Scenario: Бейдж и группа для catched - **WHEN** загрузка находится в состоянии `catched` - **THEN** её бейдж статуса имеет человекочитаемую подпись для `catched` - **AND** загрузка попадает в активную группу списка #### Scenario: Заголовок catched без имени - **GIVEN** загрузка в `catched` с пустым `download.display_name` - **WHEN** рендерится карточка/страница загрузки - **THEN** заголовок берётся из фолбека (распознанное название или усечённый источник), без ошибок отображения - **AND** секция раздачи/живого прогресса не показывается (раздачи ещё нет) #### Scenario: Самообновление при переходе в downloading - **GIVEN** открытая карточка загрузки в `catched` - **WHEN** worker перевёл загрузку в `downloading` - **THEN** интерфейс без перезагрузки показывает состояние `downloading` (бейдж, имя, живой прогресс) - **AND** самообновление продолжается, потому что задача осталась наблюдаемой ### Requirement: Обзор жизненного цикла в карточке списка Карточка загрузки в списке SHALL показывать обзорную мета-строку для решения о судьбе раздачи: метку `ID:` перед копируемым идентификатором загрузки, дату добавления раздачи (всегда), размер раздачи и рейтинг отдачи. Контекст загрузки MUST NOT показываться в карточке списка — он доступен на странице `/download/{id}`. Дата добавления SHALL показываться всегда как абсолютная дата и относительная давность (например «`2026-06-30 · 5 дней назад`»); источником SHALL быть время добавления раздачи в источник (`source_added_at`, qBittorrent `added_on`) с фолбэком на время создания загрузки (`created_at`), согласованным с порядком списка. Рейтинг отдачи SHALL браться из живого снимка телеметрии; если торрента нет в снимке (источник ушёл из qBittorrent), рейтинг SHALL отображаться прочерком «—». Размер раздачи SHALL браться из живого снимка (общий размер торрента), а при отсутствии торрента в снимке — из суммарного размера разложенных файлов загрузки; если неизвестно ни то, ни другое — прочерк «—». Карточка, пришедшая **самообновлением**, SHALL показывать те же значения, что и карточка в полном рендере списка: фоновое обновление MUST NOT подменять известное значение прочерком. #### Scenario: Метка идентификатора - **WHEN** рендерится карточка загрузки в списке - **THEN** перед значением `download.id` показана метка «ID:», а кнопка копирования копирует именно `download.id` #### Scenario: Дата добавления показана всегда - **WHEN** рендерится любая карточка списка - **THEN** в ней показана дата добавления раздачи абсолютной датой и относительной давностью - **AND** если `source_added_at` неизвестно, используется `created_at` #### Scenario: Рейтинг из живого снимка - **WHEN** торрент загрузки присутствует в живом снимке - **THEN** в карточке показан его рейтинг отдачи - **AND** если торрента в снимке нет, рейтинг показан прочерком «—» #### Scenario: Размер с фолбэком на разложенные файлы - **WHEN** торрент загрузки присутствует в живом снимке - **THEN** размер раздачи в карточке берётся из общего размера торрента - **AND** если торрента в снимке нет, но у загрузки есть разложенные файлы — размер берётся из суммарного размера этих файлов #### Scenario: Самообновление не теряет размер - **GIVEN** торрента нет в живом снимке, а файлы задачи разложены - **WHEN** карточка пришла самообновлением, а не полным рендером списка - **THEN** размер показан по тому же фолбэку, а не прочерком «—» #### Scenario: Контекст не в карточке - **WHEN** у загрузки есть переданный контекст - **THEN** он не показывается в карточке списка, но доступен на странице `/download/{id}`