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

75 lines
6.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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.