Files
avandClaude Opus 4.8 3f1a928000 Единое окно: полное пользовательское удаление загрузки (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>
2026-07-10 12:17:51 +03:00

6.2 KiB
Raw Permalink Blame History

Why

«Единое окно» реализовано наполовину: jellybit распознаёт ручное удаление и метит рассинхрон (state-reconciliationtarget_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.