Обновление с индикатором хода (этапы, скачанные МБ, перезапуск ядра); в мастере ключ продукта не просится повторно

- /admin/setup/update/progress: этап, байты, целевая версия ядра; ApplyUpdate/ApplyBundle отчитываются;
- окно в layout: формы установки обновлений отправляются скриптом, окно ждёт перезапуска ядра и сверяет версию;
- шаг 1 мастера: «Ключ продукта введён», смена ключа — под ссылкой.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
zuevav
2026-09-24 22:04:08 +03:00
parent b9ca30bef1
commit 775b4214b6
5 changed files with 235 additions and 1 deletions
@@ -572,6 +572,80 @@ label { font-size:13px; font-weight:500; }
})();
})();
</script>
<div id="upd-modal" style="display:none;position:fixed;inset:0;background:rgba(10,14,24,.55);z-index:9999;align-items:center;justify-content:center;padding:16px">
<div style="background:var(--card,#fff);color:var(--text,#111);border-radius:12px;max-width:520px;width:100%;padding:22px 24px;box-shadow:0 12px 40px rgba(0,0,0,.35)">
<h3 style="margin:0 0 4px;font-size:18px">Обновление bj</h3>
<p id="upd-stage" style="margin:0 0 12px;color:var(--muted,#667)">Подготовка…</p>
<div style="height:12px;background:var(--card-2,#eef0f4);border-radius:6px;overflow:hidden;border:1px solid var(--border,#dde)">
<div id="upd-bar" style="height:100%;width:0;background:var(--accent,#2b50d8);transition:width .4s"></div>
</div>
<p id="upd-detail" style="margin:8px 0 0;font-size:13px;color:var(--muted,#667);min-height:18px"></p>
<div id="upd-done" style="display:none;margin-top:14px;text-align:right"><button type="button" class="btn" onclick="location.reload()">Готово</button></div>
</div>
</div>
<script>
/* Окно с ходом обновления: формы установки обновлений отправляются из скрипта, а страница
опрашивает /admin/setup/update/progress. При обновлении ядра bj перезапускается — связь
пропадает, окно ждёт возвращения и сверяет версию ядра с целевой. */
(function(){
var modal=document.getElementById('upd-modal'); if(!modal) return;
var stageEl=document.getElementById('upd-stage'), bar=document.getElementById('upd-bar'),
detail=document.getElementById('upd-detail'), doneEl=document.getElementById('upd-done');
function mb(n){ return (n/1048576).toFixed(n<10485760?1:0)+' МБ'; }
function show(){ modal.style.display='flex'; }
function finish(text, ok){
stageEl.textContent=text; bar.style.width='100%';
bar.style.background= ok ? 'var(--ok,#2e8b57)' : 'var(--err,#c0392b)';
doneEl.style.display='block';
}
function run(form){
var startVer=null, target='', seenActive=false, postDone=false, lost=false, t0=Date.now(), seq0=null;
show(); stageEl.textContent='Отправка запроса…'; bar.style.width='3%'; detail.textContent='';
var xhr=new XMLHttpRequest(); xhr.open('POST', form.action);
var file=form.querySelector('input[type=file]');
if(file && file.files.length){
xhr.upload.onprogress=function(e){ if(e.lengthComputable){ stageEl.textContent='Загрузка файла на сервер';
bar.style.width=Math.max(3,Math.round(e.loaded/e.total*25))+'%'; detail.textContent=mb(e.loaded)+' из '+mb(e.total); } };
}
xhr.onloadend=function(){ postDone=true; };
xhr.send(new FormData(form));
function poll(){
fetch('/admin/setup/update/progress',{cache:'no-store',credentials:'same-origin'}).then(function(r){ if(!r.ok) throw 0; return r.json(); }).then(function(p){
if(startVer===null){ startVer=p.version; seq0=p.seq; }
if(p.target) target=p.target;
if(lost){ // bj вернулся после перезапуска
if(!target || p.version===target || p.version!==startVer){ finish('Готово: bj перезапущен, работает версия '+p.version+'.', true); detail.textContent=''; return; }
}
var fresh = p.seq!==seq0 || seenActive || postDone;
if(p.active){ seenActive=true;
var steps=p.steps||4, base=(p.step-1)/steps*100, part=100/steps;
if(p.total>0 && p.step===2){ base+=part*Math.min(1,p.done/p.total); detail.textContent='Скачано '+mb(p.done)+' из '+mb(p.total); }
else if(p.step===3){ detail.textContent='Обычно несколько секунд; при обновлении ядра страница ненадолго пропадёт — это нормально.'; }
bar.style.width=Math.max(5,Math.round(base))+'%';
stageEl.textContent='Шаг '+p.step+' из '+steps+': '+p.stage;
} else if(p.finished && fresh){
if(p.error){ finish('Обновление не выполнено', false); detail.textContent=p.error; return; }
if(target && p.version!==target){ stageEl.textContent='Шаг 4 из 4: перезапуск bj'; bar.style.width='90%'; detail.textContent='Ждём, пока bj поднимется с новой версией…'; }
else { finish('Готово', true); detail.textContent=p.result||''; return; }
} else if(postDone && Date.now()-t0>4000 && !seenActive){
finish('Готово', true); detail.textContent='Результат — в строке «Последняя проверка».'; return;
}
setTimeout(poll, 800);
}).catch(function(){
lost=true; stageEl.textContent='Шаг 4 из 4: перезапуск bj'; bar.style.width='92%';
detail.textContent='bj перезапускается с новой версией — страница вернётся сама.';
setTimeout(poll, 1500);
});
}
setTimeout(poll, 400);
}
document.addEventListener('submit', function(e){
var f=e.target; if(!f || !f.action || e.defaultPrevented) return;
if(!/\/admin\/setup\/update\/(apply|upload)$/.test(new URL(f.action, location.href).pathname)) return;
e.preventDefault(); run(f);
});
})();
</script>
</body>
</html>
{{end}}