Мастер: «Обновление» — первый пункт, к которому можно вернуться

Новый шаг 14 «Обновление» в отдельной первой главе мастера. Секция
«Обновления» вынесена из admin_setup.html в {{define "updateSection"}}
(part_fancy.html) и переиспользуется и в настройках, и в шаге мастера —
один источник правды. В мастере рендерится обычным блоком (без
settings-section/active), заголовок h1 скрыт (есть свой h2 шага).

Шаг необязательный (не блокирует путь), зелёный пока нет версии новее;
при наличии обновления — предупреждающая точка. Добавлена пятая глава,
поэтому wizChaptersLine расширен до «Пять глав».

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XG3zNMMf2LMoDpXo22mwMX
This commit is contained in:
fontvielle
2026-10-19 17:22:29 +03:00
parent 2df7b0f54a
commit b7360b93b2
6 changed files with 110 additions and 93 deletions
@@ -936,88 +936,7 @@
</section>
{{/* ============ ОБНОВЛЕНИЯ ============ */}}
<section class="settings-section" id="sec-update">
<h1>Обновления</h1>
{{template "verline" (.Versions.Get "bj-server")}}
<div class="card" id="upd-status" data-last="{{if not .Settings.Update.LastCheck.IsZero}}{{.Settings.Update.LastCheck.Unix}}{{else}}0{{end}}" data-has="{{if .UpdatesAvailable}}1{{else}}0{{end}}">
{{if .UpdatesAvailable}}
<div class="stc stc-warn"><span class="stc-ic"></span><div class="stc-b"><b>Доступно обновление{{with .Settings.Update.Available}} {{.}}{{end}}</b><small>{{if .Settings.Update.Notes}}{{.Settings.Update.Notes}}{{else}}новые версии компонентов — список ниже, в «Компоненты и версии»{{end}}</small></div>
<form method="post" action="/admin/setup/update/apply" style="margin:0" onsubmit="return confirm('Установить обновление? Службы, которые обновляются, перезапустятся на несколько секунд.');"><input type="hidden" name="section" value="sec-update"><button type="submit" class="btn">Установить обновление</button></form></div>
{{else if and (not .Settings.Update.LastCheck.IsZero) .UpdateFailed}}
<div class="stc stc-err"><span class="stc-ic"></span><div class="stc-b"><b>Проверить обновления не удалось</b><small>{{.Settings.Update.LastResult}} · {{msk .Settings.Update.LastCheck}}</small></div>
<button type="button" class="btn btn-secondary" data-upd-check>Проверить ещё раз</button></div>
{{else}}
<div class="stc stc-ok"><span class="stc-ic"></span><div class="stc-b"><b>Установлены последние версии</b><small id="upd-when">{{if .Settings.Update.LastCheck.IsZero}}ещё не проверялось{{else}}проверено {{msk .Settings.Update.LastCheck}}{{end}}{{if .Settings.Update.AutoCheck}} · bj проверяет сам каждые 6 часов{{end}}</small></div>
<button type="button" class="btn btn-secondary" data-upd-check>Проверить сейчас</button></div>
{{end}}
{{if and .License.Present (not .License.AllowsUpdates)}}<p class="muted" style="margin-top:6px;font-size:12px;color:var(--warn)">В лицензии нет модуля «Обновления».</p>{{end}}
</div>
<script>(function(){
var box=document.getElementById('upd-status'), sec=document.getElementById('sec-update'), busy=false;
function check(force){
if(busy) return; var last=+box.dataset.last||0, now=Date.now()/1000, tried=0;
try{ tried=+sessionStorage.getItem('bj-upd-auto')||0; }catch(e){}
if(!force && (now-last<600 || now-tried<600)) return; // проверяли меньше 10 минут назад (или уже пробовали сами)
try{ sessionStorage.setItem('bj-upd-auto',String(Math.floor(now))); }catch(e){}
busy=true; var b=box.querySelector('.stc'); if(b){ b.className='stc stc-run'; var t=b.querySelector('b'); if(t) t.textContent='Проверяю обновления…'; }
fetch('/admin/setup/update/check',{method:'POST',headers:{'X-BJ-Ajax':'1'},cache:'no-store'}).then(function(r){return r.json()}).then(function(d){
box.dataset.last=String(Math.floor(Date.now()/1000));
location.reload(); // состояние компонентов — с сервера
}).catch(function(){ busy=false; location.reload(); });
}
box.addEventListener('click',function(e){ if(e.target.closest('[data-upd-check]')) check(true); });
document.querySelectorAll('.settings-nav [data-sec="update"]').forEach(function(b){ b.addEventListener('click',function(){ setTimeout(function(){ check(false); },0); }); });
window.addEventListener('load',function(){ if(sec.classList.contains('active')) check(false); });
})();</script>
<div class="card">
<details class="adv-box" {{if .UpdatesAvailable}}open{{end}}>
<summary><span class="sum-h">Компоненты и версии</span><span class="muted">что установлено на этом сервере и что есть в выпуске; можно обновить один компонент</span></summary>
<div style="overflow-x:auto"><table style="margin-top:8px">
<thead><tr><th>Компонент</th><th>Установлена</th><th>Доступна</th><th>Служба</th><th></th></tr></thead>
<tbody>
{{range .Components}}<tr>
<td><strong>{{.Title}}</strong><div class="muted" style="font-size:11px"><code>{{.Name}}</code>{{if eq .Name "crypto-service.jar"}}{{with $.Versions.Get "validata"}}{{if .Version}} · работает с «Валидата Клиент L» {{.Version}}{{end}}{{end}}{{end}}</div></td>
<td>{{if .Present}}{{if .Installed}}{{.Installed}}{{else}}<span class="muted">неизвестна</span>{{end}}{{else}}<span class="muted">не установлен</span>{{end}}</td>
<td>{{if .Available}}{{.Available}}{{if .New}} <span class="hero-status warn">к установке</span>{{else if .Update}} <span class="hero-status warn">новее</span>{{end}}{{if .Blocked}}<div class="muted" style="font-size:11px;color:var(--warn)">{{.Blocked}}</div>{{end}}{{else}}<span class="muted">нет данных</span>{{end}}</td>
<td><code style="font-size:11px">{{.Unit}}</code></td>
<td>{{if .Update}}<form method="post" action="/admin/setup/update/apply" style="margin:0" onsubmit="return confirm('{{if .New}}Установить модуль {{.Available}}? Будет включена его служба.{{else}}Обновить компонент до {{.Available}}? Его служба перезапустится.{{end}}');"><input type="hidden" name="section" value="sec-update"><input type="hidden" name="component" value="{{.Name}}"><button type="submit" class="btn btn-ok">{{if .New}}Установить{{else}}Обновить{{end}}</button></form>{{end}}</td>
</tr>{{end}}
</tbody>
</table></div>
</details>
</div>
<div class="card">
<details class="adv-box">
<summary><span class="sum-h">Расширенные настройки</span><span class="muted">источник и канал обновлений, обновление из файла для сервера без выхода в интернет</span></summary>
<h3 class="adv-h">Обновление из файла</h3>
<p class="muted" style="margin:0 0 8px;font-size:12.5px"><b>Закрытый контур.</b> Если у сервера нет выхода к серверу обновлений, скачайте пакет обновления на портале клиента (lic.zetit.ru → Установка → «Пакет обновления») и загрузите его кнопкой «Обновить из файла». Система сама распознаёт, что внутри — ядро, модули или всё вместе — и ставит в правильном порядке. Модуль, которому нужно более новое ядро, чем установлено, будет пропущен с подсказкой обновить сначала ядро.</p>
<div style="display:flex;gap:10px;flex-wrap:wrap">
<form method="post" action="/admin/setup/update/upload" enctype="multipart/form-data" style="margin:0;display:flex;gap:8px;align-items:center;flex-wrap:wrap" onsubmit="return this.bundle.files.length>0 && confirm('Установить обновление из файла? Подпись пакета будет проверена; поставятся компоненты новее текущих — сначала ядро, затем модули. Службы перезапустятся.');">
<input type="hidden" name="section" value="sec-update">
<input type="file" name="bundle" accept=".zip,application/zip" required style="max-width:260px">
<button type="submit" class="btn btn-secondary" title="Пакет обновления с портала клиента — для сервера без выхода к серверу обновлений">Обновить из файла</button>
</form>
</div>
<h3 class="adv-h">Источник обновлений</h3>
<form method="post" action="/admin/setup/update" style="margin-top:12px;display:grid;gap:10px;max-width:680px">
<label>URL артефактории</label>
<input type="text" name="base_url" value="{{.Settings.Update.BaseURL}}" placeholder="https://lic.zetit.ru/updates (по умолчанию)">
<label>Канал</label>
<select name="channel">
<option value="stable" {{if eq .Settings.Update.Channel "stable"}}selected{{end}}>stable — стабильный</option>
<option value="beta" {{if eq .Settings.Update.Channel "beta"}}selected{{end}}>beta — предварительный</option>
</select>
<label>Публичный ключ издателя (base64 Ed25519)</label>
<input type="text" name="public_key" value="{{.Settings.Update.PublicKey}}" placeholder="зашит в релиз; переопределить здесь">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer"><input type="checkbox" name="auto_check" {{if .Settings.Update.AutoCheck}}checked{{end}} style="width:auto"> Проверять автоматически (раз в 6 часов)</label>
<div><button type="submit" class="btn">Сохранить</button></div>
</form>
</details>
</div>
</section>
{{template "updateSection" .}}
{{/* ============ ЛИЦЕНЗИЯ ============ */}}
{{/* ============ РЕЗЕРВНОЕ КОПИРОВАНИЕ ============ */}}