## 1. qbt — удаление раздачи
- [x] 1.1 В `internal/qbt/qbt.go` добавить `Delete(ctx, hashes []string, deleteFiles bool) error`:
`POST /api/v2/torrents/delete`, форма `hashes=
||…` + `deleteFiles`.
Логирование `ExtCall{Operation:"torrents/delete"}` как у `Add`; 2xx/пустой
ответ — успех, отсутствие хеша ошибкой не считать.
- [x] 1.2 Тест `internal/qbt/qbt_test.go`: форма запроса delete (hashes через `|`,
`deleteFiles=true`), обработка ответа.
## 2. layout — снятие ссылок без гарда последней копии
- [x] 2.1 Добавить в `Layouter` и реализацию `internal/layout/layout.go` путь
снятия целевых ссылок **без** `ErrLastCopy` (напр. `Remove(ctx, links)`):
безусловный `unlink` только своих ссылок строго под `paths.movies`/`series`;
санитизация/проверка «под библиотекой» сохраняется, `paths.downloads` не
трогаем.
## 3. worker — команда Delete
- [x] 3.1 Расширить интерфейс `QBittorrent` воркера методом `Delete(...)`
(`internal/worker/worker.go`); проброс реализации `qbt.Client`.
- [x] 3.2 В `internal/worker/review.go` добавить `Delete(ctx, id string) error`:
под `w.mu`; `GetDownload`; предусловие `d.State ∈ {done, orphaned,
target_missing}`, иначе `ErrConflict`. Порядок: (а) снять цель как в `Undo`
(`LatestBatchID`/`ListFileLinksByBatch`/фильтр `isLaidOut`, `superseded`
пропускаем) через новый `layouter.Remove`, затем `DeleteFileLinksByBatch`; (б)
`qbt.Delete(d.Infohashes, deleteFiles=true)` (хеши уже загружены с задачей,
отдельный store-геттер не нужен) — ошибку присутствующей раздачи пробросить
(не метить `deleted`), отсутствие раздачи ошибкой не считать; (в)
`transition(ctx, d, StateDeleted, "user_delete", <причина>)`. Delete
source-preflight (`ensureSourceReady`/`reconcileToReality`) НЕ вызывает —
толерантен к отсутствию источника.
- [x] 3.3 Логировать удаление как осознанное пользовательское снятие источника
(`slog`, без секретов): id, состояние-источник, снятые ссылки, `code=user_delete`.
## 4. Транспорт HTTP/веб + подтверждение
- [x] 4.1 Роут `POST /ui/downloads/{id}/delete` → хендлер (по образцу
`handleUndo`), финализация через `surfaceAction`; трансляция `ErrConflict` →
409/фрагмент как у прочих действий, а ошибка qBittorrent (не `ErrConflict`) —
внятный отказ пользователю (не тихий успех), не 200-без-эффекта.
- [x] 4.2 Флаг `Deletable` (состояние ∈ {done, orphaned, target_missing}) в
расчётах кнопок (`httpapi.go:648`, `download.go:108`).
- [x] 4.3 Danger-секция в самом низу `web/templates/partials/download_main.html`:
кнопка «Удалить» → confirm (htmx-своп фрагмента «Точно удалить? …
[Да, удалить] [Отмена]») → `POST …/delete`. Деградация без htmx
(``/отдельный шаг), ошибка на htmx-пути = 200 + фрагмент.
- [ ] 4.4 (Опц., НЕ в этом change) JSON-роут `POST /api/downloads/{id}/delete`.
Осознанно отложено: REST в спеке помечен опциональным, а деструктивный endpoint
без естественного шага подтверждения заводим только по реальной нужде.
## 5. Транспорт Telegram + подтверждение
- [x] 5.1 В `internal/tgbot` добавить действие `delete`: callback `delete:`
показывает confirm-keyboard (`delete_confirm:` / «Отмена»); удаление — на
подтверждающем callback (`Reviewer.Delete`). Кнопка «Удалить» — в карточке
задачи для состояний done/orphaned/target_missing.
- [x] 5.2 Ветка ошибок как у прочих действий (`ErrConflict` → понятный отказ;
ошибка qBittorrent → отдельное сообщение об отказе, не тихий успех), после
успеха — `answer` + `refreshCard`.
## 6. Тесты воркера
- [x] 6.1 Delete из `done`: сняты ссылки + вызван `qbt.Delete(deleteFiles=true)`
+ переход `deleted`/`user_delete`.
- [x] 6.2 Delete из `orphaned`: последняя копия снята (гард выключен), отсутствие
раздачи не ошибка, переход `deleted`.
- [x] 6.3 Delete из `target_missing`: снятие цели идемпотентно (нет живых ссылок),
раздача снесена, переход `deleted`.
- [x] 6.4 Delete из `review`/иного — отказ `ErrConflict`, состояние не меняется.
- [x] 6.5 Ошибка `qbt.Delete` при присутствующей раздаче — задача остаётся, в
`deleted` не уходит; повторный delete идемпотентен.
## 7. Ревью и сверка
- [x] 7.1 `task test` и `task lint` зелёные.
- [x] 7.2 Ревью кода (второй чекпоинт) перед archive.
- [x] 7.3 `openspec validate download-delete --strict` зелёный.
- [x] 7.4 Синк нарратива `docs/specs/workflow.md`: явные пользовательские рёбра
`done/orphaned/target_missing → deleted` (delete) и различие Undo vs delete;
удалить `docs/tasks/items/udalenie-edinoe-okno.md` и строку из индекса беклога.