- условие самообновления — доменный предикат store.State.IsObservable() вместо фазы catched; один поллер на поверхность, интервалы 5 с и 15 с - отказ тика отвечает 200 и самозавершающимся фрагментом с корневым id цели вместо 404/500, который htmx не свопит - заведён ADR-2026-08-10-observability-is-not-terminality, переписан раздел «Живой поллинг» в конвенции веб-UI
57 lines
4.5 KiB
Markdown
57 lines
4.5 KiB
Markdown
# Наблюдаемость поверхности не выводится из терминальности задачи
|
||
|
||
- **Дата:** 2026-08-10
|
||
- **Источник:** openspec/changes/archive/2026-08-10-card-live-refresh/design.md
|
||
|
||
## Решение
|
||
|
||
Веб-UI обновляет себя, пока задача **наблюдаема** — то есть её состояние ещё
|
||
может измениться без участия человека, — а не пока она нетерминальна.
|
||
Предикат `store.State.IsObservable()` живёт в домене рядом с `IsTerminal()` и
|
||
даёт: все нетерминальные плюс `failed`, `target_missing`, `orphaned`. Замолкают
|
||
`done`, `cancelled`, `reverted`, `deleted`.
|
||
|
||
## Почему
|
||
|
||
Очевидный предикат — «обновляемся, пока задача не терминальна» — оказался
|
||
неверным, и это выяснилось на ревью дизайна, до кода. Цитата из источника:
|
||
|
||
> Терминальность в проекте значит «не активна», а не «навсегда»: фоновая сверка
|
||
> двигает часть терминальных сама — `ListRecoverable` возвращает в поток
|
||
> `failed`/`stuck` с кодами `magnet_timeout` и `stalled`, а `desyncStates`
|
||
> переоценивает `done`, `target_missing` и `orphaned`. Карточка, застывшая по
|
||
> `IsTerminal`, показывала бы «Ошибка» у задачи, которая уже качается, — ровно
|
||
> тот дефект, ради которого затеян change.
|
||
|
||
`done` в перечень наблюдаемых не вошёл, и это отдельное решение с ценой:
|
||
|
||
> Переход `done → target_missing`/`orphaned` означает, что файлы удалили руками
|
||
> мимо сервиса, — событие редкое, а карточек `done` в списке больше всех.
|
||
> Платить за редкий случай постоянным фоновым запросом на каждую разложенную
|
||
> задачу дороже, чем показать её новое состояние при следующем заходе.
|
||
|
||
## Рассмотренные варианты
|
||
|
||
- **Наблюдать только нетерминальные** (как задумывалось изначально) — проще
|
||
всего и не заводит второго предиката. Отвергнут: задача, оживлённая сверкой из
|
||
`failed`, висела бы на экране с надписью «Ошибка» до перезагрузки, причём
|
||
соседние карточки при этом обновлялись бы — застывшая читалась бы как
|
||
достоверная.
|
||
- **Наблюдать всё, терминальные — редким тиком** — снимает вопрос целиком.
|
||
Отвергнут: список из сотни разложенных задач слал бы пустые запросы вечно, а
|
||
критерий приёмки «завершённая карточка себя не опрашивает» пришлось бы
|
||
отменить.
|
||
|
||
## Последствия
|
||
|
||
- `+` смена состояния становится видимой независимо от того, кто её сделал:
|
||
воркер, веб-UI, Telegram или фоновая сверка.
|
||
- `+` условие обновления выражено одним доменным предикатом; второго перечня
|
||
состояний в транспорте нет, и завести его нельзя не заметив.
|
||
- `−` в домене стало два перечня состояний вместо одного, и второй выведен из
|
||
поведения воркера (`desyncStates`, `ListRecoverable`) вручную. Расширение
|
||
сверки новым состоянием молча вернёт застывшую карточку — связки, которая бы
|
||
это ловила, нет.
|
||
- `−` карточка `failed`, `target_missing` или `orphaned` опрашивает сервер, пока
|
||
открыта вкладка: эти состояния живут долго и копятся (срока хранения нет).
|