OpenSpec: влить дельты htmx-action-swap в спеки, архив change

Sync новых требований в openspec/specs/: web-ui («Действия обновляют
интерфейс на месте») и review («Петлевые действия ревью обновляют экран на
месте»). Change перемещён в changes/archive/2026-07-04-htmx-action-swap.
Конвенция web-ui.md актуализирована: сняты маркеры «будем» по реализованному.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
av
2026-07-04 14:42:06 +03:00
co-authored by Claude Opus 4.8
parent 2f8e6e3576
commit 576fc4e6c0
9 changed files with 147 additions and 30 deletions
@@ -0,0 +1,71 @@
## ADDED Requirements
### Requirement: Действия обновляют интерфейс на месте
Мутирующие действия над загрузкой в списке (`/`) и на странице `/download/{id}` SHALL выполняться htmx-запросом и обновлять затронутую область HTML на месте (partial swap), без навигации на другую страницу и без сброса контекста списка (фильтр, поиск, страница пагинации, позиция прокрутки).
Сервер SHALL отвечать на такой запрос HTML-фрагментом обновлённой области, а не
редиректом.
Область свопа SHALL соответствовать поверхности действия: в списке — карточка
загрузки (`<article class="card">`) целиком, отражающая новое состояние, бейдж
и допустимый набор действий; на странице `/download/{id}` — содержимое
страницы, отражающее новое состояние загрузки. После свопа набор показанных
действий MUST соответствовать новому состоянию (см. «Действия соответствуют
состоянию»).
Поведение MUST деградировать без htmx: если запрос действия пришёл без признака
htmx (нет заголовка `HX-Request`), обработчик SHALL отвечать прежним
PRG-редиректом, и действие исполняется тем же доменным вызовом. Формы действий
остаются обычными POST-формами.
Ошибка действия (доменная или валидации) SHALL показываться на месте — в
подменённом фрагменте той же области, — а не только через параметр `?err=`
после редиректа; при ошибке активное состояние загрузки не меняется молча.
Ответ на htmx-запрос действия SHALL иметь статус `200` даже при ошибке действия
(иначе htmx не подменит фрагмент): сообщение об ошибке несёт сам фрагмент.
После свопа карточка SHALL оставаться на своём месте в списке, даже если её
новое состояние уже не подходит под активный фильтр; согласованность списка с
фильтром восстанавливается при следующей полной загрузке. Клиентского
переупорядочивания или пересчёта доменного состояния не выполняется.
#### Scenario: Откат из карточки списка обновляет карточку на месте
- **GIVEN** в списке есть загрузка в состоянии `done` с действием отката
- **WHEN** пользователь нажимает «Откатить» (htmx активен)
- **THEN** карточка этой загрузки подменяется на месте на её новое состояние
(`reverted`) с соответствующим бейджем и набором действий
- **AND** список не перезагружается: фильтр, поиск, страница и позиция прокрутки
сохраняются
#### Scenario: Действие со страницы загрузки оставляет на странице
- **GIVEN** открыта страница `GET /download/{id}` загрузки в состоянии `done`
- **WHEN** пользователь нажимает «Откатить» или «Привязать заново» (htmx активен)
- **THEN** содержимое страницы обновляется на месте под новое состояние
загрузки, без перехода на список и без прыжка прокрутки наверх
#### Scenario: Деградация без htmx — прежний редирект
- **WHEN** действие над загрузкой приходит POST-запросом без заголовка
`HX-Request` (htmx недоступен)
- **THEN** обработчик исполняет то же доменное действие и отвечает
PRG-редиректом, как раньше; поведение без JavaScript не ломается
#### Scenario: Ошибка действия показана на месте
- **GIVEN** пользователь запускает действие через htmx
- **WHEN** доменный вызов возвращает ошибку (например, состояние уже изменилось)
- **THEN** ответ имеет статус `200`, а сообщение об ошибке показывается в
подменённом фрагменте той же области, а не только на отдельной странице после
редиректа
- **AND** активное состояние загрузки не меняется
#### Scenario: Свопнутая карточка остаётся вне фильтра
- **GIVEN** список отфильтрован по группе состояний (например, `review`) и в нём
есть карточка загрузки
- **WHEN** действие через htmx переводит загрузку в состояние вне этого фильтра
(например, `cancelled`)
- **THEN** карточка подменяется на месте новым состоянием и остаётся видимой до
следующей полной загрузки списка, без клиентского переупорядочивания