diff --git a/deploy/pzmanager.service b/deploy/pzmanager.service index b4e1c58..1815edd 100644 --- a/deploy/pzmanager.service +++ b/deploy/pzmanager.service @@ -19,7 +19,11 @@ KillMode=mixed TimeoutStopSec=300 # Всё своё панель держит в /opt/pzmanager, домашние каталоги ей не нужны. -NoNewPrivileges=true +# +# NoNewPrivileges здесь намеренно не выставлен: он запрещает процессу и его +# потомкам повышать привилегии, а значит ломает sudo — и вместе с ним кнопки +# обновления и перезапуска панели. Прикрытием служит sudoers: сервисному +# пользователю разрешена ровно одна команда, pzmanager-admin. PrivateTmp=true ProtectSystem=full ProtectHome=true diff --git a/web/static/app.js b/web/static/app.js index 6543bb8..41e78bd 100644 --- a/web/static/app.js +++ b/web/static/app.js @@ -1241,7 +1241,7 @@ async function loadUpdateInfo() { const notes = $("#update-notes"); notes.hidden = !(info.available && info.notes); - if (!notes.hidden) $("#update-notes-body").textContent = info.notes; + if (!notes.hidden) renderReleaseNotes(info.notes, info.url); $("#btn-manager-restart").disabled = !info.can_apply; $("#btn-manager-restart").title = info.can_apply @@ -1249,6 +1249,47 @@ async function loadUpdateInfo() { : "Недоступно: панель не может управлять своей службой"; } +// Описание релиза приходит из Gitea разметкой Markdown, и показывать её как +// есть неудобно: первым идёт раздел про установку, который в уже работающей +// панели ни к чему. Берём только список изменений. +function renderReleaseNotes(text, url) { + const body = $("#update-notes-body"); + body.replaceChildren(); + + const changesSection = text.split(/^##\s+Изменения\s*$/m)[1]; + const items = (changesSection ?? text) + .split("\n") + .map((line) => line.trim()) + .filter((line) => line.startsWith("- ")) + .map((line) => line.slice(2).trim()) + .filter(Boolean); + + if (items.length) { + const list = document.createElement("ul"); + items.forEach((item) => { + const li = document.createElement("li"); + li.textContent = item; + list.appendChild(li); + }); + body.appendChild(list); + } else { + // Непривычный формат описания — показываем как есть, лишь бы не потерять. + const raw = document.createElement("pre"); + raw.textContent = text.trim(); + body.appendChild(raw); + } + + if (url) { + const link = document.createElement("a"); + link.href = url; + link.target = "_blank"; + link.rel = "noopener noreferrer"; + link.className = "release-link"; + link.textContent = "Полное описание релиза"; + body.appendChild(link); + } +} + $("#btn-update-check").addEventListener("click", async () => { const button = $("#btn-update-check"); button.disabled = true; diff --git a/web/static/index.html b/web/static/index.html index 380aaa1..54056a8 100644 --- a/web/static/index.html +++ b/web/static/index.html @@ -284,7 +284,7 @@ diff --git a/web/static/style.css b/web/static/style.css index b1d7a43..56102d8 100644 --- a/web/static/style.css +++ b/web/static/style.css @@ -465,16 +465,32 @@ input.input-sm, .input-sm { width: auto; padding: 6px 10px; font-size: 13px; } .release-notes { margin: 10px 0 0; - padding: 12px; + padding: 12px 14px; background: var(--bg); border: 1px solid var(--border); border-radius: 8px; - max-height: 260px; - overflow: auto; - white-space: pre-wrap; - word-break: break-word; - font-size: 12.5px; + max-height: 240px; + /* Прокручивается сам блок, а не страница под ним. */ + overflow-y: auto; + overscroll-behavior: contain; + font-size: 13px; line-height: 1.5; } +.release-notes ul { margin: 0; padding-left: 20px; } +.release-notes li { margin-bottom: 5px; } +.release-notes li:last-child { margin-bottom: 0; } + +.release-notes pre { + margin: 0; + white-space: pre-wrap; + word-break: break-word; + font-size: 12.5px; +} + +.release-link { display: inline-block; margin-top: 10px; color: var(--info); } + +/* Команду для терминала удобнее читать моноширинным. */ +#update-manual { font-family: var(--mono); word-break: break-all; } + .update-available { color: var(--accent); }