- Раскладка docs/ приведена к канону 2: заведены passport/architecture/ database/security/review и research; docs/specs, drafts, backlog, review/ и BRIEF.md разобраны и удалены, беклог переехал в docs/tasks (34 задачи, 6 целей, слаги на английский). - Нарративы specs удалены как дубли openspec-спек после поимённой сверки; остаток заведён задачами (редактор маппинга ревью, крайние случаи именования), отказ от сущности title промоутнут в ADR. - Проектные копии агентов и скиллов ревью/пайплайна удалены в пользу плагинов av-dev-pm и av-dev-pipeline; в task gate добавлен шаг canon вместо er-schema.
82 lines
6.2 KiB
Markdown
82 lines
6.2 KiB
Markdown
## 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/tasks/items/udalenie-edinoe-okno.md` и строку из индекса беклога.
|