База 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:
@@ -218,9 +218,9 @@
|
||||
<section class="dq-s">
|
||||
<h3 class="dq-h">Где база DEPO</h3>
|
||||
<div class="dq-grid dq-srv">
|
||||
<label>Адрес сервера<input type="text" name="server" value="{{.Settings.Fancy.Server}}" placeholder="IP-адрес или имя\экземпляр" required autofocus spellcheck="false"></label>
|
||||
<label>Адрес сервера<input type="text" name="server" data-need="адрес сервера DEPO" value="{{.Settings.Fancy.Server}}" placeholder="IP-адрес или имя\экземпляр" required autofocus spellcheck="false"></label>
|
||||
<label>Порт<input type="text" name="port" value="{{if .Settings.Fancy.Port}}{{.Settings.Fancy.Port}}{{else if $pg}}5432{{else}}1433{{end}}" inputmode="numeric"></label>
|
||||
<label>Имя базы<input type="text" name="depo_database" value="{{.Settings.Fancy.DepoDatabase}}" placeholder="как называется на сервере" required pattern="[A-Za-z][A-Za-z0-9_]*" title="латинские буквы, цифры и _, начинается с буквы" spellcheck="false"></label>
|
||||
<label>Имя базы<input type="text" name="depo_database" data-need="имя базы DEPO" value="{{.Settings.Fancy.DepoDatabase}}" placeholder="как называется на сервере" required pattern="[A-Za-z][A-Za-z0-9_]*" title="латинские буквы, цифры и _, начинается с буквы" spellcheck="false"></label>
|
||||
</div>
|
||||
<p class="dq-note">Точное имя базы учёта DEPO — bj будет работать только с ней. Перед подготовкой bj проверит, что в ней есть учётные таблицы DEPO, и в чужую базу ничего не запишет.</p>
|
||||
</section>
|
||||
@@ -249,8 +249,8 @@
|
||||
<div class="dq-panel dq-box" id="wiz-depo-auto">
|
||||
<div class="dq-box-h">Администратор SQL <span>— нужен один раз, чтобы подготовить базу; bj его не сохраняет</span></div>
|
||||
<div class="dq-grid dq-2">
|
||||
<label>Логин<input type="text" name="admin_user" autocomplete="off" spellcheck="false" placeholder="{{if $pg}}postgres{{else}}sa{{end}}"></label>
|
||||
<label>Пароль<input type="password" name="admin_password" autocomplete="new-password"></label>
|
||||
<label>Логин<input type="text" name="admin_user" data-need="логин администратора SQL" autocomplete="off" spellcheck="false" placeholder="{{if $pg}}postgres{{else}}sa{{end}}"></label>
|
||||
<label>Пароль<input type="password" name="admin_password" data-need="пароль администратора SQL" autocomplete="new-password"></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -258,8 +258,8 @@
|
||||
<div class="dq-panel dq-box dq-box-bj" id="wiz-depo-login">
|
||||
<div class="dq-box-h">Логин bj <span>— под ним bj будет работать с базой DEPO</span></div>
|
||||
<div class="dq-grid dq-2">
|
||||
<label>Логин<input type="text" name="bj_user" value="{{.DepoUser}}" autocomplete="off" spellcheck="false"></label>
|
||||
<label><span class="dq-lbl-row">Пароль <button type="button" class="dq-link" id="wiz-depo-gen">↻ другой пароль</button></span><input type="text" name="bj_password" value="{{.DepoPass}}" autocomplete="off" spellcheck="false" class="dq-mono"></label>
|
||||
<label>Логин<input type="text" name="bj_user" data-need="логин bj" value="{{.DepoUser}}" autocomplete="off" spellcheck="false"></label>
|
||||
<label><span class="dq-lbl-row">Пароль <button type="button" class="dq-link" id="wiz-depo-gen">↻ другой пароль</button></span><input type="text" name="bj_password" data-need="пароль bj" value="{{.DepoPass}}" autocomplete="off" spellcheck="false" class="dq-mono"></label>
|
||||
</div>
|
||||
<p class="dq-note">bj придумал их сам. Можно заменить своими — <b>bj сохранит и будет использовать ровно то, что здесь написано</b>: подготовка создаст на сервере этот логин с этим паролем<span id="wiz-depo-login-script"> (в скрипте — они же)</span>.</p>
|
||||
<p class="dq-warn">Если логин с таким именем на сервере уже есть, ему будет поставлен этот пароль — программа, которая им пользуется, перестанет входить. Тогда впишите другое имя.</p>
|
||||
@@ -286,8 +286,8 @@
|
||||
|
||||
<div class="dq-panel" id="wiz-depo-existing">
|
||||
<div class="dq-grid dq-2">
|
||||
<label>Логин bj<input type="text" name="user" value="{{.Settings.Fancy.User}}" autocomplete="off" spellcheck="false"></label>
|
||||
<label>Пароль<input type="password" name="password" autocomplete="new-password" placeholder="{{if .Settings.Fancy.Password}}пусто — прежний{{end}}"></label>
|
||||
<label>Логин bj<input type="text" name="user" data-need="логин bj" value="{{.Settings.Fancy.User}}" autocomplete="off" spellcheck="false"></label>
|
||||
<label>Пароль<input type="password" name="password" {{if not .Settings.Fancy.Password}}data-need="пароль bj"{{end}} autocomplete="new-password" placeholder="{{if .Settings.Fancy.Password}}пусто — прежний{{end}}"></label>
|
||||
</div>
|
||||
<button type="submit" class="btn dq-go">Проверить подключение</button>
|
||||
</div>
|
||||
@@ -408,7 +408,52 @@
|
||||
<div class="card">
|
||||
<h2><span class="dot {{if .Done.Postgres}}ok{{else}}err{{end}}"></span>База данных bj (PostgreSQL)</h2>
|
||||
<p>База самого bj: журнал заявок и поручений, история обмена с НРД и DEPO, пользователи.</p>
|
||||
{{if .Done.Postgres}}<div class="wiz-ok"><i class="wiz-ic"></i><span>База готова{{if .PGSummary}}: {{.PGSummary}}{{end}}. Её создал установщик — ничего делать не нужно.</span></div>{{end}}
|
||||
{{if and .Done.Postgres (pgLocal .Settings.Postgres.DSN)}}<div class="wiz-ok"><i class="wiz-ic"></i><span>База готова{{if .PGSummary}}: {{.PGSummary}}{{end}}. Её создал установщик — ничего делать не нужно.</span></div>
|
||||
<details style="margin-top:14px" {{if .Flash}}open{{end}}><summary style="cursor:pointer;color:var(--accent)">Хранить базу bj на своём сервере PostgreSQL (необязательно)</summary>
|
||||
<p class="muted" style="font-size:13.5px;margin:10px 0 0">Если у организации есть свой сервер PostgreSQL — с резервным копированием и администратором баз, — базу bj можно держать на нём. bj перенесёт данные, сверит число строк в каждой таблице и переключится сам; база на этом сервере останется запасной копией. Нужно: пустая база на сервере организации, пользователь — её владелец, доступ с адреса bj ({{serverIP}}) к порту PostgreSQL. Только PostgreSQL той же версии, что здесь, или новее.</p>
|
||||
<form method="post" action="/admin/setup/postgres/move" id="pg-form" class="pg-form">
|
||||
<label class="pg-wide">Сервер PostgreSQL<input type="text" name="pg_host" placeholder="db.example.local" required autocomplete="off"></label>
|
||||
<label>Порт<input type="text" name="pg_port" value="5432" inputmode="numeric" autocomplete="off"></label>
|
||||
<label>База<input type="text" name="pg_db" value="bj" required autocomplete="off"></label>
|
||||
<label>Пользователь<input type="text" name="pg_user" value="bj" required autocomplete="off"></label>
|
||||
<label>Пароль<input type="password" name="pg_password" autocomplete="new-password"></label>
|
||||
<label>Шифрование<select name="pg_ssl"><option value="prefer">если сервер умеет (prefer)</option><option value="require">обязательно (require)</option><option value="verify-full">обязательно, с проверкой сертификата</option><option value="disable">без шифрования</option></select></label>
|
||||
<div class="pg-wide" style="display:flex;gap:8px;flex-wrap:wrap">
|
||||
<button type="submit" name="check" value="1" class="btn btn-secondary pg-go">Проверить подключение</button>
|
||||
<button type="submit" class="btn pg-go" data-confirm="Перенести базу bj на сервер организации и переключиться? bj перезапустится; на время переноса (обычно до минуты) лучше не проводить переводы.">Перенести базу и переключиться</button>
|
||||
</div>
|
||||
<div class="dq-run pg-wide" id="pg-run" hidden aria-live="polite"><ol class="dq-steps" id="pg-steps"></ol><div class="dq-run-msg" id="pg-msg" hidden></div></div>
|
||||
</form>
|
||||
<style>.pg-form{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;max-width:720px;margin-top:12px}.pg-form label{display:block;margin:0}.pg-form label input,.pg-form label select{width:100%;box-sizing:border-box;margin-top:4px}.pg-wide{grid-column:1/-1}@media (max-width:600px){.pg-form{grid-template-columns:minmax(0,1fr)}}</style>
|
||||
<script>(function(){
|
||||
var f=document.getElementById('pg-form'); if(!f||!window.fetch||!window.TextDecoder) return;
|
||||
var run=document.getElementById('pg-run'), list=document.getElementById('pg-steps'), msg=document.getElementById('pg-msg');
|
||||
function li(k){ return list.querySelector('[data-k="'+k+'"]'); }
|
||||
function mark(el,st,text){ if(!el) return; el.className='dq-st is-'+st; if(text){ var d=el.querySelector('.dq-st-d'); if(!d){ d=document.createElement('div'); d.className='dq-st-d'; el.querySelector('.dq-st-t').appendChild(d);} d.textContent=text; } }
|
||||
function onStep(x){ if(x.state==='run'){ var e=li(x.step); if(!e){ e=document.createElement('li'); e.dataset.k=x.step; e.innerHTML='<span class="dq-st-ic"></span><div class="dq-st-t"><span></span></div>'; list.appendChild(e);} e.querySelector('.dq-st-t > span').textContent=x.text; mark(e,'run'); } else mark(li(x.step),x.state,x.text); }
|
||||
f.addEventListener('submit',function(ev){
|
||||
var b=ev.submitter; if(!b) return; ev.preventDefault();
|
||||
if(b.dataset.confirm && !confirm(b.dataset.confirm)) return;
|
||||
var btns=[].slice.call(f.querySelectorAll('.pg-go')); btns.forEach(function(x){x.disabled=true;});
|
||||
list.innerHTML=''; msg.hidden=true; run.hidden=false;
|
||||
var fd=new FormData(f); if(b.name) fd.set(b.name,b.value);
|
||||
var final=null;
|
||||
fetch(f.action,{method:'POST',body:new URLSearchParams(fd),headers:{'Content-Type':'application/x-www-form-urlencoded'},cache:'no-store'}).then(function(r){
|
||||
if(!r.ok||!r.body) throw new Error('bad');
|
||||
var rd=r.body.getReader(), dec=new TextDecoder(), buf='';
|
||||
function line(t){ if(!t.trim()) return; var x=JSON.parse(t); if(x.done) final=x; else onStep(x); }
|
||||
function pump(){ return rd.read().then(function(c){ if(c.done){ line(buf); return; } buf+=dec.decode(c.value,{stream:true}); var i; while((i=buf.indexOf('\n'))>=0){ line(buf.slice(0,i)); buf=buf.slice(i+1);} return pump(); }); }
|
||||
return pump();
|
||||
}).then(function(){ fin(final,b); }, function(){ fin(null,b); });
|
||||
function fin(x,b){
|
||||
msg.textContent=x&&x.msg?x.msg:'Связь с bj прервалась — обновите страницу.'; msg.hidden=false;
|
||||
if(x&&x.ok&&!b.name){ msg.textContent+=' Страница обновится, когда bj перезапустится.'; setTimeout(function(){ location.href='/admin/wizard?step=2'; },20000); return; }
|
||||
btns.forEach(function(x){x.disabled=false;});
|
||||
}
|
||||
});
|
||||
})();</script>
|
||||
</details>
|
||||
{{else if .Done.Postgres}}<div class="wiz-ok"><i class="wiz-ic"></i><span>База bj — на сервере организации{{if .PGSummary}}: {{.PGSummary}}{{end}}.{{if .Settings.Postgres.Local}} Прежняя база на этом сервере сохранена как запасная копия.{{end}}</span></div>{{end}}
|
||||
|
||||
{{if not .Done.Postgres}}<div class="help-block">
|
||||
<strong>Обычно базу создаёт установщик</strong> — если вы ставили bj с образа или командой установки, этот шаг уже выполнен. Если база bj на отдельном сервере PostgreSQL — введите строку подключения ниже{{if devOn}}, или поднимите локальную базу автоматически (режим разработки){{end}}.
|
||||
@@ -425,15 +470,15 @@
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<details style="margin-top:14px" {{if not .Settings.Postgres.DSN}}open{{end}}>
|
||||
<summary style="cursor:pointer;color:var(--accent)">{{if .Settings.Postgres.DSN}}Подключить другую базу (обычно не нужно){{else}}Вариант Б — у меня уже есть PostgreSQL, введу DSN сам{{end}}</summary>
|
||||
{{if or devOn (not .Done.Postgres)}}<details style="margin-top:14px" {{if not .Settings.Postgres.DSN}}open{{end}}>
|
||||
<summary style="cursor:pointer;color:var(--accent)">{{if .Settings.Postgres.DSN}}Подключить другую базу строкой подключения (без переноса данных){{else}}Вариант Б — у меня уже есть PostgreSQL, введу DSN сам{{end}}</summary>
|
||||
<form method="post" action="/admin/setup/postgres" style="margin-top:12px">
|
||||
<label>DSN (строка подключения) <span class="tooltip" title="Формат: postgres://пользователь:пароль@хост:порт/база?sslmode=disable. Например: postgres://bj:secret@db.example.com:5432/bj?sslmode=require">?</span></label>
|
||||
<input type="text" name="dsn" value="{{.Settings.Postgres.DSN}}" placeholder="postgres://bj:secret@127.0.0.1:5432/bj?sslmode=disable" style="width:100%;padding:8px;margin-top:6px">
|
||||
<p class="muted" style="margin-top:8px">При сохранении выполняется проверка подключения (Ping). Если БД недоступна — будет ошибка.</p>
|
||||
<button type="submit" class="btn" style="margin-top:8px">Сохранить и проверить</button>
|
||||
</form>
|
||||
</details>
|
||||
</details>{{end}}
|
||||
|
||||
{{template "wizNav" .}}
|
||||
{{if and devOn (not .Done.Postgres)}}<p style="margin-top:8px"><a href="/admin/wizard?step=3&skip=postgres" style="font-size:13px">Пропустить (режим разработки: in-memory — без сохранения сделок)</a></p>{{end}}
|
||||
@@ -634,7 +679,7 @@
|
||||
<a href="https://{{serverIP}}:8443/admin/">https://{{serverIP}}:8443/</a>
|
||||
</div>
|
||||
<p class="muted" style="font-size:12.5px;margin:4px 0 0">Чтобы адрес не менялся, задайте постоянный: на консоли сервера — <code>sudo bj-network</code>.</p>
|
||||
{{if .AutoDone}}<p class="muted" style="font-size:12.5px;margin:16px 0 0;padding-top:12px;border-top:1px solid var(--border)">Уже сделано установщиком: {{range $i, $a := .AutoDone}}{{if $i}} · {{end}}✓ {{$a}}{{end}}</p>{{end}}
|
||||
{{if .AutoDone}}<p class="muted" style="font-size:12.5px;margin:16px 0 0;padding-top:12px;border-top:1px solid var(--border)">Уже сделано установщиком: {{range $i, $a := .AutoDone}}{{if $i}} · {{end}}✓ {{$a}}{{end}}{{if and .Done.Postgres (pgLocal .Settings.Postgres.DSN)}}. <a href="/admin/wizard?step=2">Базу bj можно держать и на своём сервере PostgreSQL →</a>{{end}}</p>{{end}}
|
||||
<script>
|
||||
(function(){ // пояс компьютера пользователя — предложить одной кнопкой (не менять молча: сервер может стоять в другом городе)
|
||||
var sel=document.getElementById('wiz-tz-zone'), tz='';
|
||||
|
||||
@@ -763,6 +763,49 @@ details:has(> pre.copyable) { min-width:0; max-width:100%; }
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<style>.need-miss{border-color:var(--err)!important;box-shadow:0 0 0 3px color-mix(in srgb,var(--err) 18%,transparent)!important}
|
||||
.need-msg{color:var(--err);font-size:13.5px;margin:8px 0 0;flex-basis:100%}</style>
|
||||
<script>
|
||||
/* Обязательные поля (06.10.2026): кнопка не уходит с пустыми видимыми полями — bj пишет, какие именно не заполнены,
|
||||
подсвечивает их и ставит курсор в первое. Поля помечены required или data-need="как назвать"; скрытые (другая
|
||||
вкладка, другой способ) не проверяются. window.bjNeed(form, btn) — то же для кнопок без отправки формы. */
|
||||
(function(){
|
||||
function visible(el){ return !el.disabled && el.type!=='hidden' && !!(el.offsetWidth||el.offsetHeight||el.getClientRects().length); }
|
||||
function name(el){
|
||||
var n=el.getAttribute('data-need')||el.getAttribute('aria-label')||'';
|
||||
if(!n){ var l=el.closest('label'); if(l){ var c=l.cloneNode(true); c.querySelectorAll('input,select,textarea,button,.muted,.tooltip').forEach(function(x){x.remove()}); n=c.textContent.replace(/\s+/g,' ').replace(/\(.*?\)/g,'').trim(); } }
|
||||
return n||el.placeholder||el.name;
|
||||
}
|
||||
function msgBox(f, btn){
|
||||
var host=(btn&&btn.parentElement)||f, m=host.querySelector(':scope > .need-msg');
|
||||
if(!m){ m=document.createElement('div'); m.className='need-msg'; m.setAttribute('role','alert'); host.appendChild(m); }
|
||||
return m;
|
||||
}
|
||||
window.bjNeed=function(f, btn){
|
||||
var miss=[], bad=[], first=null;
|
||||
f.querySelectorAll('[required],[data-need]').forEach(function(el){
|
||||
el.classList.remove('need-miss');
|
||||
if(!visible(el)) return;
|
||||
var empty = el.type==='file' ? !(el.files&&el.files.length) : (el.type==='checkbox'||el.type==='radio') ? false : !String(el.value||'').trim();
|
||||
if(empty){ miss.push(name(el)); el.classList.add('need-miss'); first=first||el; }
|
||||
else if(el.checkValidity && !el.checkValidity()){ bad.push(name(el)); el.classList.add('need-miss'); first=first||el; }
|
||||
});
|
||||
var old=f.querySelectorAll('.need-msg'); old.forEach(function(x){ x.remove(); });
|
||||
if(!first) return true;
|
||||
var t=[]; if(miss.length) t.push('Не заполнено: '+miss.join(', ')); if(bad.length) t.push('Заполнено неверно: '+bad.join(', '));
|
||||
msgBox(f, btn).textContent=t.join('. ')+'.';
|
||||
try{ first.focus({preventScroll:false}); first.scrollIntoView({block:'center',behavior:'smooth'}); }catch(e){}
|
||||
return false;
|
||||
};
|
||||
document.addEventListener('DOMContentLoaded',function(){ document.querySelectorAll('form').forEach(function(f){ f.noValidate=true; }); });
|
||||
document.addEventListener('submit',function(e){
|
||||
var f=e.target; if(!(f instanceof HTMLFormElement)) return;
|
||||
var b=e.submitter; if(b&&b.hasAttribute('formnovalidate')) return;
|
||||
if(!window.bjNeed(f, b)){ e.preventDefault(); e.stopImmediatePropagation(); }
|
||||
}, true);
|
||||
document.addEventListener('input',function(e){ var el=e.target; if(el.classList&&el.classList.contains('need-miss')&&String(el.value||'').trim()) el.classList.remove('need-miss'); });
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<form method="post" action="/admin/setup/alerts-smtp" style="margin-top:12px">
|
||||
<label style="display:flex;align-items:center;gap:8px;font-weight:400;margin-bottom:12px"><input type="checkbox" name="enabled" style="width:auto" {{if .Settings.Alerts.SMTP.Enabled}}checked{{end}}> Включить почтовые уведомления</label>
|
||||
<div class="grid" style="grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px">
|
||||
<div><label>SMTP-сервер</label><input type="text" name="host" value="{{.Settings.Alerts.SMTP.Host}}" placeholder="smtp.example.ru" style="margin-top:3px"></div>
|
||||
<div><label>SMTP-сервер</label><input type="text" name="host" data-need="SMTP-сервер" value="{{.Settings.Alerts.SMTP.Host}}" placeholder="smtp.example.ru" style="margin-top:3px"></div>
|
||||
<div><label>Порт</label><input type="text" id="smtp-port" name="port" value="{{if .Settings.Alerts.SMTP.Port}}{{.Settings.Alerts.SMTP.Port}}{{else}}587{{end}}" style="margin-top:3px"></div>
|
||||
<div><label>Шифрование</label>
|
||||
<select id="smtp-security" name="security" style="margin-top:3px" onchange="smtpSyncPort(this.value)">
|
||||
@@ -18,7 +18,7 @@
|
||||
<div class="grid" style="grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px;margin-top:12px">
|
||||
<div><label>Логин</label><input type="text" name="username" value="{{.Settings.Alerts.SMTP.Username}}" autocomplete="off" style="margin-top:3px"></div>
|
||||
<div><label>Пароль {{if .Settings.Alerts.SMTP.Password}}<span class="muted">(задан; пусто — оставить)</span>{{end}}</label><input type="password" name="password" autocomplete="new-password" style="margin-top:3px"></div>
|
||||
<div><label>Отправитель (From)</label><input type="text" name="from" value="{{.Settings.Alerts.SMTP.From}}" placeholder="bj@example.ru" style="margin-top:3px"></div>
|
||||
<div><label>Отправитель (From)</label><input type="text" name="from" data-need="отправитель (From)" value="{{.Settings.Alerts.SMTP.From}}" placeholder="bj@example.ru" style="margin-top:3px"></div>
|
||||
</div>
|
||||
<div style="margin-top:12px;max-width:420px"><label>Проверочное письмо — кому</label><input type="text" name="test_to" value="{{if .Settings.Alerts.SMTP.TestTo}}{{.Settings.Alerts.SMTP.TestTo}}{{else}}{{.Settings.Alerts.SMTP.To}}{{end}}" placeholder="it@example.ru" style="margin-top:3px;width:100%">
|
||||
<span class="muted" style="font-size:12px;display:block;margin-top:4px;line-height:1.4">По умолчанию — администратор системы из блока «Кому писать».</span></div>
|
||||
|
||||
@@ -9,13 +9,13 @@
|
||||
<form method="post" action="/admin/setup/backup" id="bk-form" style="display:grid;gap:10px;max-width:640px">
|
||||
<label class="chk" style="display:flex;align-items:center;gap:8px;font-weight:400"><input type="checkbox" name="enabled" style="width:auto"{{if $b.Enabled}} checked{{end}}> <b>Делать копии по расписанию</b></label>
|
||||
<div style="display:grid;grid-template-columns:2fr 1fr;gap:10px">
|
||||
<label>Сетевая папка — как в проводнике<input name="share" value="{{smbWin $b.Share}}" placeholder="\\nas\backup"></label>
|
||||
<label>Сетевая папка — как в проводнике<input name="share" data-need="сетевая папка" value="{{smbWin $b.Share}}" placeholder="\\nas\backup"></label>
|
||||
<label>Подпапка для копий<input name="dir" value="{{$b.Dir}}" placeholder="bj-backup"></label>
|
||||
</div>
|
||||
<p class="muted" style="font-size:12px;margin:-4px 0 0">Например <code>\\10.10.10.11\backup</code> — та же папка, что открывается в проводнике Windows с этим логином. Копии ложатся в подпапку внутри неё.</p>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
|
||||
<label>Логин хранилища<input name="user" value="{{$b.User}}" autocomplete="off"></label>
|
||||
<label>Пароль {{if $b.PasswordSet}}<span class="muted">(задан; пусто — оставить)</span>{{end}}<input name="password" type="password" autocomplete="new-password"></label>
|
||||
<label>Логин хранилища<input name="user" data-need="логин хранилища" value="{{$b.User}}" autocomplete="off"></label>
|
||||
<label>Пароль {{if $b.PasswordSet}}<span class="muted">(задан; пусто — оставить)</span>{{end}}<input name="password" type="password" {{if not $b.PasswordSet}}data-need="пароль хранилища"{{end}} autocomplete="new-password"></label>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
|
||||
<label>Как часто<select name="every_hours">{{$e := $b.EveryHours}}{{if not $e}}{{$e = 6}}{{end}}{{range $h := intList 1 2 3 4 6 8 12 24}}<option value="{{$h}}"{{if eq $h $e}} selected{{end}}>{{if eq $h 24}}раз в сутки (02:00){{else}}каждые {{$h}} ч{{end}}</option>{{end}}</select></label>
|
||||
@@ -39,7 +39,7 @@
|
||||
if(withForm){ ['share','dir','user','password'].forEach(function(n){ var v=f[n].value.trim(); if(v) p.set(n,v); }); }
|
||||
return fetch(url,{method:'POST',body:p,cache:'no-store'}).then(function(r){return r.json()});
|
||||
}
|
||||
document.getElementById('bk-test').onclick=function(){ out.textContent='Проверяю доступ…';
|
||||
document.getElementById('bk-test').onclick=function(){ if(window.bjNeed&&!window.bjNeed(this.form,this)) return; out.textContent='Проверяю доступ…';
|
||||
post('/admin/setup/backup/test',true).then(function(d){ out.innerHTML=d.ok?'<span style="color:var(--ok)">✓ '+esc(d.output)+'</span>':'<span style="color:var(--err)">✗ '+esc(d.error)+'</span>'; }); };
|
||||
document.getElementById('bk-run').onclick=function(){ out.textContent='Запускаю копию…';
|
||||
post('/admin/setup/backup/run',false).then(function(d){ if(!d.ok){out.innerHTML='<span style="color:var(--err)">✗ '+esc(d.error)+'</span>';return}
|
||||
@@ -47,7 +47,7 @@
|
||||
var iv=setInterval(function(){ fetch('/admin/setup/backup/status',{cache:'no-store'}).then(function(r){return r.json()}).then(function(s){
|
||||
var b=s.backup; if(b && Date.parse(b.at)>=t0-60000){ clearInterval(iv); out.innerHTML=b.ok?'<span style="color:var(--ok)">✓ копия готова: '+esc(b.dir)+', '+esc(b.msg)+'</span>':'<span style="color:var(--err)">✗ '+esc(b.msg)+'</span>'; }
|
||||
if(Date.now()-t0>600000) clearInterval(iv); }); },3000); }); };
|
||||
document.getElementById('bk-list').onclick=function(){ out.textContent='Читаю список…';
|
||||
document.getElementById('bk-list').onclick=function(){ if(window.bjNeed&&!window.bjNeed(this.form,this)) return; out.textContent='Читаю список…';
|
||||
post('/admin/setup/backup/list',true).then(function(d){ if(!d.ok){out.innerHTML='<span style="color:var(--err)">✗ '+esc(d.error)+'</span>';return}
|
||||
if(!d.list||!d.list.length){ out.textContent='Копий на хранилище нет.'; return; }
|
||||
out.innerHTML='Копий: <b>'+d.list.length+'</b>. «Проверить» скачивает копию и сверяет контрольные суммы — ничего не восстанавливает.'+
|
||||
|
||||
@@ -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{'&':'&','<':'<','>':'>','"':'"'}[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); }); }
|
||||
}
|
||||
|
||||
@@ -9,17 +9,17 @@
|
||||
<input type="hidden" name="check" value="1">
|
||||
<div style="display:grid;grid-template-columns:2.2fr .8fr 1.6fr;gap:10px">
|
||||
<label>Сервер QUIK МНП
|
||||
<input type="text" name="server" value="{{.Settings.Quik.Server}}" placeholder="адрес или имя\экземпляр" style="width:100%" required></label>
|
||||
<input type="text" name="server" data-need="адрес сервера QUIK МНП" value="{{.Settings.Quik.Server}}" placeholder="адрес или имя\экземпляр" style="width:100%" required></label>
|
||||
<label>Порт
|
||||
<input type="number" name="port" value="{{if .Settings.Quik.Port}}{{.Settings.Quik.Port}}{{else}}1433{{end}}" style="width:100%"></label>
|
||||
<label>База МНП{{/* без имени базы модуль не подключается — обязательно */}}
|
||||
<input type="text" name="database" value="{{.Settings.Quik.Database}}" placeholder="имя базы модуля" style="width:100%" required spellcheck="false"></label>
|
||||
<input type="text" name="database" data-need="имя базы модуля неторговых поручений" value="{{.Settings.Quik.Database}}" placeholder="имя базы модуля" style="width:100%" required spellcheck="false"></label>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
|
||||
<label>Логин
|
||||
<input type="text" name="user" value="{{.Settings.Quik.User}}" style="width:100%" required autocomplete="off"></label>
|
||||
<input type="text" name="user" data-need="логин SQL для QUIK" value="{{.Settings.Quik.User}}" style="width:100%" required autocomplete="off"></label>
|
||||
<label>Пароль
|
||||
<input type="password" name="password" placeholder="{{if .Settings.Quik.Password}}сохранён — пусто, чтобы не менять{{end}}" autocomplete="new-password" style="width:100%"></label>
|
||||
<input type="password" name="password" {{if not .Settings.Quik.Password}}data-need="пароль SQL для QUIK"{{end}} placeholder="{{if .Settings.Quik.Password}}сохранён — пусто, чтобы не менять{{end}}" autocomplete="new-password" style="width:100%"></label>
|
||||
</div>
|
||||
<details {{if quikNeedsTrust .Settings.Quik}}open{{end}}><summary style="cursor:pointer;color:var(--accent);font-size:13px">Дополнительно: шифрование</summary>
|
||||
<div style="display:grid;gap:10px;margin-top:8px">
|
||||
|
||||
Reference in New Issue
Block a user