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:
+14
-24
@@ -11,14 +11,6 @@
|
||||
канал = логи, публичный = сообщение + корреляционный ключ). Здесь — только
|
||||
специфика htmx-транспорта, без дублирования.
|
||||
|
||||
> Статус. Полностью по этой конвенции сейчас сделан только своп блока источника
|
||||
> на ревью (`reviewBlockAction` + партиал `review_source_block`) и поллинг
|
||||
> прогресса/раздачи. Общий раскат свопа на карточку списка, страницу загрузки и
|
||||
> петлю ревью (`buildCardView`/`buildDownloadView`, `surface`, `ActionError`,
|
||||
> `handleFragReview`) проектируется и внедряется в change
|
||||
> `openspec/changes/htmx-action-swap/` — там те же решения подробно. Ниже —
|
||||
> **целевой** подход; где он ещё не в коде, помечено «будем».
|
||||
|
||||
## Стек и границы
|
||||
|
||||
htmx-first: `chi` + `html/template` (server-rendered) + htmx. Ничего сверх этого:
|
||||
@@ -41,18 +33,16 @@ htmx-first: `chi` + `html/template` (server-rendered) + htmx. Ничего св
|
||||
**и** как ответ-фрагмент того же обработчика (`s.render(w, "name", view)`).
|
||||
Отдельного markup для фрагмента не заводим — иначе он дрейфует от страницы.
|
||||
|
||||
**Инвариант: корень `{{define}}` — это элемент с целевым `id`** (`#source-block`,
|
||||
`#dl-live-{id}`, `#seeding-{id}`; будем — `#card-{id}`, `#download-main`,
|
||||
`#review-main`). `hx-swap="outerHTML"` заменяет весь корневой узел; если ответный
|
||||
**Инвариант: корень `{{define}}` — это элемент с целевым `id`** (`#card-{id}`,
|
||||
`#download-main`, `#review-main`, `#source-block`, `#dl-live-{id}`,
|
||||
`#seeding-{id}`). `hx-swap="outerHTML"` заменяет весь корневой узел; если ответный
|
||||
фрагмент не несёт тот же корневой `id`, следующее действие/поллер не найдёт
|
||||
таргет. Разметку и `id` держим в одном партиале, чтобы страница и своп-ответ не
|
||||
разъезжались.
|
||||
|
||||
Сборку view выносим в переиспользуемую функцию (`buildReviewView`; будем —
|
||||
`buildCardView`, `buildDownloadView`) и зовём её и на полной странице, и во
|
||||
фрагменте — чтобы htmx-ветка не копипастила сборку. Часть сборки сейчас ещё
|
||||
инлайновая (`handleIndex` — карточка, `handleDownload` — страница); извлечение
|
||||
билдеров — направление рефакторинга в `htmx-action-swap`, не свершившийся факт.
|
||||
Сборку view выносим в переиспользуемую функцию (`buildCardView`,
|
||||
`buildDownloadView`, `buildReviewView`) и зовём её и на полной странице, и во
|
||||
фрагменте — чтобы htmx-ветка не копипастила сборку.
|
||||
|
||||
## Обработчик действия: ветвление htmx / редирект
|
||||
|
||||
@@ -103,9 +93,10 @@ htmx по умолчанию **не свопит DOM на ответы 4xx/5xx**
|
||||
|
||||
- Сообщение — нейтральный текст публичного канала через `userErr`/`classifyErr`
|
||||
(см. [errors.md](errors.md)); сырой `err.Error()` наружу не идёт.
|
||||
- Ошибку кладём в **отдельное поле** под ошибку действия (`BlockError`; будем —
|
||||
`ActionError`), не перегружая доменные поля (`Note`/`error_msg`/`.Error`): у
|
||||
`target_missing` `Note` непуст и перекрыл бы сообщение.
|
||||
- Ошибку кладём в **отдельное поле** под ошибку действия (`ActionError` в
|
||||
карточке/`download_main`, `BlockError` в блоке источника), не перегружая
|
||||
доменные поля (`Note`/`error_msg`/`.Error`): у `target_missing` `Note` непуст и
|
||||
перекрыл бы сообщение.
|
||||
- **При ошибке активное состояние не меняем** — перечитанный view показывает
|
||||
прежний выбор плюс сообщение.
|
||||
|
||||
@@ -152,16 +143,15 @@ htmx по умолчанию **не свопит DOM на ответы 4xx/5xx**
|
||||
**Асинхронные действия.** Если доменное действие асинхронно (переводит в
|
||||
промежуточное состояние — `recognizing` у `rerecognize`/`refine`, работу
|
||||
доделывает воркер), своп отдаёт **промежуточное** состояние, а не мнимый
|
||||
результат; готовый итог догоняем самозавершающимся поллером (будем —
|
||||
`handleFragReview`, опрос `recognizing` до `review`). Не обещаем в UI мгновенный
|
||||
итог async-операции.
|
||||
результат; готовый итог догоняем самозавершающимся поллером (`handleFragReview`,
|
||||
опрос `recognizing` до `review`). Не обещаем в UI мгновенный итог async-операции.
|
||||
|
||||
## Различение поверхности одного действия
|
||||
|
||||
Если один роут действия зовут с разных страниц и своп-ответ должен быть разным
|
||||
фрагментом (карточка списка vs `download_main`), различаем **явным скрытым полем
|
||||
формы** `surface=list|download` (будем), а не эвристикой по `HX-Target`/`Referer`
|
||||
— поле самодокументируемо и не зависит от резолва таргета.
|
||||
формы** `surface=list|download`, а не эвристикой по `HX-Target`/`Referer` — поле
|
||||
самодокументируемо и не зависит от резолва таргета.
|
||||
|
||||
## Статика, вендоринг, кэш
|
||||
|
||||
|
||||
Reference in New Issue
Block a user