6a68dc927f
- «Время и сеть»: пояс компьютера пользователя предлагается одной кнопкой, пояс применяется сразу при выборе; сервер времени — только если не синхронизируется; «сделано установщиком» — строкой внизу. - «Лицензия»: модули словами, а не кодами (quik-mnp → «QUIK: неторговые поручения…»); пояснения — только пока ключа нет; поле ключа крупное, одно. - «Учёт DEPO»: главный вопрос «Как подготовить базу» первым, короткие подписи; кружки переключателей больше не сплющиваются длинным текстом. - «QUIK»: одна кнопка «Сохранить и проверить»; база и шифрование — под «Дополнительно», без «true/disable» и TrustServerCertificate; автоматический цикл и справочник получателей в мастере свёрнуты. - «Организация»: без логотипа (он в «Оформлении»), кнопки одного цвета. «Шлюз НРД», «Ключ», «Оповещения»: по одному абзацу, письма и устройство автообновления сертификатов — под раскрытием. - Мастер и «Подготовка модулей» — без меню системы; подзаголовок не повторяет названия глав. - Подсказка о восстановлении из копии ведёт на первую страницу (в мастере его больше нет). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
66 lines
7.7 KiB
HTML
66 lines
7.7 KiB
HTML
{{/* Резервное копирование — общее для «Настройка → Резервное копирование» и мастера (данные — SetupData). */}}
|
|
{{define "backupCard"}}
|
|
{{$b := .Settings.Backup}}{{$bs := backupStatus}}
|
|
<div class="card">
|
|
<h2><span class="dot {{if not $b.Enabled}}warn{{else if and $bs $bs.OK}}ok{{else}}err{{end}}"></span>Копии сервера bj на сетевое хранилище</h2>
|
|
<p class="muted">Что копируется: база bj (заявки, сделки, кабинет), база и папка шлюза НРД (настройки, сертификаты, полученные от НРД документы), настройки и данные bj. Пароль хранилища bj у себя не хранит — он лежит на сервере в файле, доступном только системе. Восстановить копию можно на первой странице нового сервера bj — «Восстановить из резервной копии».</p>
|
|
<p style="margin:8px 0">{{if $bs}}Последняя копия: <b>{{backupWhen $bs.At}}</b> — {{if $bs.OK}}<span style="color:var(--ok)">✓ {{$bs.Dir}}, {{$bs.Msg}}</span>{{else}}<span style="color:var(--err)">✗ {{$bs.Msg}}</span>{{end}}{{else}}<span class="muted">Копий ещё не было.</span>{{end}}</p>
|
|
<style>#bk-form label:not(.chk){display:block;margin:0}#bk-form label:not(.chk) input,#bk-form label:not(.chk) select{width:100%;box-sizing:border-box;margin-top:4px}</style>
|
|
<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>Хранилище (SMB)<input name="share" value="{{$b.Share}}" placeholder="//nas/backup"></label>
|
|
<label>Папка<input name="dir" value="{{$b.Dir}}" placeholder="bj-backup"></label>
|
|
</div>
|
|
<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>
|
|
</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>
|
|
<label>Хранить, дней<input name="keep_days" type="number" min="1" max="365" value="{{if $b.KeepDays}}{{$b.KeepDays}}{{else}}14{{end}}"></label>
|
|
</div>
|
|
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
|
<button class="btn" type="submit">Сохранить</button>
|
|
<button class="btn" type="button" id="bk-test" style="background:var(--card);color:var(--text);border:1px solid var(--border)">Проверить доступ</button>
|
|
<button class="btn" type="button" id="bk-run" style="background:var(--card);color:var(--text);border:1px solid var(--border)">Сделать копию сейчас</button>
|
|
<button class="btn" type="button" id="bk-list" style="background:var(--card);color:var(--text);border:1px solid var(--border)">Копии на хранилище</button>
|
|
</div>
|
|
<div id="bk-out" class="muted" style="font-size:13px"></div>
|
|
</form>
|
|
<script>
|
|
(function(){
|
|
var f=document.getElementById('bk-form'), out=document.getElementById('bk-out');
|
|
function esc(s){return String(s).replace(/[&<>"]/g,function(c){return{'&':'&','<':'<','>':'>','"':'"'}[c]})}
|
|
function nice(n){ var m=n.match(/^(\d{4})-(\d{2})-(\d{2})_(\d{2})(\d{2})$/); return m? m[3]+'.'+m[2]+'.'+m[1]+' '+m[4]+':'+m[5] : n; }
|
|
function post(url, withForm){
|
|
var p=new URLSearchParams();
|
|
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='Проверяю доступ…';
|
|
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}
|
|
out.textContent='Копия делается (обычно до минуты)…'; var t0=Date.now();
|
|
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='Читаю список…';
|
|
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>. «Проверить» скачивает копию и сверяет контрольные суммы — ничего не восстанавливает.'+
|
|
'<div style="display:grid;gap:4px;margin-top:6px">'+d.list.slice(0,60).map(function(n){
|
|
return '<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap"><code>'+esc(nice(n))+'</code><button type="button" class="btn bk-verify" data-name="'+esc(n)+'" style="padding:2px 10px;font-size:12px;background:var(--card);color:var(--text);border:1px solid var(--border)">Проверить</button><span class="bk-vres" style="font-size:12px"></span></div>';
|
|
}).join('')+'</div>';
|
|
out.querySelectorAll('.bk-verify').forEach(function(btn){ btn.onclick=function(){
|
|
var res=btn.nextElementSibling; res.textContent='скачиваю и проверяю…'; btn.disabled=true;
|
|
var p=new URLSearchParams(); ['share','dir','user','password'].forEach(function(k){ var v=f[k].value.trim(); if(v) p.set(k,v); }); p.set('name',btn.dataset.name);
|
|
fetch('/admin/setup/backup/verify',{method:'POST',body:p,cache:'no-store'}).then(function(r){return r.json()}).then(function(v){
|
|
btn.disabled=false; res.innerHTML=v.ok?'<span style="color:var(--ok)">✓ '+esc(v.output)+'</span>':'<span style="color:var(--err)">✗ '+esc(v.error)+'</span>'; });
|
|
}; }); }); };
|
|
})();
|
|
</script>
|
|
</div>
|
|
{{end}}
|