- условие самообновления — доменный предикат store.State.IsObservable() вместо фазы catched; один поллер на поверхность, интервалы 5 с и 15 с - отказ тика отвечает 200 и самозавершающимся фрагментом с корневым id цели вместо 404/500, который htmx не свопит - заведён ADR-2026-08-10-observability-is-not-terminality, переписан раздел «Живой поллинг» в конвенции веб-UI
15 KiB
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}