Мутирующие действия больше не уводят со страницы: 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>
93 lines
7.5 KiB
Markdown
93 lines
7.5 KiB
Markdown
## 1. Извлечение переиспользуемых view-builder'ов
|
||
|
||
- [x] 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`
|
||
- [x] 1.2 Вынести сборку страницы загрузки в `buildDownloadView(id, rd)` —
|
||
перенести инлайн-сборку из `handleDownload` (`download.go:86-142`);
|
||
`handleDownload` зовёт её. Добавить в view отдельное поле `ActionError`
|
||
- [x] 1.3 Добавить в view карточки отдельное поле `ActionError` (НЕ
|
||
переиспользовать `card-meta`/`.Error` — они несут `Note`/`error_msg`)
|
||
|
||
## 2. Партиалы и фрагменты (единый источник разметки)
|
||
|
||
- [x] 2.1 Выделить карточку в `partials/card.html` (`{{define "card"}}`) с
|
||
`id="card-{{.ID}}"` на корневом `<article>`; `index.html` — цикл через
|
||
`{{template "card" .}}`. Инвариант: корень `define` == элемент с целевым `id`
|
||
- [x] 2.2 Обернуть `<main>` страницы загрузки в `{{define "download_main"}}` с
|
||
`id="download-main"`; `download.html` включает его
|
||
- [x] 2.3 Обернуть тело ревью в `{{define "review_main"}}` с `id="review-main"`
|
||
(внутри остаётся `review_source_block`); `review.html` включает его
|
||
- [x] 2.4 В обеих карточка/`download_main` добавить разметку слота `ActionError`
|
||
(видим только при непустом значении), не конфликтуя с `Note`/error-баннером
|
||
- [x] 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-*` (намеренно: единственный маркер «это выход из ревью»)
|
||
- [x] 2.6 В формы общих действий (`undo`/`relink`/`retry`/`cancel`), доступных и
|
||
в списке, и на `/download/{id}`, добавить скрытое поле
|
||
`surface=list|download` — им обработчик выбирает фрагмент ответа
|
||
|
||
## 3. Авто-поллер recognizing на экране ревью
|
||
|
||
- [x] 3.1 Добавить fragment-эндпоинт `GET /fragments/downloads/{id}/review` →
|
||
`handleFragReview`, рендерит `review_main` по текущему состоянию (по образцу
|
||
`handleFragProgress`/`handleFragSeeding` в `live.go`)
|
||
- [x] 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 → фрагмент
|
||
|
||
- [x] 4.1 Ввести помощник `cardAction` (по образцу `reviewBlockAction`):
|
||
доменный вызов; на `isHTMX` перечитать состояние, отрендерить `card` для
|
||
одной загрузки; на ошибке — тот же фрагмент с заполненным `ActionError` и
|
||
**HTTP 200** (иначе htmx не свопит); без htmx — прежний редирект
|
||
- [x] 4.2 Ввести помощник `downloadAction` аналогично, рендер `download_main`
|
||
(тоже 200 + `ActionError` на ошибке)
|
||
- [x] 4.3 Перевести `handleUndo`, `handleRelink`, `handleUIRetry` на выбор
|
||
помощника/фрагмента по полю `surface` (list→`cardAction`,
|
||
download→`downloadAction`); без htmx — прежний редирект
|
||
- [x] 4.4 Переписать единый `handleUICancel`: на `isHTMX` — своп по `surface`
|
||
(list→`card`, download→`download_main`); на не-htmx (в т.ч. форма выхода из
|
||
ревью и режим без JS) → редирект на `/`, как сейчас. Убрать деление «в
|
||
контексте ревью не трогать» — обработчик один
|
||
- [x] 4.5 Перевести петлевые `handleRefine`, `handleRerecognize` на рендер
|
||
`review_main` при `isHTMX` (200 + `.Error` на ошибке), сохранив
|
||
редирект-ветку на `/review/{id}` без htmx. Выходы ревью
|
||
(`handleApply`/`handleDefer`) не трогать
|
||
- [x] 4.6 Ошибки везде переводить существующими `userErr`/`classifyErr` (наружу
|
||
только нейтральный текст); секреты в логи не попадают — паритет сохранить
|
||
|
||
## 5. Тесты
|
||
|
||
- [x] 5.1 htmx-запрос (`HX-Request: true`) действия из списка (`surface=list`)
|
||
→ 200 + фрагмент `card` с новым состоянием, не редирект
|
||
- [x] 5.2 htmx-запрос действия со страницы (`surface=download`) → 200 +
|
||
`download_main`; без `HX-Request` → прежний 303-редирект
|
||
- [x] 5.3 Петля ревью: `rerecognize`/`refine` при `HX-Request` → 200 +
|
||
`review_main` в состоянии `recognizing` с поллером; без htmx → 303 на
|
||
`/review/{id}`
|
||
- [x] 5.4 `handleFragReview`: пока `recognizing` — фрагмент содержит поллер;
|
||
после перехода в `review` — фрагмент без поллера (опрос прекращается)
|
||
- [x] 5.5 Ошибка действия: htmx-ответ — **200 + сообщение в теле** (в
|
||
`ActionError`/`.Error`), состояние не меняется; выход ревью (`cancel` без
|
||
`hx-*`) — по-прежнему навигация
|
||
- [x] 5.6 retry из списка при htmx → карточка `downloading` с `progress`-поллером
|
||
|
||
## 6. Проверка и оформление
|
||
|
||
- [x] 6.1 `task lint` и `task test` зелёные
|
||
- [ ] 6.2 Ручная проверка (`task run`): откат из списка — остаёшься в списке
|
||
(фильтр/скролл целы); откат/relink со страницы загрузки — остаёшься на ней;
|
||
rerecognize/refine — экран ревью свопится в recognizing и сам обновляется до
|
||
готового плана без тыканья; выключенный JS — прежний редирект-поток работает
|
||
- [ ] 6.3 Ревью кода (второй чекпоинт), затем sync дельт в `openspec/specs/` и
|
||
архивирование change
|