Files
avandClaude Opus 4.8 bb245a90a3 Веб-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>
2026-07-04 10:34:45 +03:00

8.1 KiB
Raw Permalink Blame History

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

  1. Goose-миграция: ALTER TABLE file_link ADD COLUMN size INTEGER NOT NULL DEFAULT 0.
  2. Обновить ER-схему docs/specs/database.md (колонка file_link.size).
  3. Down-миграция дропает колонку (пересоздание таблицы — как в существующих миграциях проекта, SQLite без DROP COLUMN до нужной версии — при необходимости).
  4. Откат безопасен: новые поля в карточке деградируют до «—», старый бинарь игнорирует колонку.