Автоматический режим 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:
zuevav
2026-10-07 08:29:25 +03:00
parent 50303c1790
commit a53b9f9e9f
8 changed files with 138 additions and 43 deletions
+5 -1
View File
@@ -331,7 +331,11 @@ SQL
mine=false; [ -f "$CONF" ] && grep -q "^$B" "$CONF" && mine=true
u=$( [ "$mine" = true ] && sed -n "/^$B/,/^$E/p" "$CONF" | sed -n 's/^ *valid users *= *//p' | head -1 )
[ "$manual" = true ] && [ -z "$u" ] && u=$(sed -n '/^\[igate_exchange\]/,/^\[/p' "$CONF" | sed -n 's/^ *valid users *= *//p' | head -1)
echo "{\"ok\":true,\"on\":$( [ "$mine" = true ] || [ "$manual" = true ] && echo true || echo false),\"manual\":$manual,\"user\":$(q "$u"),\"smbd\":$(q "$(systemctl is-active smbd 2>/dev/null)")}"; exit 0
# куда подключаться из проводника: адрес и имя сервера, открытые папки (test3 / prod)
ip=$(hostname -I 2>/dev/null | awk '{print $1}'); hn=$(hostname -s 2>/dev/null)
sh=""; [ "$mine" = true ] && for n in $(sed -n "/^$B/,/^$E/p" "$CONF" | sed -n 's/^\[\(.*\)\]$/\1/p'); do sh="$sh${sh:+,}$(q "$n")"; done
[ "$manual" = true ] && [ -z "$sh" ] && sh=$(q igate_exchange)
echo "{\"ok\":true,\"on\":$( [ "$mine" = true ] || [ "$manual" = true ] && echo true || echo false),\"manual\":$manual,\"user\":$(q "$u"),\"smbd\":$(q "$(systemctl is-active smbd 2>/dev/null)"),\"ip\":$(q "$ip"),\"host\":$(q "$hn"),\"shares\":[$sh]}"; exit 0
fi
[ "$manual" = true ] && err "папка igate_exchange уже открыта вручную в $CONF — bj её не меняет; чтобы управлять из bj, уберите её из smb.conf"
if [ "$on" != true ]; then
+11
View File
@@ -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
+33
View File
@@ -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("явный выбор «Оператор» перезаписан")
}
}
+9 -2
View File
@@ -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>
+2 -2
View File
@@ -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{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[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>