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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
zuevav
2026-10-07 00:00:35 +03:00
parent 3a9bc5d7fa
commit 4adc1398f6
16 changed files with 832 additions and 50 deletions
@@ -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='';