Веб-UI: обзор жизненного цикла в карточке загрузки

Карточка списка на главной теперь даёт краткий обзор «от загрузки до
решения об удалении»: метка «ID:» перед идентификатором, дата добавления
(абсолютная + относительная, всегда), размер раздачи и рейтинг отдачи.
Спойлер контекста убран — контекст смотрят на /download/{id}.

Данные:
- рейтинг и общий размер — из живого снимка воркера (qbt total_size →
  worker.Live.TotalSize); размер доступен для любой раздачи в снимке;
- размер-фолбэк, когда торрента нет в qBittorrent (orphaned) — сумма
  размеров разложенных файлов: новая колонка file_link.size, layouter
  пишет размер при линковке, ридер LayoutSizeByDownload суммирует по
  странице одним запросом (дедуп по dst_path);
- дата — source_added_at → фолбэк created_at, показ в TZ сервера.

handleIndex читает снимок для всех карточек (map-lookup), рейтинг/размер
статичны на рендере (без поллинга). Миграция 0007, ER-схема обновлена.
Change download-card-lifecycle-overview влит в спеки и заархивирован.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
av
2026-07-04 10:34:45 +03:00
co-authored by Claude Opus 4.8
parent 9ed732e49e
commit bb245a90a3
27 changed files with 845 additions and 49 deletions
+21
View File
@@ -90,3 +90,24 @@ SHALL создавать. Безопасность SHALL держаться на
- **WHEN** выполняется раскладка файла
- **THEN** файл копируется, ссылка получает статус `copied`, в лог пишется предупреждение
### Requirement: Запись размера разложенного файла
При линковке файла раскладка SHALL сохранять его размер в байтах вместе с
записью о созданной ссылке (`file_link`). Сохранённый размер SHALL позволять
вычислить суммарный размер разложенных файлов загрузки — он служит фолбэком
размера раздачи, когда торрента нет в qBittorrent (например, состояние
`orphaned`, где файлы библиотеки — последняя копия данных). Запись размера MUST
NOT влиять на инвариант неприкосновенности источника (по-прежнему только
`mkdir`/`link`/копия в цель).
#### Scenario: Размер сохраняется при линковке
- **WHEN** раскладка создаёт хардлинк (или копию при copy-fallback) файла
- **THEN** размер этого файла в байтах сохраняется в записи ссылки `file_link`
#### Scenario: Суммарный размер доступен без торрента
- **WHEN** у загрузки есть разложенные файлы, а её торрента нет в qBittorrent
- **THEN** суммарный размер разложенных файлов доступен как размер раздачи для
показа в карточке
+11 -4
View File
@@ -22,16 +22,23 @@ TBD - created by archiving change live-status. Update Purpose after archive.
### Requirement: Состав телеметрии
Телеметрия одной раздачи SHALL включать прогресс (доля 0..1), скорость
загрузки и ETA, а для сидирующих раздач дополнительно — рейтинг, число сидов
и пиров, объём отданного и скорость отдачи. Значения SHALL извлекаться из
ответа qBittorrent `/torrents/info` без дополнительного сетевого вызова.
Телеметрия одной раздачи SHALL включать прогресс (доля 0..1), скорость загрузки,
ETA и общий размер раздачи (total size); а для сидирующих раздач дополнительно —
рейтинг, число сидов и пиров, объём отданного и скорость отдачи. Общий размер
SHALL быть доступен для любой раздачи, присутствующей в снимке (не только
сидирующей). Значения SHALL извлекаться из ответа qBittorrent `/torrents/info`
без дополнительного сетевого вызова.
#### Scenario: Телеметрия качающейся задачи
- **WHEN** торрент задачи находится в состоянии загрузки
- **THEN** в снимке для неё доступны прогресс, скорость загрузки и ETA
#### Scenario: Общий размер доступен для любой раздачи в снимке
- **WHEN** торрент задачи присутствует в снимке в любом состоянии
- **THEN** в телеметрии для неё доступен общий размер раздачи
#### Scenario: Телеметрия сидирующей задачи
- **WHEN** торрент задачи завершён и раздаётся
+61 -9
View File
@@ -262,12 +262,11 @@ jellybit (`created_at`). Порядок MUST быть согласован ме
### Requirement: Клиентские взаимодействия без сборки
Веб-UI SHALL реализовывать клиентскую логику без шага сборки и без реактивных
фреймворков: копирование идентификатора загрузки (vanilla JS) и раскрытие
контекста нативным `<details>`. Основной копируемый идентификатор в карточке
списка и в шапке страницы просмотра SHALL быть `download.id` (ULID) — тот же
ключ, что пишется в логи (`download_id`). Все действия над загрузкой SHALL
выполняться через формы/htmx (раундтрип на сервер), без клиентского пересчёта
доменного состояния.
фреймворков: копирование идентификатора загрузки (vanilla JS). Основной
копируемый идентификатор в карточке списка и в шапке страницы просмотра SHALL
быть `download.id` (ULID) — тот же ключ, что пишется в логи (`download_id`). Все
действия над загрузкой SHALL выполняться через формы/htmx (раундтрип на сервер),
без клиентского пересчёта доменного состояния.
#### Scenario: Копирование идентификатора загрузки
@@ -276,8 +275,61 @@ jellybit (`created_at`). Порядок MUST быть согласован ме
- **THEN** значение `download.id` копируется в буфер обмена без перезагрузки
страницы
#### Scenario: Раскрытие контекста без JS
#### Scenario: Действия только через раундтрип
- **WHEN** пользователь раскрывает спойлер переданного контекста
- **THEN** контекст показывается нативным `<details>`, без скриптов
- **WHEN** пользователь выполняет действие над загрузкой
- **THEN** оно исполняется формой/htmx-запросом на сервер, без клиентского
пересчёта доменного состояния
### 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 браться из живого снимка (общий размер торрента), а при
отсутствии торрента в снимке — из суммарного размера разложенных файлов загрузки;
если неизвестно ни то, ни другое — прочерк «—».
#### 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: Контекст не в карточке
- **WHEN** у загрузки есть переданный контекст
- **THEN** он не показывается в карточке списка, но доступен на странице
`/download/{id}`