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

106 lines
8.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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. Откат безопасен: новые поля в карточке деградируют до «—», старый бинарь
игнорирует колонку.