Мутирующие действия больше не уводят со страницы: 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>
7.5 KiB
7.5 KiB
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}/review→handleFragReview, рендерит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