Карточка списка на главной теперь даёт краткий обзор «от загрузки до
решения об удалении»: метка «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>
8.1 KiB
Context
Карточка списка на главной (web/templates/index.html, view-model downloadView
в internal/httpapi) сейчас показывает заголовок, download.id, бейдж
состояния, спойлер контекста и — для качающихся — прогресс. Живая телеметрия
раздачи живёт в in-memory снимке воркера (worker.Live, capability
live-status) и в БД не персиститься; handleIndex читает снимок только для
карточек в состоянии downloading. Размер разложенных файлов нигде не хранится:
file_link содержит пути и статус, без размера.
Задача — дать в карточке обзор для решения «пора удалять»: возраст (дата добавления) и рейтинг отдачи, плюс размер. Возраст и рейтинг — ровно те два критерия, по которым пользователь удаляет раздачи.
Goals / Non-Goals
Goals:
- Дата добавления в карточке всегда (абсолют + относительная давность).
- Рейтинг отдачи и размер раздачи в карточке из живого снимка.
- Размер работает и после исчезновения торрента из qBittorrent (случай
orphaned) — через сохранённый размер разложенных файлов. - Убрать спойлер контекста из карточки (контекст уже есть на
/download/{id}).
Non-Goals:
- Живое (поллинг) обновление рейтинга/размера в списке — значения статичны на момент рендера (рейтинг меняется медленно; поллинг сейчас только у прогресс-бара).
- Сиды/пиры, время сидирования, объём отданного в карточке — вне объёма.
- Персист рейтинга в БД — он нужен, пока торрент в qBittorrent, тогда и доступен.
- Бэкофилл размера для уже разложенных до миграции загрузок (см. Risks).
Decisions
Размер разложенных файлов — колонка file_link.size
Фолбэк размера (когда торрента нет в снимке) берём из БД: новая колонка
file_link.size INTEGER, которую layouter заполняет при линковке (размер уже
известен из os.Stat/qbt.File.Size в момент создания ссылки). Ридер отдаёт
SUM(size) по разложенным файлам загрузки одним запросом вместе с листингом.
Почему так, а не иначе:
- Stat на лету при рендере — N системных вызовов на карточку под пагинацией; I/O в горячем пути рендера, зависимость показа от доступности ФС. Отклонено.
- Не показывать фолбэк — у
orphaned(файлы библиотеки = последняя копия данных) размер как раз важен для решения об удалении. Отклонено. - Колонка в БД: ноль I/O при рендере, размер зафиксирован в момент раскладки (когда файл точно на месте). Цена — миграция goose + правка layouter.
Размер при наличии торрента — total_size из снимка
В qbt.Torrent добавляем TotalSize int64 \json:"total_size"`(qBittorrent отдаёт его в том же/torrents/info, без отдельного вызова), пробрасываем в worker.Live.TotalSize. При рендере: снимок есть → TotalSize; снимка нет → SUM(file_link.size)`; нет ни того ни другого → «—».
handleIndex читает снимок для всех карточек
Сейчас liveFor(d) вызывается только для downloading. Расширяем на все
карточки страницы — это map-lookup по волатильному снимку (worker), без сети и
без БД, стоимость незначительна при pageSize карточек. Рейтинг/размер попадают
в downloadView на рендере.
Дата и относительная давность
Формат «2006-01-02 · N дней назад». Абсолютную часть форматируем в TZ сервера
(Europe/Moscow), относительную считаем от now в том же TZ. Базис — как в
сортировке списка: source_added_at → фолбэк created_at (оба хранятся в UTC,
парсятся store.ParseTime). Относительные подписи — вспомогательный форматтер в
internal/httpapi рядом с существующими (fmtBytes, fmtRatio, fmtETA).
Форматтеры и вёрстка
Переиспользуем fmtBytes (размер) и fmtRatio (рейтинг) из internal/httpapi/live.go.
Мета-строку карточки выносим отдельным партиалом либо инлайним в index.html —
решается при реализации; спойлер контекста удаляем из index.html.
Risks / Trade-offs
- Уже разложенные до миграции загрузки не имеют
file_link.size(колонкаDEFAULT 0/NULL) → их фолбэк-размер = 0/«—», пока торрент отсутствует в снимке. Дляdone/сидирующих торрент обычно ещё в qBittorrent, поэтому размер берётся изtotal_sizeи проблема почти не проявляется. → Митигация: бэкофилл не делаем (сложность ради редкого края); при желании — отдельная разовая задача. Показываем «—» честно, а не 0. - Рейтинг/размер статичны на рендере (без поллинга) → в списке значение
может слегка отставать от реального. → Приемлемо: рейтинг меняется медленно,
точные живые цифры есть на
/download/{id}; полная перезагрузка списка освежает. - Чтение снимка для всех карточек чуть увеличивает работу
handleIndex. → Митигация: это lookup в готовой in-memory карте подpageSizeэлементов; сетевых/БД-обращений не добавляется. SUM(file_link.size)— доп. агрегат к листингу. → Один запрос батчем по id страницы (какattachInfohashes), не N+1.
Migration Plan
- Goose-миграция:
ALTER TABLE file_link ADD COLUMN size INTEGER NOT NULL DEFAULT 0. - Обновить ER-схему
docs/specs/database.md(колонкаfile_link.size). - Down-миграция дропает колонку (пересоздание таблицы — как в существующих
миграциях проекта, SQLite без
DROP COLUMNдо нужной версии — при необходимости). - Откат безопасен: новые поля в карточке деградируют до «—», старый бинарь игнорирует колонку.