Карточка списка на главной теперь даёт краткий обзор «от загрузки до
решения об удалении»: метка «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>
106 lines
8.1 KiB
Markdown
106 lines
8.1 KiB
Markdown
## 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. Откат безопасен: новые поля в карточке деградируют до «—», старый бинарь
|
||
игнорирует колонку.
|