925c516e1e
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL
1293 lines
152 KiB
HTML
1293 lines
152 KiB
HTML
{{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 or .AdvancedUnlocked (index .Modules "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>
|
||
<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>
|
||
<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>
|
||
<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>
|
||
<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="{{.Section}}"]');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 .AdvancedUnlocked}}
|
||
{{/* ============ ОБМЕН FANCY (MSSQL) — перспективный модуль ============ */}}
|
||
<section class="settings-section" id="sec-fancy">
|
||
<h1>Обмен Fancy (MSSQL / PostgreSQL)</h1>
|
||
{{template "verline" (.Versions.Get "depo")}}
|
||
<div class="card" id="sec-fancy-conn">
|
||
<h2><span class="dot {{if .Settings.Fancy.DepoTablesCheckOK}}ok{{else if .Settings.Fancy.Server}}warn{{else}}err{{end}}"></span>Подключение к серверу БД клиента</h2>
|
||
<p class="muted">Сервер учётной базы Fancy DEPO: картотека лиц, счета и договоры, остатки, опции (признак квалифицированного инвестора), трансферные таблицы обмена. Поддерживаются <strong>Microsoft SQL Server</strong> и <strong>PostgreSQL</strong>. База указывается ниже, в карточке «Трансферные таблицы DEPO».</p>
|
||
<form method="post" action="/admin/setup/fancy" style="margin-top:12px;display:grid;gap:10px;max-width:560px">
|
||
<label>Тип СУБД
|
||
<select name="db_type" id="fancy-dbtype" style="width:100%" onchange="var p=document.getElementById('fancy-port');if(!p.value||p.value=='1433'||p.value=='5432'){p.value=this.value=='postgres'?'5432':'1433';}">
|
||
<option value="mssql" {{if not .Settings.Fancy.IsPostgres}}selected{{end}}>Microsoft SQL Server</option>
|
||
<option value="postgres" {{if .Settings.Fancy.IsPostgres}}selected{{end}}>PostgreSQL</option>
|
||
</select></label>
|
||
<label>Сервер (host или host\instance)
|
||
<input type="text" name="server" value="{{.Settings.Fancy.Server}}" placeholder="10.10.10.x" style="width:100%" required></label>
|
||
<label>Порт
|
||
<input type="number" id="fancy-port" name="port" value="{{if .Settings.Fancy.Port}}{{.Settings.Fancy.Port}}{{else}}{{.Settings.Fancy.DefaultPort}}{{end}}" style="width:100%"></label>
|
||
<label>Пользователь (логин БД)
|
||
<input type="text" name="user" value="{{.Settings.Fancy.User}}" placeholder="bj_user" style="width:100%" required></label>
|
||
<label>Пароль
|
||
<input type="password" name="password" placeholder="{{if .Settings.Fancy.Password}}•••••• — оставьте пустым, чтобы не менять{{else}}пароль SQL-логина{{end}}" autocomplete="off" style="width:100%"></label>
|
||
<label>Шифрование канала
|
||
<select name="encrypt" style="width:100%">
|
||
<option value="disable" {{if eq .Settings.Fancy.Encrypt "disable"}}selected{{end}}>disable — без TLS (типично для LAN)</option>
|
||
<option value="true" {{if eq .Settings.Fancy.Encrypt "true"}}selected{{end}}>true — с TLS</option>
|
||
</select></label>
|
||
<div style="display:flex;gap:8px;flex-wrap:wrap"><button type="submit" class="btn btn-ok">Сохранить</button><button type="submit" formaction="/admin/setup/fancy/test" class="btn">Проверить соединение</button></div>
|
||
</form>
|
||
</div>
|
||
|
||
<div class="card" id="sec-fancy-depo-tables">
|
||
<h2><span class="dot {{if .Settings.Fancy.DepoTablesCheckOK}}ok{{else if .Settings.Fancy.Server}}warn{{else}}err{{end}}"></span>Трансферные таблицы DEPO</h2>
|
||
<p class="muted">Через эти таблицы bj обменивается с учётной системой DEPO: поручения <code>FTV_M2M</code> и их строки <code>FTV_M2M_LINES</code>, события <code>FTV_M2M_EVENTS</code>, справочник участников НРД <code>FTV_M2M_HANDBOOK</code>. Таблицы создаются в базе DEPO на сервере из подключения выше ({{if .DepoTables.Postgres}}PostgreSQL{{else}}Microsoft SQL Server{{end}}). Создание можно повторять: добавляются только недостающие таблицы, колонки и индексы, данные не меняются.</p>
|
||
<form method="post" action="/admin/setup/fancy/depo-tables" style="margin-top:10px;display:grid;gap:10px;max-width:720px">
|
||
<input type="hidden" name="section" value="sec-fancy-depo-tables">
|
||
<label>База DEPO
|
||
<input type="text" name="depo_database" value="{{.Settings.Fancy.DepoDatabase}}" placeholder="{{.DepoTables.DB}}" pattern="[A-Za-z][A-Za-z0-9_]*" style="width:100%"></label>
|
||
<label>Признак ИИС в названии брокерского договора <span class="muted" style="font-weight:400">(с чего начинается номер договора ИИС в DEPO; несколько — через запятую)</span>
|
||
<input type="text" name="iis_contract_marker" value="{{.Settings.Fancy.IISContractMarker}}" placeholder="БРИИС" style="width:100%"></label>
|
||
<label style="display:flex;gap:8px;align-items:flex-start;font-weight:600"><input type="radio" name="depo_tables_mode" value="auto" {{if eq .Settings.Fancy.DepoTablesMode "auto"}}checked{{end}}> <span>Создаёт bj <span class="muted" style="font-weight:400">— вводите логин с правами на создание таблиц в базе DEPO. Он используется один раз и не сохраняется; bj заодно выдаст права своему логину.</span></span></label>
|
||
<label style="display:flex;gap:8px;align-items:flex-start;font-weight:600"><input type="radio" name="depo_tables_mode" value="script" {{if eq .Settings.Fancy.DepoTablesMode "script"}}checked{{end}}> <span>Создаёт администратор СУБД по скрипту <span class="muted" style="font-weight:400">— скачайте скрипт и передайте администратору, после выполнения нажмите «Проверить таблицы».</span></span></label>
|
||
<div><button type="submit" class="btn">Сохранить</button></div>
|
||
</form>
|
||
|
||
{{if eq .Settings.Fancy.DepoTablesMode "auto"}}
|
||
<form method="post" action="/admin/setup/fancy/depo-tables/create" style="margin-top:12px;display:grid;gap:10px;max-width:720px">
|
||
<input type="hidden" name="section" value="sec-fancy-depo-tables">
|
||
<div><b>Логин с правами на базу {{.DepoTables.DB}}</b><br>
|
||
<span class="muted" style="font-size:12px">{{if .DepoTables.Postgres}}Владелец базы или суперпользователь.{{else}}Роль db_owner в базе (или db_ddladmin + db_securityadmin).{{end}} Если оставить поля пустыми, bj попробует своим логином {{.DepoTables.User}}.</span></div>
|
||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:10px">
|
||
<label>Логин <input type="text" name="admin_user" autocomplete="off" placeholder="{{if .DepoTables.Postgres}}postgres{{else}}sa{{end}}" style="width:100%"></label>
|
||
<label>Пароль <input type="password" name="admin_password" autocomplete="new-password" style="width:100%"></label>
|
||
</div>
|
||
<div><button type="submit" class="btn btn-ok" onclick="return confirm('Создать трансферные таблицы в базе {{.DepoTables.DB}}?')">⚙ Создать таблицы</button></div>
|
||
</form>
|
||
{{else if eq .Settings.Fancy.DepoTablesMode "script"}}
|
||
<div style="margin-top:12px"><b>Скрипт для администратора СУБД</b> — <a href="/admin/setup/fancy/depo-tables/script.sql">скачать {{.DepoTables.ScriptFile}}</a>
|
||
<pre class="copyable" style="max-height:360px;overflow:auto">{{.DepoTables.Script}}</pre></div>
|
||
{{end}}
|
||
|
||
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:12px">
|
||
<form method="post" action="/admin/setup/fancy/depo-tables/check" style="margin:0"><input type="hidden" name="section" value="sec-fancy-depo-tables"><button type="submit" class="btn btn-ok">✓ Проверить таблицы</button></form>
|
||
<span class="muted" style="font-size:12px">логином bj: таблицы на месте, колонки совпадают, есть права на чтение и запись</span>
|
||
</div>
|
||
{{if .Settings.Fancy.DepoTablesCheck}}<p style="margin-top:10px;font-size:13px;{{if .Settings.Fancy.DepoTablesCheckOK}}color:var(--ok){{else}}color:var(--err){{end}}">{{if .Settings.Fancy.DepoTablesCheckOK}}✓{{else}}✗{{end}} {{.Settings.Fancy.DepoTablesCheck}} <span class="muted">({{.Settings.Fancy.DepoTablesCheckAt.Format "02.01.2006 15:04"}})</span></p>{{end}}
|
||
</div>
|
||
|
||
<div class="card" id="sec-fancy-depo-job">
|
||
<h2><span class="dot {{if .Settings.Fancy.DepoJobCheckOK}}ok{{else if .Settings.Fancy.DepoJobMode}}warn{{else}}err{{end}}"></span>Где запускается обработка трансферных таблиц DEPO</h2>
|
||
<p class="muted">Поручения и события, которые bj пишет в <code>FTV_M2M*</code>, обрабатывает процедура <code>{{.DepoJob.Proc}}</code> в базе DEPO. Выберите, кто её запускает, выполните инструкцию и нажмите «Проверить готовность» — без зелёной отметки обмен с DEPO работать не будет.</p>
|
||
<form method="post" action="/admin/setup/fancy/depo-job" style="margin-top:10px;display:grid;gap:10px;max-width:720px">
|
||
<input type="hidden" name="section" value="sec-fancy-depo-job">
|
||
<label style="display:flex;gap:8px;align-items:flex-start;font-weight:600"><input type="radio" name="depo_job_mode" value="bj" {{if eq .Settings.Fancy.DepoJobMode "bj"}}checked{{end}}> <span>Внутри bj <span class="muted" style="font-weight:400">— bj сам вызывает процедуру по расписанию. На сервере клиента нужно одно право на процедуру. Рекомендуется.</span></span></label>
|
||
<label style="display:flex;gap:8px;align-items:flex-start;font-weight:600"><input type="radio" name="depo_job_mode" value="sql-auto" {{if eq .Settings.Fancy.DepoJobMode "sql-auto"}}checked{{end}}> <span>Джоба SQL Agent, создаёт bj <span class="muted" style="font-weight:400">— логину bj выдаётся роль оператора SQL Agent, bj создаёт джобу кнопкой.</span></span></label>
|
||
<label style="display:flex;gap:8px;align-items:flex-start;font-weight:600"><input type="radio" name="depo_job_mode" value="sql-manual" {{if eq .Settings.Fancy.DepoJobMode "sql-manual"}}checked{{end}}> <span>Джоба SQL Agent, настраивает администратор <span class="muted" style="font-weight:400">— передаёте скрипт, bj проверяет, что джоба работает.</span></span></label>
|
||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px">
|
||
<label>Интервал, секунд <input type="number" name="depo_job_every_sec" min="5" max="3600" value="{{.DepoJob.Every}}" style="width:100%"></label>
|
||
<label>Процедура <input type="text" name="depo_job_proc" value="{{.Settings.Fancy.DepoJobProc}}" placeholder="dbo.FTV_M2M_JOB" style="width:100%"></label>
|
||
<label>Работает от имени <input type="text" name="depo_job_run_as" value="{{.Settings.Fancy.DepoJobRunAs}}" placeholder="SYSDBA" style="width:100%"></label>
|
||
</div>
|
||
<p class="muted" style="font-size:12px;margin:0">Процедура обработки выполняется от имени пользователя базы DEPO (у разработчика Fancy это <code>SYSDBA</code>). У клиента такого пользователя может не быть или он называется иначе: «Проверить готовность» проверит, что он есть в базе.</p>
|
||
<div><button type="submit" class="btn">Сохранить способ</button></div>
|
||
</form>
|
||
|
||
{{if eq .Settings.Fancy.DepoJobMode "bj"}}
|
||
<div style="margin-top:12px"><b>Инструкция для администратора SQL клиента</b> — выполнить один раз:
|
||
<pre class="copyable">{{.DepoJob.GrantScript}}</pre></div>
|
||
{{else if eq .Settings.Fancy.DepoJobMode "sql-auto"}}
|
||
<div style="margin-top:12px"><b>Инструкция для администратора SQL клиента</b> — выдать логину bj права оператора SQL Agent (SQL Agent должен быть запущен):
|
||
<pre class="copyable">{{.DepoJob.AgentRights}}</pre>
|
||
<form method="post" action="/admin/setup/fancy/depo-job/create" style="margin:8px 0 0"><input type="hidden" name="section" value="sec-fancy-depo-job"><button type="submit" class="btn btn-ok" onclick="return confirm('Создать джобу SQL Agent {{.DepoJob.Name}} на сервере клиента?')">⚙ Создать джобу</button></form></div>
|
||
{{else if eq .Settings.Fancy.DepoJobMode "sql-manual"}}
|
||
<div style="margin-top:12px"><b>Инструкция для администратора SQL клиента</b> — создать джобу (SQL Agent должен быть запущен, служебная учётная запись Agent должна иметь право выполнять процедуру):
|
||
<pre class="copyable">{{.DepoJob.AgentScript}}</pre></div>
|
||
{{end}}
|
||
|
||
{{if .Settings.Fancy.DepoJobMode}}
|
||
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:12px">
|
||
<form method="post" action="/admin/setup/fancy/depo-job/check" style="margin:0"><input type="hidden" name="section" value="sec-fancy-depo-job"><button type="submit" class="btn btn-ok">✓ Проверить готовность</button></form>
|
||
<span class="muted" style="font-size:12px">для вариантов с джобой bj пишет служебное событие BJ_PING и ждёт до 40 с, пока обработчик его отметит</span>
|
||
</div>
|
||
{{if .Settings.Fancy.DepoJobCheck}}<p style="margin-top:10px;font-size:13px;{{if .Settings.Fancy.DepoJobCheckOK}}color:var(--ok){{else}}color:var(--err){{end}}">{{if .Settings.Fancy.DepoJobCheckOK}}✓{{else}}✗{{end}} {{.Settings.Fancy.DepoJobCheck}} <span class="muted">({{.Settings.Fancy.DepoJobCheckAt.Format "02.01.2006 15:04"}})</span></p>{{end}}
|
||
{{if and (eq .Settings.Fancy.DepoJobMode "bj") .Settings.Fancy.DepoJobLast}}<p class="muted" style="font-size:12px;margin-top:6px">Последний вызов процедуры — {{.Settings.Fancy.DepoJobLast}}</p>{{end}}
|
||
{{end}}
|
||
</div>
|
||
|
||
</section>
|
||
|
||
{{/* ============ QUIK МНП ============ */}}
|
||
<section class="settings-section" id="sec-quik">
|
||
<h1>QUIK — Модуль неторговых поручений</h1>
|
||
{{template "verline" (.Versions.Get "quik-mnp")}}
|
||
<div class="card">
|
||
<h2><span class="dot {{if .Settings.Quik.Verified}}ok{{else if .Settings.Quik.Server}}warn{{else}}err{{end}}"></span>Подключение к БД МНП QUIK (MSSQL)</h2>
|
||
<p class="muted">При выводе бумаг клиентом bj блокирует их количество в QUIK через API Модуля неторговых поручений (хранимые процедуры). БД <strong>боевая</strong> — проверка только на чтение (версия + наличие M2M-объектов), без записи. Подключение под логином с ролью <code>InstrEu</code>.<br>
|
||
<a href="/admin/help/quik"><strong>Инструкция для администратора QUIK</strong></a> — что настроить в QuikAdmin для M2M · <a href="/admin/docs/">документы ARQA</a>.</p>
|
||
<form method="post" action="/admin/setup/quik" style="margin-top:12px;display:grid;gap:10px;max-width:560px">
|
||
<label>Сервер (host или host\instance)
|
||
<input type="text" name="server" value="{{.Settings.Quik.Server}}" placeholder="адрес SQL Server QUIK" style="width:100%" required></label>
|
||
<label>Порт
|
||
<input type="number" name="port" value="{{if .Settings.Quik.Port}}{{.Settings.Quik.Port}}{{else}}1433{{end}}" placeholder="8849" style="width:100%"></label>
|
||
<label>Имя БД (если требуется, иначе оставьте пустым)
|
||
<input type="text" name="database" value="{{.Settings.Quik.Database}}" placeholder="(по умолчанию)" style="width:100%"></label>
|
||
<label>Логин (роль InstrEu)
|
||
<input type="text" name="user" value="{{.Settings.Quik.User}}" placeholder="логин SQL" style="width:100%" required></label>
|
||
<label>Пароль
|
||
<input type="password" name="password" placeholder="{{if .Settings.Quik.Password}}•••••• — оставьте пустым, чтобы не менять{{else}}пароль SQL-логина{{end}}" autocomplete="off" style="width:100%"></label>
|
||
<label>Шифрование канала
|
||
<select name="encrypt" style="width:100%">
|
||
<option value="true" {{if eq .Settings.Quik.Encrypt "true"}}selected{{end}}>true — с TLS (боевой сервер обычно требует)</option>
|
||
<option value="disable" {{if eq .Settings.Quik.Encrypt "disable"}}selected{{end}}>disable — без TLS</option>
|
||
</select></label>
|
||
<label style="display:flex;gap:8px;align-items:center;font-weight:400">
|
||
<input type="checkbox" name="trust" value="1" {{if .Settings.Quik.Trust}}checked{{end}}>
|
||
Доверять сертификату сервера (TrustServerCertificate — для самоподписанного)</label>
|
||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||
<button type="submit" class="btn btn-ok">Сохранить</button>
|
||
</div>
|
||
</form>
|
||
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:12px">
|
||
<form method="post" action="/admin/setup/quik/test" style="margin:0"><button type="submit" class="btn">Проверить подключение (чтение)</button></form>
|
||
</div>
|
||
<p class="muted" style="margin-top:12px">Статус: {{if .Settings.Quik.Verified}}<span style="color:var(--ok)">✓ подключение проверено</span>{{else}}<span class="muted">не проверено</span>{{end}}</p>
|
||
</div>
|
||
|
||
<div class="card" id="sec-quik-cycle">
|
||
<h2><span class="dot {{if .Settings.Quik.Emulate}}warn{{else if not .Settings.Quik.WritesEnabled}}warn{{else if or .Settings.Quik.AutoOut .Settings.Quik.AutoIn}}ok{{else}}warn{{end}}"></span>Автоматический цикл M2M в МНП{{if .Settings.Quik.Emulate}} <span class="hero-status warn">{{if devOn}}имитация{{else}}остановлен{{end}}</span>{{end}}</h2>
|
||
<p class="muted">Что делает bj в Модуле неторговых поручений по ходу перевода: <strong>исходящий</strong> — при отправке в НРД регистрирует «Вывод ЦБ (M2M)» (QUIK блокирует бумаги), по подтверждению подписывает (списание), по отказу снимает с причиной; <strong>входящий</strong> — после приёма нашего поручения НРД регистрирует «Ввод ЦБ (M2M)» (зачисление сразу). Каждый шаг виден в карточке перевода и на странице <a href="/admin/quik">QUIK МНП</a>.</p>
|
||
<form method="post" action="/admin/setup/quik/cycle" style="margin-top:12px;display:grid;gap:10px;max-width:640px">
|
||
<input type="hidden" name="section" value="sec-quik-cycle">
|
||
<label style="display:flex;gap:8px;align-items:center;font-weight:600">
|
||
<input type="checkbox" name="writes_enabled" value="1" {{if .Settings.Quik.WritesEnabled}}checked{{end}}>
|
||
Разрешить записи в МНП (главный предохранитель — база боевая)</label>
|
||
{{if devOn}} <label style="display:flex;gap:8px;align-items:center;font-weight:600">
|
||
<input type="checkbox" name="emulate" value="1" {{if .Settings.Quik.Emulate}}checked{{end}}>
|
||
Имитация: шаги цикла (блокировка, подпись, снятие, зачисление) проходят и отмечаются «имитация», в МНП ничего не пишется — пока нет пользователя QUIK для блокировок и лимитов. Имеет приоритет над записью.</label>{{end}}
|
||
<label style="display:flex;gap:8px;align-items:center;font-weight:400;margin-left:22px">
|
||
<input type="checkbox" name="auto_out" value="1" {{if .Settings.Quik.AutoOut}}checked{{end}}>
|
||
Исходящие переводы: регистрировать при отправке, подписывать / снимать по ответу автоматически</label>
|
||
<label style="display:flex;gap:8px;align-items:center;font-weight:400;margin-left:22px">
|
||
<input type="checkbox" name="auto_in" value="1" {{if .Settings.Quik.AutoIn}}checked{{end}}>
|
||
Входящие переводы: зачислять в QUIK после приёма поручения НРД автоматически</label>
|
||
<label style="display:flex;gap:8px;align-items:center;font-weight:400;margin-left:22px">
|
||
<input type="checkbox" name="poll_terminal" value="1" {{if .Settings.Quik.PollTerminal}}checked{{end}}>
|
||
Принимаем поручения M2M из QUIK: клиентам разрешена подача «Вывод ЦБ (M2M)» из терминала / мобильного приложения — bj опрашивает их и создаёт заявку оператору. Если фирма такую подачу не разрешает, оставьте выключенным: раздел «Поручения QUIK» их показывать не будет.</label>
|
||
<label>Торговый счёт для регистрации поручений
|
||
<input type="text" name="account" value="{{.Settings.Quik.Account}}" placeholder="L01+00000F00" style="width:100%"></label>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
|
||
<label>МНП-инструмент вывода <span class="muted">(пусто = взять из класса INSTROUTM2M)</span>
|
||
<input type="text" name="sec_out" value="{{.Settings.Quik.SecOut}}" placeholder="RUROUTM2M" style="width:100%"></label>
|
||
<label>МНП-инструмент ввода <span class="muted">(пусто = из класса INSTRINM2M)</span>
|
||
<input type="text" name="sec_in" value="{{.Settings.Quik.SecIn}}" placeholder="RURINM2M" style="width:100%"></label>
|
||
</div>
|
||
<label>Исключения кодов клиентов <span class="muted">(по умолчанию «БР-001» → «BR001»; строка «номер портфеля=код QUIK»)</span>
|
||
<textarea name="client_code_map" rows="3" style="width:100%;font-family:ui-monospace,monospace;font-size:12px" placeholder="БР-4-4=BR44">{{quikCodeMap .Settings.Quik.ClientCodeMap}}</textarea></label>
|
||
<div style="display:flex;gap:8px;flex-wrap:wrap"><button type="submit" class="btn btn-ok">Сохранить</button></div>
|
||
</form>
|
||
</div>
|
||
|
||
{{if .Settings.Quik.LastReport}}
|
||
<div class="card">
|
||
<h2>Полный ответ сервера (последняя проверка)</h2>
|
||
<p class="muted">Можно выделить и скопировать целиком — для отправки коллегам/аутсорсеру QUIK.
|
||
{{if not .Settings.Quik.LastAt.IsZero}}Проверка от {{.Settings.Quik.LastAt.Format "02.01.2006 15:04:05"}} UTC.{{end}}</p>
|
||
<div style="display:flex;gap:8px;margin:8px 0">
|
||
<button type="button" class="btn" onclick="var t=document.getElementById('quik-report').textContent;navigator.clipboard&&navigator.clipboard.writeText(t);this.textContent='Скопировано ✓';setTimeout(()=>this.textContent='Скопировать отчёт',1500)">Скопировать отчёт</button>
|
||
</div>
|
||
<pre id="quik-report" style="white-space:pre-wrap;word-break:break-word;background:var(--surface-2,#0d1117);color:var(--code-fg,#c9d1d9);padding:14px;border-radius:8px;font-size:12.5px;line-height:1.5;overflow:auto;max-height:420px;user-select:all">{{.Settings.Quik.LastReport}}</pre>
|
||
</div>
|
||
{{end}}
|
||
</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) ============ */}}
|
||
<section class="settings-section" id="sec-quikexport">
|
||
{{.QuikExport.HTML}}
|
||
|
||
{{/* модуль НПР/УДС работает поверх базы экспорта QUIK — его настройки здесь же */}}
|
||
<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>
|
||
<h1>Лицензия</h1>
|
||
</section>
|
||
|
||
{{/* ============ КРИПТОГРАФИЯ ============ */}}
|
||
<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>
|
||
<label>Путь к модулю PKCS#11</label>
|
||
<input type="text" name="module_path" value="{{.Settings.Crypto.ModulePath}}" placeholder="/opt/Validata/VDCSP/lib/amd64/libvdpkcs11.so">
|
||
<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")}}
|
||
<div class="card" id="sec-org">
|
||
<h2><span class="dot {{if and .OrgINN .OrgCode}}ok{{else}}err{{end}}"></span>Наша организация</h2>
|
||
<p class="muted">Депозитарий — участник MOEX МОСТ, от имени которого bj отправляет и принимает переводы: ИНН передающего и зачисляющего депозитария в пакетах, код отправителя и код фирмы в QUIK. Выберите себя из справочника участников НРД или введите данные вручную.</p>
|
||
{{if and .OrgINN .OrgCode}}<p style="margin:8px 0 0"><b>{{if .OrgName}}{{.OrgName}}{{else}}—{{end}}</b> · ИНН <code>{{.OrgINN}}</code> · код <code>{{.OrgCode}}</code>{{if not .Settings.NSD.OrgINN}} <span class="muted">(ИНН взят из справочника по коду)</span>{{end}}</p>
|
||
{{else}}<p style="margin:8px 0 0;color:var(--err)">Организация не задана — переводы не будут сформированы.</p>{{end}}
|
||
<form method="post" action="/admin/setup/org" style="margin-top:12px;display:grid;gap:10px;max-width:680px">
|
||
<input type="hidden" name="section" value="sec-org">
|
||
<label>Найти себя в справочнике участников M2M</label>
|
||
<div class="hb-wrap"><input id="orgq" placeholder="Выберите из списка или введите ИНН, название, код" autocomplete="off"><button type="button" class="hb-toggle" title="Весь список">▾</button></div>
|
||
<div id="orgres" class="hb-res"></div>
|
||
<style>
|
||
.hb-box{position:relative}
|
||
.hb-res{margin-top:4px;max-height:320px;overflow:auto;border-radius:8px;border:1px solid var(--border);background:var(--card-2);padding:4px}
|
||
.hb-res:empty{display:none}
|
||
.hb-wrap{display:flex;gap:6px}.hb-wrap input{flex:1}
|
||
.hb-toggle{padding:0 12px;border-radius:8px;border:1px solid var(--border);background:var(--card);color:var(--text);cursor:pointer}
|
||
.hb-hit{display:block;width:100%;text-align:left;padding:7px 10px;margin:2px 0;border-radius:8px;border:1px solid var(--border);background:var(--card);color:var(--text);cursor:pointer;font-size:13px}
|
||
.hb-hit:hover{border-color:var(--accent)}
|
||
.hb-m{color:var(--muted);font-size:12px}
|
||
</style>
|
||
<script>
|
||
// hbFind — подбор участника справочника НРД по ИНН, названию или коду (без учёта регистра и кавычек).
|
||
function hbNorm(s){return String(s||'').toLowerCase().replace(/ё/g,'е').replace(/[«»"'“”.,()]/g,' ').replace(/\s+/g,' ').trim()}
|
||
function hbFind(list,q){q=hbNorm(q);
|
||
if(!q)return list.slice().sort((a,b)=>(a.kind===b.kind?0:a.kind==='depo'?-1:1)||hbNorm(a.name).localeCompare(hbNorm(b.name),'ru'));
|
||
const words=q.split(' ');
|
||
return list.map(p=>{const hay=hbNorm([p.inn,p.code,p.name,p.full_name,p.display_name,p.eng_name].join(' '));
|
||
if(!words.every(w=>hay.includes(w)))return null;
|
||
const score=(String(p.inn).startsWith(q)||String(p.code).toLowerCase().startsWith(q)?0:hbNorm(p.name).startsWith(q)||hbNorm(p.display_name).startsWith(q)?1:2)+(p.kind==='depo'?0:0.5);
|
||
return {p,score}}).filter(Boolean).sort((a,b)=>a.score-b.score||hbNorm(a.p.name).localeCompare(hbNorm(b.p.name),'ru')).map(x=>x.p)}
|
||
function hbBind(inp,box,list,onPick){
|
||
const esc=s=>String(s??'').replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
|
||
let hits=[],picked=inp.dataset.picked==='1';
|
||
const q=()=>picked?'':inp.value;
|
||
const draw=()=>{hits=hbFind(list,q());box.innerHTML=(hits.length?'<div class="hb-m" style="padding:2px 4px">'+(q().trim()?'Найдено: ':'Все участники: ')+hits.length+'</div>'+hits.map((p,i)=>'<button type="button" class="hb-hit" data-i="'+i+'" data-inn="'+esc(p.inn)+'" data-code="'+esc(p.code)+'"><b>'+esc(p.name)+'</b> <span class="hb-m">ИНН '+esc(p.inn)+' · '+esc(p.code)+(p.kind==='broker'?' · брокер':'')+'</span></button>').join(''):'<div class="hb-m" style="padding:6px 2px">В справочнике не найдено — введите ИНН и наименование в поля ниже.</div>');box.classList.add('open')};
|
||
const close=()=>{box.innerHTML='';box.classList.remove('open')};
|
||
inp.addEventListener('focus',()=>{if(picked)inp.select();draw()});
|
||
inp.addEventListener('input',()=>{picked=false;draw()});
|
||
// Enter выбирает только при единственном совпадении: иначе молча ушёл бы первый попавшийся.
|
||
inp.addEventListener('keydown',e=>{if(e.key==='Enter'){e.preventDefault();if(hits.length===1)pick(hits[0]);else if(hits.length>1){const m=box.querySelector('.hb-m');if(m)m.textContent='Найдено: '+hits.length+' — уточните запрос или выберите мышью'}}if(e.key==='Escape')close()});
|
||
const pick=p=>{onPick(p);picked=true;close()};
|
||
box.addEventListener('mousedown',e=>e.preventDefault());
|
||
// Участник — по ИНН и коду с самой кнопки, а не по номеру строки: список мог перерисоваться между показом и нажатием.
|
||
box.addEventListener('click',e=>{const b=e.target.closest('.hb-hit');if(!b)return;const p=list.find(x=>String(x.inn)===b.dataset.inn&&String(x.code)===b.dataset.code)||hits[+b.dataset.i];if(p)pick(p)});
|
||
inp.addEventListener('blur',()=>setTimeout(close,150));
|
||
const tg=inp.parentNode.querySelector('.hb-toggle');
|
||
if(tg)tg.addEventListener('click',()=>{if(box.classList.contains('open'))close();else{inp.focus();draw()}});
|
||
}
|
||
{
|
||
const inp=document.getElementById('orgq'),f=inp.form;
|
||
hbBind(inp,document.getElementById('orgres'),{{.Participants}}||[],p=>{f.org_code.value=p.code;f.org_inn.value=p.inn;f.org_name.value=p.full_name||p.name;inp.value=p.name+' · ИНН '+p.inn})
|
||
}
|
||
</script>
|
||
{{if not .Participants}}<span class="muted" style="font-size:12px">Справочник участников ещё не получен — запросите его в «Диагностике» или введите данные вручную.</span>{{end}}
|
||
<label>Код участника</label>
|
||
<input type="text" name="org_code" value="{{if .Settings.NSD.OrgCode}}{{.Settings.NSD.OrgCode}}{{else if .Suggest}}{{.Suggest.OrgCode}}{{end}}" placeholder="MC0000000000" maxlength="12">
|
||
<label>ИНН</label>
|
||
<input type="text" name="org_inn" value="{{if .Settings.NSD.OrgINN}}{{.Settings.NSD.OrgINN}}{{else if .Suggest}}{{.Suggest.OrgINN}}{{end}}" maxlength="10" inputmode="numeric">
|
||
<label>Наименование</label>
|
||
<input type="text" name="org_name" value="{{if .Settings.NSD.OrgName}}{{.Settings.NSD.OrgName}}{{else if .Suggest}}{{.Suggest.OrgName}}{{end}}">
|
||
{{if .Suggest}}{{if .Suggest.OrgName}}<span class="muted" style="font-size:12px">Из DEPO: {{.Suggest.OrgName}}, ИНН {{.Suggest.OrgINN}}, код {{.Suggest.OrgCode}} — подставлено в пустые поля.</span>{{end}}{{end}}
|
||
<div><button type="submit" class="btn btn-ok">Сохранить</button></div>
|
||
</form>
|
||
</div>
|
||
<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) ============ */}}
|
||
<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>
|
||
|
||
{{/* ============ ЕСИА ФИНАНС (внешняя платформа ЛК) ============ */}}
|
||
<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>
|
||
|
||
{{/* ============ ДИАГНОСТИКА (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></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>
|
||
<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)">
|
||
</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}}
|
||
{{define "verline"}}{{if .Title}}<p class="muted" style="font-size:12.5px;margin:-6px 0 12px">{{.Title}}: {{if .Version}}<strong>{{.Version}}</strong>{{else}}не определена{{end}}{{with .Detail}} · {{.}}{{end}}{{with .Err}} · <span style="color:var(--err)">{{.}}</span>{{end}}</p>{{end}}{{end}}
|