Единое окно: полное пользовательское удаление загрузки (delete)
Вторая половина «единого окна»: команда «Удалить» снимает наши библиотечные хардлинки (гард последней копии осознанно выключен, в отличие от Undo) и сносит раздачу с файлами из qBittorrent (deleteFiles=true) → терминальный deleted. Доступна из done/orphaned/target_missing, идемпотентна к отсутствующей стороне; инициатор различается через error_code=user_delete. Подтверждение обязательно: веб — danger-секция внизу страницы (hx-confirm + details), Telegram — двухшаговый inline-confirm. qbt.Delete + layout.Remove (unlink без ErrLastCopy, только свои ссылки под movies/series). Граф переходов не менялся — рёбра уже были. OpenSpec: state-reconciliation +1 требование; синк workflow.md; беклог закрыт. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
## 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
|
||||
<!-- нет новых capability -->
|
||||
|
||||
### 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.
|
||||
Reference in New Issue
Block a user