efe56bdde1
- Необязательные шаги «Модули», «Оповещения и копии», «Учётные записи» и каждый модуль отдельно можно отложить кнопкой «Настроить позже»: мастер считает их завершёнными, глава «Запуск» закрывается. Отметку можно снять («Вернуть в мастер»); как только шаг настроен по-настоящему, отметка не показывается. Хранится в /var/lib/bj/.bj/wizard-deferred.json (wizdefer.go). - Шаг «Оповещения и копии» показывает, что уже настроено: почта, Zabbix, резервные копии — видно частичную настройку, сделанную в «Настройке». - Плашка «Требуется настройка — N из M» на главной считалась по старому списку из 9 шагов и расходилась с мастером. Теперь и мастер, и главная используют одну функцию wizDone и путь wizPath — те же шаги, лицензия и отметки «отложено». Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
735 lines
71 KiB
HTML
735 lines
71 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; }
|
||
.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); }
|
||
/* установка ключа (общий блок 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); }
|
||
/* ход подготовки: строки шагов со значком состояния */
|
||
.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" data-need="адрес сервера DEPO" 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" data-need="имя базы DEPO" 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" data-need="логин администратора SQL" autocomplete="off" spellcheck="false" placeholder="{{if $pg}}postgres{{else}}sa{{end}}"></label>
|
||
<label>Пароль<input type="password" name="admin_password" data-need="пароль администратора SQL" 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" data-need="логин bj" 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" data-need="пароль bj" 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" data-need="логин bj" value="{{.Settings.Fancy.User}}" autocomplete="off" spellcheck="false"></label>
|
||
<label>Пароль<input type="password" name="password" {{if not .Settings.Fancy.Password}}data-need="пароль bj"{{end}} 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 and .Done.Postgres (pgLocal .Settings.Postgres.DSN)}}<div class="wiz-ok"><i class="wiz-ic"></i><span>База готова{{if .PGSummary}}: {{.PGSummary}}{{end}}. Её создал установщик — ничего делать не нужно.</span></div>
|
||
<details style="margin-top:14px" {{if .Flash}}open{{end}}><summary style="cursor:pointer;color:var(--accent)">Хранить базу bj на своём сервере PostgreSQL (необязательно)</summary>
|
||
<p class="muted" style="font-size:13.5px;margin:10px 0 0">Если у организации есть свой сервер PostgreSQL — с резервным копированием и администратором баз, — базу bj можно держать на нём. bj перенесёт данные, сверит число строк в каждой таблице и переключится сам; база на этом сервере останется запасной копией. Нужно: пустая база на сервере организации, пользователь — её владелец, доступ с адреса bj ({{serverIP}}) к порту PostgreSQL. Только PostgreSQL той же версии, что здесь, или новее.</p>
|
||
<form method="post" action="/admin/setup/postgres/move" id="pg-form" class="pg-form">
|
||
<label class="pg-wide">Сервер PostgreSQL<input type="text" name="pg_host" placeholder="db.example.local" required autocomplete="off"></label>
|
||
<label>Порт<input type="text" name="pg_port" value="5432" inputmode="numeric" autocomplete="off"></label>
|
||
<label>База<input type="text" name="pg_db" value="bj" required autocomplete="off"></label>
|
||
<label>Пользователь<input type="text" name="pg_user" value="bj" required autocomplete="off"></label>
|
||
<label>Пароль<input type="password" name="pg_password" autocomplete="new-password"></label>
|
||
<label>Шифрование<select name="pg_ssl"><option value="prefer">если сервер умеет (prefer)</option><option value="require">обязательно (require)</option><option value="verify-full">обязательно, с проверкой сертификата</option><option value="disable">без шифрования</option></select></label>
|
||
<div class="pg-wide" style="display:flex;gap:8px;flex-wrap:wrap">
|
||
<button type="submit" name="check" value="1" class="btn btn-secondary pg-go">Проверить подключение</button>
|
||
<button type="submit" class="btn pg-go" data-confirm="Перенести базу bj на сервер организации и переключиться? bj перезапустится; на время переноса (обычно до минуты) лучше не проводить переводы.">Перенести базу и переключиться</button>
|
||
</div>
|
||
<div class="dq-run pg-wide" id="pg-run" hidden aria-live="polite"><ol class="dq-steps" id="pg-steps"></ol><div class="dq-run-msg" id="pg-msg" hidden></div></div>
|
||
</form>
|
||
<style>.pg-form{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;max-width:720px;margin-top:12px}.pg-form label{display:block;margin:0}.pg-form label input,.pg-form label select{width:100%;box-sizing:border-box;margin-top:4px}.pg-wide{grid-column:1/-1}@media (max-width:600px){.pg-form{grid-template-columns:minmax(0,1fr)}}</style>
|
||
<script>(function(){
|
||
var f=document.getElementById('pg-form'); if(!f||!window.fetch||!window.TextDecoder) return;
|
||
var run=document.getElementById('pg-run'), list=document.getElementById('pg-steps'), msg=document.getElementById('pg-msg');
|
||
function li(k){ return list.querySelector('[data-k="'+k+'"]'); }
|
||
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'){ 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); }
|
||
f.addEventListener('submit',function(ev){
|
||
var b=ev.submitter; if(!b) return; ev.preventDefault();
|
||
if(b.dataset.confirm && !confirm(b.dataset.confirm)) return;
|
||
var btns=[].slice.call(f.querySelectorAll('.pg-go')); btns.forEach(function(x){x.disabled=true;});
|
||
list.innerHTML=''; msg.hidden=true; run.hidden=false;
|
||
var fd=new FormData(f); if(b.name) fd.set(b.name,b.value);
|
||
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) 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(){ fin(final,b); }, function(){ fin(null,b); });
|
||
function fin(x,b){
|
||
msg.textContent=x&&x.msg?x.msg:'Связь с bj прервалась — обновите страницу.'; msg.hidden=false;
|
||
if(x&&x.ok&&!b.name){ msg.textContent+=' Страница обновится, когда bj перезапустится.'; setTimeout(function(){ location.href='/admin/wizard?step=2'; },20000); return; }
|
||
btns.forEach(function(x){x.disabled=false;});
|
||
}
|
||
});
|
||
})();</script>
|
||
</details>
|
||
{{else if .Done.Postgres}}<div class="wiz-ok"><i class="wiz-ic"></i><span>База bj — на сервере организации{{if .PGSummary}}: {{.PGSummary}}{{end}}.{{if .Settings.Postgres.Local}} Прежняя база на этом сервере сохранена как запасная копия.{{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}}
|
||
|
||
{{if or devOn (not .Done.Postgres)}}<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>{{end}}
|
||
|
||
{{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}}
|
||
{{template "nsdRequisites" .}}
|
||
{{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}}
|
||
{{template "quikModeCard" (withRet . "/admin/wizard?step=5")}}
|
||
{{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 class="muted" style="font-size:13.5px;margin:4px 0 0">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>
|
||
<p class="muted" style="font-size:13px;margin:8px 0 0">bj работает со шлюзом, который сам поставил на этом сервере: адрес — <code>http://localhost:8090</code>, менять его не нужно. Меняйте, только если у организации уже есть свой интеграционный шлюз НРД на другом сервере и bj должен работать через него: укажите адрес его веб-API (обычно порт 8090). Ключ подписи и каналы тогда настраиваются на том шлюзе.</p>
|
||
<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">Адрес веб-API шлюза НРД
|
||
<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}}
|
||
<details style="margin-top:18px">
|
||
<summary style="cursor:pointer;font-weight:600;font-size:15px">Установленные сертификаты ({{len .Certs}})</summary>
|
||
<table style="margin-top:10px">
|
||
<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>
|
||
</details>
|
||
{{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 timeSynced}}<span style="color:var(--ok)">✓ синхронизируется</span>{{else}}<span style="color:var(--warn)">⚠ не синхронизируется</span>
|
||
<form method="post" action="/admin/setup/time-auto" style="display:inline;margin-left:8px">
|
||
<input type="hidden" name="return_to" value="/admin/wizard?step=12">
|
||
<button type="submit" class="btn" style="padding:5px 12px;font-size:13px">Синхронизировать</button>
|
||
</form>{{end}}
|
||
</div>
|
||
<p class="muted" style="font-size:12.5px;margin:4px 0 0">bj сам подберёт сервер точного времени: сначала российские серверы ВНИИФТРИ, и только если из вашей сети они недоступны — попросит указать свой (контроллер домена или маршрутизатор сети).</p>
|
||
<details style="margin-top:6px" {{if not timeSynced}}open{{end}}><summary style="cursor:pointer;color:var(--accent);font-size:13px">Указать свой сервер времени вручную</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 style="margin-top:18px">
|
||
<div class="wiz-row" style="margin-bottom:8px"><span class="wiz-row-l">Рабочее время</span>{{if and .Settings.WorkHours.Enabled (not .Settings.WorkHours.SavedAt.IsZero)}}<span style="color:var(--ok)">✓ {{.Settings.WorkHours.Text}}</span>{{else if .Settings.WorkHours.SavedAt.IsZero}}<span style="color:var(--warn)">⚠ не задано</span>{{else}}<span class="muted">без ограничения</span>{{end}}</div>
|
||
{{template "workHoursForm" (whForm .Settings.WorkHours "/admin/wizard?step=12")}}
|
||
</div>
|
||
|
||
<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>
|
||
<form method="post" action="/admin/setup/hostname" style="margin-top:12px">
|
||
<input type="hidden" name="return_to" value="/admin/wizard?step=12">
|
||
<label for="wiz-hostnames" class="wiz-row-l" style="display:block;margin-bottom:4px">Доменное имя сервера</label>
|
||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||
<input id="wiz-hostnames" name="hostnames" value="{{range $i, $h := .Settings.HTTPS.Hostnames}}{{if $i}} {{end}}{{$h}}{{end}}" placeholder="например, m2m.corp.example.ru" aria-label="Доменное имя сервера" style="min-width:300px">
|
||
<button type="submit" class="btn">Сохранить</button>
|
||
</div>
|
||
<p class="muted" style="font-size:12.5px;margin:4px 0 0">Если для сервера в вашей сети заведено доменное имя — впишите его, и bj вставит имя в сертификат: интерфейс будет открываться по нему без предупреждения браузера. Саму запись имени (A-запись → IP сервера) заводит администратор на вашем DNS / контроллере домена. Несколько имён — через пробел.</p>
|
||
</form>
|
||
{{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}}{{if and .Done.Postgres (pgLocal .Settings.Postgres.DSN)}}. <a href="/admin/wizard?step=2">Базу bj можно держать и на своём сервере PostgreSQL →</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}}
|
||
|
||
{{/* ============= ШАГ 14: Обновление (глава «Обновление») ============= */}}
|
||
{{if eq .Step 14}}
|
||
<div class="card">
|
||
<h2><span class="dot {{if .StepDone}}ok{{else}}warn{{end}}"></span>Обновление</h2>
|
||
<p>Проверка и установка обновлений bj. Это первый пункт мастера — сюда можно вернуться в любой момент.</p>
|
||
{{with .Fancy}}{{template "updateSection" .}}{{end}}
|
||
{{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>
|
||
{{if not .Ready}}<form method="post" action="/admin/setup/wizard-defer" style="margin:8px 0 0;display:flex;gap:8px;align-items:center;flex-wrap:wrap"><input type="hidden" name="step" value="11"><input type="hidden" name="module" value="{{.Key}}">{{if .Deferred}}<span class="muted" style="font-size:12.5px">⏸ отложено — мастер считает модуль завершённым</span><button type="submit" name="undo" value="1" class="btn" style="padding:3px 10px;font-size:12.5px;background:var(--card);color:var(--text);border:1px solid var(--border-strong,#c9cdd8)">Вернуть</button>{{else}}<button type="submit" class="btn" style="padding:3px 10px;font-size:12.5px;background:var(--card);color:var(--text);border:1px solid var(--border-strong,#c9cdd8)">Настроить позже</button>{{end}}</form>{{end}}
|
||
</div>
|
||
{{end}}
|
||
{{template "wizDefer" .}}
|
||
{{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>
|
||
<div class="wiz-info" style="margin:10px 0"><span>Сейчас: почта — {{if and .Settings.Alerts.SMTP.Enabled .Settings.Alerts.SMTP.Host .Settings.Alerts.SMTP.To}}<b style="color:var(--ok)">✓ настроена</b>{{else}}<b style="color:var(--warn)">не настроена</b>{{end}}; Zabbix — {{if .Settings.Alerts.Zabbix.Enabled}}<b style="color:var(--ok)">✓ включён</b>{{else}}выключен{{end}}; резервные копии — {{if .Settings.Backup.Enabled}}<b style="color:var(--ok)">✓ включены</b>{{else}}<b style="color:var(--warn)">выключены</b>{{end}}. Шаг закрывается, когда есть оповещения (почта или Zabbix) и включены копии.</span></div>
|
||
<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 "wizDefer" .}}
|
||
{{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>
|
||
{{with .Fancy}}{{template "accountsAdd" (withRet . "/admin/wizard?step=13")}}{{template "accountsAD" (withRet . "/admin/wizard?step=13")}}
|
||
{{end}}
|
||
{{template "wizDefer" .}}
|
||
{{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 "m2mAutoCard" (withRet . "/admin/wizard?step=10")}}
|
||
{{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}}
|