feat(panel): adaptive monitoring poll intervals (v1.x closure phase 1)
test / test (push) Has been cancelled
test / test (push) Has been cancelled
Replace fixed 5 s hx-trigger polling with data-poll markers and panel.js scheduling: 5 s while active, 30 s when idle, none when tab is hidden. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -30,6 +30,11 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
- Monitoring screens (status, mail queue, system log, deliveries) use adaptive
|
||||||
|
HTMX polling: 5 s while the operator is active on the page, 30 s when the tab
|
||||||
|
is visible but idle, and no requests while the tab is hidden. Scheduling
|
||||||
|
lives in `panel.js` (`data-poll` markers) instead of `hx-trigger="every …"`,
|
||||||
|
which would need `unsafe-eval` under the panel's CSP.
|
||||||
- The delivery page is laid out in two columns: what the journal recorded on
|
- The delivery page is laid out in two columns: what the journal recorded on
|
||||||
the left, what happened to the message on the right, and the delivery log at
|
the left, what happened to the message on the right, and the delivery log at
|
||||||
full width under both. The facts the page used to stack one per line — domain,
|
full width under both. The facts the page used to stack one per line — domain,
|
||||||
|
|||||||
@@ -153,8 +153,11 @@ unless noted.
|
|||||||
| `/backup` | Full backup download, domain import |
|
| `/backup` | Full backup download, domain import |
|
||||||
| `/account` | Admin username/password |
|
| `/account` | Admin username/password |
|
||||||
|
|
||||||
HTMX polling refreshes monitoring fragments; polling does not extend session
|
HTMX polling refreshes monitoring fragments (5 s while the operator is active on
|
||||||
idle timeout (only non-`HX-Request` GET and mutating requests count as activity).
|
the page, 30 s when the tab is visible but idle, none when hidden — scheduled in
|
||||||
|
`panel.js` via `data-poll`, not `hx-trigger="every …"`); polling does not extend
|
||||||
|
session idle timeout (only non-`HX-Request` GET and mutating requests count as
|
||||||
|
activity).
|
||||||
|
|
||||||
### Sessions
|
### Sessions
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -54,7 +54,8 @@
|
|||||||
- **Фаза 2 плана ревизии выполнена (2026-08-06)** (GUI polish, P2): опрос мониторинговых страниц не уходит на сервер, пока вкладка скрыта — фильтр повешен на `htmx:beforeRequest` в `panel.js`, а не на встроенный в htmx фильтр триггера (тот вычисляется через `new Function`, что CSP панели `default-src 'self'` без `unsafe-eval` молча ломает); тёмная тема переписана с каскада `!important` на переопределение CSS-переменных в одном блоке `prefers-color-scheme: dark`; дублирующее правило `main { max-width }` сведено к одному базовому плюс задокументированные постраничные оверрайды. Только CSS/JS, поведения сервера не касается; вживую не проверялось (нет Docker локально) — кандидат на следующий прогон на стенде.
|
- **Фаза 2 плана ревизии выполнена (2026-08-06)** (GUI polish, P2): опрос мониторинговых страниц не уходит на сервер, пока вкладка скрыта — фильтр повешен на `htmx:beforeRequest` в `panel.js`, а не на встроенный в htmx фильтр триггера (тот вычисляется через `new Function`, что CSP панели `default-src 'self'` без `unsafe-eval` молча ломает); тёмная тема переписана с каскада `!important` на переопределение CSS-переменных в одном блоке `prefers-color-scheme: dark`; дублирующее правило `main { max-width }` сведено к одному базовому плюс задокументированные постраничные оверрайды. Только CSS/JS, поведения сервера не касается; вживую не проверялось (нет Docker локально) — кандидат на следующий прогон на стенде.
|
||||||
- **Фаза 3 плана ревизии выполнена (2026-08-06)** (operational improvements, P2–P3): (1) log-tailer сохраняет позицию чтения — таблица `logtail_state` (миграция `0003`, `internal/store/logtail.go`) хранит offset + отпечаток первых 512 байт лога, `internal/logtail/offset.go` решает откуда стартовать: отпечаток совпал → продолжаем с offset (дочитывается хвост, написанный пока панель лежала); не совпал (лог сменился/пересоздан) → читаем файл с начала (повторный разбор безвреден, `UpdateStatus` идемпотентен); записи нет вовсе (первый запуск) → с конца, как раньше. Запись offset — не чаще раза в 5 с, плюс форс при ротации и на выключении; сохраняется позиция *потреблённых* байт (минус недочитанная частичная строка). (2) L2-лимит перестал промахиваться при параллельных сессиях: между проверкой на MAIL FROM и вставкой строки на end-of-message сообщение не видно в БД, поэтому N одновременных сессий пропускали друг друга — теперь к счёту из БД добавляются «в полёте» (`internal/milter/inflight.go`, общий на процесс реестр резерваций); резервация освобождается после записи в send-log, на ABORT и по TTL 10 минут (у go-milter нет колбэка на закрытие соединения, а вечная резервация — это fail-closed-дрейф, которого у лимитера быть не должно). Транзакция «count+insert», как предлагал review, невозможна буквально: эти два шага разнесены по разным стадиям SMTP-транзакции. Тесты: restart/rotation-resume для tailer'а, четыре сценария резерваций для лимита. `gofmt`/`go vet` чистые; `go test ./...` — падения только известные Windows-специфичные (`internal/domain`, `TestFollowTailsAndRotates`). Не проверено на стенде (нет Docker локально) — кандидат на следующий прогон на стенде.
|
- **Фаза 3 плана ревизии выполнена (2026-08-06)** (operational improvements, P2–P3): (1) log-tailer сохраняет позицию чтения — таблица `logtail_state` (миграция `0003`, `internal/store/logtail.go`) хранит offset + отпечаток первых 512 байт лога, `internal/logtail/offset.go` решает откуда стартовать: отпечаток совпал → продолжаем с offset (дочитывается хвост, написанный пока панель лежала); не совпал (лог сменился/пересоздан) → читаем файл с начала (повторный разбор безвреден, `UpdateStatus` идемпотентен); записи нет вовсе (первый запуск) → с конца, как раньше. Запись offset — не чаще раза в 5 с, плюс форс при ротации и на выключении; сохраняется позиция *потреблённых* байт (минус недочитанная частичная строка). (2) L2-лимит перестал промахиваться при параллельных сессиях: между проверкой на MAIL FROM и вставкой строки на end-of-message сообщение не видно в БД, поэтому N одновременных сессий пропускали друг друга — теперь к счёту из БД добавляются «в полёте» (`internal/milter/inflight.go`, общий на процесс реестр резерваций); резервация освобождается после записи в send-log, на ABORT и по TTL 10 минут (у go-milter нет колбэка на закрытие соединения, а вечная резервация — это fail-closed-дрейф, которого у лимитера быть не должно). Транзакция «count+insert», как предлагал review, невозможна буквально: эти два шага разнесены по разным стадиям SMTP-транзакции. Тесты: restart/rotation-resume для tailer'а, четыре сценария резерваций для лимита. `gofmt`/`go vet` чистые; `go test ./...` — падения только известные Windows-специфичные (`internal/domain`, `TestFollowTailsAndRotates`). Не проверено на стенде (нет Docker локально) — кандидат на следующий прогон на стенде.
|
||||||
- **Добор по плану ревизии выполнен (2026-08-06):** (1) проект переехал на единственную площадку — GitHub (Codeberg уходит): вместе с URL, лицензионными шапками SVG/HTML и docs переехал путь Go-модуля на `github.com/mixeme/selfpost` (`go.mod`, `test/e2e/go.mod`, все импорты, `MODULE` в Makefile, `-ldflags` в Dockerfile и development.md) — оставлять импорты на исчезающем хосте нельзя, `go get`/`go install` сломались бы; (2) ссылки на архивную спецификацию убраны из кода целиком — не только «spec 7.x», как просило ревью, но и «spec 4/5/6/8/9», страдавшие тем же, каждая заменена на живой документ с секцией там, где документ большой; (3) [architecture.md](architecture.md) § Code layers — диаграмма слоёв (A2); (4) `TestParseDelivery` расширен экзотикой mail.log — и **вскрыл реальный баг**: шаблон брал `status=` жадно, то есть последнее вхождение в строке, а Postfix дописывает ответ удалённого сервера дословно, поэтому отказ с `status=sent` в тексте ответа попадал в журнал как доставленный (исправлено на ленивый разбор); (5) `CONTRIBUTING.md` перенесён в 2.x, бамп тега образа и git-тег оставлены в [roadmap.md](roadmap.md) § v1.x. `gofmt`/`go vet` чистые в обоих модулях, `go test ./...` — падения только известные Windows-специфичные (`internal/domain`, `TestFollowTailsAndRotates`). На стенде не проверялось (нет Docker локально).
|
- **Добор по плану ревизии выполнен (2026-08-06):** (1) проект переехал на единственную площадку — GitHub (Codeberg уходит): вместе с URL, лицензионными шапками SVG/HTML и docs переехал путь Go-модуля на `github.com/mixeme/selfpost` (`go.mod`, `test/e2e/go.mod`, все импорты, `MODULE` в Makefile, `-ldflags` в Dockerfile и development.md) — оставлять импорты на исчезающем хосте нельзя, `go get`/`go install` сломались бы; (2) ссылки на архивную спецификацию убраны из кода целиком — не только «spec 7.x», как просило ревью, но и «spec 4/5/6/8/9», страдавшие тем же, каждая заменена на живой документ с секцией там, где документ большой; (3) [architecture.md](architecture.md) § Code layers — диаграмма слоёв (A2); (4) `TestParseDelivery` расширен экзотикой mail.log — и **вскрыл реальный баг**: шаблон брал `status=` жадно, то есть последнее вхождение в строке, а Postfix дописывает ответ удалённого сервера дословно, поэтому отказ с `status=sent` в тексте ответа попадал в журнал как доставленный (исправлено на ленивый разбор); (5) `CONTRIBUTING.md` перенесён в 2.x, бамп тега образа и git-тег оставлены в [roadmap.md](roadmap.md) § v1.x. `gofmt`/`go vet` чистые в обоих модулях, `go test ./...` — падения только известные Windows-специфичные (`internal/domain`, `TestFollowTailsAndRotates`). На стенде не проверялось (нет Docker локально).
|
||||||
- **Дальше:** релизный гейт (Фаза 0) закрыт по существу — e2e C.4 и ревизия § D пройдены; остаются только шаги, которые делаются в момент резки версии (бамп тега образа в compose, git tag) по явной команде пользователя. Все polish-фазы плана ревизии (1, 1.5, 2, 3) закрыты.
|
- **v1.x-closure Фаза 1 выполнена (2026-08-08)** (адаптивный опрос мониторинга): четыре HTMX-фрагмента (`status_body`, `mail_queue_body`, `system_log_body`, `deliveries_rows`) несут `data-poll` и `hx-trigger="load"` только для первого запроса; `panel.js` планирует следующий опрос после `htmx:afterSwap` / `htmx:responseError` — 5 s при активности оператора на странице, 30 s при видимой, но простаивающей вкладке, 0 при скрытой (`beforeRequest` + сброс таймеров на `visibilitychange`). Без `hx-trigger="every … [expr]"` (CSP / `unsafe-eval`). Docs: `architecture.md`, `roadmap.md`, CHANGELOG. На стенде не проверялось.
|
||||||
|
- **Дальше:** v1.x-closure [v1.x-closure-plan.md](v1.x-closure-plan.md) — **Фаза 1 закрыта** (адаптивный опрос мониторинга: 5 s / 30 s / 0 при скрытой вкладке, `data-poll` в `panel.js`). Следующий шаг — **Фаза 2** (mail.log в `/data` + postqueue reconcile). Релизный гейт по коду закрыт; бамп тега образа и git tag — по явной команде (Фазы 4–5).
|
||||||
- **Принятые риски** — [security.md](security.md). **Опционально v1.x / 2.x** — [roadmap.md](roadmap.md) (хвост документации, send-log gaps, Фаза O1+, роль администратора домена).
|
- **Принятые риски** — [security.md](security.md). **Опционально v1.x / 2.x** — [roadmap.md](roadmap.md) (хвост документации, send-log gaps, Фаза O1+, роль администратора домена).
|
||||||
- **Прод:** инстанс с реальным Let's Encrypt сертификатом и живым deliverability (DKIM/SPF pass). Контейнер там всё ещё на образе v1.0 — Фаза 14 в него не выкатывалась. При апгрейде: админа один раз разлогинит (сменилось имя cookie), а от reverse-proxy требуется передача исходного `Host` (Apache-фрагмент из `deploy/` это делает).
|
- **Прод:** инстанс с реальным Let's Encrypt сертификатом и живым deliverability (DKIM/SPF pass). Контейнер там всё ещё на образе v1.0 — Фаза 14 в него не выкатывалась. При апгрейде: админа один раз разлогинит (сменилось имя cookie), а от reverse-proxy требуется передача исходного `Host` (Apache-фрагмент из `deploy/` это делает).
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -71,14 +71,15 @@ git-тег `vX.Y.Z`; `implementation-plan.md` в `docs/archive/`, ссылок
|
|||||||
Стоит одного абзаца со списком всех файлов `docs/` и одной строкой на каждый.
|
Стоит одного абзаца со списком всех файлов `docs/` и одной строкой на каждый.
|
||||||
Мелочь, но именно она делает набор документов набором, а не россыпью.
|
Мелочь, но именно она делает набор документов набором, а не россыпью.
|
||||||
|
|
||||||
**Опрос мониторинга у открытой, но незанятой вкладки.** Скрытая вкладка уже не
|
**Опрос мониторинга у открытой, но незанятой вкладки.** ~~Скрытая вкладка уже не
|
||||||
опрашивает сервер (фильтр на `htmx:beforeRequest` в
|
опрашивает сервер (фильтр на `htmx:beforeRequest` в
|
||||||
[panel.js](../internal/web/static/panel.js)). Остаток: вкладка на переднем
|
[panel.js](../internal/web/static/panel.js)). Остаток: вкладка на переднем
|
||||||
плане, с которой не работают, всё равно ходит раз в 5 с. Кандидат — адаптивный
|
плане, с которой не работают, всё равно ходит раз в 5 с. Кандидат — адаптивный
|
||||||
интервал (5 с при активности, 30 с при простое) по `htmx:afterRequest` без
|
интервал (5 с при активности, 30 с при простое) по `htmx:afterRequest` без
|
||||||
изменения `hx-trigger`. Ценность низкая: нагрузка — один SQL-запрос и рендер
|
изменения `hx-trigger`. Ценность низкая: нагрузка — один SQL-запрос и рендер
|
||||||
фрагмента, так что это скорее гигиена, чем экономия. Допустимый исход —
|
фрагмента, так что это скорее гигиена, чем экономия. Допустимый исход —
|
||||||
осознанно не делать.
|
осознанно не делать.~~ **Закрыто (v1.x-closure Фаза 1):** адаптивный интервал
|
||||||
|
5 с / 30 с / 0 (скрытая вкладка) в `panel.js` через `data-poll`.
|
||||||
|
|
||||||
**Send-log vs `mail.log` (частично закрыто).** Persist позиции чтения сделан
|
**Send-log vs `mail.log` (частично закрыто).** Persist позиции чтения сделан
|
||||||
(таблица `logtail_state`, миграция `0003`): после
|
(таблица `logtail_state`, миграция `0003`): после
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
|
|
||||||
## Общий чек-лист прогресса
|
## Общий чек-лист прогресса
|
||||||
|
|
||||||
- [ ] **Фаза 1** — адаптивный опрос мониторинга
|
- [x] **Фаза 1** — адаптивный опрос мониторинга
|
||||||
- [ ] **Фаза 2** — mail.log в `/data` + postqueue reconcile
|
- [ ] **Фаза 2** — mail.log в `/data` + postqueue reconcile
|
||||||
- [ ] **Фаза 3** — docs: development.md, README, удаление планов и `docs/archive/`
|
- [ ] **Фаза 3** — docs: development.md, README, удаление планов и `docs/archive/`
|
||||||
- [ ] **Фаза 4** — релизный коммит `1.0.0` (по явной команде)
|
- [ ] **Фаза 4** — релизный коммит `1.0.0` (по явной команде)
|
||||||
@@ -60,12 +60,12 @@
|
|||||||
|
|
||||||
### Чек-лист
|
### Чек-лист
|
||||||
|
|
||||||
- [ ] `status_body.html`, `mail_queue_body.html`, `system_log_body.html`, `deliveries_rows.html` — `hx-trigger="load"` + маркер polling
|
- [x] `status_body.html`, `mail_queue_body.html`, `system_log_body.html`, `deliveries_rows.html` — `hx-trigger="load"` + маркер polling
|
||||||
- [ ] `panel.js` — `lastActivity`, schedule после `htmx:afterRequest` (5s / 30s)
|
- [x] `panel.js` — `lastActivity`, schedule после `htmx:afterRequest` (5s / 30s)
|
||||||
- [ ] `htmx:afterSwap` — реинициализация polling-элементов
|
- [x] `htmx:afterSwap` — реинициализация polling-элементов
|
||||||
- [ ] `beforeRequest` для hidden tab — оставлен
|
- [x] `beforeRequest` для hidden tab — оставлен
|
||||||
- [ ] `architecture.md` — одна строка про polling
|
- [x] `architecture.md` — одна строка про polling
|
||||||
- [ ] CHANGELOG `[Unreleased]`
|
- [x] CHANGELOG `[Unreleased]`
|
||||||
- [ ] Стенд (опционально): idle → ~30s; активность → ~5s; hidden → 0 запросов
|
- [ ] Стенд (опционально): idle → ~30s; активность → ~5s; hidden → 0 запросов
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const usernameKey ctxKey = 0
|
|||||||
// everything except a GET request carrying HX-Request: the four monitoring
|
// everything except a GET request carrying HX-Request: the four monitoring
|
||||||
// fragments (/status/fragment, /mail-queue/body, /system-log/body,
|
// fragments (/status/fragment, /mail-queue/body, /system-log/body,
|
||||||
// /deliveries/rows)
|
// /deliveries/rows)
|
||||||
// poll every 5s regardless of whether anyone is looking at the tab, so
|
// poll on a timer regardless of whether anyone is looking at the tab, so
|
||||||
// counting those as activity would make "N days idle" mean "N days since a
|
// counting those as activity would make "N days idle" mean "N days since a
|
||||||
// browser tab was last open" instead.
|
// browser tab was last open" instead.
|
||||||
func (s *Server) requireAuth(next http.Handler) http.Handler {
|
func (s *Server) requireAuth(next http.Handler) http.Handler {
|
||||||
|
|||||||
@@ -144,7 +144,7 @@
|
|||||||
//
|
//
|
||||||
// Each pass looks its targets up by id rather than holding on to elements
|
// Each pass looks its targets up by id rather than holding on to elements
|
||||||
// found once: the status page replaces its cards wholesale every five
|
// found once: the status page replaces its cards wholesale every five
|
||||||
// seconds (hx-swap on #status-body), and anything remembered here would be
|
// seconds (adaptive polling on #status-body), and anything remembered here would be
|
||||||
// measuring boxes that had left the document.
|
// measuring boxes that had left the document.
|
||||||
var sectionLinks = [];
|
var sectionLinks = [];
|
||||||
|
|
||||||
@@ -204,19 +204,95 @@
|
|||||||
initSectionIndex();
|
initSectionIndex();
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Skip polling while the tab is hidden ------------------------------
|
// --- Adaptive monitoring polling ---------------------------------------
|
||||||
// The monitoring pages (status, mail queue, system log, deliveries) poll
|
// The four monitoring fragments carry data-poll and hx-trigger="load" for
|
||||||
// every 5s via hx-trigger="every 5s". htmx has a built-in way to make that
|
// the first fetch only. panel.js schedules the rest: 5 s while the operator
|
||||||
// conditional (an event filter, hx-trigger="every 5s [expr]"), but it
|
// is active on the page, 30 s when the tab is visible but idle, and nothing
|
||||||
// evaluates the filter with `new Function`, which the panel's CSP
|
// while the tab is hidden. hx-trigger="every Ns [expr]" could express some
|
||||||
// (default-src 'self', no 'unsafe-eval') would silently break. Skipping the
|
// of that, but the filter is evaluated with `new Function`, which the
|
||||||
// request here instead needs nothing beyond what the CSP already allows: a
|
// panel's CSP (default-src 'self', no 'unsafe-eval') would silently break.
|
||||||
// request due while the tab is hidden is simply not sent, and the next
|
var pollActiveMs = 5000;
|
||||||
// request after it becomes visible again picks up on schedule as usual.
|
var pollIdleMs = 30000;
|
||||||
|
// No pointer/keyboard/scroll input for this long → treat the tab as idle.
|
||||||
|
var userIdleMs = 30000;
|
||||||
|
var lastActivity = Date.now();
|
||||||
|
var pollTimers = Object.create(null);
|
||||||
|
|
||||||
|
["mousedown", "mousemove", "keydown", "scroll", "touchstart"].forEach(function (evt) {
|
||||||
|
document.addEventListener(evt, function () {
|
||||||
|
lastActivity = Date.now();
|
||||||
|
}, { passive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
function pollDelayMs() {
|
||||||
|
return Date.now() - lastActivity < userIdleMs ? pollActiveMs : pollIdleMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerPoll(el) {
|
||||||
|
htmx.ajax("GET", el.getAttribute("hx-get"), {
|
||||||
|
target: "#" + el.id,
|
||||||
|
swap: el.getAttribute("hx-swap") || "outerHTML"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function schedulePoll(el) {
|
||||||
|
if (!el || !el.id || !el.hasAttribute("data-poll")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (pollTimers[el.id]) {
|
||||||
|
clearTimeout(pollTimers[el.id]);
|
||||||
|
delete pollTimers[el.id];
|
||||||
|
}
|
||||||
|
if (document.hidden) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var id = el.id;
|
||||||
|
pollTimers[id] = setTimeout(function () {
|
||||||
|
delete pollTimers[id];
|
||||||
|
var current = document.getElementById(id);
|
||||||
|
if (!current || !current.hasAttribute("data-poll")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (document.hidden) {
|
||||||
|
schedulePoll(current);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
triggerPoll(current);
|
||||||
|
}, pollDelayMs());
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPollElementReady(el) {
|
||||||
|
if (!el || !el.hasAttribute("data-poll")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Swapped-in markup still carries hx-trigger="load"; strip it so htmx does
|
||||||
|
// not issue a duplicate GET on top of the response we just received.
|
||||||
|
el.removeAttribute("hx-trigger");
|
||||||
|
schedulePoll(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.addEventListener("htmx:afterSwap", function (ev) {
|
||||||
|
onPollElementReady(ev.detail.elt);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.addEventListener("htmx:responseError", function (ev) {
|
||||||
|
onPollElementReady(ev.detail.elt);
|
||||||
|
});
|
||||||
|
|
||||||
document.body.addEventListener("htmx:beforeRequest", function (ev) {
|
document.body.addEventListener("htmx:beforeRequest", function (ev) {
|
||||||
var trigger = ev.target.getAttribute && ev.target.getAttribute("hx-trigger");
|
if (document.hidden && ev.target.hasAttribute && ev.target.hasAttribute("data-poll")) {
|
||||||
if (document.hidden && trigger && trigger.indexOf("every") !== -1) {
|
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.addEventListener("visibilitychange", function () {
|
||||||
|
if (document.hidden) {
|
||||||
|
Object.keys(pollTimers).forEach(function (id) {
|
||||||
|
clearTimeout(pollTimers[id]);
|
||||||
|
delete pollTimers[id];
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.querySelectorAll("[data-poll]").forEach(schedulePoll);
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{{define "deliveries_rows"}}
|
{{define "deliveries_rows"}}
|
||||||
<div id="deliveries-rows" hx-get="/deliveries/rows?domain={{.FilterDomain}}&app={{.FilterApp}}&p={{.Page}}"
|
<div id="deliveries-rows" data-poll hx-get="/deliveries/rows?domain={{.FilterDomain}}&app={{.FilterApp}}&p={{.Page}}"
|
||||||
hx-trigger="every 5s" hx-swap="outerHTML">
|
hx-trigger="load" hx-swap="outerHTML">
|
||||||
{{if .Rows}}
|
{{if .Rows}}
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{{define "mail_queue_body"}}
|
{{define "mail_queue_body"}}
|
||||||
<div id="mail-queue-body" hx-get="/mail-queue/body" hx-trigger="every 5s" hx-swap="outerHTML">
|
<div id="mail-queue-body" data-poll hx-get="/mail-queue/body" hx-trigger="load" hx-swap="outerHTML">
|
||||||
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
|
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
|
||||||
<span class="code">{{if .Output}}{{.Output}}{{else}}Queue is empty.{{end}}</span>
|
<span class="code">{{if .Output}}{{.Output}}{{else}}Queue is empty.{{end}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
|
|
||||||
{{/* The status page's section index, shown in the navigation column (see the
|
{{/* The status page's section index, shown in the navigation column (see the
|
||||||
"sections" block in layout.html). The first six cards are the ones the
|
"sections" block in layout.html). The first six cards are the ones the
|
||||||
polling fragment replaces every five seconds; their ids are part of
|
polling fragment replaces on a timer; their ids are part of
|
||||||
status_body.html and do not change with the reading, so the links here hold
|
status_body.html and do not change with the reading, so the links here hold
|
||||||
across a refresh. */}}
|
across a refresh. */}}
|
||||||
{{define "sections"}}
|
{{define "sections"}}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{{define "status_body"}}
|
{{define "status_body"}}
|
||||||
<div id="status-body" hx-get="/status/fragment" hx-trigger="every 5s" hx-swap="outerHTML">
|
<div id="status-body" data-poll hx-get="/status/fragment" hx-trigger="load" hx-swap="outerHTML">
|
||||||
<div class="card" id="overall">
|
<div class="card" id="overall">
|
||||||
<h2>Overall <span class="st st-{{.OverallStatus}}">{{.OverallStatus}}</span></h2>
|
<h2>Overall <span class="st st-{{.OverallStatus}}">{{.OverallStatus}}</span></h2>
|
||||||
<p class="muted">{{.OverallHeading}}</p>
|
<p class="muted">{{.OverallHeading}}</p>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{{define "system_log_body"}}
|
{{define "system_log_body"}}
|
||||||
<div id="system-log-body" hx-get="/system-log/body" hx-trigger="every 5s" hx-swap="outerHTML">
|
<div id="system-log-body" data-poll hx-get="/system-log/body" hx-trigger="load" hx-swap="outerHTML">
|
||||||
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
|
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
|
||||||
<span class="code">{{if .Lines}}{{range .Lines}}{{.}}
|
<span class="code">{{if .Lines}}{{range .Lines}}{{.}}
|
||||||
{{end}}{{else}}No log lines yet.{{end}}</span>
|
{{end}}{{else}}No log lines yet.{{end}}</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user