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