База 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
@@ -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>. «Проверить» скачивает копию и сверяет контрольные суммы — ничего не восстанавливает.'+