/** * Фоновый poll статуса обновлений + баннер «Обновить / Позже». * WespUpdateNotifier.init({ dialog: WespDialog | WespKioskDialog }); */ (function (global) { var DISMISS_KEY = "wesp_update_dismissed"; var DEFAULT_POLL_MS = 300000; var PROGRESS_POLL_MS = 500; var opts = {}; var pollTimer = null; var progressTimer = null; var lastStatus = null; var lastProgress = null; function ensureDom() { if (document.getElementById("wespUpdateBanner")) return; var banner = document.createElement("div"); banner.id = "wespUpdateBanner"; banner.setAttribute("role", "region"); banner.setAttribute("aria-label", "Доступно обновление"); banner.innerHTML = '
' + '
' + '
' + '
' + '' + '' + '' + "
"; document.body.appendChild(banner); var overlay = document.createElement("div"); overlay.id = "wespUpdateOverlay"; overlay.setAttribute("aria-hidden", "true"); overlay.innerHTML = '
' + '

Идёт обновление

' + '

Подготовка…

' + '
' + '
' + '
' + "
" + '
' + '0%' + '' + "
" + "
" + "
"; document.body.appendChild(overlay); document.getElementById("wespUpdateBannerLater").addEventListener("click", dismissBanner); document.getElementById("wespUpdateBannerDetails").addEventListener("click", toggleDetails); document.getElementById("wespUpdateBannerInstall").addEventListener("click", onInstallClick); } function isDismissed(version) { try { return sessionStorage.getItem(DISMISS_KEY) === String(version || ""); } catch (e) { return false; } } function dismissBanner() { if (lastStatus && lastStatus.pending_version) { try { sessionStorage.setItem(DISMISS_KEY, String(lastStatus.pending_version)); } catch (e) {} } hideBanner(); } function hideBanner() { var el = document.getElementById("wespUpdateBanner"); if (el) { el.classList.remove("wesp-update-banner--visible", "wesp-update-banner--expanded"); } } function showBanner(status) { if (!status || !status.update_available || !status.pending_version) { hideBanner(); return; } if (isDismissed(status.pending_version)) { hideBanner(); return; } ensureDom(); var el = document.getElementById("wespUpdateBanner"); var text = document.getElementById("wespUpdateBannerText"); var changelog = document.getElementById("wespUpdateBannerChangelog"); if (!el || !text) return; text.innerHTML = "Доступна версия " + escapeHtml(status.pending_version) + "" + (status.pending_name ? " — " + escapeHtml(status.pending_name) : "") + ". Сейчас: " + escapeHtml(status.current_version || "—") + "."; if (changelog) { changelog.textContent = status.pending_body || ""; changelog.style.display = status.pending_body ? "" : "none"; } var detailsBtn = document.getElementById("wespUpdateBannerDetails"); if (detailsBtn) { detailsBtn.style.display = status.pending_body ? "" : "none"; } el.classList.add("wesp-update-banner--visible"); } function toggleDetails() { var el = document.getElementById("wespUpdateBanner"); if (el) el.classList.toggle("wesp-update-banner--expanded"); } function showInstallingOverlay(message, progress) { ensureDom(); var overlay = document.getElementById("wespUpdateOverlay"); if (overlay) { overlay.classList.add("wesp-update-overlay--visible"); overlay.setAttribute("aria-hidden", "false"); } if (message) { var bodyEl = document.getElementById("wespUpdateOverlayBody"); if (bodyEl) bodyEl.textContent = message; } applyProgressUi(progress); hideBanner(); } function hideInstallingOverlay() { var overlay = document.getElementById("wespUpdateOverlay"); if (overlay) { overlay.classList.remove("wesp-update-overlay--visible"); overlay.setAttribute("aria-hidden", "true"); } } function applyProgressUi(progress) { if (!progress || typeof progress !== "object") return; lastProgress = progress; var titleEl = document.getElementById("wespUpdateOverlayTitle"); var bodyEl = document.getElementById("wespUpdateOverlayBody"); var barEl = document.getElementById("wespUpdateProgressBar"); var pctEl = document.getElementById("wespUpdateProgressPercent"); var detailEl = document.getElementById("wespUpdateProgressDetail"); var pct = Number(progress.percent); if (!Number.isFinite(pct)) pct = 0; pct = Math.max(0, Math.min(100, Math.round(pct))); if (titleEl && progress.target_version) { titleEl.textContent = "Обновление до версии " + progress.target_version; } if (bodyEl && progress.message) { bodyEl.textContent = progress.message; } if (barEl) { barEl.style.width = pct + "%"; barEl.setAttribute("aria-valuenow", String(pct)); } if (pctEl) pctEl.textContent = pct + "%"; if (detailEl) { detailEl.textContent = progress.detail || ""; detailEl.style.display = progress.detail ? "" : "none"; } } function stopProgressPolling() { if (progressTimer) { clearInterval(progressTimer); progressTimer = null; } } function startProgressPolling() { stopProgressPolling(); progressTimer = setInterval(function () { fetchStatus().then(function (status) { if (!status) return; if (status.update_progress) { applyProgressUi(status.update_progress); } else if (status.is_updating) { applyProgressUi({ message: "Выполняется обновление…", percent: lastProgress ? lastProgress.percent : 5, }); } }); }, PROGRESS_POLL_MS); } function escapeHtml(s) { return String(s) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function fetchStatus() { var url = opts.statusUrl || "/api/updates/status"; return fetch(url, { credentials: "same-origin", cache: "no-store" }).then(function (r) { if (r.status === 401 || r.status === 403) return null; return r.json().catch(function () { return null; }); }); } function applyStatus(status) { if (!status || typeof status !== "object") return; lastStatus = status; if (status.update_available && status.pending_version) { try { var dismissed = sessionStorage.getItem(DISMISS_KEY); if (dismissed && dismissed !== String(status.pending_version)) { sessionStorage.removeItem(DISMISS_KEY); } } catch (e) {} } showBanner(status); } function pollOnce() { return fetchStatus().then(function (status) { applyStatus(status); }); } function onInstallClick() { var dialog = opts.dialog; if (!dialog || typeof dialog.confirm !== "function") { if (!global.confirm("Система перезапустится. Продолжить?")) return; doInstall(); return; } var msg = "Будет загружена и установлена версия " + (lastStatus && lastStatus.pending_version ? lastStatus.pending_version : "") + ".\n\nСистема перезапустится. Продолжить?"; dialog .confirm(msg, { title: "Обновление WESP", okText: "Обновить", cancelText: "Отмена", danger: true, }) .then(function (ok) { if (ok) doInstall(); }); } function waitForServerAndReload() { var url = opts.statusUrl || "/api/updates/status"; var attempts = 0; var maxAttempts = 90; var intervalMs = 2000; showInstallingOverlay( "Обновление установлено. Ожидаем перезапуск сервера — страница обновится автоматически.", { message: "Ожидание перезапуска сервера…", percent: 100, detail: null } ); function scheduleNext() { if (attempts >= maxAttempts) { var bodyEl = document.getElementById("wespUpdateOverlayBody"); if (bodyEl) { bodyEl.innerHTML = "Сервер долго не отвечает. Если перезапуск уже завершился, " + ''; var btn = document.getElementById("wespUpdateManualReload"); if (btn) btn.addEventListener("click", function () { global.location.reload(); }); } return; } setTimeout(tick, intervalMs); } function showRollbackOverlay(state) { var ver = (state && (state.previous_version || state.message)) || "—"; var msg = (state && state.message) || "Обновление отменено. Восстановлена предыдущая версия."; showInstallingOverlay(msg, { message: msg, percent: 100, target_version: ver, }); var bar = document.getElementById("wespUpdateProgressBar"); var panel = document.querySelector("#wespUpdateOverlay .wesp-update-overlay-panel"); if (bar) bar.classList.add("wesp-update-progress-bar--error"); if (panel) panel.classList.add("wesp-update-overlay-panel--rollback"); var bodyEl = document.getElementById("wespUpdateOverlayBody"); if (bodyEl) { bodyEl.innerHTML = escapeHtml(msg) + ' '; var btn = document.getElementById("wespUpdateManualReload"); if (btn) btn.addEventListener("click", function () { global.location.reload(); }); } } function tick() { attempts += 1; applyProgressUi({ message: "Ожидание перезапуска сервера…", percent: 100, detail: "~" + attempts * 2 + " с", }); fetch(url, { credentials: "same-origin", cache: "no-store" }) .then(function (r) { return r.json().catch(function () { return null; }).then(function (body) { return { ok: r.ok, status: r.status, body: body }; }); }) .then(function (res) { var st = res.body && res.body.last_update_state; if (st && st.status === "rolled_back") { showRollbackOverlay(st); return; } if (res.ok || res.status === 401 || res.status === 403) { global.location.reload(); return; } scheduleNext(); }) .catch(function () { scheduleNext(); }); } setTimeout(tick, 3000); } function doInstall() { var targetVersion = lastStatus && lastStatus.pending_version ? lastStatus.pending_version : ""; showInstallingOverlay("Подготовка к обновлению…", { message: "Подготовка к обновлению…", percent: 0, target_version: targetVersion, }); startProgressPolling(); var url = opts.installUrl || "/api/updates/install"; fetch(url, { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json" }, body: "{}" }) .then(function (r) { return r.json().catch(function () { return {}; }).then(function (body) { return { ok: r.ok, body: body }; }); }) .then(function (res) { stopProgressPolling(); if (!res.ok) { hideInstallingOverlay(); var errMsg = (res.body && (res.body.error || res.body.message)) || "Не удалось установить обновление"; if (opts.dialog && opts.dialog.alert) { opts.dialog.alert(errMsg, { title: "Ошибка обновления", variant: "danger" }); } else { global.alert(errMsg); } pollOnce(); return; } waitForServerAndReload(); }) .catch(function () { stopProgressPolling(); hideInstallingOverlay(); if (opts.dialog && opts.dialog.alert) { opts.dialog.alert("Сетевая ошибка при установке обновления.", { title: "Ошибка", variant: "danger" }); } }); } function init(options) { opts = options || {}; if (!opts.dialog) { opts.dialog = global.WespDialog || global.WespKioskDialog || null; } ensureDom(); pollOnce(); var interval = Number(opts.pollIntervalMs); if (!Number.isFinite(interval) || interval < 60000) interval = DEFAULT_POLL_MS; if (pollTimer) clearInterval(pollTimer); pollTimer = setInterval(pollOnce, interval); } global.WespUpdateNotifier = { init: init, pollOnce: pollOnce, dismissBanner: dismissBanner, }; })(typeof window !== "undefined" ? window : globalThis);