Files
site/apps/web/public/wesp/js/wesp-update-notifier.js

407 lines
14 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Фоновый 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 =
'<div class="wesp-update-banner-inner">' +
'<div class="wesp-update-banner-text" id="wespUpdateBannerText"></div>' +
'<div class="wesp-update-banner-changelog" id="wespUpdateBannerChangelog"></div>' +
'<div class="wesp-update-banner-actions">' +
'<button type="button" class="wesp-update-banner-btn" id="wespUpdateBannerDetails">Подробнее</button>' +
'<button type="button" class="wesp-update-banner-btn" id="wespUpdateBannerLater">Позже</button>' +
'<button type="button" class="wesp-update-banner-btn wesp-update-banner-btn--primary" id="wespUpdateBannerInstall">Обновить</button>' +
"</div></div>";
document.body.appendChild(banner);
var overlay = document.createElement("div");
overlay.id = "wespUpdateOverlay";
overlay.setAttribute("aria-hidden", "true");
overlay.innerHTML =
'<div class="wesp-update-overlay-panel" role="alertdialog" aria-modal="true">' +
'<h2 class="wesp-update-overlay-title" id="wespUpdateOverlayTitle">Идёт обновление</h2>' +
'<p class="wesp-update-overlay-body" id="wespUpdateOverlayBody">Подготовка…</p>' +
'<div class="wesp-update-progress-wrap" id="wespUpdateProgressWrap">' +
'<div class="wesp-update-progress-track">' +
'<div class="wesp-update-progress-bar" id="wespUpdateProgressBar"></div>' +
"</div>" +
'<div class="wesp-update-progress-meta">' +
'<span class="wesp-update-progress-percent" id="wespUpdateProgressPercent">0%</span>' +
'<span class="wesp-update-progress-detail" id="wespUpdateProgressDetail"></span>' +
"</div>" +
"</div>" +
"</div>";
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 =
"Доступна версия <strong>" +
escapeHtml(status.pending_version) +
"</strong>" +
(status.pending_name ? " — " + escapeHtml(status.pending_name) : "") +
". Сейчас: <strong>" +
escapeHtml(status.current_version || "—") +
"</strong>.";
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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
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 =
"Сервер долго не отвечает. Если перезапуск уже завершился, " +
'<button type="button" class="wesp-update-banner-btn wesp-update-banner-btn--primary" id="wespUpdateManualReload">Обновить страницу</button>';
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) +
' <button type="button" class="wesp-update-banner-btn wesp-update-banner-btn--primary" id="wespUpdateManualReload">Обновить страницу</button>';
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);