Files
jellybit/openspec/changes/archive/2026-08-10-card-live-refresh/proposal.md
T
av a5d873b62d web-ui: карточка и страница обновляются, пока задачу может двигать фон
- условие самообновления — доменный предикат store.State.IsObservable() вместо
  фазы catched; один поллер на поверхность, интервалы 5 с и 15 с
- отказ тика отвечает 200 и самозавершающимся фрагментом с корневым id цели
  вместо 404/500, который htmx не свопит
- заведён ADR-2026-08-10-observability-is-not-terminality, переписан раздел
  «Живой поллинг» в конвенции веб-UI
2026-08-10 14:02:38 +03:00

5.4 KiB

Why

Загрузка докачалась, воркер увёл её в распознавание и дальше в ревью — а в списке она по-прежнему «Загружается», без кнопки «Ревью →». Верное состояние появляется только после того, как человек сам перезагрузит страницу.

Живое обновление сегодня привязано к двум отдельным фазам: карточка опрашивает себя, пока задача в catched, а прогресс — пока она в downloading. Выйдя из downloading, задача не опрашивается ничем, хотя сменить состояние ей предстоит ещё не раз (распознавание, ревью, раскладка) и часть этих смен идёт вообще без участия того, кто смотрит на список: их делает воркер или человек из Telegram.

What Changes

  • Самообновление карточки списка привязывается к нетерминальности задачи, а не к фазе catched: карточка обновляется, пока задача жива, и перестаёт — когда та встала окончательно.
  • У карточки остаётся один источник обновления. Сейчас в downloading их было бы два (сама карточка и вложенный фрагмент прогресса), и они опрашивали бы одно и то же дважды, подменяя разметку друг друга. Живые цифры прогресса приходят вместе с карточкой.
  • Страница /download/{id} живёт по тому же правилу: самообновляется, пока задача нетерминальна.
  • Частота обновления перестаёт быть одинаковой: карточка с живыми цифрами (скорость, ETA) обновляется чаще, чем карточка, у которой меняется только состояние. Цена тика у второй поверхности выше — сборка страницы загрузки считает предпросмотр раскладки и ходит в файловую систему.
  • Секция «Раздача» на странице загрузки перестаёт опрашивать сервер сама — она лежит внутри области, которую страница обновляет целиком, и два опроса на одну поверхность мешали бы друг другу.
  • Тик самообновления, не сумевший прочитать задачу, перестаёт быть молчаливым: он объясняет положение дел и прекращает опрос вместо бесконечного стука в сервер.
  • BREAKING для внутреннего контракта фрагментов: фрагменты прогресса и раздачи перестают быть самостоятельными поллерами. Наружного API это не касается — /api/** не меняется.

Capabilities

New Capabilities

Новых нет.

Modified Capabilities

  • web-ui: требование «Отображение промежуточного состояния catched» обобщается — самообновление интерфейса перестаёт быть свойством одной фазы и становится свойством живой задачи; условие остановки — терминальное состояние.
  • live-status: требование «Живой прогресс активных загрузок» — сценарий «Завершение останавливает поллинг» сегодня описывает наблюдаемый дефект как норму. Прекращаться должен показ живых цифр, а не обновление карточки.

Impact

  • internal/httpapi: toView и buildDownloadView (условие самообновления), обработчики фрагментов карточки и прогресса;
  • web/templates/partials/card.html, progress.html, download_main.html;
  • нагрузка: число фоновых запросов на открытую страницу меняется — считается в design.md;
  • вне scope: переход на SSE (задача sse-live-updates), любые изменения /api/** и состава живой телеметрии;
  • вне scope и названо сознательно: экран /review/{id} сохраняет фазовое самообновление, заказанное спекой review («пока загрузка в recognizing»). Третья поверхность приводится к общему правилу отдельной задачей — иначе change тянет за собой ещё одну capability.