Files
jellybit/openspec/changes/htmx-action-swap/tasks.md
T
avandClaude Opus 4.8 2f8e6e3576 Веб-UI: htmx-своп действий на месте вместо PRG-редиректа
Мутирующие действия больше не уводят со страницы: undo/relink/retry/cancel
в списке свопят карточку (#card-{id}), на /download/{id} — #download-main;
петля ревью (refine/rerecognize) свопит #review-main и допалливает
recognizing до готового плана (fragment /fragments/downloads/{id}/review,
every 2s). Выходы ревью (apply/defer/cancel) остаются навигацией. Без htmx —
прежний PRG-редирект (деградация). Ошибка действия на htmx-пути — HTTP 200
с сообщением в фрагменте (ActionError), иначе htmx не свопит DOM.

Разметка вынесена в партиалы card/download_main/review_main (корень = элемент
с целевым id), различение поверхности — скрытым полем surface=list|download.
Извлечены buildCardView/buildDownloadView. handleSetProvider переведён на
reviewBlockAction (консистентность source-действий).

Реализация change htmx-action-swap (OpenSpec).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 14:37:51 +03:00

7.5 KiB
Raw Blame History

1. Извлечение переиспользуемых view-builder'ов

  • 1.1 Вынести сборку карточки списка в buildCardView(d, now, live, layoutSize) — перенести обвязку из handleIndex (httpapi.go:291-304: liveFor, ratioText, sizeText, layoutSizes[id], buildProgress); handleIndex теперь зовёт её в цикле. Билдер MUST выставлять IsDownloading/Progress (для retry→downloading карточка обязана нести progress-партиал с поллером) и Ratio/Size
  • 1.2 Вынести сборку страницы загрузки в buildDownloadView(id, rd) — перенести инлайн-сборку из handleDownload (download.go:86-142); handleDownload зовёт её. Добавить в view отдельное поле ActionError
  • 1.3 Добавить в view карточки отдельное поле ActionError (НЕ переиспользовать card-meta/.Error — они несут Note/error_msg)

2. Партиалы и фрагменты (единый источник разметки)

  • 2.1 Выделить карточку в partials/card.html ({{define "card"}}) с id="card-{{.ID}}" на корневом <article>; index.html — цикл через {{template "card" .}}. Инвариант: корень define == элемент с целевым id
  • 2.2 Обернуть <main> страницы загрузки в {{define "download_main"}} с id="download-main"; download.html включает его
  • 2.3 Обернуть тело ревью в {{define "review_main"}} с id="review-main" (внутри остаётся review_source_block); review.html включает его
  • 2.4 В обеих карточка/download_main добавить разметку слота ActionError (видим только при непустом значении), не конфликтуя с Note/error-баннером
  • 2.5 Проставить hx-post/hx-target/hx-swap="outerHTML" на свопимые формы: карточка (undo/relink/retry/cancel#card-{id}), страница загрузки (те же → #download-main), петля ревью (refine/rerecognize#review-main). Выходы ревью (apply/defer/cancel) — обычные POST-формы БЕЗ hx-* (намеренно: единственный маркер «это выход из ревью»)
  • 2.6 В формы общих действий (undo/relink/retry/cancel), доступных и в списке, и на /download/{id}, добавить скрытое поле surface=list|download — им обработчик выбирает фрагмент ответа

3. Авто-поллер recognizing на экране ревью

  • 3.1 Добавить fragment-эндпоинт GET /fragments/downloads/{id}/reviewhandleFragReview, рендерит review_main по текущему состоянию (по образцу handleFragProgress/handleFragSeeding в live.go)
  • 3.2 В review_main блок состояния recognizing (review.html:33-38) оснастить поллером hx-get="/fragments/downloads/{id}/review" hx-trigger="every 2s" hx-target="#review-main" hx-swap="outerHTML"; ручную ссылку «Обновить» оставить как fallback без JS. Когда состояние выходит из recognizing, review_main возвращается без поллера — опрос сам прекращается

4. Обработчики: ветка htmx → фрагмент

  • 4.1 Ввести помощник cardAction (по образцу reviewBlockAction): доменный вызов; на isHTMX перечитать состояние, отрендерить card для одной загрузки; на ошибке — тот же фрагмент с заполненным ActionError и HTTP 200 (иначе htmx не свопит); без htmx — прежний редирект
  • 4.2 Ввести помощник downloadAction аналогично, рендер download_main (тоже 200 + ActionError на ошибке)
  • 4.3 Перевести handleUndo, handleRelink, handleUIRetry на выбор помощника/фрагмента по полю surface (list→cardAction, download→downloadAction); без htmx — прежний редирект
  • 4.4 Переписать единый handleUICancel: на isHTMX — своп по surface (list→card, download→download_main); на не-htmx (в т.ч. форма выхода из ревью и режим без JS) → редирект на /, как сейчас. Убрать деление «в контексте ревью не трогать» — обработчик один
  • 4.5 Перевести петлевые handleRefine, handleRerecognize на рендер review_main при isHTMX (200 + .Error на ошибке), сохранив редирект-ветку на /review/{id} без htmx. Выходы ревью (handleApply/handleDefer) не трогать
  • 4.6 Ошибки везде переводить существующими userErr/classifyErr (наружу только нейтральный текст); секреты в логи не попадают — паритет сохранить

5. Тесты

  • 5.1 htmx-запрос (HX-Request: true) действия из списка (surface=list) → 200 + фрагмент card с новым состоянием, не редирект
  • 5.2 htmx-запрос действия со страницы (surface=download) → 200 + download_main; без HX-Request → прежний 303-редирект
  • 5.3 Петля ревью: rerecognize/refine при HX-Request → 200 + review_main в состоянии recognizing с поллером; без htmx → 303 на /review/{id}
  • 5.4 handleFragReview: пока recognizing — фрагмент содержит поллер; после перехода в review — фрагмент без поллера (опрос прекращается)
  • 5.5 Ошибка действия: htmx-ответ — 200 + сообщение в телеActionError/.Error), состояние не меняется; выход ревью (cancel без hx-*) — по-прежнему навигация
  • 5.6 retry из списка при htmx → карточка downloading с progress-поллером

6. Проверка и оформление

  • 6.1 task lint и task test зелёные
  • 6.2 Ручная проверка (task run): откат из списка — остаёшься в списке (фильтр/скролл целы); откат/relink со страницы загрузки — остаёшься на ней; rerecognize/refine — экран ревью свопится в recognizing и сам обновляется до готового плана без тыканья; выключенный JS — прежний редирект-поток работает
  • 6.3 Ревью кода (второй чекпоинт), затем sync дельт в openspec/specs/ и архивирование change