Files
Bridge-and-Join-s/internal/lkgateway/web/templates/admin_setup.html
T

1056 lines
121 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">
<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 .Settings.Crypto.Profile}}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}}
<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 .Settings.Update.Available}}<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">
{{/* ============ ОБЗОР ============ */}}
<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>{{.Name}}</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>Обмен Fancy (MSSQL / PostgreSQL)</h1>
{{template "fancySection" .}}
</section>
{{/* ============ QUIK МНП ============ */}}
<section class="settings-section" id="sec-quik">
<h1>QUIK — Модуль неторговых поручений</h1>
{{template "quikSection" .}}
</section>
{{end}}{{/* /AdvancedUnlocked — конец перспективных модулей Fancy/QUIK */}}
{{/* ============ ПОЛЬЗОВАТЕЛИ И ВХОД ============ */}}
<section class="settings-section" id="sec-users">
{{$U := .Users}}{{$A := .Settings.Auth}}
<h1>Пользователи и вход</h1>
<p class="muted">Кто может входить в bj и что ему разрешено. Вход — локальной учётной записью или доменной (Active Directory), роль доменного пользователя назначается по его группам AD.</p>
{{if $U.Open}}<div class="card" 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><span class="dot {{if $U.Open}}warn{{else}}ok{{end}}"></span>Роли</h2>
<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>
</div>
<div class="card">
<h2>Пользователи ({{len $U.Users}})</h2>
{{range $U.Users}}
<form method="post" action="/admin/setup/users" style="display:grid;grid-template-columns:1.1fr 1.4fr 1.2fr 1fr auto;gap:8px;align-items:end;padding:10px 0;border-bottom:1px solid var(--border)">
<input type="hidden" name="login" value="{{.Login}}"><input type="hidden" name="source" value="{{.Source}}">
<div><div style="font-weight:600">{{.Login}} {{if eq .Source "ad"}}<span class="badge" style="font-size:10px">AD</span>{{end}}{{if not .Active}}<span class="badge rejected" style="font-size:10px">заблокирован</span>{{end}}</div>
<div class="muted" style="font-size:11px">{{if not .LastLogin.IsZero}}вход {{msk .LastLogin}}{{else}}ещё не входил{{end}}{{if .Note}} · {{.Note}}{{end}}</div>
{{if .ADGroups}}<div class="muted" style="font-size:11px">группы: {{range $i,$g := .ADGroups}}{{if $i}}, {{end}}{{$g}}{{end}}</div>{{end}}</div>
<label style="margin:0;font-size:11px">Имя <input type="text" name="name" value="{{.Name}}" style="width:100%"><span class="muted">E-mail</span> <input type="text" name="email" value="{{.Email}}" style="width:100%"></label>
<label style="margin:0;font-size:11px">Роль <select name="role" style="width:100%">{{$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 style="margin-top:4px">{{$m := .Modules}}{{range $U.Modules}}<label style="display:inline-flex;gap:4px;align-items:center;font-weight:400;margin:0 8px 0 0;font-size:11px"><input type="checkbox" name="modules" value="{{.Key}}" {{if has $m .Key}}checked{{end}}>{{.Title}}</label>{{end}}</div></label>
<label style="margin:0;font-size:11px">{{if eq .Source "local"}}Новый пароль <input type="password" name="password" placeholder="оставьте пустым" autocomplete="new-password" style="width:100%">{{else}}<span class="muted">пароль проверяет AD</span>{{end}}
<div style="margin-top:4px"><label style="display:inline-flex;gap:4px;align-items:center;font-weight:400;margin:0;font-size:11px"><input type="checkbox" name="active" value="1" {{if .Active}}checked{{end}}>активен</label></div></label>
<div style="display:flex;gap:6px"><button class="btn btn-ok" type="submit">Сохранить</button><button class="btn" type="submit" formaction="/admin/setup/users/delete" onclick="return confirm('Удалить пользователя {{.Login}}?')">Удалить</button></div>
</form>
{{else}}<p class="muted">Пользователей нет.</p>{{end}}
<h3 style="margin:16px 0 6px">{{if $U.Open}}Создать первого администратора{{else}}Добавить пользователя{{end}}</h3>
<form method="post" action="/admin/setup/users" style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:10px;max-width:900px;align-items:end">
<label>Логин <input type="text" name="login" placeholder="ivanov" required style="width:100%"></label>
<label>Имя <input type="text" name="name" placeholder="Иванов И.И." style="width:100%"></label>
<label>Роль <select name="role" style="width:100%">{{range $U.Roles}}<option value="{{.Key}}" {{if and $U.Open (eq .Key "admin")}}selected{{end}}>{{.Title}}</option>{{end}}</select></label>
<label>Источник <select name="source" style="width:100%" {{if $U.Open}}disabled{{end}}><option value="local">Локальный (пароль в bj)</option><option value="ad" {{if not $A.ADEnabled}}disabled{{end}}>Active Directory</option></select></label>
<label>Пароль (для локального, от 8 символов) <input type="password" name="password" autocomplete="new-password" style="width:100%"></label>
<label>E-mail <input type="text" name="email" style="width:100%"></label>
{{if $U.Modules}}<label>Модули <div>{{range $U.Modules}}<label style="display:inline-flex;gap:4px;align-items:center;font-weight:400;margin:0 8px 0 0;font-size:12px"><input type="checkbox" name="modules" value="{{.Key}}">{{.Title}}</label>{{end}}</div></label>{{end}}
<div><input type="hidden" name="active" value="1"><button class="btn btn-ok" type="submit">{{if $U.Open}}Создать администратора{{else}}Добавить{{end}}</button></div>
</form>
<p class="muted" style="font-size:12px;margin-top:8px">Доменного пользователя можно заранее завести с источником «Active Directory», чтобы выдать ему модули; роль при входе возьмётся из групп AD. Если потерян доступ администратора — удалите файл <code>/var/lib/bj/.bj/users.json</code> на сервере: система вернётся в режим первичной настройки.</p>
</div>
<div class="card">
<h2><span class="dot {{if $A.ADEnabled}}ok{{else}}warn{{end}}"></span>Вход через Active Directory</h2>
<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) <span class="muted">пусто = взять с контроллера</span> <input type="text" name="ad_base_dn" value="{{$A.ADBaseDN}}" placeholder="DC=corp,DC=example,DC=ru" 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>Сервисная учётная запись <span class="muted">для перепроверки групп, только чтение</span> <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;margin-bottom:4px">Группы AD → роли <span class="muted" style="font-weight:400;font-size:12px">(имя группы или полный 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 btn-ok" 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" type="submit">Проверить AD</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" type="submit">Перепроверить сейчас</button></form>
{{else}}<span class="muted"><span class="dot warn"></span>Перепроверка групп выключена: роль доменного пользователя меняется только при следующем входе. Укажите сервисную учётную запись выше.</span>{{end}}
</div>
</div>
<div class="card">
<h2>Активные сессии ({{len $U.Sessions}})</h2>
{{if $U.Sessions}}<table style="font-size:12.5px"><thead><tr><th>Пользователь</th><th>Вход</th><th>Активность</th><th>Адрес</th><th>Браузер</th><th></th></tr></thead><tbody>
{{range $U.Sessions}}<tr><td><b>{{.Login}}</b></td><td>{{msk .Created}}</td><td>{{msk .Seen}}</td><td><code>{{.IP}}</code></td><td class="muted" style="font-size:11px;max-width:320px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{.UA}}</td>
<td><form method="post" action="/admin/setup/users/session-drop"><input type="hidden" name="token" value="{{.Token}}"><button class="btn" type="submit">Завершить</button></form></td></tr>{{end}}
</tbody></table>{{else}}<p class="muted">Сессий нет.</p>{{end}}
</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 "verline" (.Versions.Get "crypto-service.jar")}}{{template "verline" (.Versions.Get "validata")}}{{template "verline" (.Versions.Get "java")}}
<div class="card" style="background:var(--accent-weak)">
<h2>Установка ключа на флешку</h2>
<p class="muted">Пошаговый мастер: загрузить архив НРД с паролем → запись на флешку → справочник сертификатов → проверка Валидаты → готово.</p>
<a href="/admin/setup/keywizard" class="btn btn-ok" style="margin-top:10px;display:inline-block">Открыть мастер установки ключа →</a>
</div>
<div class="card">
<h2><span class="dot {{if .Settings.Crypto.Profile}}ok{{else}}warn{{end}}"></span>СКЗИ «Валидата Клиент L»</h2>
<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}}. Подробно — <a href="/admin/help/crypto">справка</a>.</p>
<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>
<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('Перезапустить crypto-service? Поднимется через ~5 сек.');"><button type="submit" class="btn btn-warn">↻ crypto-service</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>
</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>
{{/* Сертификаты УЦ */}}
<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>
</section>
{{/* ============ НРД ============ */}}
<section class="settings-section" id="sec-nsd">
<h1>НРД</h1>
{{template "verline" (.Versions.Get "ish")}}
{{template "orgSection" .}}
<div class="card" id="m2m-auto">
<h2><span class="dot {{if .Settings.M2MAuto.Enabled}}ok{{else}}warn{{end}}"></span>Автоматический режим M2M</h2>
<p class="muted">Без участия оператора, чтобы соблюдать сроки: <strong>ответ на входящие переводы</strong> по свежей сверке с DEPO (пакет принимается только целиком; бумага для квалифицированных инвесторов при неполной или непрочитанной квалификации клиента — отказ M2M08), <strong>подача поручения 16/1</strong>, как только НРД пришлёт черновик после нашего согласия, и <strong>подача поручения 16</strong> по исходящим после согласия контрагента. Если DEPO или ИШ недоступны — bj повторяет каждые 3 минуты и шлёт оповещение; без сверки ничего не принимается. Обрабатывается только то, что пришло не раньше чем за сутки до включения. Кнопки в карточках остаются для ручной работы.</p>
<form method="post" action="/admin/setup/m2m-auto" style="display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-top:10px">
<input type="hidden" name="section" value="m2m-auto">
<label style="display:flex;gap:8px;align-items:center;font-weight:600;margin:0"><input type="checkbox" name="enabled" value="1" {{if .Settings.M2MAuto.Enabled}}checked{{end}}> Автоматический режим включён</label>
<button type="submit" class="btn btn-ok">Сохранить</button>
</form>
{{if not .Settings.M2MAuto.ChangedAt.IsZero}}<p class="muted" style="font-size:12px;margin-top:8px">{{if .Settings.M2MAuto.Enabled}}Включён{{else}}Выключен{{end}} {{msk .Settings.M2MAuto.ChangedAt}}{{with .Settings.M2MAuto.ChangedBy}} · {{.}}{{end}}.{{if .Settings.Quik.Emulate}}{{if devOn}} Шаги QUIK МНП — в режиме имитации.{{else}} Записи в QUIK МНП сейчас не выполняются.{{end}}{{end}}</p>{{end}}
</div>
<div class="card">
<h2><span class="dot {{if .Settings.NSD.IGWBaseURL}}ok{{else}}warn{{end}}"></span>Интеграционный шлюз (ИШ)</h2>
<p class="muted">{{if not .Settings.NSD.IGWBaseURL}}{{if devOn}}<code>mock-режим</code> — без реального ИШ.{{else}}ИШ не настроен — обмен с НРД не выполняется.{{end}}{{else}}Профиль <code>{{.Settings.NSD.Profile}}</code>, ИШ <code>{{.Settings.NSD.IGWBaseURL}}</code>.{{end}} Стенды и установка — <a href="/admin/help/systems">справка</a>.</p>
<form method="post" action="/admin/setup/nsd" style="margin-top:12px;display:grid;gap:10px;max-width:680px">
<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">
<hr style="border:none;border-top:1px solid var(--border);margin:6px 0">
<p class="muted" style="margin:0">Депозитарные реквизиты (откуда списываются бумаги) — из договора/письма НРД. Нужны для формирования заявки на перевод.</p>
{{if .Settings.Fancy.Server}}<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin:4px 0">
<button type="submit" class="btn" formaction="/admin/setup/fancy/detect" formmethod="post" style="background:var(--card)">Определить из DEPO</button>
<span class="muted" style="font-size:12px">Счёт, код и раздел в НРД и карточка организации читаются из базы DEPO (Fancy) и подставляются в пустые поля — проверьте и сохраните.</span>
</div>{{end}}
{{if .Suggest}}{{if .Suggest.AccountID}}<div class="help-block" style="margin:4px 0"><strong>Найдено в DEPO:</strong> счёт {{.Suggest.AccountID}}, код {{.Suggest.DeponentCode}}, раздел {{.Suggest.SectionID}}{{if gt (len .Suggest.Sections) 1}} (открытых разделов: {{len .Suggest.Sections}}){{end}}{{if .Suggest.OrgName}}; организация {{.Suggest.OrgName}}, ИНН {{.Suggest.OrgINN}}{{end}}.{{range .Suggest.Notes}}<br><span class="muted">⚠ {{.}}</span>{{end}}</div>{{end}}{{end}}
<label>Депозитарный код</label>
<input type="text" name="deponent_code" value="{{if .Settings.NSD.DeponentCode}}{{.Settings.NSD.DeponentCode}}{{else if .Suggest}}{{.Suggest.DeponentCode}}{{end}}" placeholder="MC0000000000">
<label>Депозитарный счёт</label>
<input type="text" name="account_id" value="{{if .Settings.NSD.AccountID}}{{.Settings.NSD.AccountID}}{{else if .Suggest}}{{.Suggest.AccountID}}{{end}}" placeholder="депозитарный счёт">
<label>Раздел счёта</label>
<input type="text" name="section_id" value="{{if .Settings.NSD.SectionID}}{{.Settings.NSD.SectionID}}{{else if .Suggest}}{{.Suggest.SectionID}}{{end}}" placeholder="раздел депозитарного счёта"{{if .Suggest}}{{if gt (len .Suggest.Sections) 1}} list="depo-sections"{{end}}{{end}}>
{{if .Suggest}}{{if gt (len .Suggest.Sections) 1}}<datalist id="depo-sections">{{range .Suggest.Sections}}<option value="{{.}}">{{end}}</datalist>{{end}}{{end}}
<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>
</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">Инструкция по размещению</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">Адрес ЛК (стенд/бой), куда клиент подаёт заявку M2M. Заявка приходит к нам по контракту <code>POST /api/v1/back_office/claims/</code>, а мы возвращаем статус через <strong>Callback URL</strong>.</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>
<div class="fld">
<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}}Не настроен — уведомления в ЛК отключены. Для обмена с НРД не требуется.{{end}}</span>
</div>
<div><button type="submit" class="btn">Сохранить и проверить</button></div>
</form>
</div>
<div class="card">
<h2>Подключение к API ЛК — тест и бой</h2>
<p class="muted">Реквизиты, по которым 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>
{{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}}
{{/* ============ ДИАГНОСТИКА (id sec-tests сохранён для ссылок) ============ */}}
<section class="settings-section" id="sec-tests">
<h1>Диагностика</h1>
<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" style="max-width:60ch">Системные события (сбой или зависание фонового компонента, аварийный перезапуск) всегда попадают в ленту «Новости». Дополнительно их можно доставлять наружу — на почту оператору и/или в мониторинг Zabbix, чтобы узнать о проблеме, даже когда интерфейс bj никто не открыл.</p>
<div class="card">
<h2>Почта (SMTP)</h2>
<p class="muted">Отправка уведомлений по e-mail. Пароль хранится в конфигурации сервиса.</p>
<form method="post" action="/admin/setup/alerts-smtp" style="margin-top:12px">
<label style="display:flex;align-items:center;gap:8px;font-weight:400;margin-bottom:12px"><input type="checkbox" name="enabled" style="width:auto" {{if .Settings.Alerts.SMTP.Enabled}}checked{{end}}> Включить почтовые уведомления</label>
<div class="grid" style="grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px">
<div><label>SMTP-сервер</label><input type="text" name="host" value="{{.Settings.Alerts.SMTP.Host}}" placeholder="smtp.example.ru" style="margin-top:3px"></div>
<div><label>Порт</label><input type="text" id="smtp-port" name="port" value="{{if .Settings.Alerts.SMTP.Port}}{{.Settings.Alerts.SMTP.Port}}{{else}}587{{end}}" style="margin-top:3px"></div>
<div><label>Шифрование</label>
<select id="smtp-security" name="security" style="margin-top:3px" onchange="smtpSyncPort(this.value)">
<option value="starttls" {{if eq .Settings.Alerts.SMTP.Security "starttls"}}selected{{end}}>STARTTLS (587)</option>
<option value="ssl" {{if eq .Settings.Alerts.SMTP.Security "ssl"}}selected{{end}}>SSL/TLS (465)</option>
<option value="none" {{if eq .Settings.Alerts.SMTP.Security "none"}}selected{{end}}>Без шифрования (25)</option>
</select></div>
</div>
<div class="grid" style="grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px;margin-top:12px">
<div><label>Логин</label><input type="text" name="username" value="{{.Settings.Alerts.SMTP.Username}}" autocomplete="off" style="margin-top:3px"></div>
<div><label>Пароль</label><input type="password" name="password" value="{{.Settings.Alerts.SMTP.Password}}" autocomplete="new-password" style="margin-top:3px"></div>
<div><label>Отправитель (From)</label><input type="text" name="from" value="{{.Settings.Alerts.SMTP.From}}" placeholder="bj@example.ru" style="margin-top:3px"></div>
<div><label>Получатели (через запятую)</label><input type="text" name="to" value="{{.Settings.Alerts.SMTP.To}}" placeholder="ops@example.ru, dev@example.ru" style="margin-top:3px"></div>
</div>
<div style="margin-top:16px;display:flex;gap:8px;flex-wrap:wrap;align-items:center">
<button type="submit" class="btn">Сохранить</button>
<button type="submit" formaction="/admin/setup/alerts-smtp-test" class="btn btn-secondary">Отправить проверочное письмо</button>
</div>
</form>
{{if .Settings.Alerts.SMTP.LastMsg}}<p class="muted" style="font-size:12px;margin-top:10px">Последняя проверка: {{.Settings.Alerts.SMTP.LastMsg}}</p>{{end}}
</div>
<div class="card">
<h2>Zabbix (хук мониторинга)</h2>
<p class="muted">bj шлёт значение на элемент типа <strong>Zabbix trapper</strong> (протокол <code>zabbix_sender</code>). В Zabbix заведите host и item-trapper с этим ключом, затем триггер по значению — уведомления настроит сам Zabbix.</p>
<form method="post" action="/admin/setup/alerts-zabbix" style="margin-top:12px">
<label style="display:flex;align-items:center;gap:8px;font-weight:400;margin-bottom:12px"><input type="checkbox" name="enabled" style="width:auto" {{if .Settings.Alerts.Zabbix.Enabled}}checked{{end}}> Включить хук Zabbix</label>
<div class="grid" style="grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px">
<div><label>Сервер / прокси Zabbix</label><input type="text" name="server" value="{{.Settings.Alerts.Zabbix.Server}}" placeholder="zabbix.example.ru" style="margin-top:3px"></div>
<div><label>Порт</label><input type="text" name="port" value="{{if .Settings.Alerts.Zabbix.Port}}{{.Settings.Alerts.Zabbix.Port}}{{else}}10051{{end}}" style="margin-top:3px"></div>
<div><label>Имя хоста в Zabbix</label><input type="text" name="host" value="{{.Settings.Alerts.Zabbix.Host}}" placeholder="bj-server" style="margin-top:3px"></div>
<div><label>Ключ элемента (trapper)</label><input type="text" name="key" value="{{if .Settings.Alerts.Zabbix.Key}}{{.Settings.Alerts.Zabbix.Key}}{{else}}bj.alert{{end}}" style="margin-top:3px"></div>
</div>
<div style="margin-top:16px;display:flex;gap:8px;flex-wrap:wrap;align-items:center">
<button type="submit" class="btn">Сохранить</button>
<button type="submit" formaction="/admin/setup/alerts-zabbix-test" class="btn btn-secondary">Отправить проверочное значение</button>
</div>
</form>
{{if .Settings.Alerts.Zabbix.LastMsg}}<p class="muted" style="font-size:12px;margin-top:10px">Последняя проверка: {{.Settings.Alerts.Zabbix.LastMsg}}</p>{{end}}
</div>
<div class="card">
<h2>Адресаты уведомлений: депозитарий, бэк-офис, мидл-офис</h2>
<p class="muted">Три группы адресов и таблица «какой тип уведомления кому отправлять». Письма идут через SMTP из раздела «Почта». Адрес алертов из раздела «Почта» получает системные оповещения всегда; группы ниже добавляются по маршруту.</p>
<form method="post" action="/admin/setup/incoming-notify" style="margin-top:12px">
<label style="display:flex;align-items:center;gap:8px;font-weight:400;margin-bottom:12px"><input type="checkbox" name="enabled" style="width:auto" {{if .Settings.Incoming.Enabled}}checked{{end}}> Включить уведомления о переводах</label>
<div style="display:grid;gap:10px;max-width:640px">
<div><label>Депозитарий (через запятую)</label><input type="text" name="emails" value="{{.Settings.Incoming.Emails}}" placeholder="depo@example.ru" style="margin-top:3px;width:100%"></div>
<div><label>Бэк-офис (через запятую)</label><input type="text" name="emails_back" value="{{.Settings.Incoming.EmailsBack}}" placeholder="backoffice@example.ru" style="margin-top:3px;width:100%"></div>
<div><label>Мидл-офис (через запятую)</label><input type="text" name="emails_middle" value="{{.Settings.Incoming.EmailsMiddle}}" placeholder="middle@example.ru" style="margin-top:3px;width:100%"></div>
</div>
<table style="margin-top:14px;max-width:760px">
<thead><tr><th>Тип уведомления</th>{{range .NotifyGroups}}<th style="text-align:center">{{.Title}}</th>{{end}}</tr></thead>
<tbody>{{$inc := .Settings.Incoming}}{{range $k := .NotifyKinds}}<tr><td>{{$k.Title}}</td>{{range $g := $.NotifyGroups}}<td style="text-align:center"><input type="checkbox" name="route_{{$k.Key}}_{{$g.Key}}" value="1" {{if $inc.RouteHas $k.Key $g.Key}}checked{{end}} style="width:auto"></td>{{end}}</tr>{{end}}</tbody>
</table>
<div style="margin-top:16px;display:flex;gap:8px;flex-wrap:wrap;align-items:center">
<button type="submit" class="btn">Сохранить</button>
<button type="submit" formaction="/admin/setup/incoming-notify-test" class="btn btn-secondary">Отправить пробное уведомление</button>
</div>
</form>
<p class="muted" style="font-size:12px;margin-top:8px">Требуется настроенный и включённый SMTP в разделе «Почта».</p>
</div>
<script>
// Смена шифрования SMTP авто-подставляет типовой порт; править вручную можно.
function smtpSyncPort(sec){
var p=document.getElementById('smtp-port'); if(!p) return;
var def={starttls:'587', ssl:'465', none:'25'}[sec];
if(def) p.value=def;
}
</script>
</section>
{{/* ============ ОБНОВЛЕНИЯ ============ */}}
<section class="settings-section" id="sec-update">
<h1>Обновления</h1>
{{template "verline" (.Versions.Get "bj-server")}}
<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><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">
<h2>Компоненты</h2>
<p class="muted">Каждый компонент обновляется отдельно: своя версия в релизе, перезапускается только его служба. Файлы ставит root-установщик после повторной проверки подписи и контрольных сумм. Модули, которых нет на этом сервере, обновлением не ставятся — кроме выделенных из ядра (НПР / УДС, экспорт QUIK): если модуль входит в лицензию, он ставится вместе с обновлением, иначе его раздел перестал бы работать.</p>
<div style="overflow-x:auto"><table style="margin-top:8px">
<thead><tr><th>Компонент</th><th>Установлена</th><th>Доступна</th><th>Служба</th><th></th></tr></thead>
<tbody>
{{range .Components}}<tr>
<td><strong>{{.Title}}</strong><div class="muted" style="font-size:11px"><code>{{.Name}}</code>{{if eq .Name "crypto-service.jar"}}{{with $.Versions.Get "validata"}}{{if .Version}} · работает с «Валидата Клиент L» {{.Version}}{{end}}{{end}}{{end}}</div></td>
<td>{{if .Present}}{{if .Installed}}{{.Installed}}{{else}}<span class="muted">неизвестна</span>{{end}}{{else}}<span class="muted">не установлен</span>{{end}}</td>
<td>{{if .Available}}{{.Available}}{{if .New}} <span class="hero-status warn">к установке</span>{{else if .Update}} <span class="hero-status warn">новее</span>{{end}}{{if .Blocked}}<div class="muted" style="font-size:11px;color:var(--warn)">{{.Blocked}}</div>{{end}}{{else}}<span class="muted">нет данных</span>{{end}}</td>
<td><code style="font-size:11px">{{.Unit}}</code></td>
<td>{{if .Update}}<form method="post" action="/admin/setup/update/apply" style="margin:0" onsubmit="return confirm('{{if .New}}Установить модуль {{.Available}}? Будет включена его служба.{{else}}Обновить компонент до {{.Available}}? Его служба перезапустится.{{end}}');"><input type="hidden" name="section" value="sec-update"><input type="hidden" name="component" value="{{.Name}}"><button type="submit" class="btn btn-ok">{{if .New}}Установить{{else}}Обновить{{end}}</button></form>{{end}}</td>
</tr>{{end}}
</tbody>
</table></div>
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:12px">
{{if .UpdatesAvailable}}<form method="post" action="/admin/setup/update/apply" style="margin:0" onsubmit="return confirm('Обновить все компоненты, для которых есть новая версия?');"><input type="hidden" name="section" value="sec-update"><button type="submit" class="btn btn-ok">Обновить всё</button></form>{{end}}
<form method="post" action="/admin/setup/update/check" style="margin:0"><input type="hidden" name="section" value="sec-update"><button type="submit" class="btn btn-secondary">Проверить обновления</button></form>
<form method="post" action="/admin/setup/update/upload" enctype="multipart/form-data" style="margin:0;display:flex;gap:8px;align-items:center;flex-wrap:wrap" onsubmit="return this.bundle.files.length>0 && confirm('Установить обновление из файла? Подпись пакета будет проверена; поставятся компоненты новее текущих — сначала ядро, затем модули. Службы перезапустятся.');">
<input type="hidden" name="section" value="sec-update">
<input type="file" name="bundle" accept=".zip,application/zip" required style="max-width:260px">
<button type="submit" class="btn btn-secondary" title="Пакет обновления с портала клиента — для сервера без выхода к серверу обновлений">Обновить из файла</button>
</form>
</div>
{{if .Settings.Update.Notes}}<p class="muted" style="margin-top:10px">Что нового в релизе {{.Settings.Update.Available}}: {{.Settings.Update.Notes}}</p>{{end}}
<p class="muted" style="margin-top:8px;font-size:12px"><b>Закрытый контур.</b> Если у сервера нет выхода к серверу обновлений, скачайте пакет обновления на портале клиента (lic.zetit.ru → Установка → «Пакет обновления») и загрузите его кнопкой «Обновить из файла». Система сама распознаёт, что внутри — ядро, модули или всё вместе — и ставит в правильном порядке. Модуль, которому нужно более новое ядро, чем установлено, будет пропущен с подсказкой обновить сначала ядро.</p>
{{if not .Settings.Update.LastCheck.IsZero}}{{if .UpdateFailed}}<div class="alert alert-warn" style="margin-top:10px">{{.Settings.Update.LastResult}} <span class="muted">({{.Settings.Update.LastCheck.Format "02.01.2006 15:04"}})</span></div>{{else}}<p class="muted" style="margin-top:6px;font-size:12px">Последняя проверка: {{.Settings.Update.LastCheck.Format "02.01.2006 15:04"}}. {{.Settings.Update.LastResult}}</p>{{end}}{{end}}
{{if and .License.Present (not .License.AllowsUpdates)}}<p class="muted" style="margin-top:6px;font-size:12px;color:var(--warn)">Текущий план «{{.License.Plan}}» не включает обновления.</p>{{end}}
</div>
<div class="card">
<h2><span class="dot {{if .Settings.Update.BaseURL}}ok{{else}}warn{{end}}"></span>Источник обновлений</h2>
<p class="muted">Обновления раздаёт сервер лицензий по лицензии этой установки: модуль скачивается, только если он входит в лицензию. Каждый релиз проверяется по подписи Ed25519 и контрольным суммам, без действительной подписи установка не выполняется.</p>
<form method="post" action="/admin/setup/update" style="margin-top:12px;display:grid;gap:10px;max-width:680px">
<label>URL артефактории</label>
<input type="text" name="base_url" value="{{.Settings.Update.BaseURL}}" placeholder="https://lic.zetit.ru/updates (по умолчанию)">
<label>Канал</label>
<select name="channel">
<option value="stable" {{if eq .Settings.Update.Channel "stable"}}selected{{end}}>stable — стабильный</option>
<option value="beta" {{if eq .Settings.Update.Channel "beta"}}selected{{end}}>beta — предварительный</option>
</select>
<label>Публичный ключ издателя (base64 Ed25519)</label>
<input type="text" name="public_key" value="{{.Settings.Update.PublicKey}}" placeholder="зашит в релиз; переопределить здесь">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer"><input type="checkbox" name="auto_check" {{if .Settings.Update.AutoCheck}}checked{{end}} style="width:auto"> Проверять автоматически (раз в 6 часов)</label>
<div><button type="submit" class="btn">Сохранить</button></div>
</form>
</div>
</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">{{.License.Plan}}</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(){ show(b.dataset.sec); }); });
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}}