- выбор → поимённое подтверждение → отчёт: пачка до 20 загрузок, гарды входа на обеих границах, потолок времени и остановка после трёх подряд отказов внешнего сервиса - допуск полного удаления сведён в единую точку store.State.CanDelete() — worker, страница загрузки и Telegram больше не держат своих перечней
152 lines
13 KiB
Markdown
152 lines
13 KiB
Markdown
## 1. Единая точка допуска
|
||
|
||
- [x] 1.1 Завести `(store.State).CanDelete()` рядом с `IsTerminal`/
|
||
`IsObservable`, перечень `done`/`orphaned`/`target_missing` — одним списком
|
||
- [x] 1.2 Перевести `switch` в `worker.Delete` на `CanDelete` (проверку в ядре
|
||
не снимать)
|
||
- [x] 1.3 Перевести сборку `Deletable` в `internal/httpapi/download.go` на
|
||
`CanDelete`
|
||
- [x] 1.4 Перевести выбор клавиатуры в `internal/tgbot/render.go` на
|
||
`CanDelete` — иначе Telegram остаётся четвёртым перечнем состояний
|
||
- [x] 1.5 Тест: `CanDelete` истинно ровно для трёх состояний и ложно для
|
||
остальных (перебор всех состояний)
|
||
- [x] 1.6 Тест `internal/tgbot`: клавиатура с действием удаления приходит ровно
|
||
для тех состояний, где `CanDelete` истинно
|
||
|
||
## 2. Чтение списка разрешённых к удалению
|
||
|
||
- [x] 2.1 Добавить в `Reader` метод выборки загрузок, разрешённых к удалению,
|
||
без пагинации; порядок — как в основном списке
|
||
- [x] 2.2 Реализовать выборку в `store` через существующую механику фильтра по
|
||
состояниям (перечень состояний берётся из единой точки, второго списка не
|
||
заводить)
|
||
- [x] 2.3 Тест `store`: выборка возвращает только `done`/`orphaned`/
|
||
`target_missing`
|
||
|
||
## 3. Страница выбора
|
||
|
||
- [x] 3.1 Шаблон страницы `/delete`: строки с чекбоксами, заголовок, состояние
|
||
каждой загрузки, кнопка «Удалить выбранные», названный предел размера пачки
|
||
рядом с кнопкой, пустое состояние
|
||
- [x] 3.2 Ссылка на страницу в `web/templates/partials/header.html`, активный
|
||
пункт навигации
|
||
- [x] 3.3 Обработчик `GET /delete`: сборка представления, без `hx-*`
|
||
самообновления
|
||
- [x] 3.4 Тест: подставной читатель отдаёт задачи во всех состояниях — в
|
||
разметке есть строки только у `done`/`orphaned`/`target_missing`
|
||
- [x] 3.5 Тест: в разметке страницы нет `hx-trigger="every`
|
||
- [x] 3.6 Тест: страница называет предел размера пачки
|
||
- [x] 3.7 Тест: пустое состояние — нет разрешённых, удаление не предлагается
|
||
|
||
## 4. Разбор входа (общий для обеих границ)
|
||
|
||
- [x] 4.1 Общая функция разбора пачки: `ident.Parse` по каждому идентификатору
|
||
с отказом всего запроса, схлопывание дублей, проверка предела, отказ на пустом
|
||
наборе. Предел — именованная константа, значение **20**
|
||
- [x] 4.2 Обработчик `POST /ui/delete/confirm`: разбор через общую функцию,
|
||
чтение выбранных загрузок; ненайденная запись — строка «загрузка не найдена»,
|
||
а не молчаливый пропуск
|
||
- [x] 4.3 Отказ по пределу возвращает страницу выбора с сохранёнными отметками
|
||
и объяснением
|
||
- [x] 4.4 Шаблон страницы подтверждения: каждая выбранная загрузка названа
|
||
заголовком, идентификатором и состоянием; для `orphaned` — отметка «источник
|
||
пропал, библиотечная ссылка — последняя копия данных»; скрытые поля с
|
||
идентификаторами, признак подтверждения, кнопка исполнения и ссылка возврата
|
||
- [x] 4.5 Тест: страница подтверждения содержит заголовки всех выбранных и не
|
||
делает ни одного вызова `Delete`
|
||
- [x] 4.6 Тест: строка `orphaned` на подтверждении несёт отметку о последней
|
||
копии
|
||
- [x] 4.7 Тест: пачка сверх предела отклоняется целиком, `Delete` не зовётся, а
|
||
ответ несёт страницу выбора с сохранёнными отметками
|
||
- [x] 4.8 Тест: неразобранный идентификатор отклоняет запрос целиком
|
||
- [x] 4.9 Тест: пустой выбор — подтверждение не показывается, `Delete` не
|
||
зовётся
|
||
- [x] 4.10 Тест: идентификатор без записи назван строкой на подтверждении и в
|
||
отчёте, остальные загрузки удалены
|
||
|
||
## 5. Исполнение и отчёт
|
||
|
||
- [x] 5.1 Обработчик `POST /ui/delete`: отказ без признака подтверждения **до**
|
||
цикла, ни одного вызова `Delete`
|
||
- [x] 5.2 Тот же разбор входа, что и на подтверждении (пункт 4.1): исполняющий
|
||
запрос — самостоятельная входная граница
|
||
- [x] 5.3 Последовательный вызов `Reviewer.Delete` по каждой подтверждённой
|
||
загрузке, сбор исхода по каждой; контекст исполнения отвязан от `r.Context()`
|
||
(`context.WithoutCancel`); ошибка — через публичный канал (`userErr`)
|
||
- [x] 5.3a Потолок времени на проход (`bulkBudget`, 2 минуты), проверяемый
|
||
между единицами: начатое удаление не обрывается. Остаток — в отчёт строками
|
||
«не выполнено»
|
||
- [x] 5.4 Остановка прохода после **трёх подряд** отказов внешнего сервиса
|
||
(`ErrConflict` системным не считается и счётчик не двигает; успех счётчик
|
||
сбрасывает); остаток пачки — в отчёт строками «не выполнено» с причиной
|
||
остановки. Порог — именованная константа
|
||
- [x] 5.5 Шаблон страницы результата: удалённые поимённо, отказавшие поимённо с
|
||
причиной, невыполненный остаток и причина остановки, ссылка обратно на
|
||
`/delete`
|
||
- [x] 5.6 Тест: POST без признака подтверждения — отказ, ноль вызовов `Delete`
|
||
- [x] 5.7 Тест: гарды исполняющего запроса не слабее гардов подтверждения —
|
||
неразобранный идентификатор, пачка сверх предела и пустой набор отклоняются с
|
||
признаком подтверждения тоже, `Delete` не зовётся
|
||
- [x] 5.8 Тест: второй `Delete` возвращает ошибку — первая и третья удалены,
|
||
страница результата называет вторую и её причину
|
||
- [x] 5.9 Тест: `Delete` для `downloading` возвращает `ErrConflict` — страница
|
||
показывает отказ по ней, остальные выбранные удалены
|
||
- [x] 5.10 Тест: все удалены — отчёт называет обе как удалённые, отказов нет
|
||
- [x] 5.11 Тест: отмена запроса не прекращает пачку — `Delete` вызван по всем
|
||
подтверждённым
|
||
- [x] 5.12 Тест: сырой текст ошибки внешнего сервиса в разметку не попадает
|
||
- [x] 5.13 Тест: страница и действия работают без htmx (обычные POST-формы,
|
||
`action` рабочий)
|
||
|
||
## 6. Сдача
|
||
|
||
- [x] 6.1 `openspec validate --strict bulk-delete-page`
|
||
- [x] 6.2 `task gate` зелёный
|
||
- [x] 6.3 Поведенческая проверка вживую: поднять `task run`, пройти путь
|
||
список → подтверждение → результат
|
||
- [x] 6.4 Три числовых порога (размер пачки, число подряд идущих отказов,
|
||
потолок времени на проход) — в `docs/database.md`, раздел настроек с
|
||
числовым значением
|
||
|
||
## Критерии приёмки (из постановки)
|
||
|
||
- [x] П1 Страница открывается из шапки и показывает только те загрузки, для
|
||
которых удаление разрешено поштучно (оракул: тест `internal/httpapi` —
|
||
подставной читатель отдаёт задачи во всех состояниях, в разметке строки есть
|
||
у `done`/`orphaned`/`target_missing` и нет у остальных)
|
||
- [x] П2 Удаление уходит только после явного подтверждения, и подтверждение
|
||
называет каждую выбранную раздачу поимённо (оракул: тест — POST без признака
|
||
подтверждения отвечает отказом и не делает ни одного вызова `Delete` у
|
||
подставного воркера; ответ подтверждения содержит заголовки всех выбранных)
|
||
- [x] П3 Отказ на одной загрузке не отменяет остальных, а результат называет
|
||
удалённые и отказавшие поимённо с причиной (оракул: тест, где второй `Delete`
|
||
возвращает ошибку — первая и третья удалены, страница результата называет
|
||
вторую и её причину)
|
||
- [x] П4 Групповой путь не расширяет прав поштучного (оракул: тест — `Delete`
|
||
для `downloading` возвращает `ErrConflict`, страница показывает отказ,
|
||
остальные выбранные не затронуты)
|
||
- [x] П5 Поведение страницы записано дельта-спекой и проходит валидацию
|
||
(оракул: `openspec validate --strict` и `task gate`)
|
||
|
||
## Приёмочные критерии из рубрики ревью дизайна
|
||
|
||
- [x] Р1 Исполняется ровно подтверждённое множество, названное списком
|
||
идентификаторов, а не предикатом «всё, что сейчас в состоянии X» (оракул:
|
||
тест — задача, ставшая разрешённой после показа подтверждения, не удаляется)
|
||
- [x] Р2 Множество допустимых состояний имеет единственный дом, и групповой путь
|
||
прав не расширяет (оракул: пункты 1.5, 1.6, 5.9)
|
||
- [x] Р3 Экран подтверждения называет не только предмет, но и последствие —
|
||
состояние и снятие последней копии для `orphaned` (оракул: пункт 4.6)
|
||
- [x] Р4 Вход валидируется целиком до первого эффекта, на обеих границах
|
||
(оракул: пункты 4.7–4.9, 5.7)
|
||
- [x] Р5 Частичный отказ не отменяет остальных, повтор идемпотентен (оракул:
|
||
пункты 5.8, 5.9; повтор — уже `deleted`, `CanDelete` ложно)
|
||
- [x] Р6 Обрыв или отмена запроса не оставляет пачку на середине, исход каждой
|
||
единицы переживает потерю ответа (оракул: пункт 5.11 и `logCmd` по каждому
|
||
вызову)
|
||
- [x] Р7 Предел пачки виден там, где формируется вход, и отказ по нему не стирает
|
||
выбор (оракул: пункты 3.6, 4.7)
|
||
- [x] Р8 Транспорт доменной логики не содержит: зовётся тот же
|
||
`Reviewer.Delete`, переход состояния — в воркере (оракул: чтение диффа на
|
||
ревью кода)
|