407 lines
14 KiB
JavaScript
407 lines
14 KiB
JavaScript
/**
|
||
* Фоновый 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, "&")
|
||
.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 =
|
||
"Сервер долго не отвечает. Если перезапуск уже завершился, " +
|
||
'<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);
|