Files
Bridge-and-Join-s/internal/lkgateway/web/templates/part_igate.html
T
fontvielle a2608076a4 Интерфейс: единые термины, названия разделов и шагов, узкий экран
- клиенту — «шлюз НРД» вместо «ИШ» (установка, статусы, этапы, ключ, переводы);
  расшифровка «(ИШ)» — только в заголовке окна обновления
- контур — «тестовый (TEST3)»; в строке шлюза — «работает · версия …»
- меню = заголовки разделов: «Обмен с DEPO», «Пользователи и вход», «Оформление отчётов»
- мастер: заголовок шага = его название в колонке шагов
- вторичная кнопка — классом btn-secondary, а не стилями в разметке
- узкий экран: поля и сетки с жёсткой шириной не вылезают, таблицы прокручиваются,
  анкета участника — по высоте экрана

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
2026-10-21 23:58:54 +03:00

405 lines
44 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{/* Шлюз НРД: установка ИШ, каналы контуров TEST3 и боевого, переключение bj между контурами (мастер, шаг 8). */}}
{{define "igateInstall"}}
<style>
#ig-box h3{margin:0 0 6px;font-size:15px}
#ig-box label.f{display:block;margin:0;font-size:13px}
#ig-box label.f input,#ig-box label.f select{display:block;margin-top:4px;padding:7px 9px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px}
.ct-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin-top:10px;align-items:stretch;grid-auto-rows:1fr}
.ct{border:1px solid var(--border);border-radius:12px;padding:14px 16px;background:var(--card);display:flex;flex-direction:column;gap:8px;margin:0;height:100%;box-sizing:border-box;align-self:stretch}
.ct-on{border-color:color-mix(in srgb,var(--accent) 55%,var(--border));background:color-mix(in srgb,var(--accent-weak) 45%,var(--card))}
.ct-h{display:flex;justify-content:space-between;align-items:flex-start;gap:10px}
.ct-h b{display:block;font-size:15px}
.ct-h small{display:block;color:var(--muted);font-size:12.5px;margin-top:1px}
.ct-pill{flex:none;font-size:12px;font-weight:600;padding:3px 10px;border-radius:999px;background:var(--card-2);border:1px solid var(--border);color:var(--text-2);white-space:nowrap}
.ct-cur .ct-pill{background:var(--accent);border-color:var(--accent);color:#fff}
.ct-ok .ct-pill{background:var(--ok-weak);border-color:color-mix(in srgb,var(--ok) 35%,transparent);color:var(--ok)}
.ct-no .ct-pill{background:var(--warn-weak);border-color:color-mix(in srgb,var(--warn) 35%,transparent);color:var(--warn)}
.ct-li{list-style:none;margin:0;padding:0;display:grid;gap:3px;font-size:13px}
.ct-li li{position:relative;padding-left:20px;overflow-wrap:anywhere}
.ct-li li::before{position:absolute;left:0;top:0;font-weight:700}
.ct-li li.ok::before{content:"✓";color:var(--ok)} .ct-li li.no::before{content:"✗";color:var(--err)} .ct-li li.na::before{content:"—";color:var(--muted)}
.ct-li .muted{font-size:12px}
.ct .ct-why{margin:0;font-size:12.5px;color:var(--muted)}
.ct-act{display:flex;gap:6px;flex-wrap:wrap;margin-top:auto;padding-top:4px}
.ct-act .btn[disabled]{opacity:.5;cursor:not-allowed}
@media (max-width:760px){.ct-grid{grid-template-columns:minmax(0,1fr)}}
.ig-share{margin-top:18px;padding-top:16px;border-top:1px solid var(--border)}
.ig-sh-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-top:10px}
.ig-sh-grid label.f input{width:100%;box-sizing:border-box}
@media (max-width:600px){.ig-sh-grid{grid-template-columns:minmax(0,1fr)}}
.ig-addr{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:6px}
.ig-addr code{font-size:14px;padding:4px 10px;border-radius:8px;background:var(--card);border:1px solid var(--border);user-select:all}
.ig-addr .muted{font-size:12.5px}
#ig-box .btn.sm{padding:4px 10px;font-size:12.5px}
#ig-box .btn.ghost{background:var(--card);color:var(--text);border:1px solid var(--border-strong,#c9cdd8)}
/* ход установки ИШ: этапы по порядку, у текущего — сколько идёт и сколько обычно */
.ig-steps{list-style:none;margin:10px 0 0;padding:10px 14px;border:1px solid var(--border);border-radius:10px;display:grid;gap:2px}
.ig-steps li{display:flex;gap:10px;align-items:flex-start;padding:4px 0;font-size:13.5px;color:var(--muted)}
.ig-steps li.done{color:var(--text-2)} .ig-steps li.cur{color:var(--text);font-weight:500} .ig-steps li.err{color:var(--err)}
.ig-ic{flex:none;position:relative;width:16px;height:16px;border-radius:50%;margin-top:2px;box-sizing:border-box;border:1.5px solid var(--border-strong)}
.ig-steps li.done .ig-ic{background:var(--ok);border-color:var(--ok)}
.ig-steps li.done .ig-ic::after{content:"";position:absolute;left:50%;top:45%;width:3.5px;height:7px;border:solid #fff;border-width:0 1.6px 1.6px 0;transform:translate(-50%,-50%) rotate(45deg)}
.ig-steps li.cur .ig-ic{border:2px solid var(--accent-weak);border-top-color:var(--accent);animation:ig-spin .8s linear infinite}
.ig-steps li.err .ig-ic{background:var(--err);border-color:var(--err)}
.ig-t small{display:block;font-weight:400;color:var(--muted);font-size:12px}
.ig-t small.slow{color:var(--warn)}
@keyframes ig-spin{to{transform:rotate(360deg)}}
@media (prefers-reduced-motion:reduce){.ig-steps li.cur .ig-ic{animation:none;border-color:var(--accent)}}
</style>
<div id="ig-box">
<div class="card" style="margin:10px 0">
<h3 style="margin:0 0 4px">Шлюз на этом сервере</h3>
<div id="ig-head" class="muted">Проверяю…</div>
<p class="muted" style="font-size:12px;margin:4px 0 0">Новые версии шлюза приходят с обновлением bj и ставятся сами.</p>
<div id="ig-reinst" style="display:none;margin-top:10px"><button type="button" class="btn" id="ig-bnd">Установить шлюз НРД</button> <span class="muted" id="ig-bnd-v" style="font-size:12.5px"></span></div>
<div id="ig-ov" style="display:none;position:fixed;inset:0;z-index:9999;background:rgba(20,24,35,.55);align-items:center;justify-content:center">
<div class="card" style="max-width:600px;width:92%;margin:0;box-shadow:0 10px 40px rgba(0,0,0,.25)">
<h3 id="ig-ov-h" style="margin:0 0 8px">Обновление шлюза НРД</h3>
<p id="ig-ov-m" class="muted" style="margin:0 0 8px;font-size:13.5px"></p>
<pre id="ig-ov-log" style="display:none;max-height:220px;overflow:auto;font-size:11.5px;white-space:pre-wrap;background:var(--card-2);padding:8px;border-radius:8px;margin:0"></pre>
<div id="ig-ov-btns" style="display:none;margin-top:12px"><button type="button" class="btn" id="ig-ov-close">Закрыть</button></div>
</div>
</div>
<div id="ig-rb" style="display:none;margin-top:8px"><button type="button" class="btn btn-secondary" id="ig-rb-btn">Откатить шлюз НРД к <span id="ig-rb-v"></span></button> <span class="muted" style="font-size:12.5px">вернуть версию, которая стояла до последнего обновления (программа, данные, база шлюза)</span></div>
<ol class="ig-steps" id="ig-steps" hidden></ol>
<div id="ig-out" style="margin-top:8px;font-size:13px"></div>
<details id="ig-logbox" hidden style="margin-top:6px;font-size:12.5px"><summary style="cursor:pointer;color:var(--accent)">Журнал установки</summary>
<pre id="ig-log" style="max-height:220px;overflow:auto;font-size:11px;margin-top:6px"></pre></details>
{{if devOn}}<details style="margin-top:8px"><summary style="cursor:pointer;color:var(--accent);font-size:13px">Для разработчиков: другая версия ИШ</summary>
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:end;margin-top:8px">
<label class="f">Версия на сайте НРД<input id="ig-ver" value="107.0.930" style="width:140px"></label>
<button type="button" class="btn" id="ig-dl">Скачать с сайта НРД и установить</button>
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-top:10px">
<input type="file" id="ig-file" accept=".zip,.deb">
<button type="button" class="btn ghost" id="ig-up">Установить из файла</button>
</div>
</details>{{end}}
</div>
<div class="card" style="margin:10px 0">
<h3>Контуры НРД</h3>
<p class="muted" style="font-size:13px;margin:0 0 8px">Сначала — тестовый контур TEST3; когда переводы на нём проходят, переходите на боевой. У каждого контура свой ключ.</p>
<label class="f" style="max-width:220px">Криптография<select id="ig-crypto"><option value="gost">ГОСТ</option><option value="rsa">RSA</option></select></label>
<div id="ig-ct" class="ct-grid"></div>
<div id="ig-prod" style="display:none;border:1px solid var(--warn);border-radius:8px;padding:10px 12px;margin-top:10px">
<b>Переход на боевой контур</b>
<p class="muted" style="font-size:13px;margin:6px 0">После перехода bj отправляет в НРД настоящие переводы. На боевом контуре обычно работает настоящий учёт DEPO — укажите его базу (на TEST3 часто использовали копию). Вернуться на TEST3 можно здесь же.</p>
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:end">
<label class="f">База DEPO для боевого контура<input id="ig-depo" placeholder="как сейчас" style="width:220px"></label>
<label class="f">Для подтверждения введите БОЕВОЙ<input id="ig-confirm" autocomplete="off" style="width:160px"></label>
<button type="button" class="btn" id="ig-goprod" style="background:var(--err);border-color:var(--err)">Перейти на боевой</button>
</div>
</div>
<div class="ig-share" id="ig-share">
<h3>Папки шлюза по сети <span class="muted" style="font-weight:400;font-size:13px">— необязательно</span></h3>
<p class="muted" style="font-size:13px;margin:4px 0 0">Папки обмена шлюза (INBOX, OUTBOX, SENT, ERRORS) можно открыть по сети Windows — если другие программы организации кладут или забирают файлы через этот шлюз. bj для работы это не нужно; файлы пишутся от имени шлюза.</p>
<div id="ig-share-st" class="muted" style="font-size:13px;margin-top:8px">Проверяю…</div>
<form id="ig-share-f" class="dq" style="margin-top:12px;max-width:680px">
<div class="dq-tiles">
<label class="dq-tile"><input type="radio" name="mode" value="simple" checked><span><b>Простой</b><small>Один логин и пароль на всех, кому нужны папки.</small></span></label>
<label class="dq-tile" id="ig-sh-dtile"><input type="radio" name="mode" value="domain"><span><b>Через домен</b><small id="ig-sh-dnote">Те же группы и сотрудники домена, что и в bj, — своим входом в Windows, без отдельного пароля. Убрали из группы — доступ пропал.</small></span></label>
</div>
<label style="display:flex;gap:8px;align-items:center;font-weight:500;margin:12px 0 0"><input type="checkbox" name="enabled" style="width:auto"> Открыть папки шлюза по сети</label>
<div id="ig-sh-simple" class="ig-sh-grid">
<label class="f">Логин<input name="user" value="igate-share" autocomplete="off" spellcheck="false"></label>
<label class="f">Пароль (от 8 символов)<span style="display:flex;gap:6px"><input name="password" type="text" autocomplete="new-password" spellcheck="false" placeholder="пусто — прежний" style="flex:1;min-width:0"><button type="button" class="btn sm" id="ig-sh-gen" title="Создать надёжный пароль">Сгенерировать</button></span></label>
</div>
<small id="ig-sh-pwnote" class="muted" style="display:block;font-size:12.5px;margin-top:6px">Пароль вводят в Windows при первом открытии папок. Запишите его: bj сохраняет пароль для сетевых папок и показать позже не сможет. Пустое поле при повторном сохранении оставляет прежний пароль.</small>
<div id="ig-sh-domain" hidden>
<div id="ig-sh-access" class="muted" style="font-size:13px;margin-top:10px"></div>
<div id="ig-sh-join" hidden style="margin-top:10px">
<p style="margin:0;font-size:13.5px">Сервер bj ещё не подключён к домену <b id="ig-sh-realm"></b>. Подключить нужно один раз — учётной записью с правом добавлять компьютеры в домен; bj её не сохраняет. В домене появится компьютер <b id="ig-sh-host"></b>.</p>
<div class="ig-sh-grid">
<label class="f">Администратор домена<input name="admin" autocomplete="off" spellcheck="false" placeholder="например admin или admin@corp.local"></label>
<label class="f">Пароль<input name="adminpw" type="password" autocomplete="new-password"></label>
</div>
</div>
</div>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:12px"><button type="submit" class="btn sm" id="ig-sh-save">Сохранить</button><span id="ig-share-out" style="font-size:13px"></span></div>
</form>
</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>
<script>var IGDEV={{if devOn}}true{{else}}false{{end}};</script>
<script>
(function(){
var head=document.getElementById('ig-head'), tb=document.getElementById('ig-ct'), cout=document.getElementById('ig-cout'), out=document.getElementById('ig-out'), lg=document.getElementById('ig-log');
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};
})();
// доступ к папкам шлюза по сети (Samba): простой режим или через домен
(function(){
var f=document.getElementById('ig-share-f'), stl=document.getElementById('ig-share-st'), o=document.getElementById('ig-share-out');
if(!f) return;
var cur={}, sim=document.getElementById('ig-sh-simple'), dom=document.getElementById('ig-sh-domain'), join=document.getElementById('ig-sh-join'), acc=document.getElementById('ig-sh-access');
function mode(){ return f.mode.value; }
function sync(){ var d=mode()==='domain'; sim.hidden=d; dom.hidden=!d; join.hidden=!(d && !cur.joined);
document.getElementById('ig-sh-save').textContent=(d && !cur.joined)?'Подключить сервер к домену и открыть папки':'Сохранить'; }
function addrs(d){ var hosts=[d.ip||location.hostname]; if(d.host && d.host!==hosts[0]) hosts.push(d.host);
var names=(d.shares&&d.shares.length)?d.shares:['igate_exchange'], rows=[];
names.forEach(function(n){ hosts.forEach(function(h){ var p='\\\\'+h+'\\'+n;
rows.push('<div class="ig-addr"><code>'+esc(p)+'</code><button type="button" class="btn sm ghost" data-copy="'+esc(p)+'">Скопировать</button><span class="muted">'+(/_test$/.test(n)?'тестовый контур TEST3':'боевой контур')+(h===d.host?' · по имени сервера':'')+'</span></div>'); }); });
return rows.join(''); }
function show(d){
if(!d.ok){ stl.innerHTML=bad(d.error||'состояние не получено'); return; }
cur=d; try{ window.dispatchEvent(new CustomEvent('bj-igate-share',{detail:d})); }catch(e){}
if(d.manual){ stl.innerHTML=ok('папки открыты вручную (smb.conf) для '+esc(d.user||'—')+' — bj их не меняет'); f.hidden=true; return; }
var a=d.access||{}, g=a.groups||[], u=a.users||[];
acc.innerHTML=(g.length||u.length)?('Доступ получат: '+(g.length?'группы <b>'+g.map(esc).join('</b>, <b>')+'</b>':'')+(g.length&&u.length?'; ':'')+(u.length?'сотрудники <b>'+u.map(esc).join('</b>, <b>')+'</b>':'')+'. Список — из Настройка → Пользователи, обновляется сам.'):'<span style="color:var(--warn)">В bj пока нет групп и сотрудников домена с доступом — добавьте их в Настройка → Пользователи («Сотрудники из домена»).</span>';
var dt=document.getElementById('ig-sh-dtile'), dr=f.querySelector('input[name=mode][value=domain]');
dr.disabled=!d.ad_ready; dt.style.opacity=d.ad_ready?'':'.55';
if(!d.ad_ready) document.getElementById('ig-sh-dnote').textContent='Сначала подключите домен в bj: Настройка → Пользователи → «Сотрудники из домена».';
document.getElementById('ig-sh-realm').textContent=d.ad_domain||d.realm||'';
document.getElementById('ig-sh-host').textContent=d.host||'';
f.querySelector('input[name=mode][value='+(d.mode==='domain'?'domain':'simple')+']').checked=true;
if(d.on){
var who=d.mode==='domain'?'Входят сотрудники домена с доступом к bj — своей учётной записью Windows, пароль не спрашивается.':'Вход — логин <b>'+esc(d.user||'—')+'</b> и пароль, который задали здесь.';
stl.innerHTML='<div class="stc stc-ok" style="margin-top:4px"><span class="stc-ic"></span><div class="stc-b"><b>Папки открыты по сети'+(d.mode==='domain'?' — через домен '+esc(d.realm||''):'')+'</b><small>Вставьте адрес в адресную строку проводника Windows. '+who+'</small>'+addrs(d)+'</div></div>';
stl.querySelectorAll('[data-copy]').forEach(function(b){ b.onclick=function(){ (window.bjCopy||function(t){navigator.clipboard&&navigator.clipboard.writeText(t)})(b.dataset.copy); b.textContent='Скопировано ✓'; setTimeout(function(){b.textContent='Скопировать'},1500); }; });
} else stl.innerHTML='<span class="muted">Сейчас закрыты. Выберите, как входить, отметьте «Открыть папки шлюза по сети» и сохраните — здесь появится адрес для проводника.</span>';
f.enabled.checked=!!d.on; if(d.mode!=='domain' && d.user) f.user.value=d.user;
if(!d.on && !f.password.value){ f.password.type='text'; f.password.value=genPw();
var pn=document.getElementById('ig-sh-pwnote'); if(pn) pn.textContent='Пароль создан автоматически — запишите его (или впишите свой). Его вводят в Windows при первом открытии папок; позже bj его не покажет.'; }
sync();
}
function load(){ return fetch('/admin/setup/igate/share',{cache:'no-store'}).then(function(r){return r.json()}).then(show).catch(function(){ stl.textContent='Состояние не получено.'; }); }
f.addEventListener('change',function(e){ if(e.target.name==='mode') sync(); });
function genPw(){ var cs='ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789', a=new Uint32Array(16), s=''; crypto.getRandomValues(a); for(var i=0;i<16;i++) s+=cs[a[i]%cs.length]; return s; }
var genbtn=document.getElementById('ig-sh-gen');
if(genbtn) genbtn.addEventListener('click',function(){
f.password.type='text'; f.password.value=genPw(); f.password.focus(); f.password.select();
o.innerHTML='<span class="muted">Новый пароль создан — запишите его перед сохранением.</span>';
});
f.addEventListener('submit',function(e){ e.preventDefault();
var p=new URLSearchParams(), url='/admin/setup/igate/share', d=mode()==='domain';
if(d && !cur.joined){
if(!f.admin.value.trim()||!f.adminpw.value){ o.innerHTML=bad('не заполнено: администратор домена и пароль'); return; }
url='/admin/setup/igate/share-join'; p.set('admin',f.admin.value.trim()); p.set('adminpw',f.adminpw.value);
o.textContent='Подключаю сервер к домену… (первый раз — установка winbind, до пары минут)';
} else {
p.set('mode',mode()); if(f.enabled.checked) p.set('enabled','1');
if(!d){ p.set('user',f.user.value.trim()); p.set('password',f.password.value); }
o.textContent='Сохраняю… (первый раз — до пары минут)';
}
var pwSet=(!d)?f.password.value:'';
post(url,p).then(function(r){
if(r.ok){ o.innerHTML=ok(r.output)+(pwSet?' <b>Пароль:</b> <code>'+esc(pwSet)+'</code> <button type="button" class="btn sm" data-copy="'+esc(pwSet)+'">Скопировать</button> — запишите, посмотреть позже нельзя.':'');
var cb=o.querySelector('[data-copy]'); if(cb) cb.onclick=function(){ (window.bjCopy||function(t){navigator.clipboard&&navigator.clipboard.writeText(t)})(cb.dataset.copy); cb.textContent='Скопировано ✓'; };
} else o.innerHTML=bad(r.error);
f.password.value=''; f.adminpw.value=''; return load();
}).catch(function(){ o.innerHTML=bad('не сохранено'); }); });
load();
})();
var st=null;
function load(){
fetch('/admin/setup/igate/status',{cache:'no-store'}).then(function(r){return r.json()}).then(function(d){
st=d; try{ window.dispatchEvent(new CustomEvent('bj-igate-status',{detail:d})); }catch(e){} // сводка раздела «НРД»
if(!d.ok){ head.innerHTML=bad(d.error); return; }
var ri=document.getElementById('ig-reinst'), bb=document.getElementById('ig-bnd'), bv=document.getElementById('ig-bnd-v');
var running=d.installed&&d.engine==='Running';
var rb=document.getElementById('ig-rb'); if(rb){ var can=d.rollback&&d.rollback!==d.version; rb.style.display=can?'block':'none'; if(can) document.getElementById('ig-rb-v').textContent=d.rollback; }
if(!d.installed) head.innerHTML=bad('Шлюз НРД на этом сервере не установлен');
else head.innerHTML=(running?ok('Шлюз НРД '+d.version+' установлен и работает'):bad('Шлюз НРД установлен, но не работает'+(IGDEV?': движок '+(d.engine||'не отвечает')+', служба '+d.service:'')));
if(d.busy) head.innerHTML='<span style="color:var(--accent)">⟳ Шлюз перезапускается — '+esc(d.busy)+'; состояние — последнее известное</span>';
if(!running && d.keymedia==='missing'){ head.innerHTML='<span style="color:var(--warn)">⚠ Шлюз НРД ждёт флешку с ключом подписи — её нет в сервере. Вставьте флешку: bj перезапустит шлюз сам. Переустанавливать шлюз не нужно.</span>'; }
// не стоит или не работает — одна кнопка: поставить заново проверенную версию с сервера (систему переставлять не нужно)
ri.style.display=(running||d.keymedia==='missing')?'none':'block'; // без флешки переустановка не поможет
if(d.installing){ // bj сам ставит/обновляет ИШ: остановка шлюза — штатная, переустанавливать не нужно
head.innerHTML='<span style="color:var(--accent)">⟳ Идёт установка шлюза НРД'+(d.upgrade_to?' '+esc(d.upgrade_to):'')+' — '+esc(d.installing)+'. Шлюз недоступен 1–3 минуты; настройки и каналы сохраняются.</span>'; ri.style.display='none';
} else if(d.upgrade_to){
head.innerHTML+='<div class="muted" style="font-size:13px;margin-top:4px">⟳ Получена новая версия шлюза НРД '+esc(d.upgrade_to)+' — bj обновит шлюз сам в ближайшие минуты; настройки и каналы сохранятся.</div>';
}
if(d.installing||d.upgrade_to) ovStart(); // обновление идёт или вот-вот начнётся — страница перекрыта ходом
if(!running){
if(d.bundled){ bb.style.display=''; bb.textContent=d.installed?'Установить шлюз НРД заново':'Установить шлюз НРД'; bv.textContent='версия '+d.bundled; }
else { bb.style.display='none'; bv.textContent='Дистрибутива шлюза НРД на сервере нет — он приходит с обновлением bj («Настройка → Обновления»).'; }
}
var dep=d.deponent||'', cr=document.getElementById('ig-crypto').value;
var rows=[['test3','Тестовый TEST3','проверка переводов до боевой работы'],['prod','Боевой','настоящие переводы']].map(function(x){
var code=(x[0]==='prod'?'PROD':'TEST3')+dep, ch=(d.channels||[]).filter(function(c){return c.code===code})[0], act=d.active_contour===x[0];
var li=[], a=[], pill, cls;
function it(okk,t){ li.push('<li class="'+(okk===true?'ok':okk===false?'no':'na')+'">'+t+'</li>'); }
if(!dep){ it(false,'нет депозитарного кода — раздел «Организация»'); pill='Нет кода'; cls='no'; }
else if(!ch){
it(false,'канала в шлюзе нет'+(d.installed?' — bj создаст его сам':' — появится после установки шлюза'));
if(d.installed) a.push('<button type="button" class="btn sm" data-act="contour" data-c="'+x[0]+'">Создать канал</button>');
pill='Канала нет'; cls='no';
} else {
it(true,'канал <b>'+esc(ch.code)+'</b>'+(IGDEV?' <span class="muted">'+esc(ch.crypto+' · '+ch.endpoint)+'</span>':''));
it(!!ch.profile, ch.profile?(IGDEV?'ключ: профиль '+esc(ch.profile):'ключ установлен'):'ключ этого контура не установлен');
it(!!ch.enabled, ch.enabled?'канал включён':'канал выключен — включится после установки ключа');
var ready=ch.enabled&&ch.profile;
if(act){ pill='● Активный'; cls='cur'; }
else if(ready){ pill='Готов'; cls='ok'; }
else { pill='Нужен ключ'; cls='no'; }
if(!ch.profile) a.push('<a class="btn sm" href="/admin/wizard?step=8&contour='+x[0]+'" style="text-decoration:none">Установить ключ</a>');
if(!act){
if(ready) a.push(x[0]==='prod'?'<button type="button" class="btn sm" data-act="showprod">Сделать активным…</button>':'<button type="button" class="btn sm" data-act="switch" data-c="test3">Сделать активным</button>');
else a.push('<button type="button" class="btn sm" disabled title="Сначала установите ключ этого контура">Сделать активным</button>');
}
a.push('<button type="button" class="btn sm ghost" data-act="contour" data-c="'+x[0]+'" title="Записать канал в шлюз заново: адрес, сертификат, ключ">Обновить канал</button>');
}
var why=act?'<p class="ct-why">Через этот контур bj сейчас отправляет и получает переводы.</p>':(ch&&!(ch.enabled&&ch.profile))?'<p class="ct-why">Чтобы сделать контур активным, сначала установите его ключ (флешка с ключом этого контура).</p>':'';
return '<div class="ct ct-'+cls+(act?' ct-on':'')+'"><div class="ct-h"><div><b>'+x[1]+'</b><small>'+x[2]+'</small></div><span class="ct-pill">'+pill+'</span></div><ul class="ct-li">'+li.join('')+'</ul>'+why+(a.length?'<div class="ct-act">'+a.join('')+'</div>':'')+'</div>';
});
tb.innerHTML=rows.join('');
document.getElementById('ig-depo').placeholder=d.depo_db_prod||d.depo_db||'как сейчас';
tb.querySelectorAll('button[data-act]').forEach(function(b){ b.onclick=function(){ act(b.dataset.act,b.dataset.c,b); }; });
}).catch(function(){ head.textContent='Состояние шлюза НРД не получено.'; });
}
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'){ if(b.textContent.indexOf('Обновить')===0 && !confirm('Записать канал в шлюз заново? Шлюз перезапустится — обмен с НРД остановится до 2 минут.')) return;
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); }); }
}
document.getElementById('ig-goprod').onclick=function(){
var p=new URLSearchParams(); p.set('contour','prod'); p.set('crypto',document.getElementById('ig-crypto').value);
p.set('depo_db',document.getElementById('ig-depo').value.trim()); p.set('confirm',document.getElementById('ig-confirm').value.trim());
if(!confirm('Перейти на БОЕВОЙ контур? bj начнёт отправлять в НРД настоящие переводы.')) 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); });
};
document.getElementById('ig-crypto').onchange=load;
// этапы установщика (bj-igate-admin) по порядку и сколько каждый обычно длится, секунд
var STAGES=[['queued','Запуск установки',15],['deps','Системные компоненты',120],['dist','Дистрибутив шлюза',30],
['user','Служебный пользователь шлюза',15],['db','База шлюза',20],['install','Установка шлюза',90],
['config','Настройка шлюза',15],['schema','Подготовка базы шлюза',90],['start','Запуск шлюза',120]];
var stepsEl=document.getElementById('ig-steps'), logbox=document.getElementById('ig-logbox'), cur={key:'',since:0,s:null}, tick=0;
function stageKey(s){ // прежний установщик называл два этапа «package», а первый и последний — «start»
if(s.stage==='package') return /дистрибутив/.test(s.msg||'')?'dist':'install';
if(s.stage==='start' && /запущена/.test(s.msg||'')) return 'queued';
return s.stage; }
function mmss(n){ n=Math.max(0,Math.round(n)); return Math.floor(n/60)+':'+('0'+n%60).slice(-2); }
function draw(){
var s=cur.s; if(!s) return;
var k=stageKey(s), idx=-1; STAGES.forEach(function(x,i){ if(x[0]===k) idx=i; });
if(s.stage==='done') idx=STAGES.length;
var el=(Date.now()-cur.since)/1000;
stepsEl.hidden=false;
stepsEl.innerHTML=STAGES.map(function(x,i){
var cls=i<idx?'done':i===idx?(s.stage==='error'?'err':'cur'):'', note='';
if(i===idx && s.stage!=='error'){ var slow=el>x[2]*2;
note='<small'+(slow?' class="slow"':'')+'>идёт '+mmss(el)+' · обычно до '+mmss(x[2])+(slow?' — дольше обычного; установка работает, пока счётчик идёт; зависшая снимается сама через 40 минут':'')+'</small>'; }
return '<li class="'+cls+'"><span class="ig-ic"></span><span class="ig-t">'+esc(x[1])+note+'</span></li>'; }).join('');
}
// окно поверх страницы на время установки/обновления/отката ИШ
var ov={on:false,seen:false,t0:0,iv:null};
function ovSet(h,m,log,fin){ var o=document.getElementById('ig-ov'); o.style.display='flex';
document.getElementById('ig-ov-h').innerHTML=h; document.getElementById('ig-ov-m').textContent=m||'';
var l=document.getElementById('ig-ov-log'); if(log){ l.style.display='block'; l.textContent=log; l.scrollTop=l.scrollHeight; } else l.style.display='none';
document.getElementById('ig-ov-btns').style.display=fin?'block':'none'; }
function ovStart(){ if(window.bjIgPhase){ window.bjIgPhase(''); return; } if(ov.on) return; ov.on=true; ov.t0=Date.now(); ovTick(); ov.iv=setInterval(ovTick,3000); }
function ovTick(){
fetch('/admin/setup/igate/install-status',{cache:'no-store'}).then(function(r){return r.json()}).then(function(s){
var run=s.stage&&s.stage!=='done'&&s.stage!=='error';
if(run){ ov.seen=true; ovSet(s.stage==='rollback'?'<span style="color:var(--warn)">⟲ Обновление не прошло — возвращаю прежнюю версию шлюза</span>':'⟳ Идёт установка шлюза НРД',
(s.msg||s.stage)+' · '+(s.elapsed||0)+' с на этапе. Не закрывайте страницу: обмен с НРД на 1–3 минуты приостановлен, настройки и каналы сохраняются.', s.log, false); return; }
if(!ov.seen){
if(Date.now()-ov.t0>15*60000){ clearInterval(ov.iv); ov.on=false; document.getElementById('ig-ov').style.display='none'; return; }
ovSet('⟳ Получено обновление шлюза НРД','bj начнёт обновление автоматически в течение минуты. Не закрывайте страницу.','',false); return; }
clearInterval(ov.iv);
if(s.stage==='done'){ ovSet('<span style="color:var(--ok)">✓ '+esc(s.msg)+'</span>','Страница обновится…','',false); setTimeout(function(){ location.reload(); },2500); }
else ovSet('<span style="color:var(--err)">✗ Установка шлюза не прошла</span>', s.msg, s.log, true);
}).catch(function(){});
}
document.getElementById('ig-ov-close').onclick=function(){ location.reload(); };
function poll(){ ovStart(); var t0=Date.now(), btn=document.getElementById('ig-bnd');
btn.disabled=true; // пока идёт установка — второй раз не нажать
clearInterval(tick); tick=setInterval(draw,1000);
var iv=setInterval(function(){
fetch('/admin/setup/igate/install-status',{cache:'no-store'}).then(function(r){return r.json()}).then(function(s){
if(!s.stage) return;
if(stageKey(s)!==cur.key){ cur.key=stageKey(s); cur.since=Date.now()-(s.elapsed||0)*1000; }
cur.s=s; draw(); out.textContent='';
if(s.log){ logbox.hidden=false; lg.textContent=s.log; lg.scrollTop=lg.scrollHeight; }
if(s.stage==='done'){ clearInterval(iv); clearInterval(tick); btn.disabled=false; out.innerHTML=ok(s.msg); load(); }
if(s.stage==='error'){ clearInterval(iv); clearInterval(tick); btn.disabled=false; out.innerHTML=bad(s.msg)+' — можно нажать «Установить шлюз НРД заново»'; logbox.open=true; load(); }
}).catch(function(){});
if(Date.now()-t0>2700000){ clearInterval(iv); clearInterval(tick); } },3000); }
document.getElementById('ig-bnd').onclick=function(){
if(/заново/.test(this.textContent) && !confirm('Установить шлюз НРД заново? Шлюз остановится на 2–3 минуты, обмен с НРД будет приостановлен; настройки, каналы и ключи сохранятся.')) return;
var p=new URLSearchParams(); p.set('bundled','1');
out.textContent='Запускаю…'; cur={key:'',since:0,s:null}; post('/admin/setup/igate/install',p).then(function(d){ if(!d.ok){out.innerHTML=bad(d.error);return} out.textContent=d.output; poll(); }); };
document.getElementById('ig-rb-btn').onclick=function(){ var v=document.getElementById('ig-rb-v').textContent;
if(!confirm('Откатить шлюз НРД к версии '+v+'? Шлюз остановится на 1–3 минуты; программа, данные и база шлюза вернутся к состоянию до обновления. Пришедшую версию bj сам ставить больше не будет, пока не выйдет следующая.')) return;
out.textContent='Запускаю откат…'; cur={key:'',since:0,s:null}; post('/admin/setup/igate/rollback',new URLSearchParams()).then(function(d){ if(!d.ok){out.innerHTML=bad(d.error);return} out.textContent=d.output; poll(); }); };
if(document.getElementById('ig-dl')) document.getElementById('ig-dl').onclick=function(){ var p=new URLSearchParams(); p.set('version',document.getElementById('ig-ver').value.trim());
out.textContent='Запускаю…'; cur={key:'',since:0,s:null}; post('/admin/setup/igate/install',p).then(function(d){ if(!d.ok){out.innerHTML=bad(d.error);return} out.textContent=d.output; poll(); }); };
if(document.getElementById('ig-up')) document.getElementById('ig-up').onclick=function(){ var f=document.getElementById('ig-file').files[0]; if(!f){ out.textContent='Выберите файл igate-…-amd64.zip'; return; }
var fd=new FormData(); fd.append('file',f); out.textContent='Загружаю '+f.name+'…';
fetch('/admin/setup/igate/install',{method:'POST',body:fd}).then(function(r){return r.json()}).then(function(d){ if(!d.ok){out.innerHTML=bad(d.error);return} out.textContent=d.output; poll(); }); };
load();
// установка уже идёт (например, её начала «Подготовка модулей») — показать ход
fetch('/admin/setup/igate/install-status',{cache:'no-store'}).then(function(r){return r.json()}).then(function(s){ if(s.stage&&s.stage!=='done'&&s.stage!=='error') poll(); }).catch(function(){});
})();
</script>
{{end}}