Автоматический режим M2M по умолчанию; контуры НРД карточками; адрес папок шлюза для проводника; Zabbix — в «Диагностике»
- M2M: «bj сам — автоматически» — режим по умолчанию (где ни разу не выбирали; момент включения записывается, берутся переводы не старше суток до него); «Оператор» — запасной; явный выбор не меняется. - Контуры НРД: TEST3 и боевой — карточки с состоянием (Активный / Готов / Нужен ключ / Канала нет); «Сделать активным» видна всегда, без ключа неактивна с пояснением; «Установить ключ», «Обновить канал». - Папки шлюза по сети: после открытия — адреса для проводника (\\IP\igate_exchange и по имени сервера, боевой — igate_exchange_prod) с кнопкой «Скопировать»; bj-igate-admin share status отдаёт ip, host, shares. - Zabbix перенесён из «Оповещений» в «Диагностику». Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
@@ -32,6 +32,17 @@ type M2MAutoSettings struct {
|
||||
ChangedBy string `json:"changed_by,omitempty"`
|
||||
}
|
||||
|
||||
// migrateM2MAutoDefault — автоматический режим включён по умолчанию (решение 07.10.2026: «Оператор» — запасной
|
||||
// режим). Установка, где режим ни разу не выбирали, получает «автоматически» с моментом включения сейчас: берутся
|
||||
// только переводы, пришедшие не раньше чем за сутки до этого. Явный выбор (есть ChangedAt) не трогается.
|
||||
func migrateM2MAutoDefault(s *Settings, now time.Time) bool {
|
||||
if s.M2MAuto.Enabled || !s.M2MAuto.ChangedAt.IsZero() {
|
||||
return false
|
||||
}
|
||||
s.M2MAuto = M2MAutoSettings{Enabled: true, ChangedAt: now, ChangedBy: "по умолчанию"}
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *RuntimeConfig) UpdateM2MAuto(s M2MAutoSettings) error {
|
||||
r.mu.Lock()
|
||||
r.data.M2MAuto = s
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package lkgateway
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Автоматический режим M2M — по умолчанию: где режим не выбирали, он включается и запоминается;
|
||||
// явный выбор «Оператор» остаётся.
|
||||
func TestM2MAutoDefaultOn(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
fresh := filepath.Join(dir, "fresh.json")
|
||||
_ = os.WriteFile(fresh, []byte(`{"nsd":{"profile":"test3-gost"}}`), 0o600)
|
||||
rc, err := NewRuntimeConfig(fresh)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a := rc.Snapshot().M2MAuto
|
||||
if !a.Enabled || a.ChangedAt.IsZero() || a.ChangedBy != "по умолчанию" {
|
||||
t.Fatalf("не включён по умолчанию: %+v", a)
|
||||
}
|
||||
rc2, _ := NewRuntimeConfig(fresh) // записано: при следующем запуске момент включения тот же
|
||||
if !rc2.Snapshot().M2MAuto.ChangedAt.Equal(a.ChangedAt) {
|
||||
t.Fatal("момент включения не сохранился")
|
||||
}
|
||||
manual := filepath.Join(dir, "manual.json")
|
||||
_ = os.WriteFile(manual, []byte(`{"m2m_auto":{"enabled":false,"changed_at":"2026-10-01T10:00:00Z","changed_by":"admin"}}`), 0o600)
|
||||
rc3, _ := NewRuntimeConfig(manual)
|
||||
if rc3.Snapshot().M2MAuto.Enabled {
|
||||
t.Fatal("явный выбор «Оператор» перезаписан")
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package lkgateway
|
||||
|
||||
import (
|
||||
"log"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
@@ -364,10 +365,16 @@ func NewRuntimeConfig(path string) (*RuntimeConfig, error) {
|
||||
path = filepath.Join(home, ".bj", "setup.json")
|
||||
}
|
||||
rc := &RuntimeConfig{path: path}
|
||||
if err := rc.load(); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return nil, err
|
||||
loadErr := rc.load()
|
||||
if loadErr != nil && !errors.Is(loadErr, os.ErrNotExist) {
|
||||
return nil, loadErr
|
||||
}
|
||||
migrateNotifyRoutes(&rc.data) // и на новой установке: версия маршрутов сохранится с первой записью настроек
|
||||
if migrateM2MAutoDefault(&rc.data, time.Now()) && loadErr == nil {
|
||||
if err := rc.save(); err != nil { // момент включения — отсчёт «за сутки до» для автоматики; должен пережить перезапуск
|
||||
log.Printf("setup: автоматический режим M2M по умолчанию не записан: %v", err)
|
||||
}
|
||||
}
|
||||
// Гарантируем разумные дефолты.
|
||||
if rc.data.Crypto.Provider == "" {
|
||||
rc.data.Crypto.Provider = "stub"
|
||||
|
||||
@@ -819,6 +819,25 @@
|
||||
</tr>{{end}}</tbody></table></div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="sec-zabbix">
|
||||
<h2>Мониторинг Zabbix</h2>
|
||||
<p class="muted">bj шлёт значение на элемент типа <strong>Zabbix trapper</strong> (протокол <code>zabbix_sender</code>). В Zabbix заведите host и item-trapper с этим ключом, затем триггер по значению — уведомления настроит сам Zabbix.</p>
|
||||
<form method="post" action="/admin/setup/alerts-zabbix" 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.Zabbix.Enabled}}checked{{end}}> Включить хук Zabbix</label>
|
||||
<div class="grid" style="grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px">
|
||||
<div><label>Сервер / прокси Zabbix</label><input type="text" name="server" value="{{.Settings.Alerts.Zabbix.Server}}" placeholder="zabbix.example.ru" style="margin-top:3px"></div>
|
||||
<div><label>Порт</label><input type="text" name="port" value="{{if .Settings.Alerts.Zabbix.Port}}{{.Settings.Alerts.Zabbix.Port}}{{else}}10051{{end}}" style="margin-top:3px"></div>
|
||||
<div><label>Имя хоста в Zabbix</label><input type="text" name="host" value="{{.Settings.Alerts.Zabbix.Host}}" placeholder="bj-server" style="margin-top:3px"></div>
|
||||
<div><label>Ключ элемента (trapper)</label><input type="text" name="key" value="{{if .Settings.Alerts.Zabbix.Key}}{{.Settings.Alerts.Zabbix.Key}}{{else}}bj.alert{{end}}" 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-zabbix-test" class="btn btn-secondary">Отправить проверочное значение</button>
|
||||
</div>
|
||||
</form>
|
||||
{{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">Фоновые компоненты под самоконтролем: при сбое перезапускаются автоматически, при зависании — перезапуск всего сервиса и запись в «Новости». Ниже — живой статус.</p>
|
||||
@@ -888,28 +907,10 @@
|
||||
{{/* ============ ОПОВЕЩЕНИЯ ============ */}}
|
||||
<section class="settings-section" id="sec-alerts">
|
||||
<h1>Оповещения</h1>
|
||||
<p class="muted" style="max-width:60ch">Системные события (сбой или зависание фонового компонента, аварийный перезапуск) всегда попадают в ленту «Новости». Дополнительно их можно доставлять наружу — на почту оператору и/или в мониторинг Zabbix, чтобы узнать о проблеме, даже когда интерфейс bj никто не открыл.</p>
|
||||
<p class="muted">Кому и о чём bj пишет на почту: администратору системы — о сбоях и обновлениях, отделам — о переводах и поручениях. Все события видны и в ленте «Новости»; передать их в мониторинг Zabbix — Настройка → Диагностика.</p>
|
||||
|
||||
{{template "smtpCard" .}}
|
||||
|
||||
<div class="card">
|
||||
<h2>Zabbix (хук мониторинга)</h2>
|
||||
<p class="muted">bj шлёт значение на элемент типа <strong>Zabbix trapper</strong> (протокол <code>zabbix_sender</code>). В Zabbix заведите host и item-trapper с этим ключом, затем триггер по значению — уведомления настроит сам Zabbix.</p>
|
||||
<form method="post" action="/admin/setup/alerts-zabbix" 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.Zabbix.Enabled}}checked{{end}}> Включить хук Zabbix</label>
|
||||
<div class="grid" style="grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px">
|
||||
<div><label>Сервер / прокси Zabbix</label><input type="text" name="server" value="{{.Settings.Alerts.Zabbix.Server}}" placeholder="zabbix.example.ru" style="margin-top:3px"></div>
|
||||
<div><label>Порт</label><input type="text" name="port" value="{{if .Settings.Alerts.Zabbix.Port}}{{.Settings.Alerts.Zabbix.Port}}{{else}}10051{{end}}" style="margin-top:3px"></div>
|
||||
<div><label>Имя хоста в Zabbix</label><input type="text" name="host" value="{{.Settings.Alerts.Zabbix.Host}}" placeholder="bj-server" style="margin-top:3px"></div>
|
||||
<div><label>Ключ элемента (trapper)</label><input type="text" name="key" value="{{if .Settings.Alerts.Zabbix.Key}}{{.Settings.Alerts.Zabbix.Key}}{{else}}bj.alert{{end}}" 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-zabbix-test" class="btn btn-secondary">Отправить проверочное значение</button>
|
||||
</div>
|
||||
</form>
|
||||
{{if .Settings.Alerts.Zabbix.LastMsg}}<p class="muted" style="font-size:12px;margin-top:10px">Последняя проверка: {{.Settings.Alerts.Zabbix.LastMsg}}</p>{{end}}
|
||||
</div>
|
||||
|
||||
{{template "notifyGroupsCard" .}}
|
||||
</section>
|
||||
|
||||
@@ -382,8 +382,8 @@ label { font-size:13px; font-weight:500; }
|
||||
.stc{display:flex;gap:14px;align-items:center;padding:14px 18px;border-radius:12px;border:1px solid var(--border);background:var(--card-2)}
|
||||
.stc-ic{flex:none;position:relative;width:28px;height:28px;border-radius:50%;background:var(--border-strong)}
|
||||
.stc-b{flex:1;min-width:0}
|
||||
.stc-b b{display:block;font-size:15px;font-weight:650}
|
||||
.stc-b small{display:block;color:var(--muted);font-size:13px;line-height:1.4;margin-top:2px;overflow-wrap:anywhere}
|
||||
.stc-b > b{display:block;font-size:15px;font-weight:650}
|
||||
.stc-b > small{display:block;color:var(--muted);font-size:13px;line-height:1.4;margin-top:2px;overflow-wrap:anywhere}
|
||||
.stc form, .stc .btn{flex:none}
|
||||
.stc-ok{background:color-mix(in srgb,var(--ok) 9%,var(--card));border-color:color-mix(in srgb,var(--ok) 40%,var(--border))}
|
||||
.stc-ok .stc-ic{background:var(--ok)}
|
||||
|
||||
@@ -4,8 +4,28 @@
|
||||
#ig-box h3{margin:0 0 6px;font-size:15px}
|
||||
#ig-box label.f{display:block;margin:0;font-size:13px}
|
||||
#ig-box label.f input,#ig-box label.f select{display:block;margin-top:4px;padding:7px 9px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px}
|
||||
#ig-ct{width:100%;margin-top:8px}
|
||||
#ig-ct td{vertical-align:top;padding:6px 8px 6px 0}
|
||||
.ct-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin-top:10px}
|
||||
.ct{border:1px solid var(--border);border-radius:12px;padding:14px 16px;background:var(--card);display:flex;flex-direction:column;gap:8px}
|
||||
.ct-on{border-color:color-mix(in srgb,var(--accent) 55%,var(--border));background:color-mix(in srgb,var(--accent-weak) 45%,var(--card))}
|
||||
.ct-h{display:flex;justify-content:space-between;align-items:flex-start;gap:10px}
|
||||
.ct-h b{display:block;font-size:15px}
|
||||
.ct-h small{display:block;color:var(--muted);font-size:12.5px;margin-top:1px}
|
||||
.ct-pill{flex:none;font-size:12px;font-weight:600;padding:3px 10px;border-radius:999px;background:var(--card-2);border:1px solid var(--border);color:var(--text-2);white-space:nowrap}
|
||||
.ct-act .ct-pill, .ct-on .ct-pill{background:var(--accent);border-color:var(--accent);color:#fff}
|
||||
.ct-ok .ct-pill{background:var(--ok-weak);border-color:color-mix(in srgb,var(--ok) 35%,transparent);color:var(--ok)}
|
||||
.ct-no .ct-pill{background:var(--warn-weak);border-color:color-mix(in srgb,var(--warn) 35%,transparent);color:var(--warn)}
|
||||
.ct-li{list-style:none;margin:0;padding:0;display:grid;gap:3px;font-size:13px}
|
||||
.ct-li li{position:relative;padding-left:20px;overflow-wrap:anywhere}
|
||||
.ct-li li::before{position:absolute;left:0;top:0;font-weight:700}
|
||||
.ct-li li.ok::before{content:"✓";color:var(--ok)} .ct-li li.no::before{content:"✗";color:var(--err)} .ct-li li.na::before{content:"—";color:var(--muted)}
|
||||
.ct-li .muted{font-size:12px}
|
||||
.ct .ct-why{margin:0;font-size:12.5px;color:var(--muted)}
|
||||
.ct-act{display:flex;gap:6px;flex-wrap:wrap;margin-top:auto;padding-top:4px}
|
||||
.ct-act .btn[disabled]{opacity:.5;cursor:not-allowed}
|
||||
@media (max-width:760px){.ct-grid{grid-template-columns:minmax(0,1fr)}}
|
||||
.ig-addr{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:6px}
|
||||
.ig-addr code{font-size:14px;padding:4px 10px;border-radius:8px;background:var(--card);border:1px solid var(--border);user-select:all}
|
||||
.ig-addr .muted{font-size:12.5px}
|
||||
#ig-box .btn.sm{padding:4px 10px;font-size:12.5px}
|
||||
#ig-box .btn.ghost{background:var(--card);color:var(--text);border:1px solid var(--border-strong,#c9cdd8)}
|
||||
/* ход установки ИШ: этапы по порядку, у текущего — сколько идёт и сколько обычно */
|
||||
@@ -48,7 +68,7 @@
|
||||
<h3>Контуры НРД</h3>
|
||||
<p class="muted" style="font-size:13px;margin:0 0 8px">Сначала — тестовый контур TEST3; когда переводы на нём проходят, переходите на боевой. У каждого контура свой ключ.</p>
|
||||
<label class="f" style="max-width:220px">Криптография<select id="ig-crypto"><option value="gost">ГОСТ</option><option value="rsa">RSA</option></select></label>
|
||||
<table id="ig-ct"><tbody></tbody></table>
|
||||
<div id="ig-ct" class="ct-grid"></div>
|
||||
<div id="ig-prod" style="display:none;border:1px solid var(--warn);border-radius:8px;padding:10px 12px;margin-top:10px">
|
||||
<b>Переход на боевой контур</b>
|
||||
<p class="muted" style="font-size:13px;margin:6px 0">После перехода bj отправляет в НРД настоящие переводы. На боевом контуре обычно работает настоящий учёт DEPO — укажите его базу (на TEST3 часто использовали копию). Вернуться на TEST3 можно здесь же.</p>
|
||||
@@ -91,7 +111,7 @@
|
||||
</div>
|
||||
<script>
|
||||
(function(){
|
||||
var head=document.getElementById('ig-head'), tb=document.querySelector('#ig-ct tbody'), cout=document.getElementById('ig-cout'), out=document.getElementById('ig-out'), lg=document.getElementById('ig-log');
|
||||
var head=document.getElementById('ig-head'), tb=document.getElementById('ig-ct'), cout=document.getElementById('ig-cout'), out=document.getElementById('ig-out'), lg=document.getElementById('ig-log');
|
||||
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()})}
|
||||
@@ -146,7 +166,14 @@
|
||||
function show(d){
|
||||
if(!d.ok){ stl.innerHTML=bad(d.error||'состояние не получено'); return; }
|
||||
if(d.manual){ stl.innerHTML=ok('папки открыты вручную (smb.conf) для '+(d.user||'—')+' — bj их не меняет'); f.hidden=true; return; }
|
||||
stl.innerHTML=d.on?ok('открыты для '+esc(d.user)+': \\\\'+location.hostname+'\\igate_exchange'):'<span class="muted">закрыты</span>';
|
||||
if(d.on){
|
||||
var hosts=[d.ip||location.hostname]; if(d.host && d.host!==hosts[0]) hosts.push(d.host);
|
||||
var names=(d.shares&&d.shares.length)?d.shares:['igate_exchange'];
|
||||
var rows=[]; names.forEach(function(n){ hosts.forEach(function(h){ var p='\\\\'+h+'\\'+n;
|
||||
rows.push('<div class="ig-addr"><code>'+esc(p)+'</code><button type="button" class="btn sm ghost" data-copy="'+esc(p)+'">Скопировать</button><span class="muted">'+(n.indexOf('prod')>=0?'боевой контур':'тестовый контур TEST3')+(h===d.host?' · по имени сервера':'')+'</span></div>'); }); });
|
||||
stl.innerHTML='<div class="stc stc-ok" style="margin-top:4px"><span class="stc-ic"></span><div class="stc-b"><b>Папки открыты по сети</b><small>Вставьте адрес в адресную строку проводника Windows; вход — логин <b>'+esc(d.user||'—')+'</b> и пароль, который задали здесь.</small>'+rows.join('')+'</div></div>';
|
||||
stl.querySelectorAll('[data-copy]').forEach(function(b){ b.onclick=function(){ (window.bjCopy||function(t){navigator.clipboard&&navigator.clipboard.writeText(t)})(b.dataset.copy); b.textContent='Скопировано ✓'; setTimeout(function(){b.textContent='Скопировать'},1500); }; });
|
||||
} else stl.innerHTML='<span class="muted">Сейчас закрыты. Отметьте «Открыть папки шлюза по сети», задайте пароль и нажмите «Сохранить» — здесь появится адрес для проводника.</span>';
|
||||
f.enabled.checked=!!d.on; if(d.user) f.user.value=d.user;
|
||||
}
|
||||
fetch('/admin/setup/igate/share',{cache:'no-store'}).then(function(r){return r.json()}).then(show).catch(function(){ stl.textContent='Состояние не получено.'; });
|
||||
@@ -173,20 +200,32 @@
|
||||
else { bb.style.display='none'; bv.textContent='Дистрибутива ИШ на сервере нет — он приходит с обновлением bj («Настройка → Обновления»).'; }
|
||||
}
|
||||
var dep=d.deponent||'', cr=document.getElementById('ig-crypto').value;
|
||||
var rows=[['test3','Тестовый TEST3'],['prod','Боевой']].map(function(x){
|
||||
var rows=[['test3','Тестовый TEST3','проверка переводов до боевой работы'],['prod','Боевой','настоящие переводы']].map(function(x){
|
||||
var code=(x[0]==='prod'?'PROD':'TEST3')+dep, ch=(d.channels||[]).filter(function(c){return c.code===code})[0], act=d.active_contour===x[0];
|
||||
var s=[], a=[];
|
||||
if(!dep) s.push(bad('нет депозитарного кода (мастер → «Организация»)'));
|
||||
else if(!ch){ if(d.installed){ s.push('<span class="muted">канала в ИШ нет — bj создаст его: адрес и сертификат контура НРД известны</span>'); a.push('<button type="button" class="btn sm" data-act="contour" data-c="'+x[0]+'">Создать канал</button>'); } else s.push('<span class="muted">канал создаётся после установки ИШ</span>'); }
|
||||
else {
|
||||
s.push(ok('канал '+ch.code)+' <span class="muted" style="font-size:12px">'+esc(ch.crypto+' · '+ch.endpoint)+'</span>');
|
||||
s.push(ch.profile?ok('ключ: профиль '+ch.profile):bad('ключ этого контура не установлен'));
|
||||
s.push(ch.enabled?ok('канал включён'):'<span class="muted">канал выключен до установки ключа</span>');
|
||||
a.push('<button type="button" class="btn sm ghost" data-act="contour" data-c="'+x[0]+'">Обновить канал</button>');
|
||||
var li=[], a=[], pill, cls;
|
||||
function it(okk,t){ li.push('<li class="'+(okk===true?'ok':okk===false?'no':'na')+'">'+t+'</li>'); }
|
||||
if(!dep){ it(false,'нет депозитарного кода — раздел «Организация»'); pill='Нет кода'; cls='no'; }
|
||||
else if(!ch){
|
||||
it(false,'канала в шлюзе нет'+(d.installed?' — bj создаст его сам':' — появится после установки шлюза'));
|
||||
if(d.installed) a.push('<button type="button" class="btn sm" data-act="contour" data-c="'+x[0]+'">Создать канал</button>');
|
||||
pill='Канала нет'; cls='no';
|
||||
} else {
|
||||
it(true,'канал <b>'+esc(ch.code)+'</b> <span class="muted">'+esc(ch.crypto+' · '+ch.endpoint)+'</span>');
|
||||
it(!!ch.profile, ch.profile?'ключ: профиль '+esc(ch.profile):'ключ этого контура не установлен');
|
||||
it(!!ch.enabled, ch.enabled?'канал включён':'канал выключен — включится после установки ключа');
|
||||
var ready=ch.enabled&&ch.profile;
|
||||
if(act){ pill='● Активный'; cls='act'; }
|
||||
else if(ready){ pill='Готов'; cls='ok'; }
|
||||
else { pill='Нужен ключ'; cls='no'; }
|
||||
if(!ch.profile) a.push('<a class="btn sm" href="/admin/wizard?step=8&contour='+x[0]+'" style="text-decoration:none">Установить ключ</a>');
|
||||
if(ch.enabled && !act) a.push(x[0]==='prod'?'<button type="button" class="btn sm" data-act="showprod">Перейти на боевой…</button>':'<button type="button" class="btn sm" data-act="switch" data-c="test3">Сделать активным</button>');
|
||||
if(!act){
|
||||
if(ready) a.push(x[0]==='prod'?'<button type="button" class="btn sm" data-act="showprod">Сделать активным…</button>':'<button type="button" class="btn sm" data-act="switch" data-c="test3">Сделать активным</button>');
|
||||
else a.push('<button type="button" class="btn sm" disabled title="Сначала установите ключ этого контура">Сделать активным</button>');
|
||||
}
|
||||
a.push('<button type="button" class="btn sm ghost" data-act="contour" data-c="'+x[0]+'" title="Записать канал в шлюз заново: адрес, сертификат, ключ">Обновить канал</button>');
|
||||
}
|
||||
return '<tr><td style="white-space:nowrap;width:130px"><b>'+x[1]+'</b>'+(act?'<br><span style="color:var(--accent);font-size:12px">● активный</span>':'')+'</td><td>'+s.join('<br>')+(a.length?'<div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:6px">'+a.join('')+'</div>':'')+'</td></tr>';
|
||||
var why=act?'<p class="ct-why">Через этот контур bj сейчас отправляет и получает переводы.</p>':(ch&&!(ch.enabled&&ch.profile))?'<p class="ct-why">Чтобы сделать контур активным, сначала установите его ключ (флешка с ключом этого контура).</p>':'';
|
||||
return '<div class="ct ct-'+cls+(act?' ct-on':'')+'"><div class="ct-h"><div><b>'+x[1]+'</b><small>'+x[2]+'</small></div><span class="ct-pill">'+pill+'</span></div><ul class="ct-li">'+li.join('')+'</ul>'+why+(a.length?'<div class="ct-act">'+a.join('')+'</div>':'')+'</div>';
|
||||
});
|
||||
tb.innerHTML=rows.join('');
|
||||
document.getElementById('ig-depo').placeholder=d.depo_db_prod||d.depo_db||'как сейчас';
|
||||
|
||||
@@ -24,10 +24,10 @@
|
||||
<form method="post" action="/admin/setup/m2m-auto" id="m2m-auto-form" class="dq">
|
||||
{{if .Ret}}<input type="hidden" name="return_to" value="{{.Ret}}">{{end}}<input type="hidden" name="section" value="m2m-auto">
|
||||
<div class="dq-tiles">
|
||||
<label class="dq-tile"><input type="radio" name="mode" value="auto" {{if $a.Enabled}}checked{{end}}><span><b>bj сам — автоматически</b><small>Рекомендуется: bj отвечает на входящие переводы по сверке с DEPO и подаёт поручения в НРД без оператора, сроки НРД соблюдаются. Всё видно в журнале.</small></span></label>
|
||||
<label class="dq-tile"><input type="radio" name="mode" value="manual" {{if not $a.Enabled}}checked{{end}}><span><b>Оператор</b><small>bj сверяет перевод с DEPO и подсказывает решение, а отвечает и подаёт поручения оператор — кнопками в карточке перевода.</small></span></label>
|
||||
<label class="dq-tile"><input type="radio" name="mode" value="auto" {{if $a.Enabled}}checked{{end}}><span><b>bj сам — автоматически</b><small>Основной режим: bj отвечает на входящие переводы по сверке с DEPO и подаёт поручения в НРД без оператора, сроки НРД соблюдаются. Всё видно в журнале.</small></span></label>
|
||||
<label class="dq-tile"><input type="radio" name="mode" value="manual" {{if not $a.Enabled}}checked{{end}}><span><b>Оператор</b><small>Запасной режим — например, на время разбора сбоя: bj сверяет перевод с DEPO и подсказывает решение, а отвечает и подаёт поручения оператор кнопками в карточке перевода.</small></span></label>
|
||||
</div>
|
||||
<p class="dq-note" id="m2m-auto-st" aria-live="polite">Выбор сохраняется сразу.{{if not $a.ChangedAt.IsZero}} {{if $a.Enabled}}Включён{{else}}Выключен{{end}} {{msk $a.ChangedAt}}{{with $a.ChangedBy}} · {{.}}{{end}}.{{end}}{{if .D.Settings.Quik.Emulate}}{{if devOn}} Шаги QUIK МНП — в режиме имитации.{{else}} Записи в QUIK МНП сейчас не выполняются.{{end}}{{end}}</p>
|
||||
<p class="dq-note" id="m2m-auto-st" aria-live="polite">Выбор сохраняется сразу.{{if not $a.ChangedAt.IsZero}} {{if $a.Enabled}}Включён{{else}}Выключен{{end}} {{msk $a.ChangedAt}}{{with $a.ChangedBy}}{{if eq . "по умолчанию"}} — по умолчанию{{else}} · {{.}}{{end}}{{end}}.{{end}}{{if .D.Settings.Quik.Emulate}}{{if devOn}} Шаги QUIK МНП — в режиме имитации.{{else}} Записи в QUIK МНП сейчас не выполняются.{{end}}{{end}}</p>
|
||||
<noscript><button type="submit" class="btn">Сохранить</button></noscript>
|
||||
</form>
|
||||
<details style="margin-top:6px"><summary style="cursor:pointer;color:var(--accent);font-size:13px">Что bj делает сам в автоматическом режиме</summary>
|
||||
|
||||
Reference in New Issue
Block a user