База bj на своём PostgreSQL; «Не заполнено: …» для пустых полей; запись канала ИШ — в фоне, с ходом на странице

- шаг «База bj»: перенос на сервер PostgreSQL организации (проверка подключения, версии, прав, пустой базы;
  pg_dump → psql; сверка числа строк по таблицам; переключение и перезапуск); прежняя база — запасная копия;
  резервные копии: дамп внешней базы по её подключению, восстановление её не трогает
- все формы: пустые видимые обязательные поля называются по имени и подсвечиваются; DEPO — и на сервере
- «Обновить/Создать канал»: в фоне bj, ход по этапам с таймером (и после перезагрузки страницы), переходы
  заблокированы до конца, в конце проверка, что шлюз загрузил канал; состояние ИШ во время записи — без
  очереди к root-обработчику (страница больше не висит на «Проверяю…»)

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-10-07 00:00:35 +03:00
parent 3a9bc5d7fa
commit 4adc1398f6
16 changed files with 832 additions and 50 deletions
@@ -58,6 +58,23 @@
<button type="button" class="btn" id="ig-goprod" style="background:var(--err);border-color:var(--err)">Перейти на боевой</button>
</div>
</div>
<div class="ig-job" id="ig-job" hidden aria-live="polite">
<div class="ig-job-h"><span class="ig-spin"></span><b id="ig-job-t">Записываю канал в шлюз НРД</b><span class="muted" id="ig-job-time"></span></div>
<ol class="dq-steps" id="ig-job-steps"></ol>
<p class="muted" id="ig-job-note" style="margin:8px 0 0;font-size:13px">Обычно до двух минут: шлюз перезапускается. Не уходите со страницы — после перезагрузки ход покажется снова.</p>
</div>
<style>
.ig-job{margin-top:12px;border:2px solid var(--accent);border-radius:12px;padding:14px 16px;background:var(--accent-weak)}
.ig-job.is-ok{border-color:var(--ok);background:color-mix(in srgb,var(--ok) 8%,var(--card))}
.ig-job.is-err{border-color:var(--err);background:color-mix(in srgb,var(--err) 7%,var(--card))}
.ig-job-h{display:flex;gap:10px;align-items:center;margin-bottom:8px;flex-wrap:wrap}
.ig-job-h .muted{margin-left:auto;font-variant-numeric:tabular-nums}
.ig-spin{width:18px;height:18px;border-radius:50%;border:2px solid var(--accent-weak);border-top-color:var(--accent);animation:dq-spin .8s linear infinite;flex:none}
.ig-job.is-ok .ig-spin,.ig-job.is-err .ig-spin{display:none}
.ig-job.shake{animation:ig-shake .4s}
@keyframes ig-shake{25%{transform:translateX(-6px)}75%{transform:translateX(6px)}}
@keyframes dq-spin{to{transform:rotate(360deg)}}
</style>
<div id="ig-cout" style="margin-top:8px;font-size:13px"></div>
</div>
</div>
@@ -67,6 +84,50 @@
function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,function(c){return{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]})}
function ok(t){return '<span style="color:var(--ok)">✓ '+esc(t)+'</span>'} function bad(t){return '<span style="color:var(--err)">✗ '+esc(t)+'</span>'}
function post(u,p){return fetch(u,{method:'POST',body:p,cache:'no-store'}).then(function(r){return r.json()})}
// Запись канала идёт в фоне bj: показываем её ход (и после перезагрузки страницы), пока идёт — не даём уйти.
var job=(function(){
var box=document.getElementById('ig-job'), list=document.getElementById('ig-job-steps'), tt=document.getElementById('ig-job-t'),
tm=document.getElementById('ig-job-time'), note=document.getElementById('ig-job-note'), busy=false, t0=0, timer=0, fails=0;
var ST=[['write','Запись канала в базу шлюза'],['engine','Перезапуск шлюза и запуск движка'],['channel','Проверка: шлюз загрузил канал']];
function draw(v){
var idx=-1; ST.forEach(function(x,i){ if(x[0]===v.stage) idx=i; }); if(v.done) idx=ST.length;
list.innerHTML=ST.map(function(x,i){ var cls=i<idx?'is-ok':(i===idx?(v.done?'':'is-run'):'is-wait'); if(v.done&&!v.ok&&i===Math.min(idx,ST.length)-1) cls='is-err';
return '<li class="dq-st '+cls+'"><span class="dq-st-ic"></span><div class="dq-st-t"><span>'+x[1]+'</span></div></li>'; }).join('');
}
function tick(){ if(!t0) return; var n=Math.max(0,Math.round(Date.now()/1000-t0)); tm.textContent=Math.floor(n/60)+':'+('0'+n%60).slice(-2); }
function finish(v){
busy=false; clearInterval(timer); box.classList.remove('shake');
box.classList.add(v.ok?'is-ok':'is-err'); tt.textContent=v.ok?'Канал '+String(v.contour||'').toUpperCase()+' записан, шлюз работает':'Канал не записан';
note.textContent=v.msg||''; draw(v); load();
if(v.ok && /перезапускается/.test(v.msg||'')) setTimeout(function(){ location.reload(); },20000);
}
function poll(){
fetch('/admin/setup/igate/contour-status',{cache:'no-store'}).then(function(r){return r.json()}).then(function(v){
fails=0; if(v.started) t0=v.started;
if(v.running){ draw(v); setTimeout(poll,2000); return; }
if(v.done) finish(v); else { busy=false; box.hidden=true; }
}).catch(function(){ if(++fails<30) setTimeout(poll,3000); }); // bj мог перезапуститься — ждём
}
function watch(c){
busy=true; box.hidden=false; box.classList.remove('is-ok','is-err'); tt.textContent='Записываю канал '+String(c||'').toUpperCase()+' в шлюз НРД';
note.textContent='Обычно до двух минут: шлюз перезапускается. Не уходите со страницы — после перезагрузки ход покажется снова.';
t0=t0||Date.now()/1000; clearInterval(timer); timer=setInterval(tick,1000); tick();
box.scrollIntoView({block:'center',behavior:'smooth'}); poll();
}
// пока идёт запись — переходы и кнопки вне карточки не срабатывают
document.addEventListener('click',function(e){
if(!busy) return; var a=e.target.closest('a[href],button,input[type=submit]'); if(!a||box.contains(a)) return;
e.preventDefault(); e.stopPropagation(); box.classList.remove('shake'); void box.offsetWidth; box.classList.add('shake');
note.textContent='Подождите: идёт запись канала, шлюз перезапускается. Переход станет доступен, когда она закончится.';
box.scrollIntoView({block:'center',behavior:'smooth'});
},true);
window.addEventListener('beforeunload',function(e){ if(busy){ e.preventDefault(); e.returnValue=''; } });
// при открытии страницы: запись уже идёт или только что закончилась — показать
fetch('/admin/setup/igate/contour-status',{cache:'no-store'}).then(function(r){return r.json()}).then(function(v){
if(v.running){ t0=v.started||0; watch(v.contour); } else if(v.done){ box.hidden=false; t0=0; finish(v); }
}).catch(function(){});
return {watch:watch};
})();
var st=null;
function load(){
fetch('/admin/setup/igate/status',{cache:'no-store'}).then(function(r){return r.json()}).then(function(d){
@@ -76,6 +137,7 @@
var running=d.installed&&d.engine==='Running';
if(!d.installed) head.innerHTML=bad('ИШ на этом сервере не установлен');
else head.innerHTML=(running?ok('ИШ '+d.version+' установлен и работает'):bad('ИШ '+d.version+' установлен, но не работает: движок '+(d.engine||'не отвечает')+', служба '+d.service));
if(d.busy) head.innerHTML='<span style="color:var(--accent)">⟳ Шлюз перезапускается — '+esc(d.busy)+'; состояние — последнее известное</span>';
// не стоит или не работает — одна кнопка: поставить заново проверенную версию с сервера (систему переставлять не нужно)
ri.style.display=running?'none':'block';
if(!running){
@@ -106,8 +168,9 @@
function act(a,c,b){
if(a==='showprod'){ document.getElementById('ig-prod').style.display='block'; return; }
var p=new URLSearchParams(); p.set('contour',c); p.set('crypto',document.getElementById('ig-crypto').value);
if(a==='contour'){ b.disabled=true; cout.textContent='Записываю канал и перезапускаю ИШ (до 2 минут)…';
post('/admin/setup/igate/contour',p).then(function(d){ b.disabled=false; cout.innerHTML=d.ok?ok(d.output):bad(d.error); load(); }); }
if(a==='contour'){ b.disabled=true; cout.textContent='';
post('/admin/setup/igate/contour',p).then(function(d){ b.disabled=false; if(!d.ok){ cout.innerHTML=bad(d.error); return; } job.watch(c); })
.catch(function(){ b.disabled=false; job.watch(c); }); }
if(a==='switch'){ if(!confirm('Переключить bj на тестовый контур TEST3?')) return; cout.textContent='Переключаю…';
post('/admin/setup/igate/switch',p).then(function(d){ cout.innerHTML=d.ok?ok(d.output):bad(d.error); if(d.ok) setTimeout(function(){location.reload()},15000); }); }
}