закрыта задача bulk-delete-page, заведены три задачи из урожая ревью
This commit is contained in:
+3
-1
@@ -27,7 +27,8 @@
|
||||
- [✨ Переделать веб-UI в устанавливаемое PWA](items/web-ui-pwa.md) — текущий server-rendered UI функционален — PWA (устанавливаемое, удобное с телефона) это улучшение большого объёма, не блокер
|
||||
- [✨ Править на ревью маппинг «файл → серия» и раскладывать вручную при провале LLM](items/review-mapping-editor.md) — правка S·E, «нумеровать подряд» и ручной режим при полном провале LLM были запланированы объёмом Ф5 и не заведены задачей — в ревью сегодня можно только подсказать текстом
|
||||
- [✨ Заказать спекой крайние случаи именования: многофайловый фильм, редакции, двойная серия](items/naming-edge-cases.md) — стэкинг частей (part1/cd1), редакции [edition-…] и двойная серия SxxEyy-Eyy описаны нарративом, но в file-layout не заказаны — раскладка таких раздач не определена
|
||||
- [✨ Удалять выбранные загрузки с файлами на отдельной странице](items/bulk-delete-page.md) — удаление с файлами живёт только в danger-секции страницы одной загрузки: чтобы снести десять раздач, надо десять раз пройти путь список → карточка → подтверждение
|
||||
- [🐞 Не сносить раздачу, которой владеет другая активная загрузка](items/delete-checks-active-infohash-owner.md) — удаление старой закрытой задачи уничтожает файлы живой загрузки с тем же инфохэшем; воспроизведено падающим тестом на ревью bulk-delete-page
|
||||
- [🐞 Не оставлять задачу в done, когда ссылки сняты, а раздача не снесена](items/delete-leaves-stale-done.md) — при недоступном qBittorrent задача весь простой соседа показывает done, хотя тайтла в Jellyfin уже нет: сверка падает на первом шаге и до коррекции не доходит
|
||||
- [🔬 Канон нумерации серий и порядок у провайдера тега](items/episode-numbering-canon.md) — Косметика/редкость: порядок просмотра ок, но у тайтлов со спорным порядком (Бибоп) Jellyfin подтягивает не те подписи серий, если канон файлов ≠ дефолтный порядок провайдера тега
|
||||
- [🔬 Тексты и формат уведомлений в Telegram](items/telegram-messages-audit.md) — зонтичный проход по всем текстам бота: полнота карточек, единый язык, оформление; порождает под-задачи
|
||||
- [🔬 guessit как сервис-спутник](items/guessit-sidecar.md) — go-ptn слабее питоновского guessit — если точности пред-парса не хватит, завернуть guessit в сервис-спутник рядом с бинарём
|
||||
@@ -48,6 +49,7 @@
|
||||
- [✨ Чистить БД от терминальных задач и сырых ответов LLM старше срока хранения](items/db-retention-cleanup.md) — терминальные задачи и сырые ответы LLM копятся вечно — без авточистки список загрузок и БД деградируют по мере эксплуатации
|
||||
- [🧹 Свести термины домена в словарь единого языка](items/ubiquitous-language-glossary.md) — наименования домена расходятся между спеками, UI и кодом — нет единого глоссария (на нём же стоит агент-ревьювер наименований)
|
||||
- [🧹 Разобрать кандидатов по тестам и записать конвенцию](items/tests-convention.md) — как пишем тесты, не записано нигде: пункт «Тесты» в convention-candidates не пересматривали, он обещает фикстуры в testdata/, которых в проекте нет — а трение накопилось (четыре внешних клиента, fakeStore с инъекцией ошибок, env-гейты, флаки-прогон, diff-coverage)
|
||||
- [🐞 Не держать общий замок воркера во время обращения к qBittorrent при удалении](items/delete-holds-worker-lock.md) — на время удаления встаёт весь фон: опрос, сверка и команды остальных транспортов ждут замок до 30 с на загрузку, пачкой — минутами; замерено на ревью bulk-delete-page
|
||||
- [🔬 Потолок нагрузки: 100 одновременных загрузок, план-максимум 1000](items/scale-100-downloads.md) — Зафиксировать в НФТ ориентир 100/1000 загрузок + аудит узких мест (SQLite, воркер, поллинг)
|
||||
- [🔬 Завершение загрузки через webhook](items/completion-webhook.md) — завершение сейчас ловим поллингом qBittorrent — webhook реагировал бы быстрее, но связывает нас с его конфигом (решим по опыту эксплуатации)
|
||||
- [🔬 Кандидаты в конвенции кода](items/convention-candidates.md) — накоплен список кандидатов (внешние клиенты, конкурентность, тесты, CLI, время) — надо решить, что из них стало реальным трением, а что выдумано вперёд
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
# ✨ Удалять выбранные загрузки с файлами на отдельной странице
|
||||
|
||||
- **Тип:** feature
|
||||
- **Категория:** Ядро продукта
|
||||
- **Зачем:** удаление с файлами живёт только в danger-секции страницы одной загрузки: чтобы снести десять раздач, надо десять раз пройти путь список → карточка → подтверждение
|
||||
- **Теги:** goal:bulk-download-management
|
||||
|
||||
Отдельная страница веб-UI, доступная из шапки: список загрузок, у которых
|
||||
удаление разрешено (`done`, `orphaned`, `target_missing` —
|
||||
`internal/worker/review.go:631`), чекбокс у каждой строки и одна кнопка
|
||||
«Удалить выбранные». Кнопка ведёт на подтверждение, где выбранные раздачи
|
||||
названы поимённо, и только оттуда уходит удаление.
|
||||
|
||||
Страница отдельная, чтобы не вешать режим выбора на основной список: там
|
||||
карточки живые, самополлятся и перерисовываются, а выбор пользователя такое
|
||||
перерисовывание переживать не обязан.
|
||||
|
||||
Речь только о полном удалении (снять хардлинки + снести раздачу с файлами из
|
||||
qBittorrent). Групповой `Dismiss` (смена статуса без файлов) в эту задачу не
|
||||
входит.
|
||||
|
||||
## Затрагивает
|
||||
|
||||
- новая страница веб-UI и ссылка на неё в `web/templates/partials/header.html`;
|
||||
новый шаблон страницы и её партиалы;
|
||||
- новый POST-эндпоинт группового удаления в `internal/httpapi` (рядом с
|
||||
`POST /ui/downloads/{id}/delete`, `internal/httpapi/httpapi.go:143`);
|
||||
- `Reviewer.Delete` воркера (`internal/worker/review.go:619`) — зовётся по
|
||||
каждой выбранной загрузке;
|
||||
- qBittorrent `torrents/delete` с `deleteFiles=true` — внешний сервис,
|
||||
необратимая операция;
|
||||
- спека `web-ui`, требования «Страницы веб-UI» и «Действия соответствуют
|
||||
состоянию»;
|
||||
- спека `state-reconciliation` в части `Delete` — групповой вызов не меняет
|
||||
условий поштучного, но подтверждение человека теперь одно на пачку.
|
||||
|
||||
## Критерии приёмки
|
||||
|
||||
- Страница открывается из шапки и показывает только те загрузки, для которых
|
||||
удаление разрешено поштучно (оракул: тест `internal/httpapi` — подставной
|
||||
читатель отдаёт задачи во всех состояниях, в разметке строки есть у
|
||||
`done`/`orphaned`/`target_missing` и нет у остальных).
|
||||
- Удаление уходит только после явного подтверждения, и подтверждение называет
|
||||
каждую выбранную раздачу поимённо (оракул: тест — POST без признака
|
||||
подтверждения отвечает отказом и не делает ни одного вызова `Delete` у
|
||||
подставного воркера; ответ подтверждения содержит заголовки всех выбранных).
|
||||
- Отказ на одной загрузке не отменяет остальных, а результат называет
|
||||
удалённые и отказавшие поимённо с причиной (оракул: тест, где второй `Delete`
|
||||
возвращает ошибку — первая и третья удалены, страница результата называет
|
||||
вторую и её причину).
|
||||
- Групповой путь не расширяет прав поштучного: попытка удалить задачу в
|
||||
состоянии, где кнопка недоступна, отклоняется с тем же отказом
|
||||
(оракул: тест — `Delete` для `downloading` возвращает `ErrConflict`, страница
|
||||
показывает отказ, остальные выбранные не затронуты).
|
||||
- Поведение страницы записано дельта-спекой и проходит валидацию (оракул:
|
||||
`openspec validate --strict` и `task gate`).
|
||||
|
||||
## Рамки
|
||||
|
||||
Необратимое действие: `deleteFiles=true` сносит файлы раздачи, гард последней
|
||||
копии в `Delete` выключен сознательно. Ослаблять подтверждение ради удобства
|
||||
пачки нельзя — оно остаётся обязательным и поимённым. Групповой `Dismiss` и
|
||||
автоматическая чистка по сроку хранения (`db-retention-cleanup`) — не эта
|
||||
задача.
|
||||
@@ -0,0 +1,66 @@
|
||||
# 🐞 Не сносить раздачу, которой владеет другая активная загрузка
|
||||
|
||||
- **Тип:** fix
|
||||
- **Категория:** Ядро продукта
|
||||
- **Зачем:** удаление старой закрытой задачи уничтожает файлы живой загрузки с тем же инфохэшем; воспроизведено падающим тестом на ревью bulk-delete-page
|
||||
- **Теги:** goal:state-integrity
|
||||
|
||||
`Delete` зовёт `torrents/delete` с `deleteFiles=true` по хешам своей записи и не
|
||||
спрашивает, не владеет ли этим инфохэшем другая **активная** загрузка. Человек
|
||||
подтверждает удаление одной записи, а необратимое действие применяется к чужим
|
||||
живым данным.
|
||||
|
||||
Двигает у цели `state-integrity` строку завершения «ни один известный сегодня
|
||||
путь не оставляет состояние, которое не объясняется историей переходов»: живая
|
||||
загрузка B уходит в `failed` по пропаже источника, и по её записи не видно, что
|
||||
файлы снесла чужая операция.
|
||||
|
||||
## Воспроизведение
|
||||
|
||||
1. Довести задачу A по инфохэшу H до `done`.
|
||||
2. Добавить тот же релиз заново: `CreateDownloadIfNoActive` ищет владельца
|
||||
только среди активных, A терминальна, дедупа нет — заводится активная задача
|
||||
B по тому же H. Файлы на диске у A и B общие, qBittorrent дедуплицирует
|
||||
раздачу по хешу.
|
||||
3. Удалить A — со страницы загрузки, из Telegram или со страницы группового
|
||||
удаления.
|
||||
|
||||
Видно вместо ожидаемого: раздача снесена с файлами, B продолжает считать себя
|
||||
качающейся и уходит в `failed` после дебаунса. Если B ещё не дошла до раскладки,
|
||||
копии не остаётся нигде.
|
||||
|
||||
Прогнанные оракулы ревью (отчёт —
|
||||
`openspec/changes/archive/2026-08-10-bulk-delete-page/review/report.md`, находка
|
||||
2): `TestAdversaryDeletablePageOffersHashOwnedByActiveDownload`,
|
||||
`TestAdversaryDeleteWipesSourceOfAnotherActiveDownload`.
|
||||
|
||||
## Затрагивает
|
||||
|
||||
- `Worker.Delete` (`internal/worker/review.go`) — вызов `qbt.Delete`;
|
||||
- предикат `store.FindActiveByInfohash` — существующий, новый заводить не надо;
|
||||
- спека `state-reconciliation`, требование «Полное удаление загрузки
|
||||
пользователем» — сегодня оно про такую проверку молчит;
|
||||
- строка выбора и подтверждения на странице группового удаления, если решено
|
||||
показывать признак чужого владения;
|
||||
- qBittorrent `torrents/delete` с `deleteFiles=true` — необратимая операция.
|
||||
|
||||
## Критерии приёмки
|
||||
|
||||
- Удаление задачи, чьим инфохэшем владеет активная загрузка, отклоняется
|
||||
конфликтом и раздачу не сносит (оракул: тест `internal/worker` — задача A в
|
||||
`done` и активная B по тому же хешу, `Delete(A)` возвращает `ErrConflict`, у
|
||||
подставного клиента qBittorrent ноль вызовов `Delete`).
|
||||
- Отказ виден человеку причиной, а не общим «внутренняя ошибка» (оракул: тест
|
||||
`internal/httpapi` — строка отчёта называет, что инфохэшем владеет активная
|
||||
задача).
|
||||
- Удаление задачи, чей инфохэш никем больше не занят, работает как прежде
|
||||
(оракул: существующие тесты `Delete` остаются зелёными).
|
||||
- Поведение записано дельта-спекой `state-reconciliation` (оракул:
|
||||
`openspec validate --strict` и `task gate`).
|
||||
|
||||
## Рамки
|
||||
|
||||
Необратимое: снос раздачи с файлами. Проверка ставится в ядре, а не в
|
||||
транспорте — допуск обязан держаться без транспорта. Правило дедупа при приёме
|
||||
(«владелец ищется среди активных») эта задача не меняет: связывание v1/v2 и
|
||||
доверие к паре `xt` разбирает `infohash-identity-integrity`.
|
||||
@@ -0,0 +1,59 @@
|
||||
# 🐞 Не держать общий замок воркера во время обращения к qBittorrent при удалении
|
||||
|
||||
- **Тип:** fix
|
||||
- **Категория:** Инфраструктура
|
||||
- **Зачем:** на время удаления встаёт весь фон: опрос, сверка и команды остальных транспортов ждут замок до 30 с на загрузку, пачкой — минутами; замерено на ревью bulk-delete-page
|
||||
|
||||
`Worker.Delete` берёт `w.mu` и держит его через сетевой вызов `qbt.Delete`, хотя
|
||||
правило пакета обратное и записано рядом: медленные вызовы идут **вне** замка
|
||||
(`internal/worker/worker.go`, `Poll` зовёт `qbt.Torrents` до `Lock`). Замок при
|
||||
этом один на весь воркер, а не на задачу — комментарий пакета и
|
||||
`docs/architecture.md` называют его per-download, и это тоже неверно.
|
||||
|
||||
## Воспроизведение
|
||||
|
||||
1. Заставить qBittorrent отвечать на `torrents/delete` медленно (подставной
|
||||
клиент с задержкой; в жизни — снос раздачи с большим числом файлов на
|
||||
загруженном диске).
|
||||
2. Во время вызова дёрнуть через тот же воркер любую команду по **другой**
|
||||
загрузке.
|
||||
|
||||
Видно вместо ожидаемого: команда ждёт весь чужой сетевой вызов. Замер ревью: при
|
||||
задержке `qbt.Delete` 300 мс конкурентный `Cancel` по неизвестному
|
||||
идентификатору заблокирован на `280.450631ms`. Таймаут клиента qBittorrent —
|
||||
зашитые 30 с (поля в `[qbittorrent]` нет), поэтому верхняя граница на одну
|
||||
загрузку — 30 с, а на пачку группового удаления — минуты, в течение которых не
|
||||
идут ни опрос, ни сверка, ни уведомления.
|
||||
|
||||
Отчёт ревью с оракулами —
|
||||
`openspec/changes/archive/2026-08-10-bulk-delete-page/review/report.md`,
|
||||
находка 3.
|
||||
|
||||
## Затрагивает
|
||||
|
||||
- `Worker.Delete` (`internal/worker/review.go`) — границы критической секции;
|
||||
- `w.mu` и комментарий пакета `internal/worker` — «per-download» неверно;
|
||||
- `docs/architecture.md`, строка «Переходы состояний» в единых точках — тот же
|
||||
неверный факт;
|
||||
- потолок времени на проход группового удаления (`httpapi.bulkBudget`) и
|
||||
требование о нём в спеке `web-ui` — при сужении замка они теряют основание;
|
||||
- поле таймаута в секции `[qbittorrent]` конфига, которого сегодня нет.
|
||||
|
||||
## Критерии приёмки
|
||||
|
||||
- Команда по одной загрузке не ждёт сетевого вызова по другой (оракул: тест
|
||||
`internal/worker` с подставным клиентом, блокирующимся внутри `Delete`, —
|
||||
`w.mu.TryLock()` из теста возвращает `true`).
|
||||
- Состояние после сетевого вызова перечитывается, и удаление не переходит в
|
||||
`deleted`, если задача успела уйти из допустимого состояния (оракул: тест на
|
||||
смену состояния между шагами).
|
||||
- Факт про замок исправлен в комментарии пакета и в `docs/architecture.md`
|
||||
(оракул: `task gate`, шаг `canon`).
|
||||
- Решено и записано, остаётся ли потолок времени на проход группового удаления
|
||||
(оракул: дельта-спека и `openspec validate --strict`).
|
||||
|
||||
## Рамки
|
||||
|
||||
Трогает операцию, которая сносит файлы: разбор идемпотентности при повторе
|
||||
обязателен. Уборка собственного торрента после отмены (`processCatched`) — уже
|
||||
устроена нужным образом и служит образцом, менять её не надо.
|
||||
@@ -0,0 +1,63 @@
|
||||
# 🐞 Не оставлять задачу в done, когда ссылки сняты, а раздача не снесена
|
||||
|
||||
- **Тип:** fix
|
||||
- **Категория:** Ядро продукта
|
||||
- **Зачем:** при недоступном qBittorrent задача весь простой соседа показывает done, хотя тайтла в Jellyfin уже нет: сверка падает на первом шаге и до коррекции не доходит
|
||||
- **Теги:** goal:state-integrity
|
||||
|
||||
`Delete` снимает библиотечные ссылки раньше, чем зовёт `qbt.Delete`. Если сосед
|
||||
недоступен, локальный шаг проходит, внешний падает, и задача остаётся в `done`.
|
||||
Спека рассчитывает, что расхождение живёт «до тика сверки», но `Poll`
|
||||
возвращается на первой же ошибке `qbt.Torrents` и до `reconcileDesync` не
|
||||
доходит вовсе — значит, расхождение живёт весь простой qBittorrent.
|
||||
|
||||
Двигает у цели `state-integrity` ту же строку завершения — «ни один известный
|
||||
сегодня путь не оставляет состояние, которое не объясняется историей
|
||||
переходов»: `done` держится сколь угодно долго и историей переходов не
|
||||
объясняется.
|
||||
|
||||
## Воспроизведение
|
||||
|
||||
1. Довести задачу до `done` с существующими библиотечными ссылками.
|
||||
2. Погасить qBittorrent (в тесте — клиент на несуществующий адрес).
|
||||
3. Выполнить удаление — поштучно или пачкой.
|
||||
|
||||
Видно вместо ожидаемого: ссылки сняты, строки `file_link` удалены, раздача цела,
|
||||
место не освобождено, а карточка, список и Telegram показывают `done`. Пока
|
||||
qBittorrent лежит, ни один тик сверки этого не исправит. Замер ревью: отказ
|
||||
приходит за `524.545µs`, то есть пачка успевает пройти по всем выбранным раньше,
|
||||
чем человек поймёт, что сосед недоступен.
|
||||
|
||||
Отчёт ревью —
|
||||
`openspec/changes/archive/2026-08-10-bulk-delete-page/review/report.md`,
|
||||
находка 4.
|
||||
|
||||
## Затрагивает
|
||||
|
||||
- `Worker.Delete` (`internal/worker/review.go`) — порядок шагов и переход после
|
||||
отказа внешнего шага;
|
||||
- `Worker.Poll` (`internal/worker/worker.go`) — ранний возврат по отказу
|
||||
`qbt.Torrents` перед `reconcileDesync`;
|
||||
- спека `state-reconciliation`, требование «Полное удаление загрузки
|
||||
пользователем» — формулировка «кратковременное рассогласование до тика сверки
|
||||
ожидаемо» сегодня неверна;
|
||||
- граф переходов: `done → target_missing` уже легален, нового состояния не надо.
|
||||
|
||||
## Критерии приёмки
|
||||
|
||||
- После отказа внешнего шага при снятых ссылках задача не остаётся в `done`
|
||||
(оракул: тест `internal/worker` — клиент qBittorrent отказывает, задача
|
||||
оказывается в `target_missing`, а не в `done`).
|
||||
- В `deleted` при этом задача не переходит: место не освобождено (оракул: тот же
|
||||
тест).
|
||||
- Повторное удаление после подъёма соседа идемпотентно дожимает раздачу (оракул:
|
||||
тест — второй вызов при живом клиенте уводит задачу в `deleted`).
|
||||
- Формулировка спеки приведена к тому, что код делает (оракул:
|
||||
`openspec validate --strict` и `task gate`).
|
||||
|
||||
## Рамки
|
||||
|
||||
Порядок шагов менять можно, но снос раздачи раньше снятия ссылок — отдельное
|
||||
решение с ценой: оно убирает необратимый локальный шаг при мёртвом соседе, но
|
||||
меняет поведение при отказе на втором шаге. Ранний возврат `Poll` по отказу
|
||||
`qbt.Torrents` трогать осторожно: на нём стоит вся сверка.
|
||||
Reference in New Issue
Block a user