Files
Bridge-and-Join-s/internal/lkgateway/web/templates/admin_wizard.html
T
zuevav 44f00a006a Канал ИШ: необязательные параметры — NULL (иначе ИШ молча выкидывал канал); мастер: шаг «Учётные записи», HTTPS сразу, DEPO без процедур — не блокирует
- bj-igate-admin: пустые строки в числовых/интервальных параметрах канала ИШ разбирал с ошибкой и не
  загружал канал («There are no active channels», API 500 «more than one Channel…»); теперь NULL, как пишет
  интерфейс ИШ; старые каналы чинятся при «Создать канал»; папка архивов 1042S — по имени
- проверка «Шлюз НРД»: канал контура действительно загружен и включён (api/admin/channels/wsl)
- «Учёт DEPO»: нет процедур bj2depo — жёлтое «не забудьте доустановить», не блокирует
- HTTPS: перенаправление с http:// включено по умолчанию, только когда HTTPS реально слушает порт
- мастер: шаг «Учётные записи» (роли, создание, «Придумать пароль») перед проверкой; письма с проверки убраны;
  форма «создать» не меняет существующую запись с тем же логином

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
2026-10-06 23:19:01 +03:00

792 lines
73 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{define "content"}}
<style>
/* Мастер — как помощник установки: слева шаги по порядку, справа одна карточка, внизу «Назад» / «Далее». */
.wiz { display:grid; grid-template-columns:262px minmax(0,1fr); gap:32px; max-width:1080px; margin:10px auto 0; align-items:start; }
.wiz-rail { position:sticky; top:80px; min-width:0; }
.wiz-steps { list-style:none; margin:0; padding:0; display:grid; gap:2px; }
.wiz-st { display:flex; align-items:center; gap:12px; padding:9px 12px; border-radius:10px; color:var(--muted); font-size:14.5px; line-height:1.3; text-decoration:none; transition:background .15s var(--ease); }
.wiz-st:hover { background:var(--card-2); text-decoration:none; }
.wiz-st.done { color:var(--text-2); }
.wiz-st.cur { background:var(--accent-weak); color:var(--accent); font-weight:600; }
.wiz-num { flex:none; position:relative; width:26px; height:26px; border-radius:50%; border:1.5px solid var(--border-strong); display:grid; place-items:center; font-size:12.5px; font-weight:600; font-variant-numeric:tabular-nums; }
.wiz-st.cur .wiz-num { border-color:var(--accent); }
.wiz-st.done .wiz-num { background:var(--ok); border-color:var(--ok); }
.wiz-st.done .wiz-num::after, .wiz-ic::after { content:""; position:absolute; left:50%; top:45%; width:5px; height:10px; border:solid #fff; border-width:0 2px 2px 0; transform:translate(-50%,-50%) rotate(45deg); }
.wiz-rail-foot { margin:16px 12px 0; padding-top:14px; border-top:1px solid var(--border); font-size:12.5px; color:var(--muted); line-height:1.45; }
.wiz-rail-foot a { display:inline-block; margin-top:6px; }
.wiz-card { min-width:0; }
.wiz-card * { min-width:0; }
.wiz-card > .card { padding:28px 32px 22px; border-radius:16px; }
.wiz-card > .card > h2:first-child { font-size:21px; font-weight:680; letter-spacing:-0.015em; margin:0 0 6px; }
.wiz-card > .card > h2:first-child .dot { display:none; }
.wiz-card > .card > h2:first-child + p { color:var(--muted); font-size:14.5px; margin:0 0 18px; }
.wiz-card input:not([type=radio]):not([type=checkbox]):not([type=hidden]):not([type=file]), .wiz-card select, .wiz-card textarea { background:var(--card-2) !important; border:1px solid var(--border) !important; border-radius:10px !important; padding:10px 13px !important; font-size:14.5px; color:var(--text); }
.wiz-card input:focus, .wiz-card select:focus, .wiz-card textarea:focus { background:var(--card) !important; border-color:var(--accent) !important; outline:3px solid var(--accent-weak); }
.wiz-card .btn, .wiz-card button { border-radius:10px; }
.wiz-card input[type=radio], .wiz-card input[type=checkbox] { flex:none; width:auto; margin-top:3px; }
/* вложенные блоки общих разделов (организация, почта, копии…) внутри шага — без своей рамки, через разделитель */
.wiz-card > .card .card { border:0; border-top:1px solid var(--border); border-radius:0; box-shadow:none; background:transparent; padding:18px 0 0; margin:18px 0 0; }
.wiz-card > .card > h2:first-child + p + .card, .wiz-card > .card > .wiz-ok + .card { border-top:0; padding-top:0; margin-top:0; }
.wiz-card > .card .card form { max-width:none !important; }
.wiz-card .dot:not(.ok):not(.warn):not(.err) { display:none; }
.wiz-flash { background:var(--accent-weak); border-radius:12px; padding:12px 16px; margin:0 0 14px; font-size:14px; }
/* сведения «название — значение» и итог шага (зелёная плашка), как в помощнике установки */
.wiz-kv { display:grid; grid-template-columns:minmax(120px,190px) minmax(0,1fr); gap:10px 20px; margin:6px 0 0; font-size:14.5px; }
.wiz-kv dt { color:var(--muted); }
.wiz-kv dd { margin:0; overflow-wrap:anywhere; }
.wiz-ok, .wiz-info { display:flex; gap:10px; align-items:flex-start; padding:12px 14px; border-radius:10px; font-size:14px; margin:18px 0 0; }
.wiz-ok { background:var(--ok-weak); border:1px solid color-mix(in srgb, var(--ok) 30%, transparent); }
.wiz-info { background:var(--accent-weak); border:1px solid color-mix(in srgb, var(--accent) 22%, transparent); }
.wiz-ic { flex:none; position:relative; width:18px; height:18px; border-radius:50%; background:var(--ok); margin-top:1px; }
.wiz-ic::after { width:4px; height:8px; border-width:0 1.8px 1.8px 0; }
.wiz-nav { margin-top:26px; padding-top:18px; border-top:1px solid var(--border); display:flex; justify-content:space-between; align-items:center; gap:8px; flex-wrap:wrap; }
.wiz-back { color:var(--text-2); font-size:14.5px; font-weight:500; padding:8px 4px; }
.wiz-back:hover { color:var(--text); text-decoration:none; }
.wiz-card .wiz-next, .wiz-card .wiz-skip { border-radius:999px; padding:10px 22px; font-size:14.5px; font-weight:600; text-decoration:none; }
.wiz-card .wiz-skip { background:transparent; color:var(--text); border-color:var(--border-strong); }
.wiz-row { display:flex; align-items:center; gap:10px 20px; flex-wrap:wrap; }
.wiz-row-l { color:var(--muted); font-weight:400; font-size:14.5px; min-width:170px; margin:0; }
.wiz-opt { display:flex; gap:8px; align-items:flex-start; font-weight:400; margin:0 0 6px; }
.wiz-opt .muted { font-size:13px; }
/* Учёт DEPO: вопросы по порядку (номер — счётчиком), выбор — плитками, под ними — панель выбранного способа */
.dq { counter-reset:dq; }
.dq-s { padding:18px 0 20px; border-top:1px solid var(--border); }
.dq-s:first-child { border-top:0; padding-top:2px; }
.dq-h { display:flex; align-items:center; gap:10px; font-size:15.5px; font-weight:650; margin:0 0 12px; }
.dq-h::before { counter-increment:dq; content:counter(dq); flex:none; width:22px; height:22px; border-radius:50%; background:var(--accent-weak); color:var(--accent); display:grid; place-items:center; font-size:12px; font-weight:700; }
.dq-tiles { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:10px; }
.dq-tile { display:flex; gap:10px; align-items:flex-start; padding:12px 14px; border:1px solid var(--border); border-radius:12px; cursor:pointer; font-weight:400; margin:0; transition:border-color .15s var(--ease), background .15s var(--ease); }
.dq-tile:hover { border-color:var(--border-strong); }
.dq-tile:has(input:checked) { border-color:var(--accent); background:var(--accent-weak); }
.dq-tile b { display:block; font-weight:600; font-size:14.5px; }
.dq-tile small { display:block; color:var(--muted); font-size:13px; line-height:1.4; margin-top:3px; }
.dq-alt { display:inline-flex; gap:8px; align-items:flex-start; margin:12px 0 0; font-weight:400; font-size:13.5px; color:var(--text-2); cursor:pointer; }
.dq-grid { display:grid; gap:10px 12px; }
.dq-grid label { display:block; margin:0; font-size:13.5px; font-weight:500; color:var(--text-2); }
.dq-grid input, .dq-grid select { display:block; width:100%; box-sizing:border-box; margin-top:5px; }
.dq-srv { grid-template-columns:minmax(0,1.5fr) minmax(0,.55fr) minmax(0,1.25fr); }
.dq-2 { grid-template-columns:repeat(2,minmax(0,1fr)); }
.dq-opt { color:var(--muted); font-weight:400; }
.dq-note { color:var(--muted); font-size:13px; margin:8px 0 0; }
.dq-panel { margin-top:18px; }
.dq-panel + .dq-panel { margin-top:14px; }
.wiz-card .dq-go { margin-top:16px; border-radius:999px; padding:10px 22px; font-size:14.5px; font-weight:600; }
.wiz-card .dq-mono { font-family:var(--mono); }
.dq-sql { margin-top:14px; }
.dq-sql-head { display:flex; align-items:center; gap:8px; margin:0 0 6px; justify-content:flex-start; }
.dq-sql-st { flex:1; font-size:12.5px; color:var(--muted); }
.dq-sql pre:empty::before { content:attr(data-msg); color:var(--muted); font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif; font-size:13px; }
.dq-sql pre { max-height:380px; min-height:140px; font-size:12px; margin:0; transition:opacity .2s; }
.dq-sql pre.dq-busy { opacity:.5; }
.dq.is-pg [data-ms], .dq:not(.is-pg) [data-pg] { display:none !important; }
.dq-jobname { margin-top:14px; max-width:520px; }
.dq-jobname label { display:block; margin:0; font-size:13.5px; font-weight:500; color:var(--text-2); }
.dq-jobname input { display:block; width:100%; box-sizing:border-box; margin-top:5px; }
/* установка ключа (общий блок part_keywiz) — в виде мастера */
.keywiz ol { gap:4px !important; }
.keywiz ol li > div > div:first-child { font-weight:500 !important; font-size:14.5px; }
.keywiz ol li > span:first-child { flex-basis:24px !important; height:24px !important; font-size:12px !important; }
.keywiz .card h2 { font-size:16px; margin:0 0 6px; }
.keywiz form .btn-ok, .keywiz form .btn-secondary { justify-self:start; border-radius:999px; padding:10px 22px; font-weight:600; }
.keywiz form .btn-ok { background:var(--accent); border-color:var(--accent); }
.dq-box { border:1px solid var(--border); border-radius:12px; padding:14px 16px 16px; }
.dq-box-bj { background:color-mix(in srgb, var(--accent-weak) 45%, transparent); }
.dq-box-h { font-weight:600; font-size:14.5px; margin:0 0 10px; }
.dq-box-h span { color:var(--muted); font-weight:400; font-size:13px; }
.dq-lbl-row { display:flex; justify-content:space-between; align-items:baseline; gap:8px; }
.wiz-card .dq-link { background:none; border:0; padding:0; color:var(--accent); font-size:12.5px; font-weight:500; cursor:pointer; border-radius:4px; }
.dq-link:hover { text-decoration:underline; }
.dq-warn { margin:10px 0 0; padding:8px 12px; border-radius:8px; background:var(--warn-weak); color:var(--text-2); font-size:13px; }
/* ход подготовки: строки шагов со значком состояния */
.dq-run { margin-top:16px; border:1px solid var(--border); border-radius:12px; padding:12px 16px; }
.dq-steps { list-style:none; margin:0; padding:0; display:grid; gap:2px; }
.dq-st { display:flex; gap:10px; align-items:flex-start; padding:6px 0; font-size:14px; }
.dq-st-t { min-width:0; }
.dq-st-d { color:var(--muted); font-size:12.5px; margin-top:2px; overflow-wrap:anywhere; }
.dq-st-ic { flex:none; position:relative; width:18px; height:18px; border-radius:50%; margin-top:1px; box-sizing:border-box; }
.dq-st.is-run .dq-st-ic { border:2px solid var(--accent-weak); border-top-color:var(--accent); animation:dq-spin .8s linear infinite; }
.dq-st.is-run { color:var(--text); font-weight:500; }
.dq-st.is-ok .dq-st-ic { background:var(--ok); }
.dq-st.is-ok .dq-st-ic::after { content:""; position:absolute; left:50%; top:45%; width:4px; height:8px; border:solid #fff; border-width:0 1.8px 1.8px 0; transform:translate(-50%,-50%) rotate(45deg); }
.dq-st.is-ok:not(.is-final) { color:var(--text-2); }
.dq-st.is-final { font-weight:600; color:var(--ok); }
.dq-st.is-warn .dq-st-ic, .dq-st.is-err .dq-st-ic { color:#fff; font-size:12px; font-weight:700; line-height:18px; text-align:center; }
.dq-st.is-warn .dq-st-ic { background:var(--warn); }
.dq-st.is-warn .dq-st-ic::after { content:"!"; }
.dq-st.is-err .dq-st-ic { background:var(--err); }
.dq-st.is-err .dq-st-ic::after { content:"×"; }
.dq-st.is-err { color:var(--err); }
.dq-run-msg { margin-top:8px; padding:10px 12px; border-radius:8px; background:var(--err-weak, rgba(200,40,40,.08)); font-size:13.5px; overflow-wrap:anywhere; white-space:pre-wrap; }
@keyframes dq-spin { to { transform:rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .dq-st.is-run .dq-st-ic { animation:none; border-color:var(--accent); } }
@media (max-width:600px) { .dq-tiles, .dq-srv, .dq-2 { grid-template-columns:minmax(0,1fr); } .dq-sql-head { flex-wrap:wrap; } .dq-sql-st { flex-basis:100%; } }
.tooltip { display:inline-block; background:var(--border); color:var(--muted); border-radius:50%; width:16px; height:16px; line-height:16px; text-align:center; font-size:11px; cursor:help; margin-left:4px; }
.help-block { background:var(--accent-weak); padding:10px 14px; margin:10px 0; font-size:13px; border-radius:10px; }
.help-block strong { color:var(--accent); }
/* узкий экран: шаги — строкой с прокруткой над карточкой */
@media (max-width:820px) {
.wiz { grid-template-columns:minmax(0,1fr); gap:14px; }
.wiz-rail { position:static; }
.wiz-steps { display:flex; overflow-x:auto; gap:4px; padding-bottom:4px; scrollbar-width:none; }
.wiz-st { white-space:nowrap; padding:7px 10px; font-size:13.5px; }
.wiz-rail-foot { display:none; }
.wiz-card > .card { padding:20px 18px 16px; }
.wiz-kv { grid-template-columns:minmax(0,1fr); gap:2px 0; }
.wiz-kv dd { margin-bottom:8px; }
}
@media (max-width:600px) { .wiz-card [style*="grid-template-columns"] { grid-template-columns:minmax(0,1fr) !important; } }
</style>
<div class="wiz">
<aside class="wiz-rail" aria-label="Шаги настройки">
<ol class="wiz-steps">
{{range .Chapters}}{{range .Steps}}<li><a href="/admin/wizard?step={{.N}}" class="wiz-st{{if .Done}} done{{end}}{{if .Current}} cur{{end}}"{{if .Current}} aria-current="step"{{end}}><span class="wiz-num">{{if not .Done}}{{.Num}}{{end}}</span><span>{{.Title}}</span></a></li>{{end}}{{end}}
</ol>
<div class="wiz-rail-foot">Всё сохраняется сразу — можно прерваться и продолжить позже.<br><a href="/admin/">Настроить позже →</a></div>
</aside>
<div class="wiz-card">
{{if .Flash}}<div class="wiz-flash">{{.Flash}}</div>{{end}}
{{/* ============= ШАГ 1: Лицензия ============= */}}
{{if eq .Step 1}}
{{/* восстановление из резервной копии — только на первой странице (/first-run), в мастере его нет */}}
<div class="card">
<h2><span class="dot {{if .Done.License}}ok{{else}}err{{end}}"></span>Лицензия</h2>
{{if .Done.License}}<p>Ключ продукта открывает модули bj и обновления.</p>
<dl class="wiz-kv">
<dt>Организация</dt><dd>{{.Lic.Tenant}}</dd>
<dt>Модули</dt><dd>{{if eq .Lic.Plan "enterprise"}}все модули{{else if .Lic.Features}}{{range $i, $f := .Lic.Features}}{{if $i}}, {{end}}{{modTitle $f}}{{end}}{{else}}только ядро: обмен с НРД и переводы M2M{{end}}</dd>
<dt>Срок</dt><dd>{{.Lic.Message}}</dd>
</dl>
{{else}}<p>Введите ключ продукта — bj сам скачает и поставит модули вашей лицензии.</p>
<p class="muted" style="margin:-10px 0 0;font-size:13px">Ключ вида BJ-XXXX-XXXX-XXXX — в портале клиента <a href="https://lic.zetit.ru/portal/" target="_blank">lic.zetit.ru/portal</a> или в письме от ZETIT.</p>{{end}}
{{if and .Update .Update.HasUpdate}}
<div class="wiz-info" style="display:block">
<p style="margin:0 0 6px;font-weight:600">Есть новая версия bj — {{.Update.Available}}</p>
<p class="muted" style="margin:0 0 10px;font-size:13px">С исправлениями ядра, установщика и служб. Поставьте её до настройки: bj перезапустится примерно на 10 секунд.<br>{{.Update.Message}}</p>
<form method="post" action="/admin/setup/update/apply" style="margin:0">
<input type="hidden" name="return_to" value="/admin/wizard?step=1">
<button type="submit" class="btn">Обновить сейчас</button>
</form>
</div>
{{else if .Done.License}}<div class="wiz-ok"><i class="wiz-ic"></i><span>Лицензия активна{{if .Update}}, установлена последняя версия bj{{end}}.</span></div>{{end}}
{{if .Done.License}}<details style="margin-top:12px"><summary style="cursor:pointer;color:var(--accent);font-size:13px">Сменить ключ продукта (обычно не нужно)</summary>
<form method="post" action="/admin/setup/license/activate" style="margin-top:12px;display:grid;gap:10px">
<input type="hidden" name="return_to" value="/admin/wizard?step=1">
<label>Ключ продукта</label>
<input type="text" name="code" placeholder="BJ-XXXX-XXXX-XXXX" autocomplete="off" style="text-transform:uppercase;max-width:340px">
<button type="submit" class="btn" style="justify-self:start">{{if .Done.License}}Сменить ключ{{else}}Активировать{{end}}</button>
</form>
</details>{{else}}
<form method="post" action="/admin/setup/license/activate" style="margin-top:14px;display:flex;gap:10px;flex-wrap:wrap">
<input type="hidden" name="return_to" value="/admin/wizard?step=1">
<input type="text" name="code" placeholder="BJ-XXXX-XXXX-XXXX" aria-label="Ключ продукта" autocomplete="off" autofocus required style="flex:1 1 260px;font-size:16px;letter-spacing:.04em;text-transform:uppercase">
<button type="submit" class="btn">Активировать</button>
</form>{{end}}
{{template "wizNav" .}}
</div>
{{end}}
{{/* ============= ШАГ 3: Учёт DEPO (Fancy) — по экранам ============= */}}
{{if eq .Step 3}}
<div class="card">
<h2><span class="dot {{if .Done.Depo}}ok{{else}}err{{end}}"></span>Учётная система DEPO (Fancy)</h2>
{{if not .DepoLicensed}}
<p class="muted">Модуль обмена с DEPO и QUIK не входит в текущую лицензию — шаг пропускается.</p>
{{else}}
{{$sub := .Sub}}
{{if eq $sub "setup"}}
<p>bj сверяет каждый перевод с учётом DEPO и сам создаёт в нём поручения. Четыре вопроса по порядку — из ответов bj соберёт подготовку базы.</p>
{{$job := .Settings.Fancy.DepoJobMode}}{{$pg := eq .Settings.Fancy.DBType "postgres"}}
<form method="post" action="/admin/setup/wizard-depo/setup" id="wiz-depo" class="dq{{if $pg}} is-pg{{end}}">
<section class="dq-s">
<h3 class="dq-h">Какая СУБД у DEPO</h3>
<div class="dq-tiles">
<label class="dq-tile"><input type="radio" name="db_type" value="mssql" {{if not $pg}}checked{{end}}><span><b>Microsoft SQL Server</b></span></label>
<label class="dq-tile"><input type="radio" name="db_type" value="postgres" {{if $pg}}checked{{end}}><span><b>PostgreSQL</b></span></label>
</div>
</section>
<section class="dq-s">
<h3 class="dq-h">Где база DEPO</h3>
<div class="dq-grid dq-srv">
<label>Адрес сервера<input type="text" name="server" value="{{.Settings.Fancy.Server}}" placeholder="IP-адрес или имя\экземпляр" required autofocus spellcheck="false"></label>
<label>Порт<input type="text" name="port" value="{{if .Settings.Fancy.Port}}{{.Settings.Fancy.Port}}{{else if $pg}}5432{{else}}1433{{end}}" inputmode="numeric"></label>
<label>Имя базы<input type="text" name="depo_database" value="{{.Settings.Fancy.DepoDatabase}}" placeholder="как называется на сервере" required pattern="[A-Za-z][A-Za-z0-9_]*" title="латинские буквы, цифры и _, начинается с буквы" spellcheck="false"></label>
</div>
<p class="dq-note">Точное имя базы учёта DEPO — bj будет работать только с ней. Перед подготовкой bj проверит, что в ней есть учётные таблицы DEPO, и в чужую базу ничего не запишет.</p>
</section>
<section class="dq-s">
<h3 class="dq-h">Где запускать обработку обмена</h3>
<div class="dq-tiles" data-ms>
<label class="dq-tile"><input type="radio" name="depo_job" value="agent" {{if ne $job "bj"}}checked{{end}}><span><b>На SQL Server</b><small>Задание SQL Server Agent. Рекомендуется: работает, даже когда bj выключен.</small></span></label>
<label class="dq-tile"><input type="radio" name="depo_job" value="bj" {{if eq $job "bj"}}checked{{end}}><span><b>На сервере bj</b><small>Если SQL Server Agent нет — например, SQL Server Express.</small></span></label>
</div>
<div class="dq-jobname" id="wiz-depo-jobbox" data-ms>
<label>Имя задания SQL Server Agent<input type="text" name="depo_job_name" value="{{.DepoJobName}}" data-auto="{{if .DepoJobName}}0{{else}}1{{end}}" pattern="[A-Za-z0-9_.\-]{1,100}" title="латинские буквы, цифры, точка, дефис и _" placeholder="bj2depo_имя_базы" spellcheck="false" class="dq-mono"></label>
<p class="dq-note">Задание называется по базе: у каждой базы DEPO на сервере — своё. Если обработку этой базы уже запускает задание, впишите его имя — второе создано не будет.</p>
</div>
<p class="dq-note" data-pg>Для PostgreSQL обработку запускает сервер bj — выбирать не нужно.</p>
</section>
<section class="dq-s">
<h3 class="dq-h">Как подготовить базу</h3>
<div class="dq-tiles">
<label class="dq-tile"><input type="radio" name="mode" value="auto" {{if ne .DepoMode "script"}}checked{{end}}><span><b>Дать логин администратора</b><small>bj сделает всё сам. Логин нужен один раз — bj его не сохраняет.</small></span></label>
<label class="dq-tile"><input type="radio" name="mode" value="script" {{if eq .DepoMode "script"}}checked{{end}}><span><b>Получить скрипт</b><small>Если логин администратора не дают: администратор выполнит один скрипт.</small></span></label>
</div>
<label class="dq-alt"><input type="radio" name="mode" value="existing"> База уже подготовлена раньше — у bj есть свой логин</label>
<div class="dq-panel dq-box" id="wiz-depo-auto">
<div class="dq-box-h">Администратор SQL <span>— нужен один раз, чтобы подготовить базу; bj его не сохраняет</span></div>
<div class="dq-grid dq-2">
<label>Логин<input type="text" name="admin_user" autocomplete="off" spellcheck="false" placeholder="{{if $pg}}postgres{{else}}sa{{end}}"></label>
<label>Пароль<input type="password" name="admin_password" autocomplete="new-password"></label>
</div>
</div>
{{/* логин bj — в обоих способах: на общем сервере SQL bj_user может быть уже занят другим bj */}}
<div class="dq-panel dq-box dq-box-bj" id="wiz-depo-login">
<div class="dq-box-h">Логин bj <span>— под ним bj будет работать с базой DEPO</span></div>
<div class="dq-grid dq-2">
<label>Логин<input type="text" name="bj_user" value="{{.DepoUser}}" autocomplete="off" spellcheck="false"></label>
<label><span class="dq-lbl-row">Пароль <button type="button" class="dq-link" id="wiz-depo-gen">↻ другой пароль</button></span><input type="text" name="bj_password" value="{{.DepoPass}}" autocomplete="off" spellcheck="false" class="dq-mono"></label>
</div>
<p class="dq-note">bj придумал их сам. Можно заменить своими — <b>bj сохранит и будет использовать ровно то, что здесь написано</b>: подготовка создаст на сервере этот логин с этим паролем<span id="wiz-depo-login-script"> (в скрипте — они же)</span>.</p>
<p class="dq-warn">Если логин с таким именем на сервере уже есть, ему будет поставлен этот пароль — программа, которая им пользуется, перестанет входить. Тогда впишите другое имя.</p>
</div>
<div class="dq-panel" id="wiz-depo-autogo">
<p class="dq-note">bj проверит, что это база DEPO, создаст свой логин, таблицы обмена<span data-ms> и задание обработки</span>, выдаст права и проверит подключение.</p>
<button type="submit" class="btn dq-go">Подготовить</button>
</div>
<div class="dq-panel" id="wiz-depo-script">
<div class="dq-sql">
<div class="dq-sql-head copy-bar">
<span class="dq-sql-st" id="wiz-depo-st">Скрипт собирается из ответов выше и обновляется сам.</span>
<button type="button" id="wiz-depo-copy">Скопировать</button>
<button type="submit" formaction="/admin/setup/wizard-depo/script.sql">Скачать файлом</button>
</div>
<pre class="copyable" id="wiz-depo-sql" data-msg="Впишите имя базы DEPO — скрипт появится здесь."></pre>
</div>
<p class="dq-note"><span data-ms>Администратор выполняет скрипт в SSMS или sqlcmd под логином с правами sysadmin.</span><span data-pg>Администратор выполняет скрипт в psql суперпользователем (postgres).</span> Когда выполнит — нажмите «Проверить подключение».</p>
{{template "letterCard" (letter "dba")}}
<button type="submit" class="btn dq-go">Проверить подключение</button>
</div>
<div class="dq-panel" id="wiz-depo-existing">
<div class="dq-grid dq-2">
<label>Логин bj<input type="text" name="user" value="{{.Settings.Fancy.User}}" autocomplete="off" spellcheck="false"></label>
<label>Пароль<input type="password" name="password" autocomplete="new-password" placeholder="{{if .Settings.Fancy.Password}}пусто — прежний{{end}}"></label>
</div>
<button type="submit" class="btn dq-go">Проверить подключение</button>
</div>
{{/* ход подготовки/проверки — по шагам, пока сервер работает (ответ приходит строками) */}}
<div class="dq-run" id="wiz-depo-run" hidden aria-live="polite">
<ol class="dq-steps" id="wiz-depo-steps"></ol>
<div class="dq-run-msg" id="wiz-depo-runmsg" hidden></div>
</div>
</section>
</form>
<script>(function(){
var f=document.getElementById('wiz-depo'), pre=document.getElementById('wiz-depo-sql');
var timer=0, seq=0, last='';
function sync(){ // СУБД меняет подсказки, порт и обязательность имени базы; способ — какую панель показать
var pg=f.db_type.value==='postgres', m=f.mode.value;
f.classList.toggle('is-pg', pg);
if(pg&&f.port.value==='1433') f.port.value='5432'; else if(!pg&&f.port.value==='5432') f.port.value='1433';
f.admin_user.placeholder=pg?'postgres':'sa';
var jn=f.depo_job_name, dbv=(f.depo_database.value||'').trim(); // имя задания — по базе, пока не поправили руками
if(jn.dataset.auto==='1') jn.value=dbv?'bj2depo_'+dbv:'';
document.getElementById('wiz-depo-jobbox').hidden=f.depo_job.value!=='agent';
var show={auto:['auto','login','autogo'], script:['login','script'], existing:['existing']}[m]||[];
['auto','login','autogo','script','existing'].forEach(function(k){ document.getElementById('wiz-depo-'+k).hidden=show.indexOf(k)<0; });
document.getElementById('wiz-depo-login-script').hidden=m!=='script';
}
function load(){ // скрипт — из текущих ответов; устаревший ответ (пока печатали дальше) не показываем
if(f.mode.value!=='script') return;
var fd=new FormData(f); fd.set('view','1'); fd.delete('admin_password'); fd.delete('password');
var body=new URLSearchParams(fd).toString();
if(body===last) return; last=body;
var my=++seq; pre.classList.add('dq-busy');
fetch('/admin/setup/wizard-depo/script.sql',{method:'POST',body:body,headers:{'Content-Type':'application/x-www-form-urlencoded'},cache:'no-store'})
.then(function(r){return r.text().then(function(t){return {ok:r.ok,t:t}})})
.then(function(x){ if(my!==seq) return; pre.classList.remove('dq-busy');
if(x.ok){ pre.textContent=x.t; }
else { pre.textContent=''; pre.dataset.msg=x.t.trim().replace(/^./,function(c){return c.toUpperCase()})+' — скрипт появится здесь.'; } })
.catch(function(){ if(my!==seq) return; last=''; pre.classList.remove('dq-busy'); pre.textContent=''; pre.dataset.msg='Не получилось собрать скрипт — обновите страницу.'; });
}
function changed(){ sync(); clearTimeout(timer); timer=setTimeout(load, 350); }
f.depo_job_name.addEventListener('input', function(){ this.dataset.auto=this.value.trim()?'0':'1'; });
f.addEventListener('input', changed); f.addEventListener('change', changed);
document.getElementById('wiz-depo-copy').onclick=function(){ var b=this;
window.bjCopy(pre.textContent).then(function(ok){ b.textContent=ok?'Скопировано ✓':'Выделите и скопируйте вручную'; b.classList.toggle('done',!!ok);
setTimeout(function(){ b.textContent='Скопировать'; b.classList.remove('done'); }, 2000); }); };
document.getElementById('wiz-depo-gen').onclick=function(){ // тот же набор, что у bj: без похожих 0/O, 1/l/I
var up='ABCDEFGHJKLMNPQRSTUVWXYZ', low='abcdefghijkmnpqrstuvwxyz', dig='23456789', all=up+low+dig, a=new Uint32Array(24), p=[];
crypto.getRandomValues(a);
p.push(up[a[0]%up.length], low[a[1]%low.length], dig[a[2]%dig.length]);
for(var i=3;i<24;i++) p.push(all[a[i]%all.length]);
for(var j=p.length-1;j>0;j--){ var k=a[j]%(j+1), t=p[j]; p[j]=p[k]; p[k]=t; }
f.bj_password.value=p.join(''); changed();
};
// «Подготовить» / «Проверить подключение»: ход по шагам вместо тишины до перехода на следующий экран
var run=document.getElementById('wiz-depo-run'), list=document.getElementById('wiz-depo-steps'), rmsg=document.getElementById('wiz-depo-runmsg');
function li(key){ return list.querySelector('[data-k="'+key+'"]'); }
function mark(el, st, text){ if(!el) return; el.className='dq-st is-'+st;
if(text){ var d=el.querySelector('.dq-st-d'); if(!d){ d=document.createElement('div'); d.className='dq-st-d'; el.querySelector('.dq-st-t').appendChild(d); } d.textContent=text; } }
function onStep(x){
if(x.state==='run'){
list.querySelectorAll('.is-run').forEach(function(e){ mark(e,'ok'); });
var e=li(x.step); if(!e){ e=document.createElement('li'); e.dataset.k=x.step; e.innerHTML='<span class="dq-st-ic"></span><div class="dq-st-t"><span></span></div>'; list.appendChild(e); }
e.querySelector('.dq-st-t > span').textContent=x.text; mark(e,'run');
} else mark(li(x.step), x.state, x.text);
}
function finish(x, btns){
var cur=list.querySelector('.is-run');
if(x&&x.ok){ if(cur) mark(cur,'ok');
var e=document.createElement('li'); e.className='dq-st is-ok is-final'; e.innerHTML='<span class="dq-st-ic"></span><div class="dq-st-t"><span>Готово — открываю итог</span></div>'; list.appendChild(e);
setTimeout(function(){ location.href=x.go; }, 1200); return; }
if(cur) mark(cur,'err');
rmsg.textContent=(x&&x.msg)?x.msg.replace(/^./,function(c){return c.toUpperCase()}):'Связь с bj прервалась — обновите страницу и попробуйте ещё раз.';
rmsg.hidden=false; btns.forEach(function(b){ b.disabled=false; });
}
f.addEventListener('submit', function(ev){
var b=ev.submitter; if(!b||!b.classList.contains('dq-go')||!window.fetch||!window.TextDecoder) return;
ev.preventDefault();
var btns=[].slice.call(f.querySelectorAll('.dq-go')); btns.forEach(function(x){ x.disabled=true; });
list.innerHTML=''; rmsg.hidden=true; run.hidden=false; run.scrollIntoView({block:'nearest',behavior:'smooth'});
var fd=new FormData(f); fd.set('stream','1');
var final=null;
fetch(f.action,{method:'POST',body:new URLSearchParams(fd),headers:{'Content-Type':'application/x-www-form-urlencoded'},cache:'no-store'})
.then(function(r){
if(!r.ok||!r.body||(r.headers.get('Content-Type')||'').indexOf('ndjson')<0) throw new Error('bad');
var rd=r.body.getReader(), dec=new TextDecoder(), buf='';
function line(t){ if(!t.trim()) return; var x=JSON.parse(t); if(x.done) final=x; else onStep(x); }
function pump(){ return rd.read().then(function(c){
if(c.done){ line(buf); return; }
buf+=dec.decode(c.value,{stream:true}); var i;
while((i=buf.indexOf('\n'))>=0){ line(buf.slice(0,i)); buf=buf.slice(i+1); }
return pump(); }); }
return pump();
})
.then(function(){ finish(final, btns); }, function(){ finish(null, btns); });
});
sync(); load();
})();</script>
{{else if eq $sub "done"}}
<table style="margin:6px 0 10px">
<tr><td>Подключение к базе DEPO</td><td>{{if .Settings.Fancy.Server}}<span style="color:var(--ok)">✓ {{.Settings.Fancy.Server}}, база {{.DepoTables.DB}}, логин bj «{{.Settings.Fancy.User}}»</span>{{else}}<span style="color:var(--err)">не настроено</span>{{end}}</td></tr>
<tr><td>Таблицы обмена FTV_M2M</td><td>{{if .Settings.Fancy.DepoTablesCheckOK}}<span style="color:var(--ok)">✓ на месте</span>{{else}}<span style="color:var(--err)">✗ {{.Settings.Fancy.DepoTablesCheck}}</span>{{end}}</td></tr>
<tr><td>Запуск обработки</td><td>{{if .Settings.Fancy.DepoJobCheckOK}}<span style="color:var(--ok)">✓ {{.Settings.Fancy.DepoJobCheck}}</span>{{else}}<span style="color:var(--err)">✗ {{.Settings.Fancy.DepoJobCheck}}</span>{{end}}</td></tr>
<tr><td>Обработка событий (процедуры)</td><td>{{if .Settings.Fancy.DepoProcCheckOK}}<span style="color:var(--ok)">✓ {{.Settings.Fancy.DepoProcCheck}}</span>{{else if .Settings.Fancy.DepoProcCheck}}<span style="color:var(--warn)">⚠ {{.Settings.Fancy.DepoProcCheck}}</span>{{else}}<span class="muted">не проверялась — «Перепроверить всё»</span>{{end}}</td></tr>
</table>
{{if .Settings.Fancy.Server}}{{template "accPrefix" .}}{{end}}
{{if .Done.Depo}}<p>Учёт DEPO подключён и проверен. Дальше — «Организация»: bj предложит реквизиты вашей организации из DEPO.</p>{{else}}<p class="muted" style="font-size:13px">Не всё проверено — можно продолжить настройку (шаг 4 и дальше) и вернуться сюда позже. Пока обработка не работает, записи bj ждут в таблицах обмена со статусом TODO и будут обработаны, когда она заработает.</p>{{end}}
<form method="post" action="/admin/setup/wizard-depo/recheck" style="margin:0"><button type="submit" class="btn" style="background:var(--card);color:var(--text);border:1px solid var(--border-strong,#c9cdd8)">Перепроверить всё</button></form>
{{end}}
{{end}}
{{template "wizNav" .}}
</div>
{{end}}
{{/* ============= ШАГ 2: База bj (PostgreSQL) ============= */}}
{{if eq .Step 2}}
<div class="card">
<h2><span class="dot {{if .Done.Postgres}}ok{{else}}err{{end}}"></span>База данных bj (PostgreSQL)</h2>
<p>База самого bj: журнал заявок и поручений, история обмена с НРД и DEPO, пользователи.</p>
{{if .Done.Postgres}}<div class="wiz-ok"><i class="wiz-ic"></i><span>База готова{{if .PGSummary}}: {{.PGSummary}}{{end}}. Её создал установщик — ничего делать не нужно.</span></div>{{end}}
{{if not .Done.Postgres}}<div class="help-block">
<strong>Обычно базу создаёт установщик</strong> — если вы ставили bj с образа или командой установки, этот шаг уже выполнен. Если база bj на отдельном сервере PostgreSQL — введите строку подключения ниже{{if devOn}}, или поднимите локальную базу автоматически (режим разработки){{end}}.
</div>{{end}}
{{if and devOn (not .Settings.Postgres.DSN)}}
<div style="background:var(--bg);border:1px solid var(--accent);border-radius:6px;padding:14px;margin-top:12px">
<h3 style="margin:0 0 8px 0;font-size:15px">Вариант А — для тех, у кого нет своего PostgreSQL</h3>
<p class="muted" style="margin:0 0 10px 0">Bridge-and-Join-s сам поднимет PostgreSQL в контейнере (podman-compose), создаст БД <code>bj</code> и накатит миграции. Подходит для дев-стенда. Для продакшена лучше указать свой DSN.</p>
<form method="post" action="/admin/setup/postgres/quick-start" style="margin:0">
<button type="submit" class="btn" style="background:var(--ok)">Поднять локальный PostgreSQL автоматически</button>
<span class="muted" style="margin-left:10px;font-size:12px">~10-30 сек</span>
</form>
</div>
{{end}}
<details style="margin-top:14px" {{if not .Settings.Postgres.DSN}}open{{end}}>
<summary style="cursor:pointer;color:var(--accent)">{{if .Settings.Postgres.DSN}}Подключить другую базу (обычно не нужно){{else}}Вариант Б — у меня уже есть PostgreSQL, введу DSN сам{{end}}</summary>
<form method="post" action="/admin/setup/postgres" style="margin-top:12px">
<label>DSN (строка подключения) <span class="tooltip" title="Формат: postgres://пользователь:пароль@хост:порт/база?sslmode=disable. Например: postgres://bj:secret@db.example.com:5432/bj?sslmode=require">?</span></label>
<input type="text" name="dsn" value="{{.Settings.Postgres.DSN}}" placeholder="postgres://bj:secret@127.0.0.1:5432/bj?sslmode=disable" style="width:100%;padding:8px;margin-top:6px">
<p class="muted" style="margin-top:8px">При сохранении выполняется проверка подключения (Ping). Если БД недоступна — будет ошибка.</p>
<button type="submit" class="btn" style="margin-top:8px">Сохранить и проверить</button>
</form>
</details>
{{template "wizNav" .}}
{{if and devOn (not .Done.Postgres)}}<p style="margin-top:8px"><a href="/admin/wizard?step=3&skip=postgres" style="font-size:13px">Пропустить (режим разработки: in-memory — без сохранения сделок)</a></p>{{end}}
</div>
{{end}}
{{/* ============= ШАГ 4: Наша организация ============= */}}
{{if eq .Step 4}}
<div class="card">
<h2><span class="dot {{if .Done.Org}}ok{{else}}err{{end}}"></span>Наша организация</h2>
<p>От имени вашей организации bj отправляет и принимает переводы. {{if .DepoLicensed}}Реквизиты bj берёт из DEPO — проверьте и сохраните.{{else}}Найдите себя в справочнике участников НРД.{{end}}</p>
{{if .Suggest}}{{if .Suggest.OrgName}}{{if not .Done.Org}}<div class="wiz-ok" style="margin:0 0 14px"><i class="wiz-ic"></i><span><strong>Определено из DEPO</strong>: {{.Suggest.OrgName}}, ИНН {{.Suggest.OrgINN}}, код {{.Suggest.OrgCode}}{{if .Suggest.AccountID}}, счёт НРД {{.Suggest.AccountID}}{{end}}. Поля ниже заполнены — проверьте и нажмите «Сохранить».</span></div>{{end}}{{end}}{{end}}
{{with .Fancy}}{{template "orgSection" .}}{{end}}
<div class="card" id="nsd-req" style="margin-top:12px">
<h2><span class="dot {{if and .Settings.NSD.DeponentCode .Settings.NSD.AccountID .Settings.NSD.SectionID}}ok{{else}}err{{end}}"></span>Депозитарные реквизиты в НРД</h2>
<p class="muted" style="margin:0 0 8px">Где в НРД учитываются бумаги организации — из договора с НРД{{if .Settings.Fancy.Server}} или кнопкой «Определить из DEPO»{{end}}.</p>
{{if .Suggest}}{{if .Suggest.AccountID}}<div class="help-block" style="margin:0 0 8px"><strong>Найдено в DEPO:</strong> счёт {{.Suggest.AccountID}}, код {{.Suggest.DeponentCode}}, раздел {{.Suggest.SectionID}}.{{range .Suggest.Notes}}<br><span class="muted">⚠ {{.}}</span>{{end}}</div>{{end}}{{end}}
<div style="display:grid;gap:10px;max-width:680px">{{/* поля — в общей форме шага (org-form): одна кнопка «Сохранить» */}}
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px">
<label style="margin:0">Депозитарный код<input type="text" name="deponent_code" form="org-form" value="{{if .Settings.NSD.DeponentCode}}{{.Settings.NSD.DeponentCode}}{{else if .Suggest}}{{.Suggest.DeponentCode}}{{end}}" placeholder="MC0000000000" style="width:100%;margin-top:4px"></label>
<label style="margin:0">Депозитарный счёт<input type="text" name="account_id" form="org-form" value="{{if .Settings.NSD.AccountID}}{{.Settings.NSD.AccountID}}{{else if .Suggest}}{{.Suggest.AccountID}}{{end}}" placeholder="HL000000000C" style="width:100%;margin-top:4px"></label>
<label style="margin:0">Раздел счёта<input type="text" name="section_id" form="org-form" value="{{if .Settings.NSD.SectionID}}{{.Settings.NSD.SectionID}}{{else if .Suggest}}{{.Suggest.SectionID}}{{end}}" placeholder="36MC0000000000F00" style="width:100%;margin-top:4px"></label>
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap"><button type="submit" class="btn" form="org-form">Сохранить</button>{{if .Settings.Fancy.Server}}<button type="submit" class="btn" form="org-form" formaction="/admin/setup/fancy/detect" style="background:var(--card);color:var(--text);border:1px solid var(--border-strong,#c9cdd8)">Определить из DEPO</button>{{end}}</div>
</div>
</div>
{{template "wizNav" .}}
</div>
{{end}}
{{/* ============= ШАГ 5: QUIK МНП ============= */}}
{{if eq .Step 5}}
<div class="card">
<h2><span class="dot {{if .Done.Quik}}ok{{else}}err{{end}}"></span>QUIK</h2>
<p>Адрес и логин модуля неторговых поручений — у вашего администратора QUIK. Если модуля ещё нет, его подключает ARQA Technologies — письмо для них готово ниже.</p>
{{if .DepoLicensed}}{{template "letterCard" (letter "arqa")}}{{end}}
{{if not .DepoLicensed}}<p class="muted">Модуль неторговых поручений не входит в текущую лицензию — шаг пропускается.</p>
{{else}}{{with .Fancy}}{{template "quikSection" .}}{{end}}
{{if .Settings.Quik.Server}}{{template "mnpCheck" .}}{{end}}
{{if .Settings.Quik.M2MMissing}}<p style="color:var(--err);margin:8px 0">✗ Подключение есть, но под этим логином не видно объектов M2M: {{.Settings.Quik.M2MMissing}}. Проверьте имя базы МНП и права логина (роль InstrEu) и нажмите «Проверить» ещё раз.</p>{{end}}
{{if .Settings.Quik.Verified}}
<div class="card" id="quik-mode">
<h2>Как bj работает с QUIK</h2>
<form method="post" action="/admin/setup/quik/mode" id="quik-mode-form" class="dq">
<input type="hidden" name="return_to" value="/admin/wizard?step=5">
<div class="dq-tiles">
<label class="dq-tile"><input type="radio" name="mode" value="check" {{if not .Settings.Quik.WritesEnabled}}checked{{end}}><span><b>Только чтение</b><small>bj подключён к QUIK и читает данные, но ничего в нём не меняет: не блокирует и не разблокирует бумаги, не меняет лимиты. Для настройки и проверки — когда будете готовы, включите «Чтение и запись».</small></span></label>
<label class="dq-tile"><input type="radio" name="mode" value="auto" {{if .Settings.Quik.WritesEnabled}}checked{{end}}><span><b>Чтение и запись</b><small>Боевая работа: bj сам блокирует бумаги в QUIK при отправке перевода, списывает или снимает блокировку по ответу НРД, зачисляет входящие.</small></span></label>
</div>
<div class="dq-jobname" id="quik-acc">
<label>Торговый счёт для поручений<input type="text" name="account" value="{{.Settings.Quik.Account}}" placeholder="торговый счёт фирмы в QUIK" spellcheck="false" class="dq-mono"></label>
</div>
<p class="dq-note" id="quik-mode-st" aria-live="polite">Выбор сохраняется сразу. Тонкая настройка — инструменты МНП, коды клиентов, поручения из терминала — в «Настройка → QUIK».</p>
<noscript><button type="submit" class="btn">Сохранить</button></noscript>
</form>
<p class="dq-note">{{quikRecipientsInfo}} После первого заполнения справочника депозитариев-получателей попросите администратора QUIK перезапустить модуль неторговых поручений — QUIK читает справочник только при запуске.</p>
</div>
<script>(function(){
var f=document.getElementById('quik-mode-form'), st=document.getElementById('quik-mode-st'), acc=document.getElementById('quik-acc');
function sync(){ acc.hidden=f.mode.value!=='auto'; }
f.addEventListener('change', function(){ sync();
fetch(f.action,{method:'POST',body:new URLSearchParams(new FormData(f)),headers:{'Content-Type':'application/x-www-form-urlencoded','X-BJ-Ajax':'1'},cache:'no-store'})
.then(function(r){ return r.text().then(function(t){ st.textContent=(r.ok?'✓ Сохранено: ':'✗ ')+t; }); })
.catch(function(){ st.textContent='✗ Не сохранилось — обновите страницу и выберите ещё раз.'; });
});
f.addEventListener('submit', function(e){ if(window.fetch) e.preventDefault(); });
sync();
})();</script>
{{else}}<p class="muted" style="margin-top:12px">Когда подключение проверено, здесь появится выбор, как bj работает с QUIK.</p>{{end}}{{end}}
{{template "wizNav" .}}
</div>
{{end}}
{{/* ============= ШАГ 6: Валидата ============= */}}
{{if eq .Step 6}}
<div class="card">
<h2><span class="dot {{if .Done.Crypto}}ok{{else}}err{{end}}"></span>СКЗИ «Валидата Клиент L»</h2>
<p>СКЗИ «Валидата Клиент L» проверяет подписи НРД на квитанциях и сообщениях и нужно для работы с ключами и сертификатами (раздел «Ключ»).</p>
{{if .ValidataVer}}
<dl class="wiz-kv"><dt>Версия</dt><dd>{{.ValidataVer}}</dd><dt>Каталог</dt><dd><code>/opt/Validata/VDCSP</code></dd></dl>
<div class="wiz-ok"><i class="wiz-ic"></i><span>Валидата установлена вместе с bj — ничего настраивать не нужно.</span></div>
<p class="muted" style="font-size:13px">bj работает с Валидатой через свою службу криптографии <code>bj-crypto</code>{{if eq .CryptoSvc "active"}} — она запущена{{else}} — сейчас она не запущена: служба ставится обновлением с сервера лицензий (раздел «Лицензия», кнопка «Обновить сейчас»), а работает с ключом, установленным в разделе «Ключ»{{end}}.</p>
{{else}}
<p style="color:var(--err)">Валидата на этом сервере не найдена.</p>
<div class="help-block">
<strong>Как поставить.</strong> С образа Bridge&amp;Join она ставится сама. Если bj ставили на готовую систему: пакеты <code>zpki</code> и <code>zsdk</code> из дистрибутива НРД (<a href="https://fs.moex.com/cdp/po/ClientL_Other.zip" target="_blank">ClientL_Other.zip</a> для Ubuntu 22.04, <a href="https://fs.moex.com/cdp/po/ClientL_ALSE.zip" target="_blank">ClientL_ALSE.zip</a> для Astra Linux) — <code>sudo apt install ./zpki_*.deb ./zsdk_*.deb</code>{{if internalDocs}}, подробнее — <a href="/admin/help/crypto">справка</a>{{end}}. После установки обновите эту страницу.
</div>
{{if devOn}}<form method="post" action="/admin/setup/crypto" style="margin-top:12px">
<input type="hidden" name="provider" value="stub">
<button type="submit" class="btn" style="background:var(--card);color:var(--text);border:1px solid var(--border-strong,#c9cdd8)">Режим разработки: работать без криптографии (stub)</button>
</form>{{end}}
{{end}}
{{template "wizNav" .}}
</div>
{{end}}
{{/* ============= ШАГ 7: Шлюз НРД (до ключа: ключ ставится в профиль ИШ) ============= */}}
{{if eq .Step 7}}
<div class="card">
<h2><span class="dot {{if .Done.NSD}}ok{{else}}err{{end}}"></span>Шлюз НРД</h2>
<p>Через интеграционный шлюз НРД (ИШ) на этом сервере bj отправляет переводы и получает ответы. bj ставит и настраивает шлюз сам; доступ к тестовому и боевому контуру НРД открывает по заявке на своём сайте — как её подать, ниже.</p>
{{template "igateInstall" .}}
<div class="wiz-info" style="margin:12px 0 0"><span><b>Доступ к контурам НРД</b> — заявкой на сайте НРД, письмами НРД не подключает. Страница сервиса <a href="https://www.nsd.ru/services/novye-servisy/moex-most-dlya-m2m/" target="_blank" rel="noopener">MOEX МОСТ для M2M</a>, раздел «Как подключиться»: заявка на тестирование — система МОСТ, тип «M2M Автотестирование», затем «M2M Подключение»; после тестирования — договор с НРД и боевой контур.</span></div>
<details style="margin-top:8px"><summary style="cursor:pointer;color:var(--accent);font-size:13px">Дополнительно: адрес шлюза (обычно не нужно)</summary>
<form method="post" action="/admin/setup/nsd" style="margin-top:10px;display:grid;gap:10px;max-width:680px">
<input type="hidden" name="return_to" value="/admin/wizard?step=7">
{{if .Settings.NSD.IGWv904}}<input type="hidden" name="igw_v904" value="on">{{end}}
<input type="hidden" name="keep_requisites" value="1">
<input type="hidden" name="profile" value="{{if .Settings.NSD.Profile}}{{.Settings.NSD.Profile}}{{else}}test3-gost{{end}}">
<label style="margin:0">Адрес шлюза на этом сервере
<input type="text" name="igw_base_url" value="{{if .Settings.NSD.IGWBaseURL}}{{.Settings.NSD.IGWBaseURL}}{{else}}http://localhost:8090{{end}}" placeholder="http://localhost:8090" style="width:100%;margin-top:4px"></label>
<p id="igw-onyx" style="display:none;color:var(--err);font-size:13px;margin:0">Это адрес веб-сервиса НРД (ONYX), а не шлюза: bj работает через ИШ на этом сервере. Укажите адрес шлюза — обычно <a href="#" id="igw-def">http://localhost:8090</a>.</p>
<script>(function(){var u=document.querySelector('[name=igw_base_url]'),w=document.getElementById('igw-onyx');
function chk(){w.style.display=/onyx|nsd\.ru/i.test(u.value)?'block':'none'} u.addEventListener('input',chk); chk();
document.getElementById('igw-def').onclick=function(e){e.preventDefault();u.value='http://localhost:8090';chk()};})();</script>
<div><button type="submit" class="btn">Сохранить и проверить</button></div>
</form>
</details>
<p class="muted" style="font-size:13px;margin-top:12px">Депозитарные реквизиты в НРД: {{if and .Settings.NSD.DeponentCode .Settings.NSD.AccountID .Settings.NSD.SectionID}}код <code>{{.Settings.NSD.DeponentCode}}</code>, счёт <code>{{.Settings.NSD.AccountID}}</code>, раздел <code>{{.Settings.NSD.SectionID}}</code>{{else}}<span style="color:var(--err)">не заданы</span>{{end}} — в разделе <a href="/admin/wizard?step=4#nsd-req">«Организация»</a>.</p>
{{template "wizNav" .}}
</div>
{{end}}
{{/* ============= ШАГ 8: Ключ и сертификаты (после шлюза: ключ ставится в профиль ИШ) ============= */}}
{{if eq .Step 8}}
<div class="card">
<h2><span class="dot {{if .Done.Certs}}ok{{else}}err{{end}}"></span>Ключ и сертификаты</h2>
<p>Ключ электронной подписи организации для обмена с НРД. Его выдаёт Удостоверяющий центр Московской биржи: вам передадут архив ключа и пароль.</p>
{{if not .IgateOK}}<div class="wiz-info"><span>Ключ ставится в шлюз НРД — сначала установите <a href="/admin/wizard?step=7">шлюз</a>.</span></div>
{{else if .KeyWiz}}<div class="keywiz">{{template "keyWizBody" .KeyWiz}}</div>{{end}}
<div class="wiz-row" style="margin-top:20px">
<span class="wiz-row-l">Сертификаты НРД</span>
{{if .Settings.NSDCerts.Manual}}<span style="color:var(--warn)">⚠ автообновление выключено — «Настройка → Криптография»</span>{{else}}<span style="color:var(--ok)">✓ bj обновляет их сам</span>{{end}}
</div>
<details style="margin-top:6px;font-size:13px"><summary style="cursor:pointer;color:var(--accent)">Как это работает</summary>
<p style="margin:6px 0 0">Действующие сертификаты НРД и удостоверяющих центров Московской биржи приезжают вместе с bj и его обновлениями. Каждые 6 часов bj сверяет их со справочником шлюза: добавляет недостающие, а заменённые убирает строго после срока, объявленного НРД. Перед изменением — резервная копия и проверка подписи, при ошибке всё откатывается. Тестовые сертификаты попадают только к тестовому ключу, боевые — только к боевому.</p></details>
{{if .Settings.NSDCerts.LastRows}}<p class="muted" style="font-size:13px">Последняя сверка: {{.Settings.NSDCerts.LastRun.Format "02.01.2006 15:04"}}{{if .Settings.NSDCerts.LastProfile}}, профиль ИШ {{.Settings.NSDCerts.LastProfile}}{{end}}.</p>{{else}}{{end}}
{{if .Certs}}
<h3 style="margin-top:18px">Установленные сертификаты ({{len .Certs}})</h3>
<table>
<thead><tr><th>Владелец</th><th>Издатель</th><th>Действителен до</th><th>ИНН</th><th>Ключ</th></tr></thead>
<tbody>
{{range .Certs}}
<tr>
<td>{{.SubjectCN}}</td>
<td>{{.IssuerCN}}</td>
<td>{{.NotAfter.Format "02.01.2006"}}{{if .HasPrivateKey}}{{$d := certDaysLeft .NotAfter}}{{if lt $d 0}} <span style="color:var(--err)">истёк</span>{{else if le $d 30}} <span style="color:var(--warn)">осталось {{$d}} дн.</span>{{end}}{{end}}</td>
<td>{{if .INN}}<code>{{.INN}}</code>{{else}}—{{end}}</td>
<td>{{if .HasPrivateKey}}<span style="color:var(--ok)">есть</span>{{else}}<span class="muted">нет</span>{{end}}</td>
</tr>
{{end}}
</tbody>
</table>
{{else}}
<p class="muted" style="margin-top:12px;font-size:13px">Архива ключа ещё нет — шаг можно пропустить и вернуться позже.</p>
{{end}}
{{template "wizNav" .}}
</div>
{{end}}
{{/* ============= ШАГ 12: Время и сеть (глава «Сервер») ============= */}}
{{if eq .Step 12}}
<div class="card">
<h2><span class="dot {{if .Done.Server}}ok{{else}}warn{{end}}"></span>Время и сеть</h2>
<p>Часовой пояс, точное время и адрес сервера в сети.</p>
{{$tz := serverTimeZone}}
<form method="post" action="/admin/setup/timezone" id="wiz-tz" class="wiz-row">
<input type="hidden" name="return_to" value="/admin/wizard?step=12">
<label for="wiz-tz-zone" class="wiz-row-l">Часовой пояс</label>
<select id="wiz-tz-zone" name="zone" data-cur="{{$tz}}" onchange="this.form.submit()" style="min-width:260px">{{range timeZones}}<option value="{{.Zone}}" {{if eq .Zone $tz}}selected{{end}}>{{.Title}}</option>{{end}}</select>
</form>
<p id="wiz-tz-hint" class="muted" style="margin:4px 0 0;font-size:13px">По нему — время в журналах, уведомлениях и сроках поручений. Время НРД — московское.</p>
<div id="wiz-tz-suggest" class="wiz-info" style="display:none;align-items:center;flex-wrap:wrap"><span>На вашем компьютере пояс <b id="wiz-tz-name"></b>.</span> <button type="button" class="btn" id="wiz-tz-use" style="padding:5px 12px;font-size:13px">Использовать его</button></div>
<div class="wiz-row" style="margin-top:18px">
<span class="wiz-row-l">Точное время</span>
{{if .Done.Server}}<span style="color:var(--ok)">✓ синхронизируется</span>{{else}}<span style="color:var(--warn)">⚠ не синхронизируется</span>{{end}}
</div>
<details style="margin-top:6px" {{if not .Done.Server}}open{{end}}><summary style="cursor:pointer;color:var(--accent);font-size:13px">{{if .Done.Server}}Свой сервер времени (обычно не нужно){{else}}Укажите сервер времени вашей сети — обычно контроллер домена или маршрутизатор{{end}}</summary>
<form method="post" action="/admin/setup/time" style="display:flex;gap:8px;flex-wrap:wrap;margin-top:8px">
<input type="hidden" name="return_to" value="/admin/wizard?step=12">
<input name="servers" value="{{.Settings.Time.Servers}}" placeholder="например, 10.0.0.1" aria-label="Сервер времени" style="min-width:260px">
<button type="submit" class="btn">Сохранить</button>
</form>
</details>
<div class="wiz-row" style="margin-top:18px">
<span class="wiz-row-l">Адрес bj</span>
<a href="https://{{serverIP}}:8443/admin/">https://{{serverIP}}:8443/</a>
</div>
<p class="muted" style="font-size:12.5px;margin:4px 0 0">Чтобы адрес не менялся, задайте постоянный: на консоли сервера — <code>sudo bj-network</code>.</p>
{{if .AutoDone}}<p class="muted" style="font-size:12.5px;margin:16px 0 0;padding-top:12px;border-top:1px solid var(--border)">Уже сделано установщиком: {{range $i, $a := .AutoDone}}{{if $i}} · {{end}}✓ {{$a}}{{end}}</p>{{end}}
<script>
(function(){ // пояс компьютера пользователя — предложить одной кнопкой (не менять молча: сервер может стоять в другом городе)
var sel=document.getElementById('wiz-tz-zone'), tz='';
try{ tz=Intl.DateTimeFormat().resolvedOptions().timeZone||''; }catch(e){}
if(!tz || tz===sel.dataset.cur) return;
var opt=[].find.call(sel.options,function(o){return o.value===tz});
if(!opt) return;
document.getElementById('wiz-tz-name').textContent=opt.textContent;
document.getElementById('wiz-tz-suggest').style.display='';
document.getElementById('wiz-tz-use').onclick=function(){ sel.value=tz; sel.form.submit(); };
})();
</script>
{{template "wizNav" .}}
</div>
{{end}}
{{/* ============= ШАГ 11: Модули по лицензии (глава «Запуск») ============= */}}
{{if eq .Step 11}}
<div class="card">
<h2><span class="dot {{if .Done.Modules}}ok{{else}}warn{{end}}"></span>Модули</h2>
<p>Модули вашей лицензии уже установлены. Каждый настраивается в своём разделе — сейчас или позже.</p>
{{range .Modules}}
<div style="border:1px solid var(--border);border-radius:8px;padding:10px 12px;margin:8px 0">
<div style="display:flex;justify-content:space-between;gap:8px;align-items:center;flex-wrap:wrap">
<b><span class="dot {{if .Ready}}ok{{else}}warn{{end}}"></span>{{.Title}}</b>
<a href="{{.Link}}" class="btn" style="text-decoration:none;padding:5px 12px;background:var(--card);color:var(--text);border:1px solid var(--border-strong,#c9cdd8)">{{if .Ready}}Открыть{{else}}Настроить{{end}}</a>
</div>
<p class="muted" style="font-size:13px;margin:6px 0 2px">{{.What}}</p>
<p style="font-size:13px;margin:0;color:var({{if .Ready}}--ok{{else}}--warn{{end}})">{{if .Ready}}✓{{else}}⚠{{end}} {{.Status}}</p>
</div>
{{end}}
{{template "wizNav" .}}
</div>
{{end}}
{{/* ============= ШАГ 9: Оповещения и резервные копии ============= */}}
{{if eq .Step 9}}
<div class="card">
<h2><span class="dot {{if .Done.Alerts}}ok{{else}}warn{{end}}"></span>Оповещения и резервные копии</h2>
<p>bj напишет на почту, если НРД не отвечает, перевод застрял{{if .DepoLicensed}}, DEPO не обработал поручение{{end}} или истекает ключ, — и будет сам делать копии сервера. Настройте почтовый сервер, затем кому писать: администратору системы — о сбоях и обновлениях, отделам — о переводах и поручениях. Обмен работает и без этого, но о сбое тогда никто не узнает вовремя.</p>
<style>.wiz9 label:not(:has(input[type=checkbox])){display:block;margin:0}.wiz9 label input:not([type=checkbox]),.wiz9 label select,.wiz9 .grid input,.wiz9 .grid select{width:100%;box-sizing:border-box;margin-top:4px}</style>
<div class="wiz9">
{{with .Fancy}}
{{template "smtpCard" .}}
{{template "notifyGroupsCard" .}}
{{template "backupCard" .}}
{{end}}
</div>
{{template "wizNav" .}}
</div>
{{end}}
{{/* ============= ШАГ 13: Учётные записи (глава «Запуск», перед проверкой) ============= */}}
{{if eq .Step 13}}
<div class="card">
<h2><span class="dot {{if .Done.Users}}ok{{else}}warn{{end}}"></span>Учётные записи</h2>
<p>У каждого, кто работает с bj, — своя учётная запись: свой пароль и свои права, а в журнале видно, кто что сделал. Заведите сотрудников по ролям; права, пароли и вход через Active Directory меняются позже в разделе Настройка → Пользователи.</p>
<style>
.acc-roles{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin:12px 0 4px}
.acc-role{display:flex;gap:10px;align-items:flex-start;padding:12px 14px;border:1px solid var(--border);border-radius:12px;cursor:pointer;font-weight:400;margin:0}
.acc-role:has(input:checked){border-color:var(--accent);background:var(--accent-weak)}
.acc-role input{margin-top:3px;flex:none;width:auto}
.acc-role b{display:block;font-size:14.5px}
.acc-role small{display:block;color:var(--muted);font-size:13px;line-height:1.4;margin-top:3px}
.acc-role .acc-who{display:block;color:var(--text);font-size:12.5px;margin-top:6px}
.acc-form{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;max-width:640px;margin-top:12px}
.acc-form label{display:block;margin:0}
.acc-form label input{width:100%;box-sizing:border-box;margin-top:4px}
.acc-pw{display:flex;gap:6px;align-items:flex-end}
@media (max-width:600px){.acc-roles,.acc-form{grid-template-columns:minmax(0,1fr)}}
</style>
{{with .Fancy}}{{$U := .Users}}
<form method="post" action="/admin/setup/users" id="acc-form">
<input type="hidden" name="return_to" value="/admin/wizard?step=13"><input type="hidden" name="create" value="1">
<input type="hidden" name="active" value="1"><input type="hidden" name="source" value="local">
<h3 style="margin:16px 0 0">Кого добавить</h3>
<div class="acc-roles">
{{range $r := $U.Roles}}
<label class="acc-role"><input type="radio" name="role" value="{{$r.Key}}" {{if eq $r.Key "operator"}}checked{{end}}>
<span><b>{{$r.Title}}</b><small>{{$r.Desc}}</small>
<span class="acc-who">Уже есть: {{$n := 0}}{{range $U.Users}}{{if eq .Role $r.Key}}{{if $n}}, {{end}}{{$n = 1}}{{.Login}}{{if .Name}} ({{.Name}}){{end}}{{if not .Active}} — заблокирован{{end}}{{end}}{{end}}{{if not $n}}<span class="muted">никого</span>{{end}}</span></span></label>
{{end}}
</div>
<div class="acc-form">
<label>Логин<input type="text" name="login" placeholder="ivanov" required autocomplete="off" pattern="[^ /\@&quot;'<>]+"></label>
<label>Имя<input type="text" name="name" placeholder="Иванов Иван Иванович" autocomplete="off"></label>
<label>E-mail<input type="email" name="email" placeholder="ivanov@example.ru" autocomplete="off"></label>
<div class="acc-pw"><label style="flex:1">Пароль (от 8 символов)<input type="text" name="password" id="acc-pw" minlength="8" required autocomplete="off" spellcheck="false"></label>
<button type="button" class="btn btn-secondary" id="acc-gen" style="white-space:nowrap">Придумать</button></div>
</div>
<p class="muted" style="font-size:12.5px;margin:8px 0 0">Пароль передайте сотруднику лично; сменить его можно в разделе Настройка → Пользователи.</p>
<div style="margin-top:12px"><button type="submit" class="btn btn-ok">Создать учётную запись</button></div>
</form>
<script>(function(){
var g=document.getElementById('acc-gen'), f=document.getElementById('acc-pw'); if(!g||!f||!window.crypto) return;
g.addEventListener('click',function(){ var a='abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789', b=new Uint32Array(12), p='';
crypto.getRandomValues(b); for(var i=0;i<b.length;i++){ p+=a[b[i]%a.length]; if(i===3||i===7) p+='-'; } f.value=p; f.select(); });
})();</script>
{{end}}
{{template "wizNav" .}}
</div>
{{end}}
{{/* ============= ШАГ 10: Проверка готовности (в режиме разработки — ещё тестовая заявка) ============= */}}
{{if eq .Step 10}}
<div class="card">
{{if devOn}}
<h2><span class="dot {{if .Done.TestRun}}ok{{else}}err{{end}}"></span>Тестовая заявка</h2>
<p>Прогон полного цикла: создание заявки → валидация → подпись → отправка в НРД (или mock) → ожидание Decision → подтверждение.</p>
<div class="help-block">
<strong>Что произойдёт?</strong> Система создаст тестовую M2M-сделку, проведёт её через всю стейт-машину, и покажет результат каждого этапа. Если ИШ НРД не настроен — сработает mock (синтетический Decision через 3 секунды).
</div>
<form method="post" action="/admin/setup/test-run" style="margin-top:12px">
<button type="submit" class="btn" style="background:var(--ok);font-size:15px;padding:10px 20px">▶ Запустить тестовую заявку</button>
</form>
{{if .Settings.LastTest}}
<h3 style="margin-top:18px">Последний прогон: {{.Settings.LastTest.StartedAt.Format "02.01.2006 15:04:05"}}</h3>
<table>
<tr><td class="muted">Заявка</td><td><a href="/admin/claims/{{.Settings.LastTest.ClaimID}}">{{.Settings.LastTest.ClaimID}}</a></td></tr>
<tr><td class="muted">Финальное состояние</td><td>{{ruState .Settings.LastTest.FinalStatus}}</td></tr>
<tr><td class="muted">Результат</td><td>{{if .Settings.LastTest.OK}}<span style="color:var(--ok)">успех</span>{{else}}<span style="color:var(--err)">ошибка</span>{{end}}</td></tr>
{{if .Settings.LastTest.Message}}<tr><td class="muted">Сообщение</td><td>{{.Settings.LastTest.Message}}</td></tr>{{end}}
</table>
{{end}}
{{end}}
{{if not devOn}}<h2><span class="dot" id="rd-dot"></span>Готовность к работе</h2>{{else}}<h3 style="margin-top:18px">Готовность к работе</h3>{{end}}
<p class="muted" style="margin:4px 0 10px">bj проверяет всё, что нужно для обмена с НРД, прямо сейчас. Красное — обмен работать не будет, жёлтое — работать будет, но лучше исправить. У каждой строки — ссылка туда, где это исправляется.</p>
<div id="rd-sum" style="font-weight:600;margin:6px 0">Проверяю…</div>
<table id="rd-tab" style="width:100%"><tbody></tbody></table>
<p style="margin-top:8px"><button type="button" class="btn" id="rd-again" style="background:var(--card);color:var(--text);border:1px solid var(--border-strong,#c9cdd8)">Проверить ещё раз</button> <span class="muted" id="rd-at" style="font-size:12px"></span></p>
<script>
(function(){
var tb=document.querySelector('#rd-tab tbody'), sum=document.getElementById('rd-sum'), dot=document.getElementById('rd-dot');
function esc(s){return String(s).replace(/[&<>"]/g,function(c){return{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]})}
var ic={ok:'<span style="color:var(--ok)">✓</span>',warn:'<span style="color:var(--warn)">⚠</span>',err:'<span style="color:var(--err)">✗</span>',skip:'<span class="muted">—</span>'};
function pl(n,one,few,many){var m=n%100,d=n%10;return (m>=11&&m<=14)?many:d===1?one:(d>=2&&d<=4)?few:many}
function load(){ sum.textContent='Проверяю…'; tb.innerHTML='';
fetch('/admin/setup/readiness',{cache:'no-store'}).then(function(r){return r.json()}).then(function(d){
tb.innerHTML=d.checks.map(function(c){return '<tr><td style="width:24px;vertical-align:top">'+ic[c.state]+'</td><td style="vertical-align:top;white-space:nowrap;padding-right:10px"><b>'+esc(c.title)+'</b></td><td style="vertical-align:top">'+esc(c.detail)+(c.state!=='ok'&&c.link?' <a href="'+esc(c.link)+'">Исправить →</a>':'')+'</td></tr>'}).join('');
var red=d.checks.filter(function(c){return c.state==='err'}).length, yel=d.checks.filter(function(c){return c.state==='warn'}).length;
sum.innerHTML=d.ready?(yel?'<span style="color:var(--ok)">✓ Обмен с НРД готов к работе</span> <span class="muted" style="font-weight:400">— желательно исправить ещё '+yel+' '+pl(yel,'пункт','пункта','пунктов')+' (отмечены ⚠)</span>':'<span style="color:var(--ok)">✓ Всё готово к работе</span>'):'<span style="color:var(--err)">✗ Пока не готово: осталось '+red+' '+pl(red,'обязательный пункт','обязательных пункта','обязательных пунктов')+' (отмечены ✗)</span>';
if(dot) dot.className='dot '+(d.ready?(yel?'warn':'ok'):'err');
document.getElementById('rd-at').textContent='проверено '+d.at;
}).catch(function(){ sum.textContent='Проверка не выполнилась — обновите страницу.'; }); }
document.getElementById('rd-again').onclick=load; load();
})();
</script>
{{template "chainTest" .}}
{{template "wizNav" .}}
</div>
{{end}}
</div>
</div>
<script>(function(){var c=document.querySelector(".wiz-st.cur");if(c&&c.scrollIntoView&&window.innerWidth<=820)c.scrollIntoView({block:"nearest",inline:"center"});})();</script>
{{end}}