Files
Bridge-and-Join-s/internal/lkgateway/web/templates/part_igate.html
T
fontvielle 5757e1e46a Папки шлюза: пароль генерируется и показывается сразу при загрузке (когда папки ещё не открыты)
Раньше поле пароля было пустым с подсказкой «пусто — прежний», и оператору
приходилось нажимать «Сгенерировать». Теперь при открытии страницы, если папки
шлюза ещё не открыты по сети, bj сразу подставляет в поле новый надёжный пароль
(genPw, 16 символов, crypto.getRandomValues) и показывает его. Оператор может
оставить его или вписать свой. Если папки уже открыты — поле остаётся пустым
(пустое = оставить прежний пароль при повторном сохранении).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
2026-10-21 11:47:35 +03:00

361 lines
38 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>Шлюз на этом сервере</h3>
<div id="ig-head" class="muted">Проверяю…</div>
<p class="muted" style="font-size:13px;margin:6px 0 0">Версия ИШ — та, что проверена ZETIT вместе с bj; новые версии приходят с обновлением bj. 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>
<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 передаёт пароль в Samba и показать позже не сможет. Пустое поле при повторном сохранении оставляет прежний пароль.</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>
(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='Сохраняю… (первый раз — установка Samba, до пары минут)';
}
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';
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>';
if(!running && d.keymedia==='missing'){ head.innerHTML='<span style="color:var(--warn)">⚠ ИШ '+esc(d.version)+' ждёт флешку с ключом подписи — её нет в сервере. Вставьте флешку: bj перезапустит шлюз сам. Переустанавливать ИШ не нужно.</span>'; }
// не стоит или не работает — одна кнопка: поставить заново проверенную версию с сервера (систему переставлять не нужно)
ri.style.display=(running||d.keymedia==='missing')?'none':'block'; // без флешки переустановка не поможет
if(!running){
if(d.bundled){ bb.style.display=''; bb.textContent=d.installed?'Установить ИШ заново':'Установить ИШ'; bv.textContent='версия '+d.bundled+' — проверена ZETIT, дистрибутив на сервере'; }
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> <span class="muted">'+esc(ch.crypto+' · '+ch.endpoint)+'</span>');
it(!!ch.profile, ch.profile?'ключ: профиль '+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'){ 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','Пользователь igate и хранилище паролей',15],['db','База ИШ в PostgreSQL',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('');
}
function poll(){ 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(){ 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(); }); };
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}}