47b183a072
Система работает автоматически, ручного режима нет: выбор — только читать QUIK (настройка и проверка: ничего не блокирует, не разблокирует, не меняет лимиты) или читать и писать (боевая работа). В проверке МНП строка «Режим работы с QUIK»: только чтение — сведение (ℹ), а не предупреждение, с объяснением, как включить боевую работу. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
746 lines
69 KiB
HTML
746 lines
69 KiB
HTML
{{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; }
|
||
.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&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" .}}
|
||
<details style="margin-top:4px"><summary style="cursor:pointer;color:var(--accent);font-size:13px">Письма в НРД о доступе к контурам</summary>
|
||
{{template "letterCard" (letter "nsd-test")}}
|
||
{{template "letterCard" (letter "nsd-prod")}}
|
||
</details>
|
||
<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 .Done.NSD}}<div class="wiz-info"><span>Ключ ставится в шлюз НРД — сначала установите <a href="/admin/wizard?step=7">шлюз</a>.</span></div>{{end}}
|
||
<p style="margin:14px 0 0"><a href="/admin/setup/keywizard" class="btn" style="text-decoration:none;display:inline-block">{{if .Certs}}Установить другой ключ{{else}}Установить ключ{{end}}</a> <span class="muted" style="font-size:12.5px;margin-left:6px">архив → носитель → справочник → проверка подписи</span></p>
|
||
|
||
<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" .}}
|
||
<details style="margin:10px 0 0"><summary style="cursor:pointer;color:var(--accent);font-size:13px">Разослать уведомления отделам — депозитарий, бэк-офис, мидл-офис (необязательно)</summary>
|
||
{{template "notifyGroupsCard" .}}
|
||
</details>
|
||
{{template "backupCard" .}}
|
||
{{end}}
|
||
</div>
|
||
{{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{'&':'&','<':'<','>':'>','"':'"'}[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" .}}
|
||
<div style="margin-top:18px;padding-top:18px;border-top:1px solid var(--border)">
|
||
<h2>Письма для подключения</h2>
|
||
<p class="muted" style="margin:0 0 6px">Готовые письма с вашими реквизитами — тем, без кого подключение не закончить.</p>
|
||
{{template "letterCard" (letter "nsd-test")}}
|
||
{{template "letterCard" (letter "nsd-prod")}}
|
||
{{if .DepoLicensed}}{{template "letterCard" (letter "arqa")}}{{template "letterCard" (letter "dba")}}{{end}}
|
||
</div>
|
||
{{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}}
|