Веб-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:
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.vakhrushev.me/av/jellybit/internal/store"
|
||||
"git.vakhrushev.me/av/jellybit/internal/worker"
|
||||
@@ -184,3 +185,84 @@ func fmtRatio(r float64) string {
|
||||
}
|
||||
return fmt.Sprintf("%.2f", r)
|
||||
}
|
||||
|
||||
// mskLoc — TZ сервера (Europe/Moscow) для показа дат в веб-UI (инвариант «время
|
||||
// с явным TZ»). Если zoneinfo недоступен в окружении — деградируем до UTC:
|
||||
// дата остаётся корректной с точностью до смещения, UI не падает.
|
||||
var mskLoc = func() *time.Location {
|
||||
if loc, err := time.LoadLocation("Europe/Moscow"); err == nil {
|
||||
return loc
|
||||
}
|
||||
return time.UTC
|
||||
}()
|
||||
|
||||
// fmtDate — абсолютная дата добавления для карточки в TZ сервера.
|
||||
func fmtDate(t time.Time) string {
|
||||
return t.In(mskLoc).Format("2006-01-02")
|
||||
}
|
||||
|
||||
// humanizeAge — относительная давность («5 дней назад») от now до t. Будущее
|
||||
// (рассинхрон часов) схлопывается в «только что». Единицы огрубляются к
|
||||
// минутам/часам/дням/месяцам/годам — для обзора возраста этого достаточно.
|
||||
func humanizeAge(t, now time.Time) string {
|
||||
d := now.Sub(t)
|
||||
if d < time.Minute {
|
||||
return "только что"
|
||||
}
|
||||
switch {
|
||||
case d < time.Hour:
|
||||
n := int(d / time.Minute)
|
||||
return fmt.Sprintf("%d %s назад", n, plural(n, "минуту", "минуты", "минут"))
|
||||
case d < 24*time.Hour:
|
||||
n := int(d / time.Hour)
|
||||
return fmt.Sprintf("%d %s назад", n, plural(n, "час", "часа", "часов"))
|
||||
case d < 30*24*time.Hour:
|
||||
n := int(d / (24 * time.Hour))
|
||||
return fmt.Sprintf("%d %s назад", n, plural(n, "день", "дня", "дней"))
|
||||
case d < 365*24*time.Hour:
|
||||
n := int(d / (30 * 24 * time.Hour))
|
||||
return fmt.Sprintf("%d %s назад", n, plural(n, "месяц", "месяца", "месяцев"))
|
||||
default:
|
||||
n := int(d / (365 * 24 * time.Hour))
|
||||
return fmt.Sprintf("%d %s назад", n, plural(n, "год", "года", "лет"))
|
||||
}
|
||||
}
|
||||
|
||||
// plural выбирает русскую форму по числу (1 файл / 2 файла / 5 файлов).
|
||||
func plural(n int, one, few, many string) string {
|
||||
if n < 0 {
|
||||
n = -n
|
||||
}
|
||||
if m := n % 100; m >= 11 && m <= 14 {
|
||||
return many
|
||||
}
|
||||
switch n % 10 {
|
||||
case 1:
|
||||
return one
|
||||
case 2, 3, 4:
|
||||
return few
|
||||
default:
|
||||
return many
|
||||
}
|
||||
}
|
||||
|
||||
// sizeText — размер раздачи для карточки: живой общий размер из снимка, иначе
|
||||
// суммарный размер разложенных файлов (фолбэк для orphaned), иначе «—».
|
||||
func sizeText(l worker.Live, ok bool, layoutSize int64) string {
|
||||
if ok && l.TotalSize > 0 {
|
||||
return fmtBytes(l.TotalSize)
|
||||
}
|
||||
if layoutSize > 0 {
|
||||
return fmtBytes(layoutSize)
|
||||
}
|
||||
return "—"
|
||||
}
|
||||
|
||||
// ratioText — рейтинг отдачи для карточки: из живого снимка, иначе «—» (торрента
|
||||
// нет в qBittorrent).
|
||||
func ratioText(l worker.Live, ok bool) string {
|
||||
if !ok {
|
||||
return "—"
|
||||
}
|
||||
return fmtRatio(l.Ratio)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user