Единое окно: полное пользовательское удаление загрузки (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:
@@ -443,3 +443,94 @@ SHALL задевать раскладку в полёте.
|
||||
- **WHEN** выполняется тик поллинга
|
||||
- **THEN** восстановление `linking` их состояние не меняет
|
||||
|
||||
### Requirement: Полное удаление загрузки пользователем
|
||||
|
||||
Система SHALL предоставлять пользователю команду **«Удалить»** (delete),
|
||||
доступную из состояний `done`, `orphaned` и `target_missing` во всех транспортах
|
||||
(веб-UI и Telegram, опц. REST). Команда SHALL снимать **обе** стороны загрузки —
|
||||
целевые библиотечные хардлинки И раздачу с файлами в qBittorrent — и переводить
|
||||
задачу в терминальное `deleted`. Из прочих состояний команда доступна SHALL NOT.
|
||||
|
||||
Снятие цели SHALL идти по механике снятия ссылок последнего батча (как в `Undo`:
|
||||
`superseded` пропускаются как забранные другой загрузкой), но **отдельным путём с
|
||||
выключенным** гардом последней копии — не переиспользуя guarded-`Undo`: в отличие
|
||||
от `Undo`, delete SHALL снимать целевую ссылку, даже если она — последняя копия
|
||||
данных (`nlink <= 1`). Это осознанный выход за
|
||||
инвариант «источник неприкосновенен», поэтому delete SHALL требовать явного
|
||||
**подтверждения** пользователя перед выполнением и SHALL NOT срабатывать по
|
||||
одиночному клику/тапу. Снятие цели SHALL затрагивать только собственные ссылки
|
||||
загрузки строго под `paths.movies`/`series`; файлы источника под
|
||||
`paths.downloads` система сама трогать SHALL NOT — их удаляет qBittorrent по
|
||||
вызову API с `deleteFiles=true`.
|
||||
|
||||
В отличие от прочих команд, требующих источника, delete синхронный source-preflight
|
||||
выполнять SHALL NOT и под требование «Принудительная проверка источника/цели перед
|
||||
действием» не подпадает: цель delete — снять источник, поэтому его отсутствие
|
||||
трактуется как уже снятая сторона, а не как повод привести состояние сверкой и
|
||||
отказать. Удаление SHALL быть идемпотентным к отсутствующей стороне: в `orphaned`
|
||||
(нет источника) отсутствие раздачи в qBittorrent ошибкой считаться SHALL NOT; в
|
||||
`target_missing` (нет цели) пустой список живых ссылок обрабатывается как «нечего
|
||||
снимать». Если qBittorrent вернул ошибку при удалении присутствующей раздачи,
|
||||
система в `deleted` переходить SHALL NOT (не заявляем освобождение места, которого
|
||||
не произошло), SHALL сообщить пользователю причину отказа (это не `ErrConflict`,
|
||||
а ошибка внешнего сервиса — транслируется как таковая), и повторный delete
|
||||
идемпотентно дожимает удаление, опираясь на оставшийся `done` либо приведённый
|
||||
сверкой к реальности `target_missing` (кратковременное рассогласование до тика
|
||||
сверки ожидаемо).
|
||||
|
||||
Инициатора перехода в `deleted` система SHALL отличать от фоновой сверки:
|
||||
пользовательское удаление SHALL помечаться `error_code = "user_delete"` (сверка
|
||||
кладёт `"reconcile"`), человекочитаемую причину — в `error_msg` и лог перехода.
|
||||
Новый статус для этого система вводить SHALL NOT — переиспользуется существующее
|
||||
терминальное `deleted` (сверка его не переоценивает, см. требование о `deleted`).
|
||||
|
||||
#### Scenario: Удаление из done снимает обе стороны и освобождает место
|
||||
|
||||
- **GIVEN** задача в `done`: раздача присутствует в qBittorrent, её библиотечные
|
||||
хардлинки существуют
|
||||
- **WHEN** пользователь подтверждает «Удалить»
|
||||
- **THEN** библиотечные ссылки последнего батча снимаются
|
||||
- **AND** раздача с файлами удаляется из qBittorrent (`deleteFiles=true`)
|
||||
- **AND** задача переходит в `deleted` с `error_code = "user_delete"`
|
||||
|
||||
#### Scenario: Удаление из orphaned снимает последнюю копию осознанно
|
||||
|
||||
- **GIVEN** задача в `orphaned`: источник пропал, библиотечный хардлинк остался
|
||||
единственной копией данных (`nlink <= 1`)
|
||||
- **WHEN** пользователь подтверждает «Удалить»
|
||||
- **THEN** библиотечная ссылка снимается несмотря на то, что она последняя копия
|
||||
(гард последней копии выключен, в отличие от `Undo`)
|
||||
- **AND** отсутствие раздачи в qBittorrent ошибкой не считается
|
||||
- **AND** задача переходит в `deleted`
|
||||
|
||||
#### Scenario: Удаление из target_missing сносит остаточную раздачу
|
||||
|
||||
- **GIVEN** задача в `target_missing`: источник присутствует, цель уже удалена
|
||||
вручную
|
||||
- **WHEN** пользователь подтверждает «Удалить»
|
||||
- **THEN** снятие цели идемпотентно (живых ссылок нет)
|
||||
- **AND** раздача с файлами удаляется из qBittorrent
|
||||
- **AND** задача переходит в `deleted`
|
||||
|
||||
#### Scenario: Удаление требует подтверждения
|
||||
|
||||
- **GIVEN** задача в `done`
|
||||
- **WHEN** пользователь инициирует «Удалить», но не подтверждает действие
|
||||
- **THEN** ни ссылки, ни раздача не удаляются, состояние остаётся `done`
|
||||
|
||||
#### Scenario: Удаление недоступно из прочих состояний
|
||||
|
||||
- **GIVEN** задача в `review` (или ином состоянии вне `done`/`orphaned`/
|
||||
`target_missing`)
|
||||
- **WHEN** приходит команда «Удалить»
|
||||
- **THEN** команда отклоняется с конфликтом, состояние не меняется
|
||||
|
||||
#### Scenario: Ошибка qBittorrent не метит deleted ложно
|
||||
|
||||
- **GIVEN** задача в `done`, раздача присутствует, но qBittorrent вернул ошибку
|
||||
на удаление
|
||||
- **WHEN** пользователь подтверждает «Удалить»
|
||||
- **THEN** задача в `deleted` не переходит (место не освобождено)
|
||||
- **AND** пользователю сообщается причина отказа (ошибка qBittorrent, не тихий успех)
|
||||
- **AND** повторный delete идемпотентно дожимает удаление
|
||||
|
||||
|
||||
Reference in New Issue
Block a user