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

82 lines
6.2 KiB
Markdown
Raw 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.
## 1. qbt — удаление раздачи
- [x] 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/пустой
ответ — успех, отсутствие хеша ошибкой не считать.
- [x] 1.2 Тест `internal/qbt/qbt_test.go`: форма запроса delete (hashes через `|`,
`deleteFiles=true`), обработка ответа.
## 2. layout — снятие ссылок без гарда последней копии
- [x] 2.1 Добавить в `Layouter` и реализацию `internal/layout/layout.go` путь
снятия целевых ссылок **без** `ErrLastCopy` (напр. `Remove(ctx, links)`):
безусловный `unlink` только своих ссылок строго под `paths.movies`/`series`;
санитизация/проверка «под библиотекой» сохраняется, `paths.downloads` не
трогаем.
## 3. worker — команда Delete
- [x] 3.1 Расширить интерфейс `QBittorrent` воркера методом `Delete(...)`
(`internal/worker/worker.go`); проброс реализации `qbt.Client`.
- [x] 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`) НЕ вызывает —
толерантен к отсутствию источника.
- [x] 3.3 Логировать удаление как осознанное пользовательское снятие источника
(`slog`, без секретов): id, состояние-источник, снятые ссылки, `code=user_delete`.
## 4. Транспорт HTTP/веб + подтверждение
- [x] 4.1 Роут `POST /ui/downloads/{id}/delete` → хендлер (по образцу
`handleUndo`), финализация через `surfaceAction`; трансляция `ErrConflict`
409/фрагмент как у прочих действий, а ошибка qBittorrent (не `ErrConflict`) —
внятный отказ пользователю (не тихий успех), не 200-без-эффекта.
- [x] 4.2 Флаг `Deletable` (состояние ∈ {done, orphaned, target_missing}) в
расчётах кнопок (`httpapi.go:648`, `download.go:108`).
- [x] 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 + подтверждение
- [x] 5.1 В `internal/tgbot` добавить действие `delete`: callback `delete:<id>`
показывает confirm-keyboard (`delete_confirm:<id>` / «Отмена»); удаление — на
подтверждающем callback (`Reviewer.Delete`). Кнопка «Удалить» — в карточке
задачи для состояний done/orphaned/target_missing.
- [x] 5.2 Ветка ошибок как у прочих действий (`ErrConflict` → понятный отказ;
ошибка qBittorrent → отдельное сообщение об отказе, не тихий успех), после
успеха — `answer` + `refreshCard`.
## 6. Тесты воркера
- [x] 6.1 Delete из `done`: сняты ссылки + вызван `qbt.Delete(deleteFiles=true)`
+ переход `deleted`/`user_delete`.
- [x] 6.2 Delete из `orphaned`: последняя копия снята (гард выключен), отсутствие
раздачи не ошибка, переход `deleted`.
- [x] 6.3 Delete из `target_missing`: снятие цели идемпотентно (нет живых ссылок),
раздача снесена, переход `deleted`.
- [x] 6.4 Delete из `review`/иного — отказ `ErrConflict`, состояние не меняется.
- [x] 6.5 Ошибка `qbt.Delete` при присутствующей раздаче — задача остаётся, в
`deleted` не уходит; повторный delete идемпотентен.
## 7. Ревью и сверка
- [x] 7.1 `task test` и `task lint` зелёные.
- [x] 7.2 Ревью кода (второй чекпоинт) перед archive.
- [x] 7.3 `openspec validate download-delete --strict` зелёный.
- [x] 7.4 Синк нарратива `docs/specs/workflow.md`: явные пользовательские рёбра
`done/orphaned/target_missing → deleted` (delete) и различие Undo vs delete;
удалить `docs/backlog/udalenie-edinoe-okno.md` и строку из индекса беклога.