## Why «Единое окно» реализовано наполовину: jellybit распознаёт **ручное** удаление и метит рассинхрон (`state-reconciliation` — `target_missing`/`orphaned`/`deleted`, безопасный `Undo` с гардом последней копии, preflight). Не хватает второй половины — **удалять загрузку из самого jellybit**, не идя руками в qBittorrent/Jellyfin. Основной сценарий одного окна — «досмотрел → освободить место»: убрать и раздачу, и файлы в библиотеке разом. Из-за хардлинков иначе место не вернуть — файл в `downloads/` (qBittorrent) и наша ссылка в библиотеке указывают на **один инод**, диск освобождается только когда исчезает последняя ссылка. Значит удаление обязано снять **обе** стороны. Это осознанный выход за инвариант «источник неприкосновенен», поэтому — с подтверждением и отдельным следом в логе/причине перехода. `Undo` и `delete` — разные операции, и это надо зафиксировать: - **Undo** — «перераспознать»: снимает только наши ссылки, раздачу в qBittorrent сохраняет, гард последней копии **включён**. Уже готов. - **delete** — «убрать окончательно, освободить место»: снимает наши ссылки И сносит раздачу+файлы из qBittorrent, гард **выключен**, состояние терминальное `deleted`. ## What Changes - Ввести пользовательскую команду **«Удалить»** в `worker`, доступную из `done`, `orphaned`, `target_missing` (подчистить остатки за собой из любого состояния «уже разложено / рассинхрон»). Действие: снять наши библиотечные хардлинки последнего батча (переиспользуем механику `Undo`, `superseded` пропускаем) с **выключенным** гардом последней копии **+** удалить раздачу с файлами из qBittorrent. Идемпотентно к отсутствующей стороне (в `orphaned` нет источника, в `target_missing` нет цели). - Переиспользовать существующее терминальное состояние `deleted` — **без** нового статуса; рёбра `done/orphaned/target_missing → deleted` уже есть в `allowedTransitions`. Инициатора-пользователя отличать от сверки через `error_code = "user_delete"` (сверка кладёт `"reconcile"`), человекочитаемую причину — в `error_msg` и лог перехода. - Добавить в `qbt` метод удаления раздачи (`POST /api/v2/torrents/delete`, `deleteFiles=true`) и объявить его в интерфейсе `QBittorrent` воркера. - Добавить в `layout` путь снятия ссылок **без** гарда последней копии (delete сознательно снимает последнюю копию, в отличие от `Undo`). - Обязательное **подтверждение** во всех транспортах: веб-UI — danger-секция с кнопкой в самом низу страницы загрузки; Telegram — двухшаговый inline-confirm. ## Capabilities ### New Capabilities ### Modified Capabilities - `state-reconciliation`: добавляется пользовательская команда «Удалить» (снять источник и цель, гард последней копии выключен) с переходом `done`/`orphaned`/`target_missing` → терминальный `deleted`, отличимая от reconcile-`deleted` инициатором, с обязательным подтверждением. ## Impact - Код воркера: `internal/worker/review.go` (новый метод `Delete`; путь снятия ссылок с выключенным гардом), `internal/worker/worker.go` (расширить интерфейс `QBittorrent` методом удаления; проброс в конструктор при необходимости). - Инфраструктура: `internal/qbt/qbt.go` (метод `Delete(hashes, deleteFiles)` → `/api/v2/torrents/delete`), `internal/layout/layout.go` (снятие ссылок без `ErrLastCopy`). - Транспорты: `internal/httpapi` (роут `POST /ui/downloads/{id}/delete` + confirm, флаг `Deletable` по состоянию; опц. JSON-роут), `web/templates/partials` (danger-секция внизу страницы загрузки), `internal/tgbot` (действие `delete` + двухшаговый confirm-keyboard). - Данные: **осознанное** снятие последней копии данных при удалении из `orphaned` — только по явному подтверждению; инвариант «источник неприкосновенен» обходится намеренно и логируется как пользовательское удаление. Схема БД не меняется (переиспользуем `error_code`/`error_msg`). - Тесты: `internal/worker/review_test.go` (delete из каждого из трёх состояний, идемпотентность к отсутствующей стороне, отказ из прочих состояний), `internal/qbt/qbt_test.go` (форма запроса delete), транспортные тесты confirm.