UI/UX списка и карточки загрузки: серверные фильтр/поиск/пагинация, матч-ссылка, имя раздачи (web-ui-list-detail)
- Список: серверные фильтр по группе состояний, поиск и пагинация (GET
f/q/page/all, по 25), сортировка по времени добавления в qBittorrent
(added_on) с фолбеком на created_at и tie-break по id.
- Заголовок загрузки = имя раздачи (display_name) → распознанное название →
усечённый источник; сырой magnet вынесен в блок «Информация о торренте».
- Матч метабазы показан ссылкой на запись (страница загрузки и ревью);
URL берётся у выбранного кандидата либо строится по provider+id и типу.
- Полировка вёрстки; клиентская JS-фильтрация убрана (всё серверное, без JS).
- Миграция 0005 (display_name, source_added_at); воркер однократно
фиксирует source_added_at при поллинге/усыновлении; ER-схема обновлена.
- OpenSpec: дельты влиты в specs/{web-ui,ingest}, change заархивирован.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -28,7 +28,7 @@ func TestDiscover_AdoptsByCategory(t *testing.T) {
|
||||
st := emptyStore()
|
||||
w := newTestWorker(st, &fakeQbt{})
|
||||
w.discover(context.Background(), []qbt.Torrent{
|
||||
{Hash: ihDisc, Name: "Avatar", Category: "jellybit", State: "stalledUP"},
|
||||
{Hash: ihDisc, Name: "Avatar", Category: "jellybit", State: "stalledUP", AddedOn: 1_700_000_000},
|
||||
})
|
||||
|
||||
d := findByInfohash(st, ihDisc)
|
||||
@@ -41,6 +41,14 @@ func TestDiscover_AdoptsByCategory(t *testing.T) {
|
||||
if d.IdempotencyKey.String != ihDisc {
|
||||
t.Errorf("idempotency_key = %q", d.IdempotencyKey.String)
|
||||
}
|
||||
// Усыновление берёт заголовок из имени торрента qBittorrent и фиксирует
|
||||
// время добавления (added_on) как базис сортировки.
|
||||
if d.DisplayName != "Avatar" {
|
||||
t.Errorf("display_name = %q, want Avatar", d.DisplayName)
|
||||
}
|
||||
if !d.SourceAddedAt.Valid {
|
||||
t.Errorf("source_added_at не зафиксирован при усыновлении")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscover_AdoptsByTag(t *testing.T) {
|
||||
@@ -94,6 +102,25 @@ func TestDiscover_SkipsNoInfohash(t *testing.T) {
|
||||
|
||||
// TestPoll_AdoptsAndCompletes — сценарий пользователя целиком: помеченная и
|
||||
// уже скачанная раздача за один тик усыновляется и доходит до completed.
|
||||
func TestPoll_CapturesSourceAddedAt(t *testing.T) {
|
||||
ih := "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0"
|
||||
st := &fakeStore{downloads: map[int64]*store.Download{
|
||||
1: {ID: 1, State: store.StateDownloading, SourceType: store.SourceMagnet,
|
||||
Infohash: store.NullString(ih), IdempotencyKey: store.NullString(ih)},
|
||||
}}
|
||||
qb := &fakeQbt{torrents: []qbt.Torrent{
|
||||
{Hash: ih, Name: "X", Category: "jellybit", State: "downloading", AddedOn: 1_700_000_000},
|
||||
}}
|
||||
w := newTestWorker(st, qb)
|
||||
|
||||
if err := w.Poll(context.Background()); err != nil {
|
||||
t.Fatalf("Poll: %v", err)
|
||||
}
|
||||
if d := st.downloads[1]; !d.SourceAddedAt.Valid {
|
||||
t.Fatalf("source_added_at не захвачен при поллинге активной задачи")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPoll_AdoptsAndCompletes(t *testing.T) {
|
||||
st := emptyStore()
|
||||
qb := &fakeQbt{torrents: []qbt.Torrent{
|
||||
|
||||
Reference in New Issue
Block a user