diff --git a/internal/lkgateway/lkusers_admin.go b/internal/lkgateway/lkusers_admin.go index 1d2c0a2..b6f8678 100644 --- a/internal/lkgateway/lkusers_admin.go +++ b/internal/lkgateway/lkusers_admin.go @@ -16,6 +16,7 @@ import ( "net/http" "strconv" "strings" + "sync" "time" ) @@ -174,12 +175,52 @@ func (h *setupHandlers) lkUsersSave(w http.ResponseWriter, r *http.Request) { } // lkUsersForSetup — список для страницы настроек; ошибка базы — строкой, чтобы страница открылась. -func lkUsersForSetup(rc *RuntimeConfig) ([]LKUser, string) { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) +// lkUsersCache — список пользователей ЛК для страницы «Настройка». Берётся из кэша (мгновенно), +// обновляется в фоне с коротким таймаутом. Иначе медленный/недоступный ЛК вешал страницу на 10 с. +var lkUsersCache struct { + sync.Mutex + list []LKUser + errMsg string + at time.Time + refreshing bool +} + +func lkUsersRefresh(rc *RuntimeConfig) { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() list, err := lkListUsers(ctx, rc) + lkUsersCache.Lock() + defer lkUsersCache.Unlock() + lkUsersCache.refreshing = false + lkUsersCache.at = time.Now() if err != nil { - return nil, err.Error() + if lkUsersCache.list != nil { // ЛК не ответил — оставляем прошлый список с пометкой + lkUsersCache.errMsg = "ЛК сейчас недоступен — показан сохранённый список (" + err.Error() + ")" + return + } + lkUsersCache.errMsg = err.Error() + return } - return list, "" + lkUsersCache.list, lkUsersCache.errMsg = list, "" +} + +// lkUsersForSetup — список пользователей ЛК для страницы «Настройка»: отдаёт кэш сразу, при +// устаревании обновляет в фоне (страница не ждёт ЛК). Первый раз (холодный кэш) — одно короткое ожидание. +func lkUsersForSetup(rc *RuntimeConfig) ([]LKUser, string) { + lkUsersCache.Lock() + cold := lkUsersCache.at.IsZero() + stale := time.Since(lkUsersCache.at) > 20*time.Second + if !cold && stale && !lkUsersCache.refreshing { + lkUsersCache.refreshing = true + go lkUsersRefresh(rc) + } + list, msg := lkUsersCache.list, lkUsersCache.errMsg + lkUsersCache.Unlock() + if cold { + lkUsersRefresh(rc) // первый заход — подождём коротко (≤3 с), дальше всегда из кэша + lkUsersCache.Lock() + list, msg = lkUsersCache.list, lkUsersCache.errMsg + lkUsersCache.Unlock() + } + return list, msg } diff --git a/internal/lkgateway/web/templates/layout.html b/internal/lkgateway/web/templates/layout.html index ae8c002..22e2a62 100644 --- a/internal/lkgateway/web/templates/layout.html +++ b/internal/lkgateway/web/templates/layout.html @@ -893,9 +893,14 @@ button.dq-link, .dq-link { background:none !important; border:0 !important; padd .then(function(r){ return r.ok ? r.text() : Promise.reject(); }) .then(function(html){ if (dirty || (window.bjSelecting && window.bjSelecting())) return; // правит форму или выделяет — не трогаем + // Якорь: верхний видимый блок — чтобы при изменении высоты карточек выше обзора + // вид не «подпрыгивал». После морфинга вернём этот блок на то же место по вертикали. + var anchor = null, aTop = 0, cards = document.querySelectorAll('main .card, main aside'); + for (var ci = 0; ci < cards.length; ci++) { var r = cards[ci].getBoundingClientRect(); if (r.bottom > 0) { anchor = cards[ci]; aTop = r.top; break; } } var doc = new DOMParser().parseFromString(html, 'text/html'); bjMorphKids(document.querySelector('main'), doc.querySelector('main')); if (window.bjInitCopyBars) window.bjInitCopyBars(); + if (anchor && anchor.isConnected) { var d = anchor.getBoundingClientRect().top - aTop; if (d) window.scrollBy(0, d); } }) .catch(function(){}) .then(function(){ bjRefreshing = false; });