Files
Bridge-and-Join-s/internal/lkgateway/web/templates/admin_setup.html
T
fontvielle 7947808b26 Анкета участника M2M: открывать внутри страницы НРД (раскрытие вниз)
Ссылка вела на отдельную страницу. В разделе «НРД» карточка «Регистрация
участника M2M» теперь раскрывается вниз (<details>) и показывает анкету в том же
окне через iframe того же origin (lazy — грузится при раскрытии). Отдельная
вкладка оставлена запасной ссылкой.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
2026-10-21 16:41:16 +03:00

1148 lines
129 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{define "content"}}
{{if .Flash}}<div style="padding:12px 16px;background:var(--ok-weak);background:var(--ok-weak);border-radius:8px;margin-bottom:16px">{{.Flash}}</div>{{end}}
<div class="settings">
{{/* ===== Боковая навигация разделов с индикаторами ===== */}}
<nav class="settings-nav">
<a href="/admin/wizard" class="snav-wiz" style="display:block;margin:0 0 10px;padding:8px 12px;border:1px dashed var(--border-strong,#c9cdd8);border-radius:10px;font-size:13.5px;text-decoration:none;color:var(--accent)">↺ Мастер настройки — пройти по шагам</a>
<button data-sec="overview" class="active"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="3"/></svg></span>Обзор</button>
<button data-sec="db"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5.5" rx="7" ry="2.6"/><path d="M5 5.5v13c0 1.4 3.1 2.6 7 2.6s7-1.2 7-2.6v-13"/><path d="M5 12c0 1.4 3.1 2.6 7 2.6s7-1.2 7-2.6"/></svg></span>База данных<span class="ind {{if .Settings.Postgres.DSN}}ok{{else}}err{{end}}"></span></button>
{{if mod "quik-mnp"}}<button data-sec="fancy"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M4 8h12M12 4l4 4-4 4"/><path d="M20 16H8M12 20l-4-4 4-4"/></svg></span>Обмен Fancy<span class="ind {{if .Settings.Fancy.DepoTablesCheckOK}}ok{{else if .Settings.Fancy.Server}}warn{{end}}"></span></button>
<button data-sec="quik"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M4 20V4M4 20h16"/><rect x="7" y="12" width="2.6" height="5"/><rect x="12" y="8" width="2.6" height="9"/><rect x="17" y="14" width="2.6" height="3"/></svg></span>QUIK МНП<span class="ind {{if .Settings.Quik.Verified}}ok{{else if .Settings.Quik.Server}}warn{{end}}"></span></button>{{end}}
{{if or (mod "quik-export") (mod "npr")}}<button data-sec="quikexport"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M4 17l5-5 4 4 7-8"/><path d="M14 8h6v6"/></svg></span>Экспорт QUIK<span class="ind {{.QuikExport.Ind}}"></span></button>{{end}}
<button data-sec="users"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="8" r="3.5"/><path d="M3 20c0-3.3 2.7-6 6-6s6 2.7 6 6"/><circle cx="17" cy="9" r="2.5"/><path d="M15.5 14.5c2.8.3 5.5 2.5 5.5 5.5"/></svg></span>Пользователи<span class="ind {{if .Users.Open}}warn{{else}}ok{{end}}"></span></button>
<button data-sec="crypto"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/></svg></span>Криптография<span class="ind {{if cryptoReady}}ok{{else}}warn{{end}}"></span></button>
<button data-sec="nsd"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21h18M4 21V10M20 21V10M3 10l9-6 9 6M8 21v-7M12 21v-7M16 21v-7"/></svg></span>НРД<span class="ind {{if .Settings.NSD.IGWBaseURL}}ok{{else}}warn{{end}}"></span></button>
{{if mod "web-cabinet"}}<button data-sec="cabinet"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="9" r="3.2"/><path d="M6 19c.6-3 3-4.6 6-4.6s5.4 1.6 6 4.6"/><rect x="3" y="4" width="18" height="16" rx="2"/></svg></span>Кабинет клиента<span class="ind {{if .Settings.LK.APIKey}}ok{{else}}warn{{end}}"></span></button>{{end}}
{{if mod "esia-finance"}}<button data-sec="lk"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"/><circle cx="9" cy="11" r="2"/><path d="M6 16c.4-1.4 1.6-2.2 3-2.2s2.6.8 3 2.2"/><path d="M15 10h4M15 13h3"/></svg></span>ЕСИА Финанс<span class="ind {{if .Settings.LK.PublicURL}}ok{{else}}warn{{end}}"></span></button>{{end}}
{{if mod "onec-back"}}<button data-sec="onec"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="10" cy="6" rx="6" ry="2.5"/><path d="M4 6v10c0 1.4 2.7 2.5 6 2.5"/><path d="M16 6v4"/><path d="M4 11c0 1.4 2.7 2.5 6 2.5"/><path d="M14 16h7M18 13l3 3-3 3"/></svg></span>1С: бэк-офис<span class="ind {{if .Settings.OneC.CheckOK}}ok{{else}}warn{{end}}"></span></button>{{end}}
<button data-sec="tests"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M9 3h6M10 3v6l-5 8.5A2 2 0 0 0 6.7 21h10.6a2 2 0 0 0 1.7-3.5L14 9V3"/><path d="M8 15h8"/></svg></span>Диагностика</button>
<button data-sec="alerts"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.7 21a2 2 0 0 1-3.4 0"/></svg></span>Оповещения<span class="ind {{if or .Settings.Alerts.SMTP.Enabled .Settings.Alerts.Zabbix.Enabled}}ok{{end}}"></span></button>
<button data-sec="update"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20V8M6 12l6-6 6 6"/><path d="M5 4h14"/></svg></span>Обновления{{if .UpdatesAvailable}}<span class="ind warn"></span>{{end}}</button>
<button data-sec="backup"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="6" rx="7" ry="2.5"/><path d="M5 6v6c0 1.4 3.1 2.5 7 2.5s7-1.1 7-2.5V6"/><path d="M12 17v4M9 19l3 2 3-2"/></svg></span>Резервное копирование{{$bs := backupStatus}}{{if and .Settings.Backup.Enabled $bs}}{{if not $bs.OK}}<span class="ind err"></span>{{end}}{{else if not .Settings.Backup.Enabled}}<span class="ind warn"></span>{{end}}</button>
<button data-sec="server"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="7" rx="1.5"/><rect x="3" y="13" width="18" height="7" rx="1.5"/><path d="M7 7.5h.01M7 16.5h.01"/></svg></span>Сервер: HTTPS, время, сеть{{if or (not .Settings.HTTPS.RedirectOn) (not timeSynced)}}<span class="ind warn"></span>{{end}}</button>
<button data-sec="license"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="12" r="4"/><path d="M12 12h9M18 12v3M15 12v2"/></svg></span>Лицензия{{if .License.Present}}<span class="ind {{if .License.Valid}}ok{{else}}err{{end}}"></span>{{end}}</button>
<button data-sec="branding"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"/><circle cx="8.5" cy="10" r="1.5"/><path d="M21 16l-5-5L5 19"/></svg></span>Оформление{{if .Settings.Branding.LogoDataURI}}<span class="ind ok"></span>{{end}}</button>
{{if devOn}} <button data-sec="advanced"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/></svg></span>Перспективные{{if .AdvancedUnlocked}}<span class="ind ok"></span>{{end}}</button>{{end}}
</nav>
<div class="settings-body">
{{if .FromWizard}}<div class="card" style="background:var(--accent-weak);margin-bottom:12px;display:flex;justify-content:space-between;gap:8px;flex-wrap:wrap;align-items:center"><span>Вы настраиваете модуль из мастера настройки.</span><a href="/admin/wizard?step=11" class="btn" style="text-decoration:none">← Вернуться в мастер</a></div>{{end}}
{{/* ============ ОБЗОР ============ */}}
<section class="settings-section active" id="sec-overview">
<h1>Обзор</h1>
<div class="card">
<h2>Готовность системы: {{.ReadyCount}} из {{.TotalCount}}</h2>
<div class="grid" style="margin-top:12px">
{{range .Readiness}}
<div class="stat">
<div><span class="dot {{if .Configured}}ok{{else}}err{{end}}"></span><strong>{{if .Title}}{{.Title}}{{else}}{{.Name}}{{end}}</strong></div>
<div class="muted" style="font-size:12px;margin-top:4px">{{if .Configured}}настроено{{else}}не настроено{{end}}</div>
</div>
{{end}}
</div>
</div>
<div class="card" id="versions">
<h2>Версии систем</h2>
<p class="muted">Что установлено на этом сервере и с чем работает bj: собственные компоненты и внешние системы. Та же версия показана в разделе каждого модуля. Сведения на {{msk .Versions.At}}.</p>
<div style="overflow-x:auto"><table style="margin-top:8px">
<thead><tr><th>Система</th><th>Версия</th><th>Подробности</th></tr></thead>
<tbody>{{range .Versions.Items}}<tr>
<td>{{if and .Section (ne .Section "overview")}}<a href="#{{.Section}}" onclick="var b=document.querySelector('.settings-nav [data-sec=&quot;{{.Section}}&quot;]');if(b){b.click();return false}">{{.Title}}</a>{{else}}{{.Title}}{{end}}</td>
<td>{{if .Version}}<strong>{{.Version}}</strong>{{else}}<span class="muted">не определена</span>{{end}}</td>
<td class="muted" style="font-size:12.5px">{{.Detail}}{{with .Err}} <span style="color:var(--err)">{{.}}</span>{{end}}</td>
</tr>{{end}}</tbody>
</table></div>
<form method="post" action="/admin/setup/versions/refresh" style="margin-top:10px"><input type="hidden" name="section" value="versions"><button type="submit" class="btn btn-secondary">Обновить сведения</button></form>
</div>
</section>
{{/* ============ БАЗА ДАННЫХ ============ */}}
<section class="settings-section" id="sec-db">
<h1>База данных</h1>
{{template "verline" (.Versions.Get "postgres")}}
<div class="card">
<h2><span class="dot {{if .Settings.Postgres.DSN}}ok{{else}}err{{end}}"></span>PostgreSQL</h2>
<p class="muted">Журнал сделок m2m-core и принимающая БД. Сейчас: {{if .Settings.Postgres.DSN}}<code>подключено</code>{{else}}<code>in-memory</code> (данные не сохраняются){{end}}.</p>
{{if and devOn (not .Settings.Postgres.DSN)}}
<div style="background:var(--card-2);border:1px solid var(--accent);border-radius:10px;padding:16px;margin-top:12px">
<h3 style="margin:0 0 8px">Подключить автоматически</h3>
<p class="muted" style="margin:0 0 12px">Поднимем локальный PostgreSQL в контейнере, применим миграции и запишем DSN. Для дев-стенда. Для прода — укажите свой DSN ниже.</p>
<form method="post" action="/admin/setup/postgres/quick-start" style="margin:0">
<button type="submit" class="btn">Поднять локальный PostgreSQL</button>
</form>
</div>
{{end}}
<details {{if not .Settings.Postgres.DSN}}open{{end}} style="margin-top:14px">
<summary style="cursor:pointer;color:var(--accent);font-size:13px">{{if .Settings.Postgres.DSN}}Изменить подключение{{else}}…или ввести DSN вручную{{end}}</summary>
<form method="post" action="/admin/setup/postgres" style="margin-top:12px;display:grid;gap:10px;max-width:640px">
<label>DSN</label>
<input type="text" name="dsn" value="{{.Settings.Postgres.DSN}}" placeholder="postgres://bj:secret@127.0.0.1:5432/bj?sslmode=disable">
<p class="muted" style="margin:0">При сохранении выполняется Ping.</p>
<div><button type="submit" class="btn">Сохранить и проверить</button></div>
</form>
</details>
</div>
</section>
{{if mod "quik-mnp"}}
{{/* ============ ОБМЕН FANCY (MSSQL) — перспективный модуль ============ */}}
<section class="settings-section" id="sec-fancy">
<h1>Обмен с DEPO (Fancy)</h1>
{{template "verline" (.Versions.Get "depo")}}
{{template "fancyStatus" .}}
{{$fok := and .Settings.Fancy.Server .Settings.Fancy.DepoTablesCheckOK .Settings.Fancy.DepoJobCheckOK}}
<div class="card">
<details class="adv-box" {{if not $fok}}open{{end}}>
<summary><span class="sum-h">Перенастроить</span><span class="muted">подключение к базе DEPO, торговые счета, трансферные таблицы, признак ИИС, кто запускает обработку — если что-то изменилось</span></summary>
{{template "fancySection" .}}
</details>
</div>
</section>
{{/* ============ QUIK МНП ============ */}}
<section class="settings-section" id="sec-quik">
<h1>QUIK — Модуль неторговых поручений</h1>
{{template "quikStatus" .}}
<div class="card">
<details class="adv-box" {{if not (and .Settings.Quik.Server .Settings.Quik.Verified)}}open{{end}}>
<summary><span class="sum-h">Перенастроить</span><span class="muted">подключение к модулю, как bj работает с QUIK, поручения из терминала, расширенные настройки, проверка модуля</span></summary>
{{template "quikSection" .}}
{{if .Settings.Quik.Server}}{{template "mnpCheck" .}}{{end}}
</details>
</div>
</section>
{{end}}{{/* /AdvancedUnlocked — конец перспективных модулей Fancy/QUIK */}}
{{/* ============ ПОЛЬЗОВАТЕЛИ И ВХОД ============ */}}
<section class="settings-section" id="sec-users">
{{$U := .Users}}{{$A := .Settings.Auth}}
<h1>Пользователи и вход</h1>
<p class="muted">Кто может входить в bj и что ему разрешено. Сотрудник входит своей учётной записью bj или доменной (Active Directory) — тогда пароль проверяет домен.</p>
{{if $U.Open}}<div class="card callout" style="padding:10px 14px;margin-bottom:8px;background:color-mix(in srgb,var(--warn) 10%,var(--card));display:flex;gap:8px;align-items:center"><span class="dot warn"></span><span><b>Вход пока не требуется:</b> пользователи не созданы. Создайте первого администратора — после этого все страницы потребуют входа.</span></div>{{end}}
<div class="card">
<h2>Сотрудники ({{len $U.Users}})</h2>
{{if $U.Users}}<div class="usr-list">
{{range $U.Users}}
<details class="usr">
<summary>
<span class="usr-av" aria-hidden="true">{{avatarLetter (or .Name .Login)}}</span>
<span class="usr-main"><b>{{if .Name}}{{.Name}}{{else}}{{.Login}}{{end}}</b><small>{{.Login}}{{if .Email}} · {{.Email}}{{end}} · {{if eq .Source "ad"}}доменная учётная запись{{else}}пароль в bj{{end}} · {{if not .LastLogin.IsZero}}вход {{msk .LastLogin}}{{else}}ещё не входил{{end}}</small></span>
{{if not .Active}}<span class="badge rejected">заблокирован</span>{{end}}
<span class="usr-role">{{if $U.RoleShown .Role}}{{roleTitle .Role}}{{else}}{{$U.RoleNote .Role}}{{end}}</span>
<span class="usr-edit">Изменить</span>
</summary>
<form method="post" action="/admin/setup/users" class="usr-form">
<input type="hidden" name="login" value="{{.Login}}"><input type="hidden" name="source" value="{{.Source}}">
<label>Имя<input type="text" name="name" value="{{.Name}}" autocomplete="off"></label>
<label>E-mail<input type="text" name="email" value="{{.Email}}" autocomplete="off"></label>
<label>Роль<select name="role">{{$r := .Role}}{{if not ($U.RoleShown $r)}}<option value="{{$r}}" selected>{{$U.RoleNote $r}}</option>{{end}}{{range $U.Roles}}<option value="{{.Key}}" {{if eq .Key $r}}selected{{end}}>{{.Title}}</option>{{end}}</select></label>
{{if eq .Source "local"}}<label>Новый пароль<input type="password" name="password" placeholder="пусто — оставить прежний" autocomplete="new-password"></label>{{else}}<div class="usr-note">Пароль проверяет домен{{if .ADGroups}} · группы: {{range $i,$g := .ADGroups}}{{if $i}}, {{end}}{{$g}}{{end}}{{end}}{{if .Note}} · {{.Note}}{{end}}</div>{{end}}
<div class="usr-checks">{{$m := .Modules}}{{range $U.Modules}}<label><input type="checkbox" name="modules" value="{{.Key}}" {{if has $m .Key}}checked{{end}}>{{.Title}}</label>{{end}}
<label><input type="checkbox" name="active" value="1" {{if .Active}}checked{{end}}>может входить</label></div>
<div class="usr-act"><button class="btn" type="submit">Сохранить</button>
<button class="usr-del" type="submit" formaction="/admin/setup/users/delete" onclick="return confirm('Удалить сотрудника {{.Login}}? Войти он больше не сможет.')">Удалить сотрудника</button></div>
</form>
</details>
{{end}}</div>{{else}}<p class="muted">Пока никого — добавьте сотрудника ниже.</p>{{end}}
</div>
<div class="card">
<h2>{{if $U.Open}}Создать первого администратора{{else}}Добавить сотрудника{{end}}</h2>
{{template "accountsAdd" (withRet . "/admin/setup")}}
{{template "accountsAD" (withRet . "/admin/setup")}}
</div>
<div class="card">
<h2>Активные сессии ({{len $U.Sessions}})</h2>
{{if $U.Sessions}}<div class="ses-list">
{{range $U.Sessions}}<div class="ses"><span class="ses-main"><b>{{.Login}}</b><small>вход {{msk .Created}} · активность {{msk .Seen}} · <code>{{.IP}}</code> · <span title="{{.UA}}">{{.Device}}</span></small></span>
<form method="post" action="/admin/setup/users/session-drop" style="margin:0"><input type="hidden" name="id" value="{{.ID}}"><button class="btn btn-secondary btn-sm" type="submit">Завершить</button></form></div>{{end}}
</div>{{else}}<p class="muted">Сессий нет.</p>{{end}}
</div>
<div class="card">
<details class="adv-box">
<summary><span class="sum-h">Расширенные настройки</span><span class="muted">роли и права, домен вручную: контроллер, сертификат, группы, перепроверка, время сессии</span></summary>
<h3 class="adv-h">Роли и права</h3>
<table style="font-size:12.5px"><thead><tr><th>Роль</th><th>Что разрешено</th></tr></thead><tbody>
{{range $U.Roles}}<tr><td style="white-space:nowrap"><b>{{.Title}}</b><br><code style="font-size:11px">{{.Key}}</code></td><td>{{.Desc}}</td></tr>{{end}}
{{range $U.Hidden}}<tr class="muted"><td style="white-space:nowrap">{{.Title}}</td><td>Недоступна: в лицензии нет модуля «{{.Missing}}».</td></tr>{{end}}
</tbody></table>
<p class="muted" style="font-size:12px;margin-top:6px">{{if $U.Modules}}Подключённые модули ({{range $i, $m := $U.Modules}}{{if $i}}, {{end}}«{{$m.Title}}»{{end}}) можно выдать любому пользователю дополнительно к роли. {{end}}Роли и модули показываются по лицензии: права модуля, которого нет в лицензии, не действуют. Настройки и пользователи — только администратор.</p>
<h3 class="adv-h">Вход через Active Directory</h3>
<p class="muted">bj подключается к контроллеру домена и проверяет пароль пользователя (LDAP bind его же учётной записью). Роль назначается по членству в группах AD, включая вложенные. Пользователь появляется в списке при первом входе. Чтобы изменения групп действовали без повторного входа, укажите сервисную учётную запись только на чтение: bj будет перечитывать группы доменных пользователей по расписанию, менять роль и снимать доступ у удалённых из групп.</p>
<form method="post" action="/admin/setup/auth" style="display:grid;gap:10px;max-width:820px;margin-top:10px">
<label style="display:flex;gap:8px;align-items:center;font-weight:600;margin:0"><input type="checkbox" name="ad_enabled" value="1" {{if $A.ADEnabled}}checked{{end}}> Разрешить вход доменной учётной записью</label>
<div style="display:grid;grid-template-columns:2fr 1fr;gap:10px">
<label>Контроллер домена <input type="text" name="ad_server" value="{{$A.ADServer}}" placeholder="ldaps://dc1.corp.example.ru:636" style="width:100%"></label>
<label>Домен (суффикс UPN) <input type="text" name="ad_domain" value="{{$A.ADDomain}}" placeholder="corp.example.ru" style="width:100%"></label>
</div>
<label>База поиска (Base DN) <input type="text" name="ad_base_dn" value="{{$A.ADBaseDN}}" placeholder="пусто — возьмётся с контроллера" style="width:100%"></label>
<div style="display:flex;gap:18px;flex-wrap:wrap">
<label style="display:flex;gap:6px;align-items:center;font-weight:400;margin:0"><input type="checkbox" name="ad_starttls" value="1" {{if $A.ADStartTLS}}checked{{end}}> StartTLS для ldap://</label>
<label style="display:flex;gap:6px;align-items:center;font-weight:400;margin:0"><input type="checkbox" name="ad_skip_verify" value="1" {{if $A.ADSkipVerify}}checked{{end}}> Не проверять сертификат контроллера</label>
</div>
<label>Сертификат центра сертификации домена (PEM) <textarea name="ad_ca_pem" rows="3" style="width:100%;font-family:ui-monospace,monospace;font-size:11px" placeholder="-----BEGIN CERTIFICATE-----">{{$A.ADCAPEM}}</textarea></label>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:10px">
<label>Служебная учётная запись <input type="text" name="ad_bind_user" value="{{$A.ADBindUser}}" placeholder="svc-bj@corp.example.ru" autocomplete="off" style="width:100%"></label>
<label>Её пароль <input type="password" name="ad_bind_password" placeholder="{{if $A.ADBindPassword}}задан — пусто, чтобы не менять{{else}}пароль{{end}}" autocomplete="new-password" style="width:100%"></label>
<label>Перепроверять группы, минут <input type="number" name="ad_recheck_minutes" min="5" max="1440" value="{{if $A.ADRecheckMinutes}}{{$A.ADRecheckMinutes}}{{else}}15{{end}}" style="width:100%"></label>
</div>
<div>
<div style="font-weight:600;font-size:13.5px;margin:4px 0 6px">Группы домена → роли <span class="muted" style="font-weight:400;font-size:12.5px">имя группы или полный DN; действует первое совпадение сверху</span></div>
<div id="ad-groups" style="display:grid;gap:6px">
{{range $A.ADGroups}}<div style="display:grid;grid-template-columns:2fr 1fr;gap:8px"><input type="text" name="ad_group" value="{{.Group}}" placeholder="BJ-Operators"><select name="ad_role">{{$r := .Role}}{{if not ($U.RoleShown $r)}}<option value="{{$r}}" selected>{{$U.RoleNote $r}} (группа не действует)</option>{{end}}{{range $U.Roles}}<option value="{{.Key}}" {{if eq .Key $r}}selected{{end}}>{{.Title}}</option>{{end}}</select></div>{{end}}
{{range $i := list "1" "2"}}<div style="display:grid;grid-template-columns:2fr 1fr;gap:8px"><input type="text" name="ad_group" value="" placeholder="имя группы AD"><select name="ad_role">{{range $U.Roles}}<option value="{{.Key}}">{{.Title}}</option>{{end}}</select></div>{{end}}
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<label>Роль, если ни одна группа не совпала <select name="ad_default_role" style="width:100%"><option value="">отказать во входе</option>{{if and $A.ADDefaultRole (not ($U.RoleShown $A.ADDefaultRole))}}<option value="{{$A.ADDefaultRole}}" selected>{{$U.RoleNote $A.ADDefaultRole}} (вход будет отклонён)</option>{{end}}{{range $U.Roles}}<option value="{{.Key}}" {{if eq .Key $A.ADDefaultRole}}selected{{end}}>{{.Title}}</option>{{end}}</select></label>
<label>Выход при бездействии, часов <input type="number" name="session_hours" min="1" max="24" value="{{if $A.SessionHours}}{{$A.SessionHours}}{{else}}8{{end}}" style="width:100%"></label>
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap"><button class="btn" type="submit">Сохранить</button></div>
</form>
<form method="post" action="/admin/setup/auth/test" style="display:grid;grid-template-columns:1fr 1fr auto;gap:10px;max-width:820px;margin-top:12px;align-items:end">
<label>Проверить вход: логин домена <input type="text" name="test_login" placeholder="ivanov" style="width:100%" autocomplete="off"></label>
<label>Пароль <input type="password" name="test_password" style="width:100%" autocomplete="off"></label>
<button class="btn btn-secondary" type="submit">Проверить</button>
</form>
<p class="muted" style="font-size:12px;margin-top:6px">Без логина и пароля проверяется только доступность контроллера. С логином — вход, группы и вычисленная роль. Пароль проверки никуда не сохраняется.</p>
{{$R := $U.ADRecheck}}
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:10px;font-size:12.5px">
{{if and $A.ADEnabled $A.ADBindUser $A.ADBindPassword}}
<span><span class="dot {{if $R.Err}}err{{else if $R.At.IsZero}}warn{{else}}ok{{end}}"></span>Перепроверка групп каждые {{if $A.ADRecheckMinutes}}{{$A.ADRecheckMinutes}}{{else}}15{{end}} мин: {{if $R.At.IsZero}}ещё не выполнялась{{else}}последняя {{msk $R.At}}, проверено {{$R.Checked}}, изменений {{$R.Changed}}{{if $R.Err}} — <b>ошибка:</b> {{$R.Err}}{{end}}{{end}}</span>
<form method="post" action="/admin/setup/auth/recheck"><button class="btn btn-secondary" type="submit">Перепроверить сейчас</button></form>
{{else}}<span class="muted"><span class="dot warn"></span>Перепроверка групп выключена: роль доменного пользователя меняется только при следующем входе. Укажите сервисную учётную запись выше.</span>{{end}}
</div>
<p class="muted" style="font-size:12.5px;margin-top:14px">Если потерян доступ администратора — удалите файл <code>/var/lib/bj/.bj/users.json</code> на сервере: система вернётся в режим первичной настройки.</p>
</details>
</div>
</section>
{{/* ============ ЭКСПОРТ БИРЖЕВОЙ ИНФОРМАЦИИ (QUIK) ============ */}}
{{if or (mod "quik-export") (mod "npr")}}
<section class="settings-section" id="sec-quikexport">
{{if mod "quik-export"}}{{.QuikExport.HTML}}{{end}}
{{/* модуль НПР/УДС работает поверх базы экспорта QUIK — его настройки здесь же */}}
{{if mod "npr"}}<div class="card" id="sec-npr">
<h2><span class="dot {{if .Settings.NPR.Enabled}}ok{{else}}warn{{end}}"></span>Модуль «НПР / УДС» (риск-монитор по данным QUIK)</h2>
<p class="muted">Показывает НПР1 / НПР2 / УДС клиентов маржинальной торговли по базе экспорта QUIK (схема <code>npr</code>). Отдельно лицензируемый модуль (признак <code>npr</code>); при включении в меню появляется пункт «НПР / УДС». Подключение — логин только для чтения.</p>
{{template "verline" (.Versions.Get "bj-npr")}}
<form method="post" action="/admin/setup/npr" style="margin-top:10px;display:grid;gap:10px;max-width:640px">
<input type="hidden" name="section" value="sec-npr">
<label style="display:flex;gap:8px;align-items:center;font-weight:600"><input type="checkbox" name="enabled" value="1" {{if .Settings.NPR.Enabled}}checked{{end}}> Включить модуль</label>
<label>Строка подключения к базе экспорта QUIK <input type="text" name="dsn" value="{{.Settings.NPR.DSN}}" placeholder="postgres://quik_ro:пароль@localhost:5432/quik_export" style="width:100%;font-family:ui-monospace,monospace;font-size:12px"></label>
<div style="display:flex;gap:8px;flex-wrap:wrap"><button type="submit" class="btn btn-ok">Сохранить</button><button type="submit" formaction="/admin/setup/npr/test" class="btn">Проверить подключение</button></div>
</form>
</div>{{end}}
<h1>Лицензия</h1>
</section>
{{end}}
{{/* ============ КРИПТОГРАФИЯ ============ */}}
<section class="settings-section" id="sec-crypto">
<h1>Криптография</h1>
{{template "cryptoStatus" .}}
<div class="card">
<details class="adv-box" {{if not (and cryptoReady orgKeyCert)}}open{{end}}>
<summary><span class="sum-h">Перенастроить</span><span class="muted">ключ организации, проверка СКЗИ, сертификаты НРД в справочнике шлюза, версии</span></summary>
<div style="margin-top:16px">{{template "verline" (.Versions.Get "crypto-service.jar")}}{{template "verline" (.Versions.Get "validata")}}{{template "verline" (.Versions.Get "java")}}</div>
{{$kc := orgKeyCert}}
{{if and cryptoReady $kc}}
<div class="card" id="sec-orgkey">
<h2><span class="dot {{if lt (index $kc "Days") 30}}warn{{else}}ok{{end}}"></span>Ключ организации</h2>
<p style="margin:6px 0">Сертификат «{{index $kc "Owner"}}», выдан: {{index $kc "Issuer"}} — действует до <b>{{index $kc "Until"}}</b> (осталось {{index $kc "Days"}} дн.).</p>
{{if lt (index $kc "Days") 30}}<p style="color:var(--warn);margin:6px 0">⚠ Сертификат скоро закончится. Получите новый ключ в Удостоверяющем центре Московской биржи и установите его — без этого обмен с НРД остановится.</p>
<a href="/admin/setup/keywizard" class="btn btn-ok" style="margin-top:6px;display:inline-block">Установить новый ключ →</a>
{{else}}<p class="muted" style="font-size:13px;margin:6px 0 0">Перевыпустили ключ (новый сертификат) или нужен ключ другого контура — <a href="/admin/setup/keywizard">заменить ключ</a>: bj поможет подключить флешку или записать новый ключ из архива.</p>{{end}}
</div>
{{end}}
<div class="card">
<h2><span class="dot {{if cryptoReady}}ok{{else}}warn{{end}}"></span>СКЗИ «Валидата Клиент L»</h2>
<p style="margin:6px 0">{{if cryptoReady}}<span style="color:var(--ok)">✓ СКЗИ установлена, ключ организации подключён — документы для НРД подписывает шлюз НРД.</span>{{else}}<span style="color:var(--warn)">⚠ Ключ организации не подключён — мастер настройки, шаг «Ключ подписи».</span>{{end}}</p>
{{if devOn}}<p class="muted">Активный профиль: <code>{{if .Settings.Crypto.Profile}}{{.Settings.Crypto.Profile}}{{else}}—{{end}}</code> · провайдер {{if and (not devOn) (or (eq .Settings.Crypto.Provider "stub") (eq .Settings.Crypto.Provider ""))}}<code>не настроен</code>{{else}}<code>{{.Settings.Crypto.Provider}}</code>{{end}}. {{if internalDocs}}Подробно — <a href="/admin/help/crypto">справка</a>{{end}}.</p>{{end}}
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:12px">
<form method="post" action="/admin/setup/crypto/check" style="margin:0"><button type="submit" class="btn btn-ok">✓ Проверить СКЗИ и ключ</button></form>
{{if devOn}}<form method="post" action="/admin/setup/crypto/test-sign" style="margin:0"><button type="submit" class="btn">✎ Подписать проверочный документ</button></form>
<form method="post" action="/admin/setup/restart-crypto" style="margin:0" onsubmit="return confirm('Перезапустить проверку ключа подписи? Займёт около 5 секунд.');"><button type="submit" class="btn btn-warn">↻ Перезапустить проверку ключа</button></form>
<form method="post" action="/admin/setup/restart-server" style="margin:0" onsubmit="return confirm('Перезапустить bj-server? Через 5-10 сек страница вернётся.');"><button type="submit" class="btn btn-secondary">↻ bj-server</button></form>
{{end}}
</div>
<details style="margin-top:14px">
<summary style="cursor:pointer;color:var(--accent);font-size:13px">Параметры провайдера (для совместимости)</summary>
<form method="post" action="/admin/setup/crypto" style="margin-top:12px;display:grid;gap:10px;max-width:640px">
<label>Провайдер</label>
<select name="provider">
{{if devOn}} <option value="stub" {{if eq .Settings.Crypto.Provider "stub"}}selected{{end}}>stub — без криптографии (демо)</option>{{end}}
<option value="validata" {{if eq .Settings.Crypto.Provider "validata"}}selected{{end}}>Валидата Клиент L</option>
</select>
{{/* путь к PKCS#11 не используется: bj работает с Валидатой через bj-crypto; в Валидате 6.0 модуля libvdpkcs11.so нет */}}
<input type="hidden" name="module_path" value="{{.Settings.Crypto.ModulePath}}">
<label>UDS-сокет</label>
<input type="text" name="socket_path" value="{{.Settings.Crypto.SocketPath}}" placeholder="/run/bj/crypto.sock">
<div><button type="submit" class="btn">Сохранить</button></div>
</form>
</details>
</div>
{{/* Доверенные сертификаты НРД в справочнике ИШ */}}
<div class="card" id="sec-nsdcerts">
<h2><span class="dot {{if .NSDCerts.Pending}}warn{{else if .NSDCerts.LastRun.IsZero}}warn{{else}}ok{{end}}"></span>Сертификаты НРД в справочнике ИШ</h2>
<p class="muted">Комплект доверенных сертификатов крипто-сервера НРД поставляется вместе с bj и при обновлении приезжает всем клиентам. bj сам сверяет справочник активного профиля ИШ{{if .NSDCerts.Profile}} (<code>{{.NSDCerts.Profile}}</code>){{end}}: недостающие добавляет (только если их УЦ уже есть в справочнике — боевой сертификат не попадёт в тестовый профиль), старые удаляет строго после разрешённого НРД срока. Каждое изменение — с бэкапом, проверкой подписи и откатом при ошибке; ИШ перезапускается на несколько секунд.</p>
<table style="margin-top:12px">
<thead><tr><th>Сертификат</th><th>Контур</th><th>Действует</th><th>Статус</th></tr></thead>
<tbody>{{range .NSDCerts.Rows}}<tr>
<td><b>{{.Title}}</b><div class="muted" style="font-size:11px">{{.SubjectCN}} · выдан: {{.IssuerCN}} · серийный <code>{{.Serial}}</code></div></td>
<td>{{.Contour}}</td>
<td style="white-space:nowrap">{{.ValidFrom}} — {{.ValidTo}}</td>
<td>{{if eq .Kind "ok"}}<span style="color:var(--ok);font-weight:600">✓ {{.Status}}</span>{{else if eq .Kind "add"}}<span style="color:var(--warn);font-weight:600">⬆ {{.Status}}</span>{{else}}<span class="muted">{{.Status}}</span>{{end}}{{if .RemoveNote}}<div style="font-size:11.5px;margin-top:3px;{{if .RemoveSerial}}color:var(--warn);font-weight:600{{else}}color:var(--muted){{end}}">{{.RemoveNote}}</div>{{end}}</td>
</tr>{{end}}</tbody>
</table>
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-top:10px">
<form method="post" action="/admin/setup/nsdcerts/check" style="margin:0"><input type="hidden" name="section" value="sec-nsdcerts"><button type="submit" class="btn">↻ Сверить со справочником</button></form>
<form method="post" action="/admin/setup/nsdcerts/apply" style="margin:0" onsubmit="return confirm('Применить изменения в справочнике ИШ сейчас? Будет сделан бэкап, ИШ перезапустится на несколько секунд.');"><input type="hidden" name="section" value="sec-nsdcerts"><button type="submit" class="btn btn-ok" {{if not .NSDCerts.Pending}}disabled{{end}}>▶ Применить сейчас{{if .NSDCerts.Pending}} ({{.NSDCerts.Pending}}){{end}}</button></form>
<form method="post" action="/admin/setup/nsdcerts" style="margin:0;display:flex;align-items:center;gap:8px"><input type="hidden" name="section" value="sec-nsdcerts"><label style="display:flex;align-items:center;gap:6px;cursor:pointer;margin:0"><input type="checkbox" name="auto" {{if not .NSDCerts.Manual}}checked{{end}} style="width:auto" onchange="this.form.submit()"> автоматически (при старте и каждые 6 ч)</label></form>
{{if not .NSDCerts.LastRun.IsZero}}<span class="muted" style="font-size:12px">сверено {{.NSDCerts.LastRun.Format "02.01.2006 15:04"}}</span>{{end}}
</div>
{{if .NSDCerts.LastLog}}<pre style="margin-top:10px;font-size:11.5px;white-space:pre-wrap">{{.NSDCerts.LastLog}}</pre>{{end}}
{{if .NSDCerts.Store}}<details style="margin-top:8px"><summary class="muted" style="cursor:pointer;font-size:12px">Содержимое справочника профиля ({{len .NSDCerts.Store}} записей)</summary>
<table style="margin-top:8px;font-size:12px"><thead><tr><th>Владелец</th><th>Издатель</th><th>Серийный</th><th>До</th><th>Хранилище</th></tr></thead><tbody>{{range .NSDCerts.Store}}<tr><td>{{if eq .Kind "crl"}}<span class="muted">CRL</span>{{else}}{{.SubjectCN}}{{if .SelfSigned}} <span class="muted">(УЦ)</span>{{end}}{{end}}</td><td>{{.IssuerCN}}</td><td><code>{{.Serial}}</code></td><td>{{.NotAfter}}</td><td>{{.Store}}</td></tr>{{end}}</tbody></table></details>{{end}}
<p class="muted" style="font-size:12px;margin-top:8px">Свои сертификаты можно положить в <code>/var/lib/bj/.bj/nsd-certs/*.cer</code> — они добавляются по тем же правилам и никогда не удаляются автоматически.</p>
</div>
{{/* Сертификаты УЦ (старый способ: теперь сертификаты НРД приходят комплектом с bj) — только для разработки */}}
{{if devOn}}
<div class="card" id="sec-cacerts">
<h2><span class="dot {{if .Settings.CACerts.URLs}}ok{{else}}warn{{end}}"></span>Сертификаты УЦ (авто-загрузка)</h2>
<p class="muted">URL .cer-файлов УЦ НРД (<a href="https://www.nsd.ru/workflow/system/cryptography/" target="_blank" rel="noopener">nsd.ru</a>). Скачиваются, парсятся и сохраняются в <code>/var/lib/bj/ca-certs/</code>.</p>
<form method="post" action="/admin/setup/cacerts" style="margin-top:10px;display:grid;gap:10px;max-width:720px">
<input type="hidden" name="section" value="sec-cacerts">
<label>URL'ы (один на строку)</label>
<textarea name="urls" rows="3" style="font-family:ui-monospace,monospace;font-size:12px">{{range .Settings.CACerts.URLs}}{{.}}
{{end}}</textarea>
<label style="display:flex;align-items:center;gap:8px;cursor:pointer"><input type="checkbox" name="auto_update" {{if .Settings.CACerts.AutoUpdate}}checked{{end}} style="width:auto"> Авто-обновление раз в сутки</label>
<div style="display:flex;gap:8px"><button type="submit" class="btn">Сохранить</button></div>
</form>
<form method="post" action="/admin/setup/cacerts/fetch" style="margin-top:8px"><input type="hidden" name="section" value="sec-cacerts"><button type="submit" class="btn btn-ok">⬇ Скачать сейчас</button>{{if not .Settings.CACerts.LastFetch.IsZero}}<span class="muted" style="margin-left:10px">обновлено {{.Settings.CACerts.LastFetch.Format "02.01.2006 15:04"}}</span>{{end}}</form>
{{if .Settings.CACerts.FetchedCerts}}
<table style="margin-top:14px">
<thead><tr><th>Владелец</th><th>Тип</th><th>До</th><th>Статус</th></tr></thead>
<tbody>{{range .Settings.CACerts.FetchedCerts}}<tr><td>{{.SubjectCN}}</td><td><code>{{.Store}}</code></td><td>{{if not .NotAfter.IsZero}}{{.NotAfter.Format "02.01.2006"}}{{end}}</td><td>{{if .Error}}<span style="color:var(--err)" title="{{.Error}}">ошибка</span>{{else}}<span style="color:var(--ok)">ок</span>{{end}}</td></tr>{{end}}</tbody>
</table>
{{end}}
</div>
{{end}}
</details>
</div>
</section>
{{/* ============ НРД ============ */}}
<section class="settings-section" id="sec-nsd">
<h1>НРД</h1>
{{template "verline" (.Versions.Get "ish")}}
{{template "nsdStatus" .}}
<div class="card">
<details class="adv-box" {{if not (and .Settings.NSD.OrgINN .Settings.NSD.OrgCode .Settings.NSD.DeponentCode .Settings.NSD.AccountID .Settings.NSD.SectionID .Settings.NSD.IGWBaseURL)}}open{{end}}>
<summary><span class="sum-h">Перенастроить</span><span class="muted">организация, реквизиты в НРД, кто отвечает на переводы, шлюз и контуры, папки шлюза по сети</span></summary>
{{template "orgSection" .}}
{{template "nsdRequisites" .}}
{{template "m2mAutoCard" (withRet . "")}}
<div class="card" id="nsd-igate">
<h2><span class="dot {{if .Settings.NSD.IGWBaseURL}}ok{{else}}warn{{end}}"></span>Шлюз НРД</h2>
<p class="muted">bj сам ставит и настраивает интеграционный шлюз НРД на этом сервере: контуры TEST3 и боевой, переключение между ними, доступ к папкам шлюза по сети.</p>
{{template "igateInstall" .}}
</div>
<div class="card" id="nsd-participant">
<h2>Регистрация участника M2M</h2>
<p class="muted">Анкета участника <code>M2MTransferParticipantForm</code> — НРД требует её перед работой: без регистрации сервис МОСТ отбивает запросы кодом <b>M2M14</b>. bj заполняет анкету по реквизитам фирмы (настройки НРД и справочник участников), даёт проверить и показывает XML, затем отправляет в боевой контур через ИШ (пакет <code>#M2MPF</code>) или письмом на <code>M2MOST@nsd.ru</code>.</p>
<details id="pf-box">
<summary style="cursor:pointer;color:var(--accent)">Открыть анкету участника</summary>
<iframe id="pf-frame" title="Анкета участника M2M" style="width:100%;height:1200px;border:1px solid var(--border);border-radius:10px;margin-top:10px;background:#fff" loading="lazy"></iframe>
<p class="muted" style="font-size:12px;margin:6px 0 0"><a href="/admin/setup/participant-form" target="_blank" rel="noopener">Открыть в отдельной вкладке →</a></p>
</details>
<script>(function(){var d=document.getElementById('pf-box'),f=document.getElementById('pf-frame');
if(d&&f) d.addEventListener('toggle',function(){ if(d.open&&!f.getAttribute('src')) f.setAttribute('src','/admin/setup/participant-form'); });})();</script>
</div>
<div class="card">
<details class="adv-box">
<summary><span class="sum-h">Расширенные настройки</span><span class="muted">шлюз вручную: контур, адрес веб-сервиса, ключевой контейнер, версия ИШ — обычно bj задаёт их сам</span></summary>
<p class="muted" style="margin-top:10px">{{if not .Settings.NSD.IGWBaseURL}}{{if devOn}}<code>mock-режим</code> — без реального ИШ.{{else}}ИШ не настроен — обмен с НРД не выполняется.{{end}}{{else}}Профиль <code>{{.Settings.NSD.Profile}}</code>, ИШ <code>{{.Settings.NSD.IGWBaseURL}}</code>.{{end}}</p>
<form method="post" action="/admin/setup/nsd" style="margin-top:12px;display:grid;gap:10px;max-width:680px">
<input type="hidden" name="keep_requisites" value="1">
<label>Профиль / контур</label>
<select name="profile" id="nsd-profile">
{{/* mock и GUEST — для разработчиков; у клиента — только контуры НРД */}}
<option value="" {{if eq .Settings.NSD.Profile ""}}selected{{end}}>{{if devOn}}— mock (демо) —{{else}}— не выбран —{{end}}</option>
{{if or devOn (eq .Settings.NSD.Profile "guest-gost") (eq .Settings.NSD.Profile "guest-rsa")}} <option value="guest-gost" {{if eq .Settings.NSD.Profile "guest-gost"}}selected{{end}}>guest-gost — GUEST, ГОСТ</option>
<option value="guest-rsa" {{if eq .Settings.NSD.Profile "guest-rsa"}}selected{{end}}>guest-rsa — GUEST, RSA</option>{{end}}
<option value="test3-gost" {{if eq .Settings.NSD.Profile "test3-gost"}}selected{{end}}>test3-gost — тестовый контур НРД (TEST3), ГОСТ</option>
<option value="test3-rsa" {{if eq .Settings.NSD.Profile "test3-rsa"}}selected{{end}}>test3-rsa — тестовый контур НРД (TEST3), RSA</option>
<option value="prod-gost" {{if eq .Settings.NSD.Profile "prod-gost"}}selected{{end}}>prod-gost — боевой контур НРД, ГОСТ</option>
<option value="prod-rsa" {{if eq .Settings.NSD.Profile "prod-rsa"}}selected{{end}}>prod-rsa — боевой контур НРД, RSA</option>
</select>
<label>URL ONYX (WSL) НРД</label>
<input type="text" name="igw_base_url" id="nsd-url" value="{{.Settings.NSD.IGWBaseURL}}" placeholder="автозаполнится по профилю">
<label>Ключевой контейнер</label>
<input type="text" name="key_container" id="nsd-container" value="{{.Settings.NSD.KeyContainer}}" placeholder="напр. TEST3_GOST_CONTAINER">
<label style="display:flex;align-items:flex-start;gap:9px;font-weight:400;margin-top:6px;cursor:pointer">
<input type="checkbox" name="igw_v904" style="width:auto;margin-top:3px" {{if .Settings.NSD.IGWv904}}checked{{end}}>
<span>ИШ версии <strong>107.0.0.904+</strong> (Astra 107.0.907+)<br>
<span class="muted" style="font-size:12px">С этой версии в <code>config.xml</code> добавляется <code>businessLine=MOST</code> (обязательно для M2M по Указанию ЦБ 7317-У). На версиях младше — не включать.</span></span>
</label>
<div><button type="submit" class="btn">Сохранить и проверить</button></div>
</form>
<script>
(function() {
var urls = {
"guest-gost": ["https://gost-gt.nsd.ru/onyx-ms/OnyxEdoWSService/OnyxEdo", "GUEST_GOST_CONTAINER"],
"guest-rsa": ["https://rsa-gt.nsd.ru/onyx-ms/OnyxEdoWSService/OnyxEdo", "GUEST_RSA_CONTAINER"],
"test3-gost": ["https://gost-t3.nsd.ru/onyx-ms/OnyxEdoWSService/OnyxEdo", "TEST3_GOST_CONTAINER"],
"test3-rsa": ["https://rsa-t3.nsd.ru/onyx-ms/OnyxEdoWSService/OnyxEdo", "TEST3_RSA_CONTAINER"],
"prod-gost": ["https://gost.nsd.ru/onyx-ms/OnyxEdoWSService/OnyxEdo", "PROD_GOST_CONTAINER"],
"prod-rsa": ["https://rsa.nsd.ru/onyx-ms/OnyxEdoWSService/OnyxEdo", "PROD_RSA_CONTAINER"]
};
var p = document.getElementById("nsd-profile"), u = document.getElementById("nsd-url"), c = document.getElementById("nsd-container");
if (p) p.addEventListener("change", function() {
var v = p.value;
if (urls[v] && (!u.value || confirm("Заменить URL и контейнер на дефолт для " + v + "?"))) { u.value = urls[v][0]; c.value = urls[v][1]; }
});
})();
</script>
</details>
</div>
</details>
</div>
</section>
{{/* ============ ЛИЧНЫЙ КАБИНЕТ ============ */}}
{{/* ============ КАБИНЕТ КЛИЕНТА (наш модуль bj-lk) ============ */}}
{{if mod "web-cabinet"}}
<section class="settings-section" id="sec-cabinet">
<h1>Кабинет клиента</h1>
<p class="muted" style="margin:-8px 0 16px">Наш личный кабинет для клиентов депозитария: модуль <code>bj-lk</code> на своём сервере или PHP-хостинге, связь с ядром, учётные записи клиентов и сотрудников. Интеграция с платформой ЕСИА Финанс — отдельный раздел.</p>
{{template "verline" (.Versions.Get "bj-lk")}}
<div class="card" id="sec-lk-dist">
<h2><span class="dot {{if .Settings.LK.APIKey}}ok{{else}}warn{{end}}"></span>Кабинет клиента на своём сервере</h2>
<p class="muted">Кабинет (<code>bj-lk</code>) — отдельная программа для веб-сервера депозитария или хостинга. Своей базы у неё нет: данные она берёт у ядра по защищённому каналу и не хранит дольше запроса. Порядок: <b>1)</b> создайте ключ доступа, <b>2)</b> выберите способ связи, <b>3)</b> скачайте архив и распакуйте его на сервере кабинета, <b>4)</b> запустите программу (<code>sudo ./install.sh</code> заводит службу), <b>5)</b> откройте адрес кабинета в браузере — установщик проверит сервер, спросит ключ, проверит связь и заведёт администратора, <b>6)</b> опубликуйте кабинет через nginx с сертификатом (пример в README архива).</p>
<form method="post" action="/admin/setup/lk/dist" style="display:grid;gap:14px;max-width:780px;margin-top:12px">
<input type="hidden" name="section" value="sec-lk-dist"><input type="hidden" name="core_url_set" value="1"><input type="hidden" name="mode_set" value="1">
<div class="fld">
<label>Ключ доступа модуля</label>
<div style="display:flex;gap:8px;align-items:center;margin-top:4px;flex-wrap:wrap">
<code style="flex:1;min-width:260px;overflow-wrap:anywhere">{{if .LKKeyMask}}{{.LKKeyMask}}{{else}}не создан — кабинет не сможет обращаться к ядру{{end}}</code>
<button type="submit" name="op" value="newkey" class="btn btn-secondary">{{if .Settings.LK.APIKey}}Сменить ключ{{else}}Создать ключ{{end}}</button>
{{if .Settings.LK.APIKey}}<button type="submit" name="op" value="clearkey" class="btn btn-secondary" onclick="return confirm('Удалить ключ? Кабинет перестанет получать данные, пока не впишете новый.')">Удалить</button>{{end}}
</div>
<span class="muted" style="font-size:12px">В архив ключ не попадает: его вводят в установщике кабинета (или вписывают в <code>lk.env</code> при установке без браузера). Здесь он показан целиком — понадобится при переустановке кабинета.</span>
</div>
<div class="fld" style="gap:2px">
<label style="margin-bottom:6px">Как кабинет и ядро находят друг друга</label>
<label style="display:flex;gap:8px;align-items:flex-start;font-weight:600"><input type="radio" name="mode" value="direct" {{if not .Settings.LK.AgentMode}}checked{{end}}> <span>Кабинет обращается к ядру <span class="muted" style="font-weight:400">— обычный случай: у ядра есть адрес, доступный серверу кабинета.</span></span></label>
<label style="display:flex;gap:8px;align-items:flex-start;font-weight:600;margin-top:6px"><input type="radio" name="mode" value="agent" {{if .Settings.LK.AgentMode}}checked{{end}}> <span>Ядро подключается к кабинету <span class="muted" style="font-weight:400">— если у ядра нет адреса в интернете. Ядро само звонит кабинету по его имени, забирает запросы клиентов и возвращает ответы; входящих соединений и открытых портов не требуется.</span></span></label>
</div>
<div class="fld">
<label>Адрес ядра для кабинета <span class="muted" style="font-weight:400">— для режима «кабинет обращается к ядру»</span></label>
<input type="text" name="core_url" value="{{.Settings.LK.CoreURL}}" placeholder="https://bj.example.ru" style="margin-top:4px">
<span class="muted" style="font-size:12px">Куда кабинет обращается за данными. Если кабинет и ядро в одной сети — внутренний адрес; если в разных — тот, что открыт межсетевым экраном. Только <code>https</code>: по этому каналу идут ключ и сессии клиентов.</span>
</div>
<div class="fld">
<label>Адрес кабинета на хостинге <span class="muted" style="font-weight:400">— для режима «ядро подключается к кабинету»</span></label>
<input type="text" name="cabinet_url" value="{{.Settings.LK.CabinetURL}}" placeholder="https://lk.example.ru" style="margin-top:4px">
<span class="muted" style="font-size:12px">Имя, по которому ядро дозванивается до кабинета. Оно же — адрес, по которому входят клиенты.</span>
</div>
<label style="display:flex;gap:8px;align-items:flex-start;font-weight:600"><input type="checkbox" name="bind_network" value="1" {{if .Settings.LK.BindNetwork}}checked{{end}}> <span>Привязывать сессию клиента к его сети <span class="muted" style="font-weight:400">— к браузеру сессия привязана всегда; эта отметка добавляет сверку сети. Строже, но клиентов с мобильным интернетом будет выбрасывать при смене адреса оператором.</span></span></label>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<button type="submit" class="btn btn-ok">Сохранить</button>
{{if .Settings.LK.APIKey}}<a class="btn" href="/admin/setup/lk/dist.tar.gz">Скачать для своего сервера (Linux)</a>
<a class="btn" href="/admin/setup/lk/relay-php.zip">Скачать для PHP-хостинга</a>{{end}}
<a class="btn btn-secondary" href="/admin/help/lk-api">Инструкция по размещению</a>
</div>
</form>
{{if .Settings.LK.AgentMode}}
<div style="margin-top:14px;padding:12px 14px;border-radius:8px;background:var(--bg-soft,#f6f7f9)">
<b>Связь с кабинетом</b> <span class="dot {{if .LKAgent.Connected}}ok{{else}}err{{end}}"></span>
{{if .LKAgent.Connected}}ядро подключено к {{.LKAgent.Cabinet}}{{else}}ядро не дозвонилось до кабинета{{end}}
<div class="muted" style="font-size:12px;margin-top:4px">
{{if .LKAgent.LastOK}}Последний обмен: {{.LKAgent.LastOK}}. {{end}}Обслужено запросов: {{.LKAgent.Served}}.
{{if .LKAgent.LastErr}}<br>Ошибка: {{.LKAgent.LastErr}}{{end}}
</div>
</div>
{{end}}
<p class="muted" style="font-size:12px;margin-top:10px"><b>Где собираются страницы.</b> Для PHP-хостинга — всегда в ядре: на хостинге лежит только ретранслятор (index.php), который передаёт запросы браузера ядру. Для своего сервера установщик спросит: собирать страницы в ядре (тогда программа на сервере — тоже ретранслятор, новые разделы появляются обновлением ядра) или на месте. Ключа ни в одном архиве нет.</p>
<p class="muted" style="font-size:12px;margin-top:6px">В архиве для своего сервера: исполняемый файл, служба systemd (отдельный пользователь, ограничения доступа к файловой системе), файл настроек службы, установщик и README. Ключа в архиве нет — его спрашивает веб-установщик. Кабинет входит в модуль «Личный кабинет клиента» и обновляется отдельно от ядра.</p>
</div>
<div class="card" id="sec-lk-preview">
<h2><span class="dot {{if .LKPreview.On}}ok{{else}}warn{{end}}"></span>Кабинет на этом сервере — для просмотра</h2>
<p class="muted">Посмотреть кабинет глазами клиента можно, не размещая его на хостинге: ядро поднимает тот же кабинет на отдельном порту. Открывается по http из сети депозитария, поэтому подходит для проверки, но не для клиентов из интернета — им кабинет публикуется через хостинг или nginx с сертификатом.</p>
<form method="post" action="/admin/setup/lk/preview" style="display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap;margin-top:10px">
<input type="hidden" name="section" value="sec-lk-preview">
<label style="display:flex;gap:8px;align-items:center;font-weight:600;margin:0"><input type="checkbox" name="preview_on" value="1" {{if .Settings.LK.PreviewOn}}checked{{end}}> Запустить кабинет на этом сервере</label>
<div class="fld"><label>Порт</label><input type="text" name="preview_listen" value="{{.LKPreview.Listen}}" style="width:120px"></div>
<button type="submit" class="btn btn-ok">Применить</button>
{{if .LKPreview.On}}<a class="btn" href="{{.LKPreview.URL}}" target="_blank" rel="noopener">Открыть кабинет →</a>{{end}}
</form>
{{if .LKPreview.Err}}<p class="muted" style="color:var(--err);font-size:12px;margin-top:8px">Не запущен: {{.LKPreview.Err}}</p>{{end}}
<p class="muted" style="font-size:12px;margin-top:8px">Вход — по учётным записям из карточки «Пользователи кабинета» ниже; сотрудник видит список клиентов, клиент — свой портфель. Данные те же, что получит кабинет на хостинге.</p>
</div>
<div class="card" id="sec-lk-data">
<h2><span class="dot {{if .LKData.SnapshotOn}}ok{{else}}warn{{end}}"></span>Данные кабинета: курсы и история</h2>
<p class="muted">Чем кабинет считает стоимость портфеля. Курс нужен для выпусков с валютным номиналом (замещающие и еврооблигации): цена по ним — процент от номинала в долларах, юанях, евро. История стоимости накапливается только у нас: ни торговая система, ни брокерский учёт её не хранят.</p>
<form method="post" action="/admin/setup/lk/data" style="display:grid;gap:14px;max-width:780px;margin-top:12px">
<input type="hidden" name="section" value="sec-lk-data">
<div class="fld">
<label for="lk_fx_source">Курсы валют</label>
<select id="lk_fx_source" name="fx_source">
<option value="cbr" {{if ne .LKData.FXSource "accounting"}}selected{{end}}>Банк России — официальный курс, открытый канал без ключа</option>
<option value="accounting" {{if eq .LKData.FXSource "accounting"}}selected{{end}}>Учётная система — курс из OD_VALUES_RATES (DEPO / SPECTRE)</option>
</select>
<span class="muted">{{if .LKData.FXAt}}Последний известный комплект: {{.LKData.FXAt}}, валют {{.LKData.FXCount}}.{{else}}Курсы ещё не загружались.{{end}} Полученные курсы сохраняются в базе bj по дням, поэтому при потере связи кабинет продолжит считать по последнему известному, а не покажет «нет курса». Курс учётной системы в нашей копии отстаёт на месяцы — выбирайте его только на сервере без выхода в интернет.{{with .LKData.FXNote}} {{.}}{{end}}</span>
</div>
<div class="fld">
<label>Ежедневный срез стоимости портфелей</label>
<div style="display:flex;gap:14px;align-items:center;flex-wrap:wrap">
<select name="snapshot_mode" style="width:auto;min-width:320px">
<option value="" {{if eq .LKData.SnapshotMode ""}}selected{{end}}>Автоматически — ведём, если настроен экспорт QUIK{{if .LKData.SnapshotOn}} (сейчас ведём){{else}} (сейчас нет: экспорт не настроен){{end}}</option>
<option value="on" {{if eq .LKData.SnapshotMode "on"}}selected{{end}}>Всегда вести</option>
<option value="off" {{if eq .LKData.SnapshotMode "off"}}selected{{end}}>Не вести</option>
</select>
<label style="display:flex;gap:7px;align-items:center;font-weight:400;text-transform:none;letter-spacing:0">время
<input type="text" name="snapshot_at" value="{{.LKData.SnapshotAt}}" style="width:90px" placeholder="20:30"></label>
</div>
<span class="muted">{{if .LKData.Snapshots}}Накоплено дней: {{.LKData.Snapshots}}; последний срез {{.LKData.SnapLast}} — договоров {{.LKData.SnapRows}}.{{else}}Срезов пока нет: график в кабинете появится после первых двух дней.{{end}} Срез считает стоимость каждого портфеля по экспорту QUIK один раз в день, после закрытия торгов. Задним числом историю восстановить нельзя, поэтому включать лучше сразу.</span>
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<button class="btn" type="submit">Сохранить</button>
<button class="btn btn-secondary" type="submit" name="op" value="fx_now">Обновить курсы сейчас</button>
<button class="btn btn-secondary" type="submit" name="op" value="snapshot_now">Снять срез сейчас</button>
</div>
</form>
</div>
<div class="card" id="sec-lk-users">
<h2><span class="dot {{if .LKUsers}}ok{{else}}warn{{end}}"></span>Пользователи кабинета</h2>
<p class="muted">Учётные записи кабинета живут здесь, в базе ядра: кабинет на хостинге своей базы не имеет. Клиент привязан к номеру договора депозитария — по нему кабинет показывает портфель. Сотрудник («администратор кабинета») видит список клиентов и открывает их кабинеты на просмотр. <b>Письма не отправляются</b>: почта только хранится, пароль сообщите лично.</p>
{{if .LKUsersError}}<div class="alert alert-warn">База кабинета недоступна: {{.LKUsersError}}</div>{{end}}
{{if .LKUsers}}
<div style="overflow-x:auto"><table style="margin-top:10px">
<tr><th>Логин</th><th>Договор</th><th>Имя</th><th>Почта</th><th>Роль</th><th>Поручения</th><th>Состояние</th><th></th></tr>
{{range .LKUsers}}<tr>
<td><code>{{.Login}}</code></td><td>{{.Contract}}</td><td>{{.FullName}}</td><td>{{.Email}}</td>
<td>{{if .IsAdmin}}сотрудник{{else}}клиент{{end}}</td>
<td>{{if .CanTransfer}}разрешены{{else}}<span class="muted">просмотр</span>{{end}}</td>
<td>{{if .Active}}<span class="dot ok"></span>активна{{else}}<span class="dot err"></span>заблокирована{{end}}</td>
<td style="white-space:nowrap">
<form method="post" action="/admin/setup/lk/users" style="display:inline"><input type="hidden" name="section" value="sec-lk-users"><input type="hidden" name="id" value="{{.ID}}">
{{if .Active}}<button class="btn btn-secondary btn-sm" name="op" value="block">Заблокировать</button>{{else}}<button class="btn btn-secondary btn-sm" name="op" value="unblock">Разблокировать</button>{{end}}
{{if .IsAdmin}}<button class="btn btn-secondary btn-sm" name="op" value="admin_off">Снять права сотрудника</button>{{else}}<button class="btn btn-secondary btn-sm" name="op" value="admin_on">Сделать сотрудником</button>
{{if .CanTransfer}}<button class="btn btn-secondary btn-sm" name="op" value="transfer_off">Запретить поручения</button>{{else}}<button class="btn btn-secondary btn-sm" name="op" value="transfer_on">Разрешить поручения</button>{{end}}{{end}}
<button class="btn btn-secondary btn-sm" name="op" value="delete" onclick="return confirm('Удалить учётную запись {{.Login}}? Поручения и журнал останутся.')">Удалить</button>
</form>
</td>
</tr>{{end}}
</table></div>
{{else}}{{if not .LKUsersError}}<p class="muted">Учётных записей пока нет. Первого администратора можно завести из установщика кабинета или здесь.</p>{{end}}{{end}}
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:16px;margin-top:14px">
<form method="post" action="/admin/setup/lk/users" style="display:grid;gap:8px">
<input type="hidden" name="section" value="sec-lk-users"><input type="hidden" name="op" value="create">
<b>Новая учётная запись</b>
<input type="text" name="login" placeholder="Логин (латиницей или номер договора)" required autocomplete="off">
<input type="text" name="contract" placeholder="Номер договора в депозитарии, например БР-859" autocomplete="off">
<input type="text" name="full_name" placeholder="ФИО" autocomplete="off">
<input type="email" name="email" placeholder="Электронная почта (не рассылается)" autocomplete="off">
<input type="text" name="phone" placeholder="Телефон" autocomplete="off">
<input type="password" name="password" placeholder="Пароль: от 10 символов, буквы и цифра" required autocomplete="new-password">
<label style="font-weight:400"><input type="checkbox" name="is_admin" value="1"> сотрудник депозитария (видит клиентов, договор не нужен)</label>
<label style="font-weight:400"><input type="checkbox" name="can_transfer" value="1"> разрешить подачу поручений (по умолчанию — только просмотр)</label>
<div><button type="submit" class="btn btn-ok">Создать</button></div>
</form>
<form method="post" action="/admin/setup/lk/users" style="display:grid;gap:8px;align-content:start">
<input type="hidden" name="section" value="sec-lk-users"><input type="hidden" name="op" value="password">
<b>Сменить пароль</b>
<input type="text" name="login" placeholder="Логин" required autocomplete="off">
<input type="password" name="password" placeholder="Новый пароль" required autocomplete="new-password">
<span class="muted" style="font-size:12px">Все сессии пользователя будут завершены.</span>
<div><button type="submit" class="btn btn-secondary">Сменить</button></div>
</form>
</div>
</div>
</section>
{{end}}
{{/* ============ ЕСИА ФИНАНС (внешняя платформа ЛК) ============ */}}
{{if mod "esia-finance"}}
<section class="settings-section" id="sec-lk">
<h1>ЕСИА Финанс</h1>
<p class="muted" style="margin:-8px 0 16px">Интеграция с внешней платформой личного кабинета <strong>ЕСИА Финанс</strong>: адрес для клиента, приём заявок M2M, возврат статуса и подключение к API (тест/бой). Наш собственный кабинет настраивается в разделе «Кабинет клиента».</p>
<div class="card">
<h2><span class="dot {{if .Settings.LK.PublicURL}}ok{{else}}warn{{end}}"></span>Личный кабинет — интеграция</h2>
<p class="muted"><b>Адрес ЛК</b> — ссылка, по которой клиент входит в кабинет (для кнопки «Открыть» и дашборда). Заявками M2M bj и ЛК обмениваются одной из двух моделей: <b>pull</b> — bj сам забирает заявки из API ЛК и туда же возвращает статус (блок «Подключение к API ЛК», тест/бой); <b>push</b> — ЛК сам шлёт заявки на bj (<code>POST /api/v1/back_office/claims/</code>), а статус bj возвращает на <b>Callback URL</b> (ниже, свёрнуто).</p>
<form method="post" action="/admin/setup/lk" style="margin-top:12px;display:grid;gap:12px;max-width:640px">
<div class="fld">
<label>Адрес ЛК</label>
<div style="display:flex;gap:8px;align-items:center;margin-top:4px">
<input type="text" name="lk_public_url" value="{{.Settings.LK.PublicURL}}" placeholder="https://lk.example.ru" style="flex:1">
{{if .Settings.LK.PublicURL}}<a class="btn btn-secondary" href="{{.Settings.LK.PublicURL}}" target="_blank" rel="noopener">Открыть →</a>{{end}}
</div>
<span class="muted" style="font-size:12px">{{if devOn}}Показывается ссылкой на дашборде для первичного теста.{{else}}Адрес, по которому клиенты входят в личный кабинет.{{end}}</span>
</div>
<details {{if .Settings.LK.CallbackURL}}open{{end}} class="fld">
<summary style="cursor:pointer;color:var(--accent);font-size:13px">Callback URL — push-модель (ЛК сам шлёт заявки нам)</summary>
<div style="margin-top:8px">
<label>Callback URL <span class="muted" style="font-weight:400">(куда bj шлёт статус заявки)</span></label>
<input type="text" name="callback_url" value="{{.Settings.LK.CallbackURL}}" placeholder="https://lk.example.ru" style="margin-top:4px">
<span class="muted" style="font-size:12px">{{if not .Settings.LK.CallbackURL}}Не настроен. Нужен только в push-модели: когда ЛК сам шлёт заявки на bj. При подключении через API ЛК (pull) не используется. Для обмена с НРД не требуется.{{end}}</span>
</div>
</details>
<div><button type="submit" class="btn">Сохранить и проверить</button></div>
</form>
</div>
<div class="card">
<h2>Подключение к API ЛК (pull) — тест и бой</h2>
<p class="muted"><b>Основная модель.</b> Реквизиты, по которым bj сам обращается к API платформы ЛК ЕСИА Финанс (HTTP Basic): забирает заявки и туда же возвращает статус. Заполните нужные окружения и нажмите <strong>«Проверить связь»</strong> — сразу видно, есть подключение или нет.</p>
<div class="grid" style="grid-template-columns:repeat(auto-fit,minmax(300px,1fr));margin-top:12px">
{{/* --- Тест --- */}}
<div style="border:1px solid var(--border);border-radius:10px;padding:16px;background:var(--card-2)">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:10px">
<strong>Тестовый стенд</strong>
{{if .Settings.LK.Test.LastMsg}}<span class="badge {{if .Settings.LK.Test.LastOK}}confirmed{{else}}rejected{{end}}">{{if .Settings.LK.Test.LastOK}}связь есть{{else}}нет связи{{end}}</span>{{end}}
{{if eq .Settings.LK.ActiveEnv "test"}}<span class="badge" style="margin-left:auto">активно</span>{{end}}
</div>
<form method="post" action="/admin/setup/lk-integration" style="display:grid;gap:9px">
<input type="hidden" name="env" value="test">
<div><label>Адрес API</label><input type="text" name="base_url" value="{{.Settings.LK.Test.BaseURL}}" placeholder="https://lk.example.ru/manager/api" style="margin-top:3px"></div>
<div><label>Логин</label><input type="text" name="login" value="{{.Settings.LK.Test.Login}}" autocomplete="off" style="margin-top:3px"></div>
<div><label>Пароль</label><input type="password" name="password" value="" placeholder="{{if .Settings.LK.Test.Password}}•••••• (не менять — оставьте пустым){{end}}" autocomplete="new-password" style="margin-top:3px"></div>
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:2px">
<button type="submit" class="btn btn-secondary">Сохранить</button>
<button type="submit" formaction="/admin/setup/lk-check" class="btn">Проверить связь</button>
</div>
</form>
{{if .Settings.LK.Test.LastMsg}}<p class="muted" style="font-size:12px;margin-top:8px">Последняя проверка: {{.Settings.LK.Test.LastMsg}} · {{.Settings.LK.Test.LastAt.Format "02.01 15:04"}}</p>{{end}}
</div>
{{/* --- Бой --- */}}
<div style="border:1px solid var(--border);border-radius:10px;padding:16px;background:var(--card-2)">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:10px">
<strong>Боевой контур</strong>
{{if .Settings.LK.Prod.LastMsg}}<span class="badge {{if .Settings.LK.Prod.LastOK}}confirmed{{else}}rejected{{end}}">{{if .Settings.LK.Prod.LastOK}}связь есть{{else}}нет связи{{end}}</span>{{end}}
{{if eq .Settings.LK.ActiveEnv "prod"}}<span class="badge" style="margin-left:auto">активно</span>{{end}}
</div>
<form method="post" action="/admin/setup/lk-integration" style="display:grid;gap:9px">
<input type="hidden" name="env" value="prod">
<div><label>Адрес API</label><input type="text" name="base_url" value="{{.Settings.LK.Prod.BaseURL}}" placeholder="https://lk.example.ru/manager/api" style="margin-top:3px"></div>
<div><label>Логин</label><input type="text" name="login" value="{{.Settings.LK.Prod.Login}}" autocomplete="off" style="margin-top:3px"></div>
<div><label>Пароль</label><input type="password" name="password" value="" placeholder="{{if .Settings.LK.Prod.Password}}•••••• (не менять — оставьте пустым){{end}}" autocomplete="new-password" style="margin-top:3px"></div>
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:2px">
<button type="submit" class="btn btn-secondary">Сохранить</button>
<button type="submit" formaction="/admin/setup/lk-check" class="btn">Проверить связь</button>
</div>
</form>
{{if .Settings.LK.Prod.LastMsg}}<p class="muted" style="font-size:12px;margin-top:8px">Последняя проверка: {{.Settings.LK.Prod.LastMsg}} · {{.Settings.LK.Prod.LastAt.Format "02.01 15:04"}}</p>{{end}}
</div>
</div>
<form method="post" action="/admin/setup/lk-integration" style="margin-top:14px;display:flex;gap:10px;align-items:center;flex-wrap:wrap">
<input type="hidden" name="env" value="test">
<label style="font-weight:500">Активное окружение:</label>
<select name="active_env" onchange="this.form.submit()">
<option value="test" {{if eq .Settings.LK.ActiveEnv "test"}}selected{{end}}>Тест</option>
<option value="prod" {{if eq .Settings.LK.ActiveEnv "prod"}}selected{{end}}>Бой</option>
</select>
<span class="muted" style="font-size:12px">bj работает с ЛК через выбранное окружение.</span>
{{if devOn}} <input type="hidden" name="emulate_nsd_form" value="1">
<label style="display:flex;align-items:flex-start;gap:8px;font-weight:400;flex-basis:100%;margin-top:6px"><input type="checkbox" name="emulate_nsd" style="width:auto;margin-top:3px" {{if .Settings.LK.EmulateNSD}}checked{{end}} onchange="this.form.submit()"> <span><b>Имитация НРД</b> — заявки ЛК проходят сверку с DEPO, блокировку в QUIK и получают статус в ЛК, но в НРД не отправляются: ответ контрагента имитирует bj.{{if and (eq .Settings.LK.ActiveEnv "prod") (not .Settings.LK.EmulateNSD)}} <span style="color:var(--warn)">Сейчас включена принудительно: боевой ЛК при тестовом профиле ИШ ({{.Settings.NSD.Profile}}).</span>{{end}}</span></label>{{end}}
</form>
<div id="lk-portfolio" style="margin-top:14px;padding:12px 14px;border:1px solid var(--border);border-radius:10px">
<b>Заполнение портфеля ДЕПО бумагами из DEPO</b> {{if .Settings.LK.PortfolioSync.Enabled}}<span class="badge confirmed">включено</span>{{else}}<span class="badge draft">выключено</span>{{end}}
<p class="muted" style="margin:6px 0 8px;font-size:13px">Каждые 10 секунд, в том же проходе, что и приём заявок, bj сверяет портфель клиента в DEPO и отправляет его в ЛК (<code>/portfolio/import</code>, портфель ДЕПО) — только если остатки изменились. Номер гос. регистрации — из экспорта QUIK, раздел счёта — «Основной», хранение — НРД. Бумаги без номера гос. регистрации не отправляются (ЛК отвергает весь импорт).</p>
<form method="post" action="/admin/setup/lk-portfolio" style="margin:0 0 8px">
{{if .Settings.LK.PortfolioSync.Enabled}}<button class="btn" type="submit" name="action" value="off" style="background:var(--card);color:var(--err);border:1px solid var(--err)">Выключить заполнение портфеля ДЕПО бумагами</button>
<button class="btn" type="submit" name="action" value="sync">Заполнить сейчас</button>
{{else}}<button class="btn btn-ok" type="submit" name="action" value="on" onclick="if(!this.form.pairs.value.trim()){alert('Укажите договор ЛК и портфель DEPO, например ДД-816=БР-859');return false}">Включить заполнение портфеля ДЕПО бумагами</button>{{end}}
<div style="display:flex;gap:10px;align-items:flex-end;flex-wrap:wrap;margin-top:8px">
<label style="display:flex;flex-direction:column;gap:3px;font-size:12px;min-width:260px">Каких клиентов заполнять: договор ЛК = портфель DEPO (через «;»)
<input name="pairs" value="{{.Settings.LK.PortfolioSync.Pairs}}" placeholder="ДД-816=БР-859"></label>
<button class="btn" type="submit" name="action" value="save">Сохранить список</button>
</div>
</form>
<details style="margin:8px 0 0"><summary style="cursor:pointer;font-size:12.5px">Именование у депозитария (счета, договоры, раздел, хранение)</summary>
<form method="post" action="/admin/setup/lk-portfolio" style="display:flex;gap:10px;align-items:flex-end;flex-wrap:wrap;margin-top:8px">
<input type="hidden" name="naming" value="1">
<label style="display:flex;flex-direction:column;gap:3px;font-size:12px">Префикс торгового счёта в DEPO
<input name="account_prefix" value="{{.Settings.LK.PortfolioSync.AccountPrefix}}" placeholder="{{depoAccPrefix}}" style="width:120px"></label>
<label style="display:flex;flex-direction:column;gap:3px;font-size:12px">Номер договора депо в ЛК ({n} — номер счёта)
<input name="contract_template" value="{{.Settings.LK.PortfolioSync.ContractTemplate}}" placeholder="ДД-{n}" style="width:160px"></label>
<label style="display:flex;flex-direction:column;gap:3px;font-size:12px">Раздел счёта
<input name="section" value="{{.Settings.LK.PortfolioSync.Section}}" placeholder="Основной" style="width:130px"></label>
<label style="display:flex;flex-direction:column;gap:3px;font-size:12px">Место хранения
<input name="storage" value="{{.Settings.LK.PortfolioSync.Storage}}" placeholder="НРД" style="width:110px"></label>
<button class="btn" type="submit" name="action" value="save">Сохранить</button>
</form>
<p class="muted" style="font-size:12px;margin:4px 0 0">Пусто — значение по умолчанию (серым). Пример: счёт ВТНКЦ00000816 и шаблон ДД-{n} → договор ДД-816.</p>
</details>
<form method="post" action="/admin/setup/lk-portfolio" id="lkpf-client" style="margin:10px 0 0;padding-top:10px;border-top:1px dashed var(--border)">
<div style="font-size:13px;font-weight:600;margin-bottom:4px">Залить одному клиенту (для теста)</div>
<div style="display:flex;gap:10px;align-items:flex-end;flex-wrap:wrap">
<label style="display:flex;flex-direction:column;gap:3px;font-size:12px;min-width:320px">Клиент DEPO — ФИО, портфель или счёт
<input id="lkpf-q" autocomplete="off" placeholder="например, Погонщикова или БР-859"></label>
<input type="hidden" name="client_port" id="lkpf-port"><input type="hidden" name="client_lk" id="lkpf-lk">
<button class="btn" type="submit" name="action" value="client_push" id="lkpf-push" disabled>Залить сейчас</button>
<button class="btn" type="submit" name="action" value="client_add" id="lkpf-add" disabled>Добавить в постоянное заполнение</button>
</div>
<div id="lkpf-list" style="margin-top:6px;font-size:12.5px"></div>
<div id="lkpf-sel" class="muted" style="margin-top:4px;font-size:12.5px"></div>
</form>
<script>
(function(){
var q=document.getElementById('lkpf-q'), list=document.getElementById('lkpf-list'), sel=document.getElementById('lkpf-sel'), t;
function esc(s){return String(s).replace(/[&<>"]/g,function(c){return{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]})}
function pick(c){
document.getElementById('lkpf-port').value=c.port; document.getElementById('lkpf-lk').value=c.lk;
var ok=!!c.lk; document.getElementById('lkpf-push').disabled=!ok; document.getElementById('lkpf-add').disabled=!ok;
sel.innerHTML=ok?('Выбран: <b>'+esc(c.name)+'</b> · портфель DEPO '+esc(c.port)+' · счёт '+esc(c.account)+' → договор в ЛК <b>'+esc(c.lk)+'</b>'):'У клиента нет торгового счёта — договор в ЛК не определить';
list.innerHTML='';
}
q.addEventListener('input',function(){
clearTimeout(t); var v=q.value.trim(); if(v.length<2){list.innerHTML='';return}
t=setTimeout(function(){
fetch('/admin/setup/lk-portfolio/clients?q='+encodeURIComponent(v),{cache:'no-store'}).then(function(r){return r.json()}).then(function(d){
if(d.error){list.innerHTML='<span style="color:var(--err)">'+esc(d.error)+'</span>';return}
if(!d||!d.length){list.innerHTML='<span class="muted">не найдено</span>';return}
list.innerHTML=''; d.forEach(function(c){
var a=document.createElement('a'); a.href='#'; a.style.display='block'; a.style.padding='2px 0';
a.innerHTML=esc(c.name)+' · '+esc(c.port)+' · '+esc(c.account)+(c.lk?' → '+esc(c.lk):'');
a.onclick=function(e){e.preventDefault(); pick(c)}; list.appendChild(a);
});
}).catch(function(){list.innerHTML='<span style="color:var(--err)">поиск недоступен</span>'});
},300);
});
})();
</script>
{{range lkPortfolioStates}}<div class="muted" style="font-size:12.5px;margin-top:8px"><b>{{.LK}}</b> ← {{.Depo}}: {{if .Err}}<span style="color:var(--err)">ошибка — {{.Err}}</span>{{else if .PushedAt.IsZero}}сверено {{.CheckedAt.Format "15:04:05"}}, отправки ещё не было{{else}}отправлено {{.PushedAt.Format "02.01 15:04:05"}} ({{.Lines}} бумаг), сверено {{.CheckedAt.Format "15:04:05"}}{{end}}{{range .Skipped}}<br>пропущено: {{.}}{{end}}</div>{{end}}
</div>
<div id="lk-real-nsd" style="margin-top:14px;padding:12px 14px;border:1px solid var(--border);border-radius:10px;{{if .Settings.LK.RealNSDTest.Enabled}}background:var(--warn-weak){{end}}">
<b>Тест «боевой ЛК → тестовый контур НРД»</b> {{if .Settings.LK.RealNSDTest.Enabled}}<span class="badge awaiting_decision">включён</span>{{else}}<span class="badge draft">выключен</span>{{end}}
<p class="muted" style="margin:6px 0 8px;font-size:13px">Заявки клиентов из списка проходят у нас всё по-настоящему: сверка и поручение в DEPO, блокировка и списание в QUIK МНП (<b>боевая запись</b>), статусы в ЛК. В НРД (тестовый контур) запрос уходит с данными тест-инвестора <b>{{realNSDAs}}</b>; GUID, бумаги и счета — настоящие, ответы НРД применяются к заявке клиента. «Исполнена» в ЛК — по отчёту НРД об исполнении (MS010). Остальные заявки ЛК — как раньше. Включайте до того, как клиент подпишет заявку.</p>
<form method="post" action="/admin/setup/lk-real-nsd" style="display:flex;gap:10px;align-items:flex-end;flex-wrap:wrap" onsubmit="return !this.enabled.checked || confirm('Включить? Для клиентов из списка QUIK МНП будет записывать по-настоящему (блокировка и списание бумаг).');">
<label style="display:flex;align-items:center;gap:6px;font-weight:400"><input type="checkbox" name="enabled" style="width:auto"{{if .Settings.LK.RealNSDTest.Enabled}} checked{{end}}> включить</label>
<label style="display:flex;flex-direction:column;gap:3px;font-size:12px;min-width:260px">Паспорта клиентов (серия номер, через «;»)
<input name="docs" value="{{.Settings.LK.RealNSDTest.Docs}}" placeholder="1110 754503"></label>
<button class="btn" type="submit">Сохранить</button>
</form>
</div>
</div>
{{if devOn}} <div class="card">
<h2>Симулятор заявки M2M</h2>
<p class="muted">Пока подача через веб-форму у ЕСИА Финанс не готова — bj сам собирает заявку <code>depo_m2m</code> и <strong>отправляет её в API ЛК</strong> (<code>POST /claims/</code>) через активное окружение. Ответ показывается ниже; затем заявку ловим опросом в «Переводы». Поля предзаполнены рабочим тестовым договором.</p>
<form method="post" action="/admin/setup/lk-simulate" style="margin-top:12px">
<div class="grid" style="grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px">
<div><label>ID договора (owner_id)</label><input type="text" name="contract_id" value="1335" style="margin-top:3px"></div>
<div><label>ID клиента (user_id)</label><input type="text" name="user_id" value="771" style="margin-top:3px"></div>
<div><label>Счёт депо (account)</label><input type="text" name="account" value="123" style="margin-top:3px"></div>
<div><label>Раздел (section)</label><input type="text" name="section" value="124" style="margin-top:3px"></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="paper_name" value="ВК ао" style="margin-top:3px"></div>
<div><label>ISIN</label><input type="text" name="paper_isin" value="RU000A106YF0" style="margin-top:3px"></div>
<div><label>Вид бумаги</label>
<select name="paper_type" style="margin-top:3px">
<option value="share">Акция (share)</option>
<option value="bond">Облигация (bond)</option>
<option value="fund">Фонд (fund)</option>
<option value="depositary_receipt">Депозитарная расписка</option>
</select></div>
<div><label>№ госрегистрации</label><input type="text" name="paper_reg" value="1-01-16453-A" style="margin-top:3px"></div>
<div><label>Количество</label><input type="text" name="paper_count" value="1" style="margin-top:3px"></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="contragent_inn" value="7702165310" style="margin-top:3px"></div>
<div><label>Наименование контрагента</label><input type="text" name="contragent_name" value="Тест-депозитарий" style="margin-top:3px"></div>
<div><label>Место расчётов</label>
<select name="place_of_payment" style="margin-top:3px">
<option value="nrd">НРД</option>
<option value="spb">СПБ</option>
</select></div>
<div style="display:flex;align-items:flex-end"><label style="display:flex;align-items:center;gap:8px;font-weight:400"><input type="checkbox" name="expense_information" style="width:auto"> Передать информацию о расходах</label></div>
</div>
<div style="margin-top:16px"><button type="submit" class="btn">Собрать и отправить в ЛК</button></div>
</form>
<p class="muted" style="font-size:12px;margin-top:10px">Результат (сырой ответ API) появится вверху страницы. Если «Раздел счёта не доступен для быстрых переводов» — Финансы должны включить признак M2M на разделе счёта.</p>
</div>{{end}}
<div class="card">
<h2>Забрать заявки из ЛК</h2>
<p class="muted">Коннектор bj вычитывает заявки <code>depo_m2m</code> активного окружения в статусах «В очереди» и «В работе», которые ещё не обрабатывал, для каждой собирает M2M-пакет, отправляет в НРД и переводит заявку в статус <strong>«В работе»</strong>. По ответу НРД статус в ЛК замыкается автоматически (Исполнена / Отклонена).{{if devOn}} В режиме имитации НРД перед отправкой идёт сверка с DEPO: отказ сразу уходит в ЛК с кодом и причиной, одобренная заявка блокируется в QUIK и через 30 с получает имитированный ответ контрагента.{{end}} Штатно это делает фоновый поллер раз в минуту — кнопка для ручного запуска.</p>
<form method="post" action="/admin/setup/lk-pull" style="margin-top:12px"><button type="submit" class="btn">Забрать заявки сейчас</button></form>
<p class="muted" style="font-size:12px;margin-top:10px">Итог (сколько получено / отправлено, ошибки) появится вверху страницы.</p>
</div>
</section>
{{end}}
{{/* ============ 1С: БАЗА БЭК-ОФИСА (модуль onec-back, тестовый) ============ */}}
{{if mod "onec-back"}}
<section class="settings-section" id="sec-onec">
<h1>1С: база бэк-офиса</h1>
<p class="muted" style="margin:-8px 0 16px">Связь bj с базой 1С бэк-офиса. Модуль в разработке: сейчас здесь задаётся и проверяется подключение к базе, обмен данными появится в следующих выпусках.</p>
<div class="card">
<h2><span class="dot {{if .Settings.OneC.CheckOK}}ok{{else}}warn{{end}}"></span>Подключение к базе 1С</h2>
<p class="muted">bj обращается к базе через стандартный интерфейс OData. Его включают при публикации базы: конфигуратор 1С → Администрирование → Публикация на веб-сервере → «Публиковать стандартный интерфейс OData».</p>
<form method="post" action="/admin/setup/onec" style="margin-top:12px;display:grid;gap:12px;max-width:640px">
<div class="fld">
<label>Адрес публикации базы</label>
<input type="text" name="onec_url" value="{{.Settings.OneC.URL}}" placeholder="http://1c-server/middle" data-need="адрес публикации базы" style="margin-top:4px">
<span class="muted" style="font-size:12px">Адрес, по которому база открывается в браузере, без /odata — например http://1c-server/middle.</span>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px">
<div class="fld"><label>Пользователь 1С</label><input type="text" name="onec_user" value="{{.Settings.OneC.User}}" data-need="пользователь 1С" autocomplete="off" style="margin-top:4px"></div>
<div class="fld"><label>Пароль</label><input type="password" name="onec_password" {{if .Settings.OneC.Password}}placeholder="сохранён — не меняйте, если тот же"{{else}}data-need="пароль"{{end}} autocomplete="new-password" style="margin-top:4px"></div>
</div>
<span class="muted" style="font-size:12px">Лучше завести в 1С отдельного пользователя для bj — с правом только на нужные данные.</span>
<div><button type="submit" class="btn">Сохранить и проверить</button></div>
</form>
{{with .Settings.OneC}}{{if not .CheckedAt.IsZero}}<p style="margin:14px 0 0;font-size:13.5px"><b style="color:var(--{{if .CheckOK}}ok{{else}}err{{end}})">{{if .CheckOK}}✓ Подключение работает{{else}}✗ Подключение не работает{{end}}</b> — {{.CheckMsg}} <span class="muted">(проверено {{.CheckedAt.Format "02.01.2006 15:04"}})</span></p>{{end}}{{end}}
</div>
</section>
{{end}}
{{/* ============ ДИАГНОСТИКА (id sec-tests сохранён для ссылок) ============ */}}
<section class="settings-section" id="sec-tests">
<h1>Диагностика</h1>
<div class="card" id="sec-units">
<h2><span class="dot {{$bad := 0}}{{range .Units}}{{if not .OK}}{{$bad = 1}}{{end}}{{end}}{{if eq $bad 1}}err{{else}}ok{{end}}"></span>Службы</h2>
<p class="muted">Ядро и модули работают отдельными службами: если служба модуля не запущена, его раздел в bj отвечает ошибкой связи. Здесь видно состояние и можно перезапустить службу, не заходя на сервер.</p>
<div class="tw" style="margin-top:8px"><table>
<thead><tr><th>Служба</th><th>Что делает</th><th>Состояние</th><th>С</th><th></th></tr></thead>
<tbody>{{range .Units}}<tr>
<td style="white-space:nowrap"><code>{{.Unit}}</code></td>
<td>{{.Title}}</td>
<td>{{if .OK}}<span class="badge ok">работает</span>{{else}}<span class="badge err">{{.State}}{{with .Sub}} / {{.}}{{end}}{{with .Result}} ({{.}}){{end}}</span>{{end}}</td>
<td class="muted">{{if .Since}}{{.Since}}{{else}}—{{end}}</td>
<td style="white-space:nowrap"><form method="post" action="/admin/setup/units/restart" style="margin:0" onsubmit="return confirm('{{if .Core}}Перезапустить ядро? Страница настроек на несколько секунд станет недоступна.{{else}}Перезапустить службу {{.Unit}}?{{end}}');">
<input type="hidden" name="section" value="sec-units"><input type="hidden" name="unit" value="{{.Unit}}">
<button type="submit" class="btn btn-secondary">Перезапустить</button></form></td>
</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>
{{if .Health}}
<div class="tw" style="margin-top:12px">
<table>
<thead><tr><th>Компонент</th><th>Состояние</th><th>Последняя отметка</th><th>Последняя ошибка</th></tr></thead>
<tbody>
{{range .Health}}
<tr>
<td>{{healthLabel .Name}}</td>
<td>{{if .Healthy}}<span class="sys-status" style="--dot:#16a34a">работает</span>{{else}}<span class="sys-status" style="--dot:#dc2626">не отвечает</span>{{end}}</td>
<td class="mono">{{dur .Age}} назад</td>
<td class="muted">{{if .LastErr}}{{.LastErr}}{{else}}—{{end}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<p class="muted" style="margin-top:10px">Компоненты ещё не зарегистрированы (сервис только запущен).</p>
{{end}}
</div>
<div class="card">
<h2>Справочник участников M2M</h2>
<p class="muted">Запрос актуального справочника участников сервиса (пакет <code>#M2MHR</code> в НРД). В ответ НРД присылает <code>#M2MTH</code> — карту <strong>ИНН → MC-код</strong> депозитариев/брокеров и перечень мест расчётов. Нужно для сопоставления ИНН контрагента с кодом участника (блокеры вида M2M14/M2M15) и понимания, кто вообще зарегистрирован.</p>
<form method="post" action="/admin/setup/handbook-request" style="margin-top:12px"><button type="submit" class="btn">Запросить справочник у НРД</button></form>
<p class="muted" style="font-size:12px;margin-top:8px">Ответ придёт во входящие ИШ (~30-60с). Список появится в логе bj-server и в <code>m2m-handbook.json</code>.</p>
</div>
{{if devOn}} <div class="card">
<h2>Тестовый пакет роботу НРД</h2>
<p class="muted">Робот <code>MC0012500000</code> эмулирует вторую сторону перевода. Выберите сценарий — bj-server отправит эталонный запрос через ИШ, ответ придёт во входящие. Требуется настроенный ИШ + профиль Валидаты.</p>
<form method="post" action="/admin/setup/test-nsd" style="margin-top:12px;display:flex;gap:8px;flex-wrap:wrap;align-items:center">
<select name="scenario">
<option value="2001">2001 — Принять все бумаги</option>
<option value="2002">2002 — Принять частично</option>
<option value="1111">1111 — Ответ с отказом</option>
<option value="3333">3333 — Робот принимающая сторона</option>
</select>
<button type="submit" class="btn">Отправить роботу</button>
</form>
<p class="muted" style="margin-top:6px;font-size:12px">Ответ робота — асинхронно (~30-60 сек) во входящие ИШ.</p>
</div>
<div class="card">
<h2>Сквозной тестовый прогон (mock/реальный)</h2>
<p class="muted">Заявка с предзаполненными данными через всю цепочку до финального статуса.</p>
<form method="post" action="/admin/setup/test-run" style="margin-top:12px"><button type="submit" class="btn">Запустить тестовую заявку</button></form>
{{if .Settings.LastTest}}
<div style="margin-top:16px;padding:14px;background:var(--card-2);border:1px solid var(--border);border-radius:10px">
<strong>Последний прогон</strong>
<table style="margin-top:8px">
<tbody>
<tr><td style="width:160px" class="muted">Статус</td><td>{{if .Settings.LastTest.OK}}<span style="color:var(--ok)">✓ успешно</span>{{else}}<span style="color:var(--err)">✗ не прошёл</span>{{end}}</td></tr>
<tr><td class="muted">FSM-статус</td><td><code>{{.Settings.LastTest.FinalStatus}}</code></td></tr>
<tr><td class="muted">ClaimID</td><td><code>{{.Settings.LastTest.ClaimID}}</code> {{if .Settings.LastTest.ClaimID}}<a href="/admin/claims/{{.Settings.LastTest.ClaimID}}">→ карточка</a>{{end}}</td></tr>
<tr><td class="muted">Сообщение</td><td>{{.Settings.LastTest.Message}}</td></tr>
</tbody>
</table>
</div>
{{end}}
</div>{{end}}
</section>
{{/* ============ ОПОВЕЩЕНИЯ ============ */}}
<section class="settings-section" id="sec-alerts">
<h1>Оповещения</h1>
<p class="muted">Кому и о чём bj пишет на почту: администратору системы — о сбоях и обновлениях, отделам — о переводах и поручениях. Все события видны и в ленте «Новости»; передать их в мониторинг Zabbix — Настройка → Диагностика.</p>
{{template "smtpCard" .}}
{{template "notifyGroupsCard" .}}
</section>
{{/* ============ ОБНОВЛЕНИЯ ============ */}}
{{template "updateSection" .}}
{{/* ============ ЛИЦЕНЗИЯ ============ */}}
{{/* ============ РЕЗЕРВНОЕ КОПИРОВАНИЕ ============ */}}
<section class="settings-section" id="sec-server">
<h1>Сервер: HTTPS, время, сеть</h1>
{{$h := httpsCert}}
<div class="card" id="sec-https">
<h2><span class="dot {{if and .Settings.HTTPS.RedirectOn (not $h.Err)}}ok{{else}}warn{{end}}"></span>HTTPS — шифрование входа в bj</h2>
<p class="muted">Пароли администратора, баз данных, хранилища и ключ продукта не должны идти по сети открытым текстом. bj всегда слушает HTTPS: <a href="https://{{serverIP}}:{{if .Settings.HTTPS.Port}}{{.Settings.HTTPS.Port}}{{else}}8443{{end}}/admin/">https://{{serverIP}}:{{if .Settings.HTTPS.Port}}{{.Settings.HTTPS.Port}}{{else}}8443{{end}}/</a>. Модули bj и API на этом сервере работают по HTTP, как раньше.</p>
<p style="margin:8px 0">{{if $h.Err}}<span style="color:var(--err)">✗ сертификат: {{$h.Err}}</span>{{else}}Сертификат: <b>{{$h.CN}}</b>, до {{$h.Until.Format "02.01.2006"}} — {{if .Settings.HTTPS.Custom}}сертификат организации{{else}}самоподписанный: браузер один раз попросит подтвердить исключение; чтобы не просил — загрузите сертификат организации{{end}}.{{end}}</p>
<form method="post" action="/admin/setup/https" style="margin:8px 0">
<label style="display:flex;align-items:center;gap:8px;font-weight:400"><input type="checkbox" name="redirect" style="width:auto"{{if .Settings.HTTPS.RedirectOn}} checked{{end}}> <b>Открывать bj только по HTTPS</b> — браузер с http:// перенаправляется на https://</label>
<button type="submit" class="btn" style="margin-top:8px">Сохранить</button>
</form>
<details style="margin-top:8px"><summary style="cursor:pointer;color:var(--accent)">Сертификат организации</summary>
<form method="post" action="/admin/setup/https/cert" enctype="multipart/form-data" style="display:grid;gap:8px;max-width:520px;margin-top:8px">
<label>Сертификат (PEM, с цепочкой)<input type="file" name="cert" accept=".pem,.crt,.cer" style="width:100%"></label>
<label>Закрытый ключ (PEM)<input type="file" name="key" accept=".pem,.key" style="width:100%"></label>
<div><button type="submit" class="btn">Установить сертификат</button></div>
</form>
<form method="post" action="/admin/setup/https/selfsigned" style="margin-top:8px"><button type="submit" class="btn btn-secondary" onclick="return confirm('Создать новый самоподписанный сертификат? Сертификат организации будет заменён.')">Вернуть самоподписанный</button></form>
</details>
</div>
<div class="card" id="sec-workhours">
<h2><span class="dot {{if and .Settings.WorkHours.Enabled (not .Settings.WorkHours.SavedAt.IsZero)}}ok{{else}}warn{{end}}"></span>Рабочее время организации</h2>
<p class="muted">Когда депозитарий принимает запросы на перевод M2M.</p>
{{template "workHoursForm" (whForm .Settings.WorkHours "")}}
</div>
<div class="card" id="time">
<h2><span class="dot {{if timeSynced}}ok{{else}}warn{{end}}"></span>Время сервера</h2>
<p class="muted">Точное время нужно для подписей и обмена с НРД. В закрытом контуре внешние серверы времени недоступны — укажите сервер времени вашей сети (обычно контроллер домена или маршрутизатор).</p>
<p style="margin:8px 0">{{if timeSynced}}<span style="color:var(--ok)">✓ время синхронизируется</span>{{else}}<span style="color:var(--warn)">⚠ время не синхронизируется</span>{{end}}{{if .Settings.Time.Servers}} · серверы bj: <code>{{.Settings.Time.Servers}}</code>{{end}}</p>
<form method="post" action="/admin/setup/time" style="display:flex;gap:8px;flex-wrap:wrap;align-items:end;max-width:640px">
<label style="flex:1;min-width:240px;margin:0">Серверы времени (через пробел)<input name="servers" value="{{.Settings.Time.Servers}}" placeholder="10.10.10.1 ntp.example.ru" style="width:100%;margin-top:4px"></label>
<button type="submit" class="btn">Применить</button>
</form>
</div>
<div class="card" id="sec-network">
<h2>Сеть</h2>
<p style="margin:6px 0">Сервер: <code>{{serverAddrs}}</code></p>
<p class="muted">Чтобы адрес bj не менялся, задайте серверу постоянный адрес — на консоли сервера (или по SSH): <code>sudo bj-network</code>. Утилита спросит адрес, шлюз и DNS и применит их с защитой: если связь пропадёт, через 2 минуты вернутся прежние настройки. На Astra Linux сеть настраивается средствами системы.</p>
</div>
</section>
<section class="settings-section" id="sec-backup">
<h1>Резервное копирование</h1>
{{template "backupCard" .}}
</section>
<section class="settings-section" id="sec-license">
<div class="card">
<h2>
{{if .License.Valid}}<span class="dot ok"></span>Активна
{{else if .License.Present}}<span class="dot err"></span>Недействительна
{{else}}<span class="dot warn"></span>Не активирована{{end}}
</h2>
{{if .License.Present}}
<table style="margin-top:8px">
<tbody>
{{if .License.Tenant}}<tr><td style="width:180px" class="muted">Организация</td><td><strong>{{.License.Tenant}}</strong></td></tr>{{end}}
{{if .License.Plan}}<tr><td class="muted">Лицензия</td><td><span class="badge confirmed">{{if eq .License.Plan "enterprise"}}полная (все модули){{else}}базовая{{end}}</span></td></tr>{{end}}
<tr><td class="muted">Тип ключа</td><td>{{if .License.Online}}онлайн: выдан сервером лицензий этой установке, продлевается автоматически{{else}}офлайн (закрытый контур){{end}}</td></tr>
{{if not .License.ExpiresAt.IsZero}}<tr><td class="muted">Ключ действует до</td><td>{{.License.ExpiresAt.Format "02.01.2006"}} {{if .License.Valid}}<span class="muted">(осталось {{.License.DaysLeft}} дн.)</span>{{end}}</td></tr>{{end}}
{{if not .License.ContractUntil.IsZero}}<tr><td class="muted">Договор до</td><td>{{.License.ContractUntil.Format "02.01.2006"}}</td></tr>{{end}}
<tr><td class="muted">Обновления</td><td>{{if .License.AllowsUpdates}}<span style="color:var(--ok)">включены</span>{{else}}<span class="muted">не входят в план</span>{{end}}</td></tr>
<tr><td class="muted" style="vertical-align:top">Модули</td><td>{{range .Features}}<div style="display:flex;gap:8px;align-items:center;margin:2px 0"><span class="dot {{if .On}}ok{{else}}err{{end}}"></span>{{.Title}} <code style="font-size:11px;color:var(--muted)">{{.Key}}</code>{{if not .On}}<span class="muted" style="font-size:11px">— не входит в лицензию</span>{{end}}</div>{{end}}</td></tr>
<tr><td class="muted">Статус</td><td>{{.License.Message}}</td></tr>
</tbody>
</table>
{{else}}
<p class="muted">Лицензионный ключ не введён. Без лицензии сервис работает, но автообновления заблокированы.</p>
{{end}}
</div>
<div class="card">
<h2>Активация на сервере лицензий</h2>
<p class="muted">Введите код активации от поставщика (вида <code>BJ-XXXX-XXXX-XXXX</code>). bj зарегистрирует эту установку на сервере лицензий <code>lic.zetit.ru</code> и дальше сам продлевает ключ раз в сутки. Серверу bj нужен доступ по HTTPS к lic.zetit.ru.</p>
<form method="post" action="/admin/setup/license/activate" style="margin-top:12px;display:grid;gap:10px;max-width:520px">
<label>Код активации <input type="text" name="code" placeholder="BJ-XXXX-XXXX-XXXX" autocomplete="off" spellcheck="false" style="width:100%;font-family:ui-monospace,monospace;letter-spacing:.04em"></label>
<div style="display:flex;gap:8px;flex-wrap:wrap"><button type="submit" class="btn btn-ok">Активировать</button>{{if .License.Present}}<button type="submit" formaction="/admin/setup/license/renew" class="btn" title="Запросить у сервера лицензий свежий ключ: срок, модули, режим разработки">Обновить с сервера лицензий</button>{{end}}</div>
</form>
<p class="muted" style="font-size:12px;margin-top:12px">Установка <code>{{if .Settings.License.InstallID}}{{.Settings.License.InstallID}}{{else}}будет создана при первом обращении{{end}}</code>{{if not .Settings.License.LastSync.IsZero}} · последнее продление {{.Settings.License.LastSync.Format "02.01.2006 15:04"}}{{end}}</p>
{{if .Settings.License.LastMsg}}<p style="font-size:13px;margin-top:6px;{{if .Settings.License.LastOK}}color:var(--ok){{else}}color:var(--err){{end}}">{{if .Settings.License.LastOK}}✓{{else}}✗{{end}} {{.Settings.License.LastMsg}} <span class="muted">({{.Settings.License.LastAttempt.Format "02.01.2006 15:04"}})</span></p>{{end}}
</div>
<div class="card">
<details>
<summary style="cursor:pointer;font-weight:600">Офлайн-ключ для закрытого контура</summary>
<p class="muted" style="margin-top:8px">Если у сервера bj нет доступа в интернет, вставьте офлайн-ключ, выданный поставщиком. Он проверяется по подписи без связи с сервером лицензий и не продлевается.</p>
<form method="post" action="/admin/setup/license" style="margin-top:10px;display:grid;gap:10px;max-width:720px">
<textarea name="key" rows="3" style="font-family:ui-monospace,monospace;font-size:11px" placeholder="payload.signature.keyid"></textarea>
<div><button type="submit" class="btn">Сохранить офлайн-ключ</button></div>
</form>
</details>
</div>
</section>
{{/* ============ ОФОРМЛЕНИЕ (логотип отчётов) ============ */}}
<section class="settings-section" id="sec-branding">
<h1>Оформление отчётов</h1>
<div class="card">
<h2>Логотип организации в шапке отчётов</h2>
<p class="muted">Логотип показывается <b>справа</b> в шапке всех отчётов для регулятора (слева — логотип MOEX МОСТ / НРД){{if devOn}} и на титульной сводке пакета тестов{{end}}. Встраивается прямо в HTML — отчёт остаётся автономным. Если не загружать — используется стандартный логотип «Фонтвьель».</p>
<div style="display:flex;align-items:center;gap:18px;flex-wrap:wrap;margin:14px 0;padding:14px;background:var(--card-2);border-radius:12px">
<div>
<div class="muted" style="font-size:12px;margin-bottom:6px">Сейчас в отчётах:</div>
{{if logoSrc .Settings.Branding.LogoDataURI}}<img src="{{logoSrc .Settings.Branding.LogoDataURI}}" alt="логотип" style="height:40px;max-width:280px;background:#fff;padding:6px 10px;border-radius:8px;border:1px solid var(--border)">{{else}}<span class="muted">логотип не загружен</span>{{end}}
</div>
<div class="muted" style="font-size:12px">
{{if .Settings.Branding.LogoDataURI}}Загружен свой логотип{{if .Settings.Branding.LogoName}}: <b>{{.Settings.Branding.LogoName}}</b>{{end}}.{{else}}Свой логотип не загружен — в шапке отчётов справа пусто.{{end}}
</div>
</div>
<form method="post" action="/admin/setup/logo" enctype="multipart/form-data" style="display:flex;gap:10px;flex-wrap:wrap;align-items:center">
<input type="file" name="logo" accept="image/png,image/jpeg,image/gif,image/webp,image/svg+xml,.png,.jpg,.jpeg,.gif,.webp,.svg" required>
<button type="submit" class="btn btn-ok">Загрузить логотип</button>
</form>
<p class="muted" style="font-size:12px;margin-top:8px">Форматы: PNG, JPG, GIF, WEBP, SVG. Максимум 512 КБ. Рекомендуется горизонтальный логотип (высота в шапке — 28px).</p>
{{if .Settings.Branding.LogoDataURI}}
<form method="post" action="/admin/setup/logo/reset" style="margin-top:8px" onsubmit="return confirm('Удалить загруженный логотип?');">
<button type="submit" class="btn">Удалить логотип</button>
</form>
{{end}}
</div>
</section>
{{if devOn}} {{/* ============ ПЕРСПЕКТИВНЫЕ МОДУЛИ (под паролем) ============ */}}
<section class="settings-section" id="sec-advanced">
<h1>Перспективные модули</h1>
<p class="muted" style="max-width:64ch">Модули для будущих профилей внедрения <strong>«автомат»</strong> и <strong>«полуавтомат»</strong> (когда используется собственный личный кабинет, а не ЕСИА Финанс). Пока не в проде — скрыты, чтобы не мешать текущей работе. В профиле «через Финансы» они не нужны: проверку и блокировку бумаг закрывает сам ЕСИА Финанс.</p>
<div class="card">
<h2>Что здесь будет</h2>
<div class="tw" style="margin-top:10px"><table>
<tbody>
<tr><td style="width:34%"><strong>QUIK — Модуль неторговых поручений</strong></td><td class="muted">Блокировка/разблокировка и проверка количества бумаг в торговой системе — для собственного ЛК.</td></tr>
<tr><td><strong>Обмен Fancy</strong></td><td class="muted">Проверка заявки по учёту клиента через БД («кран») перед отправкой в НРД — для фирм без ЕСИА Финанс.</td></tr>
</tbody>
</table></div>
</div>
{{if .AdvancedUnlocked}}
<div class="card" style="border-color:var(--ok,#16a34a)">
<h2><span class="dot ok"></span>Доступ открыт</h2>
<p class="muted">Разделы <strong>«Обмен Fancy»</strong> и <strong>«QUIK МНП»</strong> появились в меню слева. Это незавершённые модули — меняйте настройки, только если понимаете последствия.</p>
<form method="post" action="/admin/setup/advanced-lock" style="margin-top:12px"><button type="submit" class="btn">Скрыть снова</button></form>
</div>
{{else}}
<div class="card">
<h2>Открыть модули</h2>
<p class="muted">Введите пароль администратора, чтобы временно открыть перспективные разделы. Доступ запоминается в этом браузере до нажатия «Скрыть».</p>
<form method="post" action="/admin/setup/advanced-unlock" style="margin-top:12px;display:flex;gap:8px;flex-wrap:wrap;align-items:center">
<input type="password" name="password" placeholder="Пароль" autocomplete="off" style="max-width:260px">
<button type="submit" class="btn">Открыть</button>
</form>
</div>
{{end}}
</section>{{end}}
</div>
</div>
<script>
(function() {
// Переключение разделов админ-центра + запоминание выбранного.
var navs = document.querySelectorAll('.settings-nav button');
var secs = document.querySelectorAll('.settings-section');
function show(id) {
navs.forEach(function(b){ b.classList.toggle('active', b.dataset.sec === id); });
secs.forEach(function(s){ s.classList.toggle('active', s.id === 'sec-' + id); });
// Запоминаем вкладку — чтобы после сохранения (редирект без хэша) вернуться
// на неё, а не на дефолтный «Обзор».
try { sessionStorage.setItem('bj-setup-tab', id); } catch(e) {}
try { history.replaceState(null, '', '#' + id); } catch(e) {}
}
// выбрали другой раздел — с его начала, а не с той же высоты, где листали прежний
navs.forEach(function(b){ b.addEventListener('click', function(){ var same=b.classList.contains('active'); show(b.dataset.sec); if(!same) window.scrollTo(0,0); }); });
var h = (location.hash || '').replace('#','');
// Хэш в URL имеет приоритет. Иначе — после сохранения (есть ?flash/?scrollto)
// восстанавливаем последнюю вкладку из sessionStorage.
var afterSave = /[?&](flash|scrollto)=/.test(location.search);
if (h && document.getElementById('sec-' + h)) {
show(h);
} else if (afterSave) {
var saved = null;
try { saved = sessionStorage.getItem('bj-setup-tab'); } catch(e) {}
if (saved && document.getElementById('sec-' + saved)) show(saved);
}
})();
</script>
{{end}}