я косяк, поправил веб. Капитан, работайте!
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,406 @@
|
||||
/**
|
||||
* Фоновый 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);
|
||||
Reference in New Issue
Block a user