Files
jellybit/openspec/changes/archive/2026-07-10-download-delete/tasks.md
T
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 Blame History

1. qbt — удаление раздачи

  • 1.1 В internal/qbt/qbt.go добавить Delete(ctx, hashes []string, deleteFiles bool) error: POST /api/v2/torrents/delete, форма hashes=<h1>|<h2>|… + deleteFiles. Логирование ExtCall{Operation:"torrents/delete"} как у Add; 2xx/пустой ответ — успех, отсутствие хеша ошибкой не считать.
  • 1.2 Тест internal/qbt/qbt_test.go: форма запроса delete (hashes через |, deleteFiles=true), обработка ответа.

2. layout — снятие ссылок без гарда последней копии

  • 2.1 Добавить в Layouter и реализацию internal/layout/layout.go путь снятия целевых ссылок без ErrLastCopy (напр. Remove(ctx, links)): безусловный unlink только своих ссылок строго под paths.movies/series; санитизация/проверка «под библиотекой» сохраняется, paths.downloads не трогаем.

3. worker — команда Delete

  • 3.1 Расширить интерфейс QBittorrent воркера методом Delete(...) (internal/worker/worker.go); проброс реализации qbt.Client.
  • 3.2 В internal/worker/review.go добавить Delete(ctx, id string) error: под w.mu; GetDownload; предусловие d.State ∈ {done, orphaned, target_missing}, иначе ErrConflict. Порядок: (а) снять цель как в Undo (LatestBatchID/ListFileLinksByBatch/фильтр isLaidOut, superseded пропускаем) через новый layouter.Remove, затем DeleteFileLinksByBatch; (б) qbt.Delete(d.Infohashes, deleteFiles=true) (хеши уже загружены с задачей, отдельный store-геттер не нужен) — ошибку присутствующей раздачи пробросить (не метить deleted), отсутствие раздачи ошибкой не считать; (в) transition(ctx, d, StateDeleted, "user_delete", <причина>). Delete source-preflight (ensureSourceReady/reconcileToReality) НЕ вызывает — толерантен к отсутствию источника.
  • 3.3 Логировать удаление как осознанное пользовательское снятие источника (slog, без секретов): id, состояние-источник, снятые ссылки, code=user_delete.

4. Транспорт HTTP/веб + подтверждение

  • 4.1 Роут POST /ui/downloads/{id}/delete → хендлер (по образцу handleUndo), финализация через surfaceAction; трансляция ErrConflict → 409/фрагмент как у прочих действий, а ошибка qBittorrent (не ErrConflict) — внятный отказ пользователю (не тихий успех), не 200-без-эффекта.
  • 4.2 Флаг Deletable (состояние ∈ {done, orphaned, target_missing}) в расчётах кнопок (httpapi.go:648, download.go:108).
  • 4.3 Danger-секция в самом низу web/templates/partials/download_main.html: кнопка «Удалить» → confirm (htmx-своп фрагмента «Точно удалить? … [Да, удалить] [Отмена]») → POST …/delete. Деградация без htmx (<details>/отдельный шаг), ошибка на htmx-пути = 200 + фрагмент.
  • 4.4 (Опц., НЕ в этом change) JSON-роут POST /api/downloads/{id}/delete. Осознанно отложено: REST в спеке помечен опциональным, а деструктивный endpoint без естественного шага подтверждения заводим только по реальной нужде.

5. Транспорт Telegram + подтверждение

  • 5.1 В internal/tgbot добавить действие delete: callback delete:<id> показывает confirm-keyboard (delete_confirm:<id> / «Отмена»); удаление — на подтверждающем callback (Reviewer.Delete). Кнопка «Удалить» — в карточке задачи для состояний done/orphaned/target_missing.
  • 5.2 Ветка ошибок как у прочих действий (ErrConflict → понятный отказ; ошибка qBittorrent → отдельное сообщение об отказе, не тихий успех), после успеха — answer + refreshCard.

6. Тесты воркера

  • 6.1 Delete из done: сняты ссылки + вызван qbt.Delete(deleteFiles=true)
    • переход deleted/user_delete.
  • 6.2 Delete из orphaned: последняя копия снята (гард выключен), отсутствие раздачи не ошибка, переход deleted.
  • 6.3 Delete из target_missing: снятие цели идемпотентно (нет живых ссылок), раздача снесена, переход deleted.
  • 6.4 Delete из review/иного — отказ ErrConflict, состояние не меняется.
  • 6.5 Ошибка qbt.Delete при присутствующей раздаче — задача остаётся, в deleted не уходит; повторный delete идемпотентен.

7. Ревью и сверка

  • 7.1 task test и task lint зелёные.
  • 7.2 Ревью кода (второй чекпоинт) перед archive.
  • 7.3 openspec validate download-delete --strict зелёный.
  • 7.4 Синк нарратива docs/specs/workflow.md: явные пользовательские рёбра done/orphaned/target_missing → deleted (delete) и различие Undo vs delete; удалить docs/backlog/udalenie-edinoe-okno.md и строку из индекса беклога.