web-ui: карточка и страница обновляются, пока задачу может двигать фон
- условие самообновления — доменный предикат store.State.IsObservable() вместо фазы catched; один поллер на поверхность, интервалы 5 с и 15 с - отказ тика отвечает 200 и самозавершающимся фрагментом с корневым id цели вместо 404/500, который htmx не свопит - заведён ADR-2026-08-10-observability-is-not-terminality, переписан раздел «Живой поллинг» в конвенции веб-UI
This commit is contained in:
+35
-14
@@ -103,27 +103,48 @@ htmx по умолчанию **не свопит DOM на ответы 4xx/5xx**
|
||||
## Живой поллинг
|
||||
|
||||
Паттерн живого обновления: фрагмент-эндпоинт под `/fragments/...` + в разметке
|
||||
`hx-get` + `hx-trigger="every Ns"` + `hx-swap="outerHTML"` (эталон —
|
||||
`progress`/`seeding`, `handleFragProgress`/`handleFragSeeding`):
|
||||
`hx-get` + `hx-trigger="every Ns"` + `hx-swap="outerHTML"`. Эталон — карточка
|
||||
списка (`card`, `handleFragCard`):
|
||||
|
||||
```html
|
||||
{{define "progress"}}<div id="dl-live-{{.ID}}"
|
||||
{{if .Active}} hx-get="/fragments/downloads/{{.ID}}/progress"
|
||||
hx-trigger="every 3s" hx-swap="outerHTML"{{end}}>
|
||||
{{define "card"}}<article class="card" id="card-{{.ID}}"
|
||||
{{if .SelfPoll}} hx-get="/fragments/downloads/{{.ID}}/card"
|
||||
hx-trigger="every {{.PollEvery}}" hx-swap="outerHTML"{{end}}>
|
||||
...
|
||||
</div>{{end}}
|
||||
</article>{{end}}
|
||||
```
|
||||
|
||||
- **Поллер самозавершается.** Когда состояние выходит из «живого» (`Active`
|
||||
ложно, торрент не сидирует), фрагмент возвращается **без `hx-*`** — htmx
|
||||
больше не опрашивает. Условие «живости» ведёт store-состояние (`downloading`
|
||||
для прогресса), а не qBittorrent.
|
||||
- **Один поллер на обновляемый корень.** Опрашивает себя корень поверхности
|
||||
(карточка списка, главная область страницы), а вложенные живые регионы —
|
||||
прогресс качания, секция раздачи — своего `hx-get` **не несут**: своп корня
|
||||
уносит их вместе с таймером, и два опроса подменяли бы разметку друг друга.
|
||||
Живые цифры приезжают вместе с корнем.
|
||||
- **Поллер самозавершается.** Опрос ведётся, пока предмет может измениться без
|
||||
участия браузера; перестал — фрагмент возвращается **без `hx-*`**, и htmx
|
||||
больше не опрашивает. Условие определяется store-состоянием
|
||||
(`State.IsObservable()`), а не qBittorrent.
|
||||
- **Отказ тика тоже самозавершается.** Не сумев прочитать задачу, тик отвечает
|
||||
`200` и фрагментом с объяснением **без `hx-*`**: htmx не свопит `4xx/5xx`,
|
||||
поэтому статус ошибки оставил бы поверхность навсегда прежней, а опрос —
|
||||
бесконечным. Фрагмент отказа обязан нести корневой `id` того узла, который он
|
||||
собой заменяет (см. инвариант выше), иначе `hx-swap` подменит не тот узел.
|
||||
- **Уровень лога у тика — `WARN`.** У повторяющегося опроса есть штатный ретрай;
|
||||
`ERROR` оставляем разовому действию человека (см. [logging.md](logging.md)).
|
||||
- **`outerHTML`-своп всего фрагмента** удаляет старый узел вместе с его
|
||||
поллером и htmx `process`-инициализирует новый — двойного опроса нет **при
|
||||
условии совпадения корневого `id`** (см. инвариант выше).
|
||||
- Данные тика — из in-memory снимка воркера (`LiveStatus.Live(infohash)`), без
|
||||
БД/сети на каждый тик; узкий контракт `LiveStatus` не зависит от способа
|
||||
доставки (поллинг сейчас, путь к SSE оставлен изолированным).
|
||||
условии совпадения корневого `id`** (см. инвариант выше). Эфемерное состояние
|
||||
разметки своп не переживает: то, что должно пережить тик (раскрытый
|
||||
`<details>`), помечается `hx-preserve`.
|
||||
- **Частота — по цене тика, и она названа числом в
|
||||
[database.md](../database.md).** Поверхность с живыми цифрами качания
|
||||
обновляется чаще (`pollFast`, вровень с частотой опроса qBittorrent — быстрее
|
||||
источника опрашивать бессмысленно), прочие наблюдаемые — реже (`pollSlow`).
|
||||
- **Тик ходит в БД, и это цена решения.** Живые цифры берутся из in-memory
|
||||
снимка воркера (`LiveStatus.Live(infohash)`), но состояние и размер раскладки
|
||||
тик читает из хранилища, а тик страницы загрузки ещё и считает предпросмотр
|
||||
раскладки с обходом ФС — отсюда и разные интервалы. Узкий контракт
|
||||
`LiveStatus` при этом не зависит от способа доставки (поллинг сейчас, путь к
|
||||
SSE оставлен изолированным).
|
||||
- **Инвариант: браузер не опрашивает qBittorrent напрямую** — только свой сервер,
|
||||
который читает снимок. Поллинг статуса UI логируем на `DEBUG` (рутинно-частое,
|
||||
см. [logging.md](logging.md)).
|
||||
|
||||
Reference in New Issue
Block a user