## 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}}"` на корневом `
`; `index.html` — цикл через `{{template "card" .}}`. Инвариант: корень `define` == элемент с целевым `id` - [x] 2.2 Обернуть `
` страницы загрузки в `{{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