- Список: серверные фильтр по группе состояний, поиск и пагинация (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>
7.1 KiB
7.1 KiB
1. Персистентность имени раздачи и времени добавления (БД + ingest + worker)
- 1.1 Миграция
0005(goose):download.display_name TEXT NOT NULL DEFAULT ''иdownload.source_added_at(nullable timestamp), обратная миграция (down) - 1.2 Добавить поля
DisplayNameиSourceAddedAtвstore.Download; писатьdisplay_nameвCreateDownload; метод однократной записиsource_added_atс SQL-гардомWHERE source_added_at IS NULL; экспортировать хелпер форматирования времени в форматеsqliteTimeLayout(UTC) - 1.3 В
internal/ingestзаполнятьDisplayNameвыведенным именемrenameпри создании загрузки; вdiscover.adopt()—display_name = t.Nameдля усыновлённых торрентов - 1.4 В
internal/workerперсиститьsource_added_atизqbt.Torrent.AddedOnв циклеPollсразу послеbyHash-сопоставления (доreconcile, для любого класса состояния) и вadopt(); формат —time.Unix(...).UTC().Format(sqliteTimeLayout); однократно (гард поd.SourceAddedAt+ SQL IS NULL); подw.mu - 1.5 Обновить ER-схему
docs/specs/database.md(колонкиdisplay_name,source_added_at) - 1.6 Тесты: ingest сохраняет
display_name; adopt пишетdisplay_nameиз имени торрента; worker пишетsource_added_atоднократно и в форматеcreated_at; пустой контекст → пустое имя
2. Серверные фильтр, поиск, сортировка и пагинация списка (store)
- 2.1 Вынести маппинг состояние↔группа фильтра в единый источник истины (используется и в UI, и в запросе); учесть
allс исключениемdeletedи осознанно — группуother(cancelled/reverted/deleted) без своего чипа - 2.2 Метод
store.ListDownloadsPage(ctx, ListFilter)→ страница + общий count:WHEREпо группе состояний,deletedтолько при «показать всё», поискLIKE/COLLATE NOCASEпоsource_ref/display_name/context/infohash, сортировкаORDER BY COALESCE(source_added_at, created_at) DESC, id DESC,LIMIT/OFFSET - 2.3 Тесты
store: фильтр по группе, поиск, скрытиеdeletedпо умолчанию, порядок по времени добавления с фолбеком + тай-брейк по id при равных метках, границы пагинации и count
3. Список на главной (httpapi + шаблоны)
- 3.1 Разбор GET-параметров
f/q/page/allвhandleIndex(клэмпpage:<1/нечисловой → 1, за пределом → пустая страница), константа размера страницы (25), вызовListDownloadsPage; расширить интерфейсReaderметодомListDownloadsPage - 3.2 View-модель списка: элементы страницы + данные пагинации + текущие фильтр/поиск; удалить ставшие мёртвыми
SearchTextи (если больше не нужен)GroupизdownloadView - 3.3
index.html: чипы-фильтр и «показать всё» как ссылки (сохраняютq), форма поиска (GET), навигация по страницам (сохраняетf/q); серверный рендер пустого состояния («ничего не найдено» при активных фильтре/поиске, иначе «пока пусто») - 3.4 Убрать клиентскую JS-фильтрацию из
app.js(applyFilters/setFilter+DOMContentLoaded-хук), оставитьcopyHashи<details>; сохранить живой прогресс активных карточек - 3.5 Заголовок карточки — фолбек-цепочка
display_name→ распознанное → усечённый источник (одна строка) - 3.6 Тесты
httpapi: параметры списка → корректная выборка/пагинация; заголовок изdisplay_nameи фолбеки
4. Страница загрузки: имя, матч-ссылка, блок торрента (httpapi + шаблоны)
- 4.1 Заголовок/шапка
/download/{id}— та же фолбек-цепочка заголовка - 4.2 Хелпер
providerURL(provider, id, mediaType)(tmdb: movie/tv; tvdb: медиазависимый путь; imdb: /title) + выбор URL матча: URL выбранного кандидата только если его provider+id == эффективнымrd.Provider/rd.ProviderID, иначе построенный; нет надёжного URL → без ссылки - 4.3 «Распознано как»: показать матч ссылкой на запись (провайдер, id, год); при отсутствии URL — текст без ссылки
- 4.4 Блок «Информация о торренте»: полный источник (magnet) + infohash с копированием
- 4.5 Тесты
httpapi: матч-ссылка при наличии/отсутствии URL; расхождение выбранного кандидата и эффективного provider_id (ссылка ведёт на эффективную запись); блок торрента
5. Ревью: матч-ссылка (httpapi + шаблон)
- 5.1 В блоке «Источник совпадения» строку «Выбрано» показывать ссылкой на запись (та же логика выбора URL, что в 4.2)
6. Полировка вёрстки (CSS/шаблоны)
- 6.1 «Распознано как»: снять
max-widthу списка полей — блок на всю ширину - 6.2 Layout-виджет: приклеить стрелку к строке файла-источника (источник и цель строго друг под другом)
- 6.3 Проверить отсутствие инлайн-
<style>с хардкодом цветов в изменённых шаблонах (правки — вjellybit.css)
7. Проверка и ревью
- 7.1
task testиtask lintзелёные - 7.2
openspec validate web-ui-list-detail --strict - 7.3 Ручная проверка: список с фильтром/поиском/пагинацией, живой прогресс не сбрасывается; заголовки; матч-ссылки; блок торрента; полировка
- 7.4 Ревью кода перед archive