Первичная настройка, часть 2: шаг 9 «Оповещения и резервные копии», шаг 10 — итоговая проверка готовности (живые проверки со ссылкой «Исправить»); проверка модуля неторговых поручений QUIK (объекты M2M, инструменты, справочник получателей, цикл, ошибки операций); восстановление из копии на /first-run по одноразовому коду; возврат на тот же шаг мастера после «Сохранить» (return_to с ?step); пароль SMTP не выводится в страницу; карточки почты, адресатов и копий — общие для настроек и мастера
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
@@ -94,6 +94,7 @@
|
||||
<section class="settings-section" id="sec-quik">
|
||||
<h1>QUIK — Модуль неторговых поручений</h1>
|
||||
{{template "quikSection" .}}
|
||||
{{if .Settings.Quik.Server}}{{template "mnpCheck" .}}{{end}}
|
||||
</section>
|
||||
|
||||
{{end}}{{/* /AdvancedUnlocked — конец перспективных модулей Fancy/QUIK */}}
|
||||
@@ -839,34 +840,7 @@
|
||||
<h1>Оповещения</h1>
|
||||
<p class="muted" style="max-width:60ch">Системные события (сбой или зависание фонового компонента, аварийный перезапуск) всегда попадают в ленту «Новости». Дополнительно их можно доставлять наружу — на почту оператору и/или в мониторинг Zabbix, чтобы узнать о проблеме, даже когда интерфейс bj никто не открыл.</p>
|
||||
|
||||
<div class="card">
|
||||
<h2>Почта (SMTP)</h2>
|
||||
<p class="muted">Отправка уведомлений по e-mail. Пароль хранится в конфигурации сервиса.</p>
|
||||
<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>Порт</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)">
|
||||
<option value="starttls" {{if eq .Settings.Alerts.SMTP.Security "starttls"}}selected{{end}}>STARTTLS (587)</option>
|
||||
<option value="ssl" {{if eq .Settings.Alerts.SMTP.Security "ssl"}}selected{{end}}>SSL/TLS (465)</option>
|
||||
<option value="none" {{if eq .Settings.Alerts.SMTP.Security "none"}}selected{{end}}>Без шифрования (25)</option>
|
||||
</select></div>
|
||||
</div>
|
||||
<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>Пароль</label><input type="password" name="password" value="{{.Settings.Alerts.SMTP.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>Получатели (через запятую)</label><input type="text" name="to" value="{{.Settings.Alerts.SMTP.To}}" placeholder="ops@example.ru, dev@example.ru" style="margin-top:3px"></div>
|
||||
</div>
|
||||
<div style="margin-top:16px;display:flex;gap:8px;flex-wrap:wrap;align-items:center">
|
||||
<button type="submit" class="btn">Сохранить</button>
|
||||
<button type="submit" formaction="/admin/setup/alerts-smtp-test" class="btn btn-secondary">Отправить проверочное письмо</button>
|
||||
</div>
|
||||
</form>
|
||||
{{if .Settings.Alerts.SMTP.LastMsg}}<p class="muted" style="font-size:12px;margin-top:10px">Последняя проверка: {{.Settings.Alerts.SMTP.LastMsg}}</p>{{end}}
|
||||
</div>
|
||||
{{template "smtpCard" .}}
|
||||
|
||||
<div class="card">
|
||||
<h2>Zabbix (хук мониторинга)</h2>
|
||||
@@ -887,35 +861,7 @@
|
||||
{{if .Settings.Alerts.Zabbix.LastMsg}}<p class="muted" style="font-size:12px;margin-top:10px">Последняя проверка: {{.Settings.Alerts.Zabbix.LastMsg}}</p>{{end}}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Адресаты уведомлений: депозитарий, бэк-офис, мидл-офис</h2>
|
||||
<p class="muted">Три группы адресов и таблица «какой тип уведомления кому отправлять». Письма идут через SMTP из раздела «Почта». Адрес алертов из раздела «Почта» получает системные оповещения всегда; группы ниже добавляются по маршруту.</p>
|
||||
<form method="post" action="/admin/setup/incoming-notify" 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.Incoming.Enabled}}checked{{end}}> Включить уведомления о переводах</label>
|
||||
<div style="display:grid;gap:10px;max-width:640px">
|
||||
<div><label>Депозитарий (через запятую)</label><input type="text" name="emails" value="{{.Settings.Incoming.Emails}}" placeholder="depo@example.ru" style="margin-top:3px;width:100%"></div>
|
||||
<div><label>Бэк-офис (через запятую)</label><input type="text" name="emails_back" value="{{.Settings.Incoming.EmailsBack}}" placeholder="backoffice@example.ru" style="margin-top:3px;width:100%"></div>
|
||||
<div><label>Мидл-офис (через запятую)</label><input type="text" name="emails_middle" value="{{.Settings.Incoming.EmailsMiddle}}" placeholder="middle@example.ru" style="margin-top:3px;width:100%"></div>
|
||||
</div>
|
||||
<table style="margin-top:14px;max-width:760px">
|
||||
<thead><tr><th>Тип уведомления</th>{{range .NotifyGroups}}<th style="text-align:center">{{.Title}}</th>{{end}}</tr></thead>
|
||||
<tbody>{{$inc := .Settings.Incoming}}{{range $k := .NotifyKinds}}<tr><td>{{$k.Title}}</td>{{range $g := $.NotifyGroups}}<td style="text-align:center"><input type="checkbox" name="route_{{$k.Key}}_{{$g.Key}}" value="1" {{if $inc.RouteHas $k.Key $g.Key}}checked{{end}} style="width:auto"></td>{{end}}</tr>{{end}}</tbody>
|
||||
</table>
|
||||
<div style="margin-top:16px;display:flex;gap:8px;flex-wrap:wrap;align-items:center">
|
||||
<button type="submit" class="btn">Сохранить</button>
|
||||
<button type="submit" formaction="/admin/setup/incoming-notify-test" class="btn btn-secondary">Отправить пробное уведомление</button>
|
||||
</div>
|
||||
</form>
|
||||
<p class="muted" style="font-size:12px;margin-top:8px">Требуется настроенный и включённый SMTP в разделе «Почта».</p>
|
||||
</div>
|
||||
<script>
|
||||
// Смена шифрования SMTP авто-подставляет типовой порт; править вручную можно.
|
||||
function smtpSyncPort(sec){
|
||||
var p=document.getElementById('smtp-port'); if(!p) return;
|
||||
var def={starttls:'587', ssl:'465', none:'25'}[sec];
|
||||
if(def) p.value=def;
|
||||
}
|
||||
</script>
|
||||
{{template "notifyGroupsCard" .}}
|
||||
</section>
|
||||
|
||||
{{/* ============ ОБНОВЛЕНИЯ ============ */}}
|
||||
@@ -992,68 +938,7 @@
|
||||
{{/* ============ РЕЗЕРВНОЕ КОПИРОВАНИЕ ============ */}}
|
||||
<section class="settings-section" id="sec-backup">
|
||||
<h1>Резервное копирование</h1>
|
||||
{{$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 у себя не хранит — он лежит на сервере в файле, доступном только системе. Восстановить копию можно в мастере настройки (шаг 1, «Восстановить из резервной копии»).</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>
|
||||
{{template "backupCard" .}}
|
||||
</section>
|
||||
|
||||
<section class="settings-section" id="sec-license">
|
||||
|
||||
@@ -29,7 +29,7 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); }
|
||||
<p>Пошаговый мастер первого запуска — состояние сохраняется после каждого шага</p>
|
||||
</div>
|
||||
|
||||
<div class="wizard-progress" style="max-width:860px">
|
||||
<div class="wizard-progress" style="max-width:940px">
|
||||
<a href="/admin/wizard?step=1" title="Перейти к шагу 1" class="wstep {{if .Done.License}}done{{end}} {{if eq .Step 1}}current{{end}}"><span class="bub">{{if .Done.License}}✓{{else}}1{{end}}</span><span class="lbl">Лицензия</span></a>
|
||||
<a href="/admin/wizard?step=2" title="Перейти к шагу 2" class="wstep {{if .Done.Postgres}}done{{end}} {{if eq .Step 2}}current{{end}}"><span class="bub">{{if .Done.Postgres}}✓{{else}}2{{end}}</span><span class="lbl">База bj</span></a>
|
||||
<a href="/admin/wizard?step=3" title="Перейти к шагу 3" class="wstep {{if .Done.Depo}}done{{end}} {{if eq .Step 3}}current{{end}}"><span class="bub">{{if .Done.Depo}}✓{{else}}3{{end}}</span><span class="lbl">Учёт DEPO</span></a>
|
||||
@@ -38,7 +38,8 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); }
|
||||
<a href="/admin/wizard?step=6" title="Перейти к шагу 6" class="wstep {{if .Done.Crypto}}done{{end}} {{if eq .Step 6}}current{{end}}"><span class="bub">{{if .Done.Crypto}}✓{{else}}6{{end}}</span><span class="lbl">Валидата</span></a>
|
||||
<a href="/admin/wizard?step=7" title="Перейти к шагу 7" class="wstep {{if .Done.Certs}}done{{end}} {{if eq .Step 7}}current{{end}}"><span class="bub">{{if .Done.Certs}}✓{{else}}7{{end}}</span><span class="lbl">Сертификаты</span></a>
|
||||
<a href="/admin/wizard?step=8" title="Перейти к шагу 8" class="wstep {{if .Done.NSD}}done{{end}} {{if eq .Step 8}}current{{end}}"><span class="bub">{{if .Done.NSD}}✓{{else}}8{{end}}</span><span class="lbl">Шлюз НРД</span></a>
|
||||
<a href="/admin/wizard?step=9" title="Перейти к шагу 9" class="wstep {{if .Done.TestRun}}done{{end}} {{if eq .Step 9}}current{{end}}"><span class="bub">{{if .Done.TestRun}}✓{{else}}9{{end}}</span><span class="lbl">Проверка</span></a>
|
||||
<a href="/admin/wizard?step=9" title="Перейти к шагу 9" class="wstep {{if .Done.Alerts}}done{{end}} {{if eq .Step 9}}current{{end}}"><span class="bub">{{if .Done.Alerts}}✓{{else}}9{{end}}</span><span class="lbl">Оповещения</span></a>
|
||||
<a href="/admin/wizard?step=10" title="Перейти к шагу 10" class="wstep {{if .Done.TestRun}}done{{end}} {{if eq .Step 10}}current{{end}}"><span class="bub">{{if .Done.TestRun}}✓{{else}}10{{end}}</span><span class="lbl">Проверка</span></a>
|
||||
</div>
|
||||
<p style="text-align:center;margin:-14px 0 18px;font-size:13px"><a href="/admin/">Пропустить мастер и перейти в систему →</a> <span class="muted">— вернуться к нему можно из «Настройка → Мастер настройки»</span></p>
|
||||
|
||||
@@ -366,6 +367,7 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); }
|
||||
<strong>Если МНП уже есть.</strong> Адрес сервера и учётные данные — у вашего администратора QUIK.</div>
|
||||
{{if not .DepoLicensed}}<p class="muted">Модуль неторговых поручений не входит в текущую лицензию — шаг пропускается.</p>
|
||||
{{else}}{{with .Fancy}}{{template "quikSection" .}}{{end}}
|
||||
{{if .Settings.Quik.Server}}{{template "mnpCheck" .}}{{end}}
|
||||
{{if .Settings.Quik.M2MMissing}}<p style="color:var(--err);margin:8px 0">✗ Подключение есть, но под этим логином не видно объектов M2M: {{.Settings.Quik.M2MMissing}}. Проверьте имя базы МНП и права логина (роль InstrEu) и нажмите «Проверить» ещё раз.</p>{{end}}
|
||||
<div class="help-block" style="margin-top:12px"><strong>Справочник депозитариев-получателей.</strong> Чтобы QUIK принимал поручения M2M, в МНП должен быть справочник депозитариев-получателей (<code>M2MDepoRecipients</code>): bj заполняет его сам по справочнику участников НРД. <b>Модуль МНП читает справочник только при запуске</b> — после первого заполнения попросите администратора QUIK перезапустить модуль неторговых поручений, иначе QUIK ответит «Неизвестный депозитарий получателя».<br><span class="muted">{{quikRecipientsInfo}}</span></div>{{end}}
|
||||
<div style="margin-top:20px;display:flex;justify-content:space-between">
|
||||
@@ -499,11 +501,34 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); }
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{/* ============= ШАГ 5: Тест-ран (режим разработки) / проверка подключений ============= */}}
|
||||
{{/* ============= ШАГ 9: Оповещения и резервные копии ============= */}}
|
||||
{{if eq .Step 9}}
|
||||
<div class="card">
|
||||
<h2><span class="dot {{if .Done.Alerts}}ok{{else}}warn{{end}}"></span>Шаг 9. Оповещения и резервные копии</h2>
|
||||
<div class="help-block"><strong>Зачем.</strong> bj сам следит за обменом: НРД не отвечает, перевод не доставлен, ошибка обработки в DEPO, истекает сертификат ключа. Чтобы об этом узнали сразу, а не открыв bj, — укажите почту. Резервные копии позволят восстановить сервер целиком (настройки, заявки, шлюз НРД) после сбоя или переустановки.<br>
|
||||
<strong>Можно позже.</strong> Шаг необязательный — обмен работает и без него, но мы настоятельно советуем настроить оба пункта.</div>
|
||||
<style>.wiz9 label:not(:has(input[type=checkbox])){display:block;margin:0}.wiz9 label input:not([type=checkbox]),.wiz9 label select,.wiz9 .grid input,.wiz9 .grid select{width:100%;box-sizing:border-box;margin-top:4px}</style>
|
||||
<div class="wiz9">
|
||||
{{with .Fancy}}
|
||||
<h3 style="margin:16px 0 6px">Почта для оповещений</h3>
|
||||
{{template "smtpCard" .}}
|
||||
{{template "notifyGroupsCard" .}}
|
||||
<h3 style="margin:16px 0 6px">Резервные копии</h3>
|
||||
{{template "backupCard" .}}
|
||||
{{end}}
|
||||
</div>
|
||||
<div style="margin-top:20px;display:flex;justify-content:space-between">
|
||||
<a href="/admin/wizard?step=8" class="btn" style="background:var(--card);color:var(--text);border:1px solid var(--border-strong,#c9cdd8);text-decoration:none">← К шагу 8</a>
|
||||
<a href="/admin/wizard?step=10" class="btn" style="{{if not .Done.Alerts}}background:var(--card);color:var(--text);border:1px solid var(--border-strong,#c9cdd8);{{end}}text-decoration:none">{{if .Done.Alerts}}К проверке →{{else}}Пропустить →{{end}}</a>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{/* ============= ШАГ 10: Проверка готовности (в режиме разработки — ещё тестовая заявка) ============= */}}
|
||||
{{if eq .Step 10}}
|
||||
<div class="card">
|
||||
{{if devOn}}
|
||||
<h2><span class="dot {{if .Done.TestRun}}ok{{else}}err{{end}}"></span>Шаг 9. Тестовая заявка</h2>
|
||||
<h2><span class="dot {{if .Done.TestRun}}ok{{else}}err{{end}}"></span>Шаг 10. Тестовая заявка</h2>
|
||||
<p>Прогон полного цикла: создание заявки → валидация → подпись → отправка в НРД (или mock) → ожидание Decision → подтверждение.</p>
|
||||
|
||||
<div class="help-block">
|
||||
@@ -523,23 +548,33 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); }
|
||||
{{if .Settings.LastTest.Message}}<tr><td class="muted">Сообщение</td><td>{{.Settings.LastTest.Message}}</td></tr>{{end}}
|
||||
</table>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<h2><span class="dot {{if .Done.TestRun}}ok{{else}}err{{end}}"></span>Шаг 9. Проверка подключений</h2>
|
||||
<p>Проверьте, что база данных, СКЗИ и шлюз НРД доступны: на странице «Статус» bj проверяет каждое подключение и показывает, что нужно исправить.</p>
|
||||
<a href="/admin/status" class="btn" style="background:var(--ok);font-size:15px;padding:10px 20px;text-decoration:none;display:inline-block;margin-top:12px">Открыть проверку подключений →</a>
|
||||
{{end}}
|
||||
{{if not devOn}}<h2><span class="dot" id="rd-dot"></span>Шаг 10. Готовность к работе</h2>{{else}}<h3 style="margin-top:18px">Готовность к работе</h3>{{end}}
|
||||
<p class="muted" style="margin:4px 0 10px">bj проверяет всё, что нужно для обмена с НРД, прямо сейчас. Красное — обмен работать не будет, жёлтое — работать будет, но лучше исправить. У каждой строки — ссылка туда, где это исправляется.</p>
|
||||
<div id="rd-sum" style="font-weight:600;margin:6px 0">Проверяю…</div>
|
||||
<table id="rd-tab" style="width:100%"><tbody></tbody></table>
|
||||
<p style="margin-top:8px"><button type="button" class="btn" id="rd-again" style="background:var(--card);color:var(--text);border:1px solid var(--border-strong,#c9cdd8)">Проверить ещё раз</button> <span class="muted" id="rd-at" style="font-size:12px"></span></p>
|
||||
<script>
|
||||
(function(){
|
||||
var tb=document.querySelector('#rd-tab tbody'), sum=document.getElementById('rd-sum'), dot=document.getElementById('rd-dot');
|
||||
function esc(s){return String(s).replace(/[&<>"]/g,function(c){return{'&':'&','<':'<','>':'>','"':'"'}[c]})}
|
||||
var ic={ok:'<span style="color:var(--ok)">✓</span>',warn:'<span style="color:var(--warn)">⚠</span>',err:'<span style="color:var(--err)">✗</span>',skip:'<span class="muted">—</span>'};
|
||||
function load(){ sum.textContent='Проверяю…'; tb.innerHTML='';
|
||||
fetch('/admin/setup/readiness',{cache:'no-store'}).then(function(r){return r.json()}).then(function(d){
|
||||
tb.innerHTML=d.checks.map(function(c){return '<tr><td style="width:24px;vertical-align:top">'+ic[c.state]+'</td><td style="vertical-align:top;white-space:nowrap;padding-right:10px"><b>'+esc(c.title)+'</b></td><td style="vertical-align:top">'+esc(c.detail)+(c.state!=='ok'&&c.link?' <a href="'+esc(c.link)+'">Исправить →</a>':'')+'</td></tr>'}).join('');
|
||||
var red=d.checks.filter(function(c){return c.state==='err'}).length, yel=d.checks.filter(function(c){return c.state==='warn'}).length;
|
||||
sum.innerHTML=d.ready?(yel?'<span style="color:var(--ok)">✓ Обмен с НРД готов к работе</span> <span class="muted" style="font-weight:400">— желательно исправить: '+yel+'</span>':'<span style="color:var(--ok)">✓ Всё готово к работе</span>'):'<span style="color:var(--err)">✗ Не готово: обязательных проблем — '+red+'</span>';
|
||||
if(dot) dot.className='dot '+(d.ready?(yel?'warn':'ok'):'err');
|
||||
document.getElementById('rd-at').textContent='проверено '+d.at;
|
||||
}).catch(function(){ sum.textContent='Проверка не выполнилась — обновите страницу.'; }); }
|
||||
document.getElementById('rd-again').onclick=load; load();
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
||||
<h3 style="margin-top:18px">Итоговая сводка</h3>
|
||||
<table>
|
||||
<tr><td class="muted">PostgreSQL</td><td>{{if .Done.Postgres}}<span style="color:var(--ok)">настроен</span>{{else}}<span class="muted">in-memory</span>{{end}}</td></tr>
|
||||
<tr><td class="muted">Крипто-провайдер</td><td>{{if .Done.Crypto}}<span style="color:var(--ok)">Валидата Клиент L{{if .ValidataVer}} {{.ValidataVer}}{{end}}</span>{{else}}<span style="color:var(--err)">не настроен</span>{{end}}</td></tr>
|
||||
<tr><td class="muted">Сертификатов установлено</td><td>{{len .Certs}}</td></tr>
|
||||
<tr><td class="muted">ИШ НРД</td><td>{{if .Done.NSD}}<span style="color:var(--ok)">{{.Settings.NSD.Profile}}</span>{{else}}<span class="muted">{{if devOn}}mock-режим{{else}}не настроен{{end}}</span>{{end}}</td></tr>
|
||||
{{if devOn}}<tr><td class="muted">Тестовый прогон</td><td>{{if .Done.TestRun}}<span style="color:var(--ok)">пройден</span>{{else}}<span class="muted">не запускался</span>{{end}}</td></tr>{{end}}
|
||||
</table>
|
||||
|
||||
<div style="margin-top:20px;display:flex;justify-content:space-between">
|
||||
<a href="/admin/wizard?step=8" class="btn" style="background:var(--card);color:var(--text);border:1px solid var(--border-strong,#c9cdd8);text-decoration:none">← К шагу 8</a>
|
||||
<a href="/admin/wizard?step=9" class="btn" style="background:var(--card);color:var(--text);border:1px solid var(--border-strong,#c9cdd8);text-decoration:none">← К шагу 9</a>
|
||||
<a href="/admin/" class="btn" style="text-decoration:none">Перейти к дашборду</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -469,7 +469,7 @@ details:has(> pre.copyable) { min-width:0; max-width:100%; }
|
||||
// получает Referer, поэтому говорим путь явно.
|
||||
if (!f.querySelector('input[name="return_to"]')) {
|
||||
var rt = document.createElement('input');
|
||||
rt.type = 'hidden'; rt.name = 'return_to'; rt.value = location.pathname;
|
||||
rt.type = 'hidden'; rt.name = 'return_to'; rt.value = location.pathname + location.search.replace(/[?&](flash|scrollto)=[^&]*/g, '').replace(/^&/, '?');
|
||||
f.appendChild(rt);
|
||||
}
|
||||
// Подкладываем секцию (ближайшую карточку) — сервер вернёт ?scrollto=.
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
{{/* Оповещения: почта и адресаты — общие для «Настройка → Оповещения» и мастера (данные — SetupData). */}}
|
||||
{{define "smtpCard"}}
|
||||
<div class="card" id="sec-smtp">
|
||||
<h2>Почта (SMTP)</h2>
|
||||
<p class="muted">Отправка уведомлений по e-mail. Пароль хранится в конфигурации сервиса.</p>
|
||||
<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>Порт</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)">
|
||||
<option value="starttls" {{if eq .Settings.Alerts.SMTP.Security "starttls"}}selected{{end}}>STARTTLS (587)</option>
|
||||
<option value="ssl" {{if eq .Settings.Alerts.SMTP.Security "ssl"}}selected{{end}}>SSL/TLS (465)</option>
|
||||
<option value="none" {{if eq .Settings.Alerts.SMTP.Security "none"}}selected{{end}}>Без шифрования (25)</option>
|
||||
</select></div>
|
||||
</div>
|
||||
<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>Получатели (через запятую)</label><input type="text" name="to" value="{{.Settings.Alerts.SMTP.To}}" placeholder="ops@example.ru, dev@example.ru" style="margin-top:3px"></div>
|
||||
</div>
|
||||
<div style="margin-top:16px;display:flex;gap:8px;flex-wrap:wrap;align-items:center">
|
||||
<button type="submit" class="btn">Сохранить</button>
|
||||
<button type="submit" formaction="/admin/setup/alerts-smtp-test" class="btn btn-secondary">Отправить проверочное письмо</button>
|
||||
</div>
|
||||
</form>
|
||||
{{if .Settings.Alerts.SMTP.LastMsg}}<p class="muted" style="font-size:12px;margin-top:10px">Последняя проверка: {{.Settings.Alerts.SMTP.LastMsg}}</p>{{end}}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Смена шифрования SMTP авто-подставляет типовой порт; править вручную можно.
|
||||
function smtpSyncPort(sec){
|
||||
var p=document.getElementById('smtp-port'); if(!p) return;
|
||||
var def={starttls:'587', ssl:'465', none:'25'}[sec];
|
||||
if(def) p.value=def;
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
{{define "notifyGroupsCard"}}
|
||||
<div class="card" id="sec-notify-groups">
|
||||
<h2>Адресаты уведомлений: депозитарий, бэк-офис, мидл-офис</h2>
|
||||
<p class="muted">Три группы адресов и таблица «какой тип уведомления кому отправлять». Письма идут через SMTP из раздела «Почта». Адрес алертов из раздела «Почта» получает системные оповещения всегда; группы ниже добавляются по маршруту.</p>
|
||||
<form method="post" action="/admin/setup/incoming-notify" 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.Incoming.Enabled}}checked{{end}}> Включить уведомления о переводах</label>
|
||||
<div style="display:grid;gap:10px;max-width:640px">
|
||||
<div><label>Депозитарий (через запятую)</label><input type="text" name="emails" value="{{.Settings.Incoming.Emails}}" placeholder="depo@example.ru" style="margin-top:3px;width:100%"></div>
|
||||
<div><label>Бэк-офис (через запятую)</label><input type="text" name="emails_back" value="{{.Settings.Incoming.EmailsBack}}" placeholder="backoffice@example.ru" style="margin-top:3px;width:100%"></div>
|
||||
<div><label>Мидл-офис (через запятую)</label><input type="text" name="emails_middle" value="{{.Settings.Incoming.EmailsMiddle}}" placeholder="middle@example.ru" style="margin-top:3px;width:100%"></div>
|
||||
</div>
|
||||
<table style="margin-top:14px;max-width:760px">
|
||||
<thead><tr><th>Тип уведомления</th>{{range .NotifyGroups}}<th style="text-align:center">{{.Title}}</th>{{end}}</tr></thead>
|
||||
<tbody>{{$inc := .Settings.Incoming}}{{range $k := .NotifyKinds}}<tr><td>{{$k.Title}}</td>{{range $g := $.NotifyGroups}}<td style="text-align:center"><input type="checkbox" name="route_{{$k.Key}}_{{$g.Key}}" value="1" {{if $inc.RouteHas $k.Key $g.Key}}checked{{end}} style="width:auto"></td>{{end}}</tr>{{end}}</tbody>
|
||||
</table>
|
||||
<div style="margin-top:16px;display:flex;gap:8px;flex-wrap:wrap;align-items:center">
|
||||
<button type="submit" class="btn">Сохранить</button>
|
||||
<button type="submit" formaction="/admin/setup/incoming-notify-test" class="btn btn-secondary">Отправить пробное уведомление</button>
|
||||
</div>
|
||||
</form>
|
||||
<p class="muted" style="font-size:12px;margin-top:8px">Требуется настроенный и включённый SMTP в разделе «Почта».</p>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,65 @@
|
||||
{{/* Резервное копирование — общее для «Настройка → Резервное копирование» и мастера (данные — 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 у себя не хранит — он лежит на сервере в файле, доступном только системе. Восстановить копию можно в мастере настройки (шаг 1, «Восстановить из резервной копии»).</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}}
|
||||
@@ -80,3 +80,26 @@
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{define "mnpCheck"}}
|
||||
<div class="card" id="mnp-check">
|
||||
<h2><span class="dot" id="mnp-dot"></span>Проверка модуля неторговых поручений</h2>
|
||||
<p class="muted" style="margin:0 0 8px">Только чтение: всё, без чего цикл M2M в МНП не работает. Ничего в QUIK не меняет.</p>
|
||||
<table style="width:100%"><tbody id="mnp-rows"></tbody></table>
|
||||
<p style="margin-top:8px"><button type="button" class="btn" id="mnp-run">Проверить МНП</button> <span class="muted" id="mnp-at" style="font-size:12px"></span></p>
|
||||
</div>
|
||||
<script>
|
||||
(function(){
|
||||
var rows=document.getElementById('mnp-rows'), dot=document.getElementById('mnp-dot');
|
||||
function esc(s){return String(s).replace(/[&<>"]/g,function(c){return{'&':'&','<':'<','>':'>','"':'"'}[c]})}
|
||||
var ic={ok:'<span style="color:var(--ok)">✓</span>',warn:'<span style="color:var(--warn)">⚠</span>',err:'<span style="color:var(--err)">✗</span>'};
|
||||
function run(){ rows.innerHTML='<tr><td class="muted">Проверяю…</td></tr>';
|
||||
fetch('/admin/setup/quik/mnp-check',{cache:'no-store'}).then(function(r){return r.json()}).then(function(d){
|
||||
rows.innerHTML=d.checks.map(function(c){return '<tr><td style="width:22px;vertical-align:top">'+ic[c.state]+'</td><td style="vertical-align:top;padding-right:10px;white-space:nowrap"><b>'+esc(c.title)+'</b></td><td style="vertical-align:top">'+esc(c.detail)+'</td></tr>'}).join('');
|
||||
dot.className='dot '+d.state; document.getElementById('mnp-at').textContent='проверено '+d.at;
|
||||
}).catch(function(){ rows.innerHTML='<tr><td>Проверка не выполнилась</td></tr>'; }); }
|
||||
document.getElementById('mnp-run').onclick=run;
|
||||
{{if .Settings.Quik.Server}}run();{{end}}
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
Reference in New Issue
Block a user