Вторая половина «единого окна»: команда «Удалить» снимает наши библиотечные хардлинки (гард последней копии осознанно выключен, в отличие от 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>
75 lines
6.2 KiB
Markdown
75 lines
6.2 KiB
Markdown
## 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.
|