Ревью: единый блок выбора источника, клик = выбор (review-unified-source-block)

Три секции экрана ревью (Догадка/Источник/Раскладка) слиты в один блок:
список вариантов (радио) → инфо о выбранном → предпросмотр раскладки. Клик
по варианту сразу выбирает и сохраняет источник и обновляет инфо+раскладку
частичным htmx-свопом блока, без полной перезагрузки и без кнопки «выбрать».

- httpapi: reviewBlockAction (htmx-aware, детект HX-Request) для
  candidate/nobase/source; вынос buildReviewView; поля SeasonSummary и
  BlockError; сводка сезонов (seasonSummary/seasonRanges)
- тип movie↔series убран из UI (read-only); удалён веб-роут /type и
  handleSetType, метод SetType из интерфейса httpapi (worker/Telegram не тронуты)
- шаблон: партиал review_source_block, ссылка «запись ↗» вне кликабельного
  label, фокус радио с клавиатуры; чистка мёртвого sourceView.Files/IsSeries
- тесты: htmx-своп выбора, htmx-путь ошибки, юнит-тесты сводки сезонов
- openspec: спеки review/web-ui синхронизированы, change заархивирован
- беклог: сложные сериальные раздачи; oob-обновление панели действий

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
av
2026-07-04 08:12:28 +03:00
co-authored by Claude Opus 4.8
parent 2ed9c9020f
commit 1f4267a046
17 changed files with 942 additions and 157 deletions
@@ -0,0 +1,132 @@
## MODIFIED Requirements
### Requirement: Команды ревью и их эффекты
Экран ревью SHALL предоставлять команды: **Применить** (создать хардлинки по
эффективному плану), **Уточнить** (добавить подсказку → перераспознать),
**Распознать заново** (повторный прогон без новой подсказки), **Игнор файла**,
**Позже** (`deferred`), **Отклонить** (`cancelled`), **Undo** (снять созданные
ссылки → `reverted`) и **Привязать заново** (из
`reverted`/`cancelled`/`target_missing` → перераспознавание с ручным
подтверждением). Экран ревью MUST NOT содержать команду переключения типа
movie↔series: тип показывается read-only, а его корректировка выполняется
мягкой подсказкой через **Уточнить**. Команды из любого транспорта SHALL
сериализоваться worker'ом под единой блокировкой; применяется последняя валидная
команда. Команды, которым нужен источник, SHALL проверять его наличие синхронно
перед действием.
#### Scenario: Применение создаёт раскладку
- **GIVEN** загрузка в `review` с эффективным планом
- **WHEN** пользователь выбирает «Применить»
- **THEN** создаются хардлинки по плану, задача переходит к раскладке
#### Scenario: Отклонить и привязать заново
- **GIVEN** загрузка в `review`
- **WHEN** пользователь «Отклонить», затем «Привязать заново»
- **THEN** задача уходит в `cancelled`, а затем снова на распознавание с ручным
подтверждением (авто-раскладка не делается)
#### Scenario: Тип не переключается кнопкой
- **GIVEN** загрузка в `review` с распознанным типом
- **WHEN** пользователь открывает экран ревью
- **THEN** отдельной команды/кнопки переключения movie↔series на экране нет
- **AND** тип показан read-only в инфо-части выбранного источника
### Requirement: Единый список источников совпадения на ревью
Экран ревью (`/review/{id}`) SHALL показывать совпавшие источники **единым
списком**, в котором распознавание нейронкой (без базы) — такая же строка,
как кандидаты метабаз (TMDB/TVDB/TVMaze), а не отдельный режим сверху.
Ровно один источник в списке SHALL быть отмечен активным (эффективный
матч). Активный источник SHALL выбираться **одним кликом/тапом по строке
варианта** (радиокнопка), без отдельной кнопки подтверждения выбора. Выбор
источника SHALL сохранять его как эффективный матч (persist) и SHALL
выполняться через раундтрип на сервер (форма/htmx), без клиентского пересчёта
доменного состояния; при этом инфо-часть и предпросмотр раскладки SHALL
немедленно обновляться под выбранный источник (частичный своп блока, без полной
перезагрузки страницы). Экран SHALL позволять операции над этим списком:
выбрать кандидата базы, переключиться на другого кандидата и снять матч с базы
обратно на нейронку («без базы»). Список источников SHALL показываться только
при наличии плана распознавания.
#### Scenario: Нейронка — строка в общем списке
- **GIVEN** загрузка в `review` с распознаванием нейронкой и одним или
несколькими кандидатами метабаз
- **WHEN** пользователь открывает `GET /review/{id}`
- **THEN** источники показаны единым списком, где строка «распознано
нейронкой» стоит наравне с кандидатами баз
- **AND** активным отмечен ровно один источник (текущий эффективный матч)
#### Scenario: Выбор кандидата одним кликом
- **GIVEN** на экране ревью выбран один кандидат метабазы
- **WHEN** пользователь кликает/тапает строку другого кандидата
- **THEN** выбранный кандидат сохраняется активным, прочие — неактивны, без
отдельного нажатия кнопки «выбрать»
- **AND** инфо-часть и предпросмотр раскладки сразу обновляются под выбранного
кандидата без полной перезагрузки страницы
#### Scenario: Снятие матча в пользу нейронки
- **GIVEN** на экране ревью активен кандидат метабазы с названием «Fargo»
- **WHEN** пользователь кликает строку «распознано нейронкой»
- **THEN** матч с базой снимается (источник — нейронка, «без базы»), тег
папки провайдера не проставляется
- **AND** поля источника — из распознавания нейронкой, без унаследованных
от прежнего кандидата название/год
## ADDED Requirements
### Requirement: Инфо и предпросмотр выбранного источника
В едином блоке выбора источника экран ревью SHALL показывать для **выбранного
(активного)** источника две части: **инфо** — тип (read-only, movie/series),
название, оригинальное название, год, для сериала — сводку сезонов (один сезон,
диапазон/список для многосезонного пака или «Спецвыпуски»), с
зарезервированным местом под режиссёра; и **предпросмотр раскладки** — целевые
пути хардлинков этого источника. Обе части SHALL относиться именно к активному
источнику и SHALL обновляться при смене выбора. Отрисовка блока (показ инфо и
предпросмотра) MUST NOT создавать хардлинки: раскладка создаётся только явным
действием «Применить». Совпадение целевых путей предпросмотра с результатом
применения регулируется требованием «Превью раскладки через единую логику
именования» (`web-ui`).
#### Scenario: Инфо и предпросмотр относятся к активному источнику
- **GIVEN** в списке активен кандидат метабазы
- **WHEN** пользователь смотрит инфо-часть и предпросмотр раскладки
- **THEN** показаны тип, название, ориг. название, год (и сводка сезонов для
сериала) именно этого источника и предпросмотр его целевых путей
#### Scenario: Просмотр блока не создаёт раскладку
- **GIVEN** экран ревью с показанным блоком выбора источника
- **WHEN** пользователь только просматривает инфо и предпросмотр, не нажимая
«Применить»
- **THEN** хардлинки не создаются, файлы под `paths.movies`/`series` не
меняются
#### Scenario: Зарезервированное место под режиссёра
- **GIVEN** режиссёр из метабазы пока не загружается
- **WHEN** отображается инфо-часть выбранного источника
- **THEN** в ней присутствует место под режиссёра, показанное пустым (или
прочерком), не ломая вёрстку
## REMOVED Requirements
### Requirement: Предпросмотр полей источника до фиксации выбора
**Reason**: Модель «раскрыть предпросмотр не-активного источника, затем нажать
«выбрать»» заменяется на «клик по варианту = выбор». Предпросмотр полей и
раскладки теперь показывается только для выбранного (активного) источника и
обновляется при смене выбора.
**Migration**: Поведение перенесено в требования «Единый список источников
совпадения на ревью» (выбор одним кликом, немедленное обновление) и «Инфо и
предпросмотр выбранного источника» (что именно показывается и что показ не
создаёт хардлинки).
@@ -0,0 +1,35 @@
## MODIFIED Requirements
### Requirement: Превью раскладки через единую логику именования
Превью целевых путей раскладки в веб-UI SHALL вычисляться той же логикой
именования, что и реальная раскладка (`internal/naming`/`internal/layout`), а
не дублировать правила в шаблоне. На экране ревью превью SHALL строиться **для
выбранного (активного) источника** — эфемерно на сервере, без записи
сохранённого матча самим показом. При смене выбранного источника превью SHALL
пересчитываться под него и обновляться частичным свопом блока. Показанные для
источника пути MUST совпадать с теми, что создались бы при применении этого
источника.
#### Scenario: Превью совпадает с реальной раскладкой
- **WHEN** на экране ревью отображается превью целевых путей для выбранного
источника
- **THEN** эти пути идентичны тем, что создаст применение этого источника (те же
правила имён, спецвыпусков, мультифайла, запрещённых символов, тега провайдера
и коллизий)
#### Scenario: Смена источника пересчитывает превью
- **GIVEN** на экране ревью показан предпросмотр раскладки активного источника
- **WHEN** пользователь выбирает другой источник в списке
- **THEN** превью пересчитывается под выбранный источник и обновляется без
полной перезагрузки страницы
#### Scenario: Переключение источника не тянет чужие поля
- **GIVEN** активен кандидат с запиненными название/год, затем выбран
источник без собственных названия/года (нейронка или ручной кандидат)
- **WHEN** строится превью и затем выполняется применение выбранного источника
- **THEN** и превью, и применение используют название/год этого источника
(из плана распознавания), без унаследованных от прежнего кандидата