Обновление панели из интерфейса и режим обновления в установщике
Установщик стал идемпотентным: на уже настроенной машине он определяет установленную версию, останавливает службу, меняет бинарник и поднимает её обратно, не трогая конфиг, миры и учётные записи. Зависимости при обновлении не переустанавливаются — это лишние минуты apt. Кнопки «Обновить» и «Перезапустить панель» появились на вкладке «Настройки» вместе с текущей версией и описанием нового релиза. Панель работает от непривилегированного пользователя и не может ни заменить свой бинарник в /usr/local/bin, ни дёрнуть systemctl. Вместо того чтобы расширять её права, установщик кладёт root-скрипт pzmanager-admin и разрешает сервисному пользователю через sudoers ровно эту одну команду. Скрипт сверяет sha256 скачанного бинарника с checksums.txt релиза. Если правила sudo нет, кнопки не будет — панель покажет команду для терминала. Перед обновлением и перезапуском игровой сервер останавливается штатным quit: иначе его погасил бы systemd уже в момент рестарта, и ожидание растянулось бы на таймаут остановки. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+120
-2
@@ -1143,6 +1143,120 @@ $("#backup-rows").addEventListener("click", async (event) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Версия панели и обновление
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let updateInfo = null;
|
||||
|
||||
async function loadUpdateInfo() {
|
||||
const info = await get("/api/update");
|
||||
updateInfo = info;
|
||||
|
||||
$("#update-current").textContent = info.current || "?";
|
||||
|
||||
const state = $("#update-state");
|
||||
const applyBtn = $("#btn-update-apply");
|
||||
const manual = $("#update-manual");
|
||||
|
||||
applyBtn.hidden = !info.available;
|
||||
manual.hidden = true;
|
||||
|
||||
if (info.error) {
|
||||
state.className = "muted small";
|
||||
state.textContent = `Не удалось проверить обновления: ${info.error}`;
|
||||
} else if (info.available) {
|
||||
state.className = "small update-available";
|
||||
state.textContent = `Доступна версия ${info.latest}`;
|
||||
applyBtn.textContent = `Обновить до ${info.latest}`;
|
||||
// Панель обновляется сама, только если установщик прописал ей права на
|
||||
// это. Иначе показываем команду для терминала.
|
||||
if (!info.can_apply) {
|
||||
applyBtn.hidden = true;
|
||||
manual.hidden = false;
|
||||
manual.textContent = `Обновить можно командой: ${info.manual_command}`;
|
||||
}
|
||||
} else {
|
||||
state.className = "muted small";
|
||||
state.textContent = info.latest
|
||||
? `Установлена последняя версия (${info.latest})`
|
||||
: "Сведений о релизах нет";
|
||||
}
|
||||
|
||||
const notes = $("#update-notes");
|
||||
notes.hidden = !(info.available && info.notes);
|
||||
if (!notes.hidden) $("#update-notes-body").textContent = info.notes;
|
||||
|
||||
$("#btn-manager-restart").disabled = !info.can_apply;
|
||||
$("#btn-manager-restart").title = info.can_apply
|
||||
? "Перезапустить службу панели"
|
||||
: "Недоступно: панель не может управлять своей службой";
|
||||
}
|
||||
|
||||
$("#btn-update-check").addEventListener("click", async () => {
|
||||
const button = $("#btn-update-check");
|
||||
button.disabled = true;
|
||||
try {
|
||||
await loadUpdateInfo();
|
||||
ok(updateInfo.available ? `Есть новая версия: ${updateInfo.latest}` : "Обновлений нет");
|
||||
} catch (error) {
|
||||
fail(error);
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
$("#btn-update-apply").addEventListener("click", async () => {
|
||||
if (!confirm(`Обновить панель до ${updateInfo.latest}?\n\n` +
|
||||
`Игровой сервер будет остановлен, панель перезапустится.`)) return;
|
||||
try {
|
||||
await post("/api/update", { version: updateInfo.latest });
|
||||
waitForRestart("Обновление запущено");
|
||||
} catch (error) {
|
||||
fail(error);
|
||||
}
|
||||
});
|
||||
|
||||
$("#btn-manager-restart").addEventListener("click", async () => {
|
||||
if (!confirm("Перезапустить панель?\n\nИгровой сервер будет остановлен.")) return;
|
||||
try {
|
||||
await post("/api/manager/restart");
|
||||
waitForRestart("Панель перезапускается");
|
||||
} catch (error) {
|
||||
fail(error);
|
||||
}
|
||||
});
|
||||
|
||||
// waitForRestart дожидается, пока служба поднимется заново, и перезагружает
|
||||
// страницу: во время перезапуска панель недоступна, и обычные опросы статуса
|
||||
// будут только сыпать ошибками.
|
||||
function waitForRestart(message) {
|
||||
ok(message + " — жду, пока панель поднимется");
|
||||
stopPolling();
|
||||
closeConsoleStream();
|
||||
|
||||
const deadline = Date.now() + 5 * 60 * 1000;
|
||||
const state = $("#update-state");
|
||||
state.className = "small";
|
||||
state.textContent = message + "…";
|
||||
|
||||
const probe = setInterval(async () => {
|
||||
if (Date.now() > deadline) {
|
||||
clearInterval(probe);
|
||||
state.textContent = "Панель не поднялась за пять минут — проверьте journalctl -u pzmanager";
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await fetch("/api/session", { credentials: "same-origin" });
|
||||
if (!response.ok) return;
|
||||
clearInterval(probe);
|
||||
location.reload();
|
||||
} catch (error) {
|
||||
// Служба ещё перезапускается — это ожидаемо.
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Настройки менеджера и пользователи
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1198,11 +1312,15 @@ async function loadSettings() {
|
||||
|
||||
const pathNote = document.createElement("p");
|
||||
pathNote.className = "muted small";
|
||||
pathNote.textContent =
|
||||
`PZ Manager ${config.version || "?"} · файл конфигурации: ${config.config_path}`;
|
||||
pathNote.textContent = `Файл конфигурации: ${config.config_path}`;
|
||||
form.appendChild(pathNote);
|
||||
|
||||
await loadUsers();
|
||||
// Проверка обновления ходит в интернет и может не ответить — она не должна
|
||||
// мешать показу остальных настроек.
|
||||
loadUpdateInfo().catch((error) => {
|
||||
$("#update-state").textContent = "Не удалось проверить обновления: " + error.message;
|
||||
});
|
||||
}
|
||||
|
||||
$("#btn-settings-save").addEventListener("click", async () => {
|
||||
|
||||
Reference in New Issue
Block a user