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

7.6 KiB
Raw Permalink Blame History

ADDED Requirements

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 идемпотентно дожимает удаление