From 969926fae35d593ca78cb0e4af8306bb31312981 Mon Sep 17 00:00:00 2001 From: Anton Vakhrushev Date: Mon, 10 Aug 2026 12:32:17 +0300 Subject: [PATCH] =?UTF-8?q?tasks:=20=D0=B7=D0=B0=D0=B2=D0=B5=D0=B4=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=B1=D0=B0=D0=B3=20=D1=83=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D1=80=D0=B5=D0=B2=D1=88=D0=B5=D0=B9=20=D0=BA=D0=B0=D1=80=D1=82?= =?UTF-8?q?=D0=BE=D1=87=D0=BA=D0=B8=20=D0=B8=20=D1=86=D0=B5=D0=BB=D1=8C=20?= =?UTF-8?q?=D0=B3=D1=80=D1=83=D0=BF=D0=BF=D0=BE=D0=B2=D0=BE=D0=B3=D0=BE=20?= =?UTF-8?q?=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix card-stale-after-download-finish: карточка списка не обновляется после выхода задачи из downloading - goal bulk-download-management + feature bulk-delete-page: удаление раздач с файлами пачкой на отдельной странице --- tasks/BACKLOG.md | 2 + tasks/ROADMAP.md | 1 + tasks/items/bulk-delete-page.md | 64 +++++++++++++++ tasks/items/bulk-download-management.md | 23 ++++++ .../items/card-stale-after-download-finish.md | 77 +++++++++++++++++++ 5 files changed, 167 insertions(+) create mode 100644 tasks/items/bulk-delete-page.md create mode 100644 tasks/items/bulk-download-management.md create mode 100644 tasks/items/card-stale-after-download-finish.md diff --git a/tasks/BACKLOG.md b/tasks/BACKLOG.md index 6513e45..f1e3979 100644 --- a/tasks/BACKLOG.md +++ b/tasks/BACKLOG.md @@ -27,6 +27,8 @@ - [✨ Переделать веб-UI в устанавливаемое PWA](items/web-ui-pwa.md) — текущий server-rendered UI функционален — PWA (устанавливаемое, удобное с телефона) это улучшение большого объёма, не блокер - [✨ Править на ревью маппинг «файл → серия» и раскладывать вручную при провале LLM](items/review-mapping-editor.md) — правка S·E, «нумеровать подряд» и ручной режим при полном провале LLM были запланированы объёмом Ф5 и не заведены задачей — в ревью сегодня можно только подсказать текстом - [✨ Заказать спекой крайние случаи именования: многофайловый фильм, редакции, двойная серия](items/naming-edge-cases.md) — стэкинг частей (part1/cd1), редакции [edition-…] и двойная серия SxxEyy-Eyy описаны нарративом, но в file-layout не заказаны — раскладка таких раздач не определена +- [🐞 Обновлять карточку списка после завершения загрузки](items/card-stale-after-download-finish.md) — завершённая загрузка висит в списке с бейджем «Загружается» до ручной перезагрузки: карточка самополлится только в catched, а прогресс перестаёт поллить, выйдя из downloading — обновлять карточку становится некому +- [✨ Удалять выбранные загрузки с файлами на отдельной странице](items/bulk-delete-page.md) — удаление с файлами живёт только в danger-секции страницы одной загрузки: чтобы снести десять раздач, надо десять раз пройти путь список → карточка → подтверждение - [🔬 Канон нумерации серий и порядок у провайдера тега](items/episode-numbering-canon.md) — Косметика/редкость: порядок просмотра ок, но у тайтлов со спорным порядком (Бибоп) Jellyfin подтягивает не те подписи серий, если канон файлов ≠ дефолтный порядок провайдера тега - [🔬 Тексты и формат уведомлений в Telegram](items/telegram-messages-audit.md) — зонтичный проход по всем текстам бота: полнота карточек, единый язык, оформление; порождает под-задачи - [🔬 guessit как сервис-спутник](items/guessit-sidecar.md) — go-ptn слабее питоновского guessit — если точности пред-парса не хватит, завернуть guessit в сервис-спутник рядом с бинарём diff --git a/tasks/ROADMAP.md b/tasks/ROADMAP.md index 1c08e22..9ee488c 100644 --- a/tasks/ROADMAP.md +++ b/tasks/ROADMAP.md @@ -20,6 +20,7 @@ - [🎯 Раскладывается не только типовая раздача](items/complex-releases.md) — типовая раздача раскладывается, а всё, что сложнее одного сезона одного тайтла, упирается в ручной разбор - [🎯 Раздача приносится и подтверждается из любого транспорта](items/ingest-and-review-interfaces.md) — путь «принести раздачу и подтвердить догадку» упирается в незакрытые куски интерфейсов, а не в логику - [🎯 По записи загрузки видно, как она сюда попала](items/state-integrity.md) — известные окна рассинхрона и потери маркеров: каждое по отдельности самоисцеляется, вместе — источник необъяснимых состояний +- [🎯 Загрузки удаляются пачкой, а не по одной](items/bulk-download-management.md) — удаление раздачи с файлами доступно только по одной кнопке на странице одной загрузки — уборка десятка раздач превращается в десяток заходов ## Сопровождение diff --git a/tasks/items/bulk-delete-page.md b/tasks/items/bulk-delete-page.md new file mode 100644 index 0000000..0332d96 --- /dev/null +++ b/tasks/items/bulk-delete-page.md @@ -0,0 +1,64 @@ +# ✨ Удалять выбранные загрузки с файлами на отдельной странице + +- **Тип:** feature +- **Категория:** Ядро продукта +- **Зачем:** удаление с файлами живёт только в danger-секции страницы одной загрузки: чтобы снести десять раздач, надо десять раз пройти путь список → карточка → подтверждение +- **Теги:** goal:bulk-download-management + +Отдельная страница веб-UI, доступная из шапки: список загрузок, у которых +удаление разрешено (`done`, `orphaned`, `target_missing` — +`internal/worker/review.go:631`), чекбокс у каждой строки и одна кнопка +«Удалить выбранные». Кнопка ведёт на подтверждение, где выбранные раздачи +названы поимённо, и только оттуда уходит удаление. + +Страница отдельная, чтобы не вешать режим выбора на основной список: там +карточки живые, самополлятся и перерисовываются, а выбор пользователя такое +перерисовывание переживать не обязан. + +Речь только о полном удалении (снять хардлинки + снести раздачу с файлами из +qBittorrent). Групповой `Dismiss` (смена статуса без файлов) в эту задачу не +входит. + +## Затрагивает + +- новая страница веб-UI и ссылка на неё в `web/templates/partials/header.html`; + новый шаблон страницы и её партиалы; +- новый POST-эндпоинт группового удаления в `internal/httpapi` (рядом с + `POST /ui/downloads/{id}/delete`, `internal/httpapi/httpapi.go:143`); +- `Reviewer.Delete` воркера (`internal/worker/review.go:619`) — зовётся по + каждой выбранной загрузке; +- qBittorrent `torrents/delete` с `deleteFiles=true` — внешний сервис, + необратимая операция; +- спека `web-ui`, требования «Страницы веб-UI» и «Действия соответствуют + состоянию»; +- спека `state-reconciliation` в части `Delete` — групповой вызов не меняет + условий поштучного, но подтверждение человека теперь одно на пачку. + +## Критерии приёмки + +- Страница открывается из шапки и показывает только те загрузки, для которых + удаление разрешено поштучно (оракул: тест `internal/httpapi` — подставной + читатель отдаёт задачи во всех состояниях, в разметке строки есть у + `done`/`orphaned`/`target_missing` и нет у остальных). +- Удаление уходит только после явного подтверждения, и подтверждение называет + каждую выбранную раздачу поимённо (оракул: тест — POST без признака + подтверждения отвечает отказом и не делает ни одного вызова `Delete` у + подставного воркера; ответ подтверждения содержит заголовки всех выбранных). +- Отказ на одной загрузке не отменяет остальных, а результат называет + удалённые и отказавшие поимённо с причиной (оракул: тест, где второй `Delete` + возвращает ошибку — первая и третья удалены, страница результата называет + вторую и её причину). +- Групповой путь не расширяет прав поштучного: попытка удалить задачу в + состоянии, где кнопка недоступна, отклоняется с тем же отказом + (оракул: тест — `Delete` для `downloading` возвращает `ErrConflict`, страница + показывает отказ, остальные выбранные не затронуты). +- Поведение страницы записано дельта-спекой и проходит валидацию (оракул: + `openspec validate --strict` и `task gate`). + +## Рамки + +Необратимое действие: `deleteFiles=true` сносит файлы раздачи, гард последней +копии в `Delete` выключен сознательно. Ослаблять подтверждение ради удобства +пачки нельзя — оно остаётся обязательным и поимённым. Групповой `Dismiss` и +автоматическая чистка по сроку хранения (`db-retention-cleanup`) — не эта +задача. diff --git a/tasks/items/bulk-download-management.md b/tasks/items/bulk-download-management.md new file mode 100644 index 0000000..1acfbe4 --- /dev/null +++ b/tasks/items/bulk-download-management.md @@ -0,0 +1,23 @@ +# 🎯 Загрузки удаляются пачкой, а не по одной + +- **Тип:** goal +- **Секция:** Направления +- **Зачем:** удаление раздачи с файлами доступно только по одной кнопке на странице одной загрузки — уборка десятка раздач превращается в десяток заходов + +Ради чего: разложенные раздачи копятся, и убирают их обычно скопом — после +просмотра сезона, при чистке диска, после серии неудачных заливок. Сегодня +удаление живёт только в danger-секции страницы одной загрузки, поэтому уборка +десяти раздач стоит десяти проходов «список → карточка → подтверждение». + +При этом удаление с файлами необратимо: оно зовёт `torrents/delete` с +`deleteFiles=true`, а гард последней копии там выключен сознательно +(инвариант «Источник неприкосновенен», исключение 1). Групповой режим обязан +сделать уборку дешевле, не сделав ошибку дешевле. + +## Завершение + +Достигнута, когда человек убирает любое число раздач одним проходом: выбирает +их в списке, один раз подтверждает удаление по перечню, где каждая раздача +названа поимённо, и видит поимённый результат — что снесено, что отказало и +почему. Ни одна раздача не сносится без того, чтобы человек увидел её в +подтверждении. diff --git a/tasks/items/card-stale-after-download-finish.md b/tasks/items/card-stale-after-download-finish.md new file mode 100644 index 0000000..800f986 --- /dev/null +++ b/tasks/items/card-stale-after-download-finish.md @@ -0,0 +1,77 @@ +# 🐞 Обновлять карточку списка после завершения загрузки + +- **Тип:** fix +- **Категория:** Ядро продукта +- **Зачем:** завершённая загрузка висит в списке с бейджем «Загружается» до ручной перезагрузки: карточка самополлится только в catched, а прогресс перестаёт поллить, выйдя из downloading — обновлять карточку становится некому + +Карточка списка показывает состояние, которого уже нет: качание кончилось, +задача ушла в распознавание и дальше в ревью, а на экране прежний бейдж и +прежний набор действий. + +Причина видна в коде. Самополлинг карточки включает поле `SelfPoll`, а оно +истинно только в `catched` (`internal/httpapi/httpapi.go:664`, +`web/templates/partials/card.html:2`). Внутренний фрагмент прогресса поллится, +пока состояние `downloading` (`internal/httpapi/live.go:88`, +`web/templates/partials/progress.html:1`). Задача, покинувшая `downloading`, не +опрашивается уже ничем. На странице `/download/{id}` то же самое: +`web/templates/partials/download_main.html:2` самополлится только в `catched` +(`internal/httpapi/download.go:116`). + +Спека нынешнее поведение не запрещает: `live-status` → «Живой прогресс активных +загрузок» прямо требует прекращать поллинг прогресса на выходе из +`downloading`, а `web-ui` → «Отображение промежуточного состояния catched» +заказывает самообновление только для перехода `catched` → `downloading`. +Поэтому починка несёт дельту спеки, а не только правку кода. + +## Воспроизведение + +1. Открыть главную `/`, добавить раздачу, дождаться состояния `downloading`. +2. Страницу не перезагружать. Дождаться, пока qBittorrent доведёт раздачу до + 100% и воркер уведёт задачу из `downloading` (в `recognizing`, дальше в + `review` или `done`). +3. Наблюдается: бейдж карточки остаётся «Загружается», прогресс-бар замирает на + последнем значении, кнопка «Ревью →» не появляется. Фоновых запросов от этой + карточки в панели сети больше нет. +4. Ожидается: карточка сама показывает новое состояние — бейдж, набор действий, + исчезнувший прогресс. +5. Перезагрузить страницу руками: карточка сразу показывает верное состояние — + расходится отображение, а не данные. + +## Затрагивает + +- фрагменты `GET /fragments/downloads/{id}/card` и + `GET /fragments/downloads/{id}/progress` (`internal/httpapi/live.go`); +- поле `SelfPoll` вида карточки и вида страницы загрузки + (`internal/httpapi/httpapi.go`, `internal/httpapi/download.go`); +- шаблоны `web/templates/partials/card.html`, `progress.html`, + `download_main.html`; +- спека `web-ui`, требование «Отображение промежуточного состояния catched» — + условие самообновления перестаёт быть привязанным к одной фазе; +- спека `live-status`, требование «Живой прогресс активных загрузок» и его + сценарий «Завершение останавливает поллинг» — прекращение поллинга прогресса + перестаёт означать прекращение обновления карточки. + +## Критерии приёмки + +- Карточка нетерминальной загрузки самополлится, и первый ответ фрагмента после + смены состояния несёт новый бейдж (оракул: тест `internal/httpapi` — рендер + карточки в `downloading` содержит `hx-get` на `/fragments/downloads/{id}/card`, + а подставной читатель, сменивший состояние на `review`, отдаёт карточку с + бейджем ревью). +- Терминальная карточка себя не опрашивает: фоновых запросов после `done`, + `failed`, `cancelled` и `deleted` нет (оракул: тот же тест — в разметке такой + карточки нет ни `hx-get`, ни `hx-trigger`). +- Появившееся действие видно без перезагрузки: карточка задачи, перешедшей в + `review`, несёт кнопку «Ревью →» (оракул: тест фрагмента карточки). +- Страница `/download/{id}` обновляет бейдж и блок действий по тому же правилу, + что и карточка (оракул: тест `internal/httpapi/download.go` — `SelfPoll` + истинен для нетерминальных состояний и ложен для терминальных). +- Правило записано в дельта-спеках обеих затронутых capability (оракул: + `openspec validate --strict` и шаг канона в `task gate`). + +## Рамки + +Чинится поллингом фрагментов: переход на SSE — отдельная задача +`sse-live-updates`, и эта починка её не делает и не отменяет. Расширение +самополлинга на все нетерминальные состояния добавляет фоновых запросов к БД — +частота и её потолок решаются в предложении, а не молча.