web-ui: добавлена страница группового удаления загрузок

- выбор → поимённое подтверждение → отчёт: пачка до 20 загрузок, гарды входа
  на обеих границах, потолок времени и остановка после трёх подряд отказов
  внешнего сервиса
- допуск полного удаления сведён в единую точку store.State.CanDelete() —
  worker, страница загрузки и Telegram больше не держат своих перечней
This commit is contained in:
av
2026-08-10 17:43:36 +03:00
parent a7c1efd8eb
commit 288be8ec34
35 changed files with 3185 additions and 31 deletions
@@ -0,0 +1,59 @@
## Why
Удалить раздачу вместе с файлами сегодня можно только со страницы одной
загрузки, из её danger-секции. Чтобы освободить место после десяти закрытых
раздач, человек десять раз проходит путь «список → карточка → подтверждение».
Работа механическая, а цена ошибки высокая: на каждом проходе он подтверждает
необратимое действие заново и легко теряет, какие раздачи уже снёс.
## What Changes
- Появляется отдельная страница веб-UI «Удаление», доступная из шапки. На ней
перечислены только те загрузки, для которых удаление с файлами разрешено
поштучно (`done`, `orphaned`, `target_missing`), у каждой строки — чекбокс.
- Одна кнопка «Удалить выбранные» ведёт на **страницу подтверждения**, где
выбранные раздачи названы поимённо. Удаление уходит только оттуда: POST без
признака подтверждения отклоняется, ни одного удаления не делает.
- Результат показывается поимённо: что удалено и что отказало, с причиной
каждого отказа. Отказ на одной загрузке не отменяет остальных.
- Групповой путь прав не расширяет: каждая выбранная загрузка проходит ту же
проверку состояния, что и поштучное удаление, и отклоняется тем же конфликтом.
- Условие «удаление разрешено» переезжает в единую точку домена
(`store.State`): сейчас оно записано дважды — в проверке воркера и в сборке
представления страницы загрузки, — а групповая страница была бы третьим
местом.
- Страница намеренно **не самообновляется**: живая перерисовка карточек стёрла
бы выбор человека. Это и есть причина, по которой режим выбора не вешается на
основной список.
Не входит: групповое закрытие без файлов (`Dismiss`), автоматическая чистка по
сроку хранения, изменение самой операции удаления одной загрузки.
## Capabilities
### New Capabilities
Новых нет: групповое удаление — это новая поверхность существующего поведения,
а не новое поведение системы.
### Modified Capabilities
- `web-ui`: добавляется страница группового удаления с обязательным поимённым
подтверждением и поимённым отчётом об исходе; ссылка на неё в шапке.
- `state-reconciliation`: требование «Полное удаление загрузки пользователем»
уточняется — подтверждение может быть одно на пачку, но остаётся обязательным
и поимённым, а условия допустимости у каждой загрузки прежние и проверяются
для каждой отдельно.
## Impact
- `internal/httpapi`: страница выбора, страница подтверждения, обработчик
группового удаления и страница результата; ссылка в шапке.
- `web/templates`: новый шаблон страницы и её партиалы,
`partials/header.html`.
- `internal/store`: единая точка «в этом состоянии удаление разрешено».
- `internal/worker`: `Reviewer.Delete` зовётся по каждой выбранной загрузке;
сама операция не меняется.
- qBittorrent `torrents/delete` с `deleteFiles=true` — внешний сервис,
необратимая операция; число вызовов за один запрос человека растёт с одного
до числа выбранных.