Мастер настройки проще: один вопрос на экран, подробности — под «Подробнее»

- «Время и сеть»: пояс компьютера пользователя предлагается одной кнопкой, пояс применяется сразу при
  выборе; сервер времени — только если не синхронизируется; «сделано установщиком» — строкой внизу.
- «Лицензия»: модули словами, а не кодами (quik-mnp → «QUIK: неторговые поручения…»); пояснения — только
  пока ключа нет; поле ключа крупное, одно.
- «Учёт DEPO»: главный вопрос «Как подготовить базу» первым, короткие подписи; кружки переключателей
  больше не сплющиваются длинным текстом.
- «QUIK»: одна кнопка «Сохранить и проверить»; база и шифрование — под «Дополнительно», без «true/disable»
  и TrustServerCertificate; автоматический цикл и справочник получателей в мастере свёрнуты.
- «Организация»: без логотипа (он в «Оформлении»), кнопки одного цвета. «Шлюз НРД», «Ключ», «Оповещения»:
  по одному абзацу, письма и устройство автообновления сертификатов — под раскрытием.
- Мастер и «Подготовка модулей» — без меню системы; подзаголовок не повторяет названия глав.
- Подсказка о восстановлении из копии ведёт на первую страницу (в мастере его больше нет).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
zuevav
2026-10-06 11:58:58 +03:00
parent 8905ebc6fb
commit 6a68dc927f
13 changed files with 234 additions and 133 deletions
+1
View File
@@ -95,6 +95,7 @@ var templateFuncs = template.FuncMap{
"devOn": authTemplateFuncs["devOn"],
"devLicensed": authTemplateFuncs["devLicensed"],
"mod": func(f string) bool { return globalRC == nil || moduleAllowed(globalRC, f) }, // модуль в лицензии
"modTitle": moduleTitle, // название признака лицензии по-человечески
"isIIS": incomingIsIIS, // входящий перевод на ИИС
"depoSubs": func() any { return depoWizardSubs }, // экраны шага 3 мастера
"authOpen": authOpen,
+11
View File
@@ -905,6 +905,17 @@ func moduleForPath(p string) string {
var moduleTitles = map[string]string{"esia-finance": "ЕСИА Финанс", "quik-mnp": "QUIK: неторговые поручения и обмен с DEPO", "quik-export": "Экспорт биржевой информации QUIK", "web-cabinet": "Личный кабинет клиента", "npr": "Риск-монитор НПР / УДС"}
// moduleTitle — признак лицензии словами (в мастере и на странице лицензии — не коды вроде quik-mnp).
func moduleTitle(f string) string {
if t, ok := moduleTitles[f]; ok {
return t
}
if f == "updates" {
return "обновления"
}
return f
}
func moduleNotLicensed(w http.ResponseWriter, f string) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusForbidden)
+80
View File
@@ -377,3 +377,83 @@ func TestDepoSetupScript(t *testing.T) {
t.Fatalf("пароль: %q", p)
}
}
// Шаги мастера отрисовываются без ошибок шаблона: лицензия, учёт DEPO одним шагом, время и сеть, проверка.
func TestWizardStepsRender(t *testing.T) {
a, err := newAdmin()
if err != nil {
t.Fatal(err)
}
priv := testLicenseKey(t)
rc := testRC(t)
now := time.Now().UTC()
tok := signTestLicense(t, priv, &license.License{Schema: 1, ID: "lic-1", Tenant: "Тест", Product: "bj-server", Plan: license.PlanPro,
IssuedAt: now, ExpiresAt: now.Add(30 * 24 * time.Hour), ContractUntil: now.AddDate(1, 0, 0), Features: []string{"updates", "quik-mnp"}})
if err := rc.SaveLicense(LicenseSettings{Key: tok}); err != nil {
t.Fatal(err)
}
h := &setupHandlers{rc: rc, tpl: &adminTemplates{a: a}}
for step, want := range map[string]string{"1": "QUIK: неторговые поручения", "3": "wiz-depo", "5": "Сохранить и проверить", "12": "wiz-tz-suggest", "10": "Проверка"} {
w := httptest.NewRecorder()
h.renderWizard(w, httptest.NewRequest(http.MethodGet, "/admin/wizard?step="+step, nil))
body := w.Body.String()
if d := os.Getenv("BJ_DUMP_PAGES"); d != "" {
_ = os.WriteFile(filepath.Join(d, "wizard-"+step+".html"), []byte(body), 0o644)
}
if w.Code != http.StatusOK || !strings.Contains(body, want) {
i := strings.Index(body, "<main")
t.Fatalf("шаг %s: код %d, нет %q\n%s", step, w.Code, want, body[max(i, 0):min(len(body), max(i, 0)+1500)])
}
if step == "3" && (strings.Contains(body, "BJ_DEPO_DATA") || !strings.Contains(body, "depo_job")) {
t.Fatal("шаг DEPO: имя нашей базы в форме или нет выбора, где работает задание")
}
if strings.Contains(body, "wiz-restore") {
t.Fatal("восстановление из копии — только на первой странице, не в мастере")
}
}
}
// Снимки страниц первичной настройки для проверки глазами: BJ_DUMP_PAGES=<каталог> go test -run TestDumpOnboardingPages.
func TestDumpOnboardingPages(t *testing.T) {
dir := os.Getenv("BJ_DUMP_PAGES")
if dir == "" {
t.Skip("BJ_DUMP_PAGES не задан")
}
priv := testLicenseKey(t)
a, err := newAdmin()
if err != nil {
t.Fatal(err)
}
rc := testRC(t)
now := time.Now().UTC()
tok := signTestLicense(t, priv, &license.License{Schema: 1, ID: "lic-1", Tenant: "АО «Пример Инвест»", Product: "bj-server", Plan: license.PlanEnterprise,
IssuedAt: now, ExpiresAt: now.Add(365 * 24 * time.Hour), ContractUntil: now.AddDate(1, 0, 0), Features: []string{"updates", "quik-mnp", "web-cabinet", "npr", "quik-export"}})
if err := rc.SaveLicense(LicenseSettings{Key: tok}); err != nil {
t.Fatal(err)
}
h := &setupHandlers{rc: rc, tpl: &adminTemplates{a: a}}
for step := 1; step <= wizardSteps; step++ {
w := httptest.NewRecorder()
h.renderWizard(w, httptest.NewRequest(http.MethodGet, fmt.Sprintf("/admin/wizard?step=%d", step), nil))
_ = os.WriteFile(filepath.Join(dir, fmt.Sprintf("wizard-%02d.html", step)), w.Body.Bytes(), 0o644)
}
var b strings.Builder
d := PrepareData{page: page{Title: "Подготовка модулей", Active: "wizard"}, Tenant: "АО «Пример Инвест»", Plan: "enterprise", Until: "поддержка до 06.10.2027", Next: "/admin/wizard?step=12", Flash: "Администратор создан."}
if err := a.prepare.ExecuteTemplate(&b, "layout", d); err != nil {
t.Fatal(err)
}
_ = os.WriteFile(filepath.Join(dir, "prepare.html"), []byte(b.String()), 0o644)
}
// Первая страница (администратор) — для снимка в TestDumpOnboardingPages.
func TestDumpFirstRunPage(t *testing.T) {
dir := os.Getenv("BJ_DUMP_PAGES")
if dir == "" {
t.Skip("BJ_DUMP_PAGES не задан")
}
var b strings.Builder
if err := firstRunTpl.Execute(&b, firstRunData{NeedKey: true}); err != nil {
t.Fatal(err)
}
_ = os.WriteFile(filepath.Join(dir, "first-run.html"), []byte(b.String()), 0o644)
}
+4
View File
@@ -161,6 +161,10 @@ func (h *setupHandlers) saveQuik(w http.ResponseWriter, r *http.Request) {
setupFlash(w, r, "QUIK МНП: ошибка сохранения: "+err.Error())
return
}
if r.FormValue("check") != "" { // «Сохранить и проверить» — одной кнопкой
h.testQuik(w, r)
return
}
setupFlash(w, r, fmt.Sprintf("QUIK МНП: настройки сохранены (%s:%d, логин %s)", s.Server, s.Port, s.User))
}
+2 -2
View File
@@ -296,10 +296,10 @@ func (h *setupHandlers) renderWizard(w http.ResponseWriter, r *http.Request) {
}
d.ChaptersLine = wizChaptersLine(d.Chapters)
if done[2] {
d.AutoDone = append(d.AutoDone, "База bj создана установщиком"+pickStr(" — "+d.PGSummary, ""))
d.AutoDone = append(d.AutoDone, "база bj")
}
if done[6] {
d.AutoDone = append(d.AutoDone, "СКЗИ «Валидата Клиент L» "+d.ValidataVer+" установлена вместе с bj")
d.AutoDone = append(d.AutoDone, "СКЗИ «Валидата» "+d.ValidataVer)
}
if step == 4 && strings.TrimSpace(s.Fancy.Server) != "" && !doneOrg(s) && depoDetectResult() == nil {
// организация ещё не задана, а база DEPO подключена — определяем реквизиты сами: карточка
@@ -22,7 +22,7 @@
</div>
<div class="card prep">
{{if .Flash}}<p style="margin:0 0 6px;color:var(--ok)">✓ {{.Flash}}</p>{{end}}
<p class="muted" style="margin:0 0 4px;font-size:13px">Лицензия: план {{.Plan}}, {{.Until}}</p>
<p class="muted" style="margin:0 0 4px;font-size:13px">Лицензия активна · {{.Until}}</p>
<div id="prep-rows"><p class="muted" style="margin:12px 0">Проверяю обновления и состав лицензии…</p></div>
<div id="prep-prog" style="display:none;margin-top:8px"><span class="muted" id="prep-stage" style="font-size:13px"></span><div class="prep-bar"><i id="prep-fill"></i></div></div>
<div class="prep-foot">
@@ -27,11 +27,16 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); }
.wiz-sub a.cur { background:var(--accent); color:#fff; border-color:var(--accent); }
.wiz-nav { margin-top:20px; display:flex; justify-content:space-between; gap:8px; flex-wrap:wrap; }
.wiz-card * { min-width:0; }
.wiz-row { display:flex; align-items:center; gap:10px 14px; flex-wrap:wrap; }
.wiz-row-l { font-weight:600; font-size:15px; min-width:130px; 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 input[type=radio], .wiz-card input[type=checkbox] { flex:none; width:auto; margin-top:3px; }
</style>
<div class="wiz-head">
<h1>Настройка Bridge&amp;Join</h1>
<p>{{.ChaptersLine}} Всё сохраняется сразу — можно прерваться и вернуться.</p>
<p>Всё сохраняется сразу — можно прерваться и продолжить позже.</p>
</div>
<div class="wizard-progress" style="max-width:720px">
@@ -39,7 +44,7 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); }
{{end}}
</div>
{{range .Chapters}}{{if and .Current (gt (len .Steps) 1)}}<div class="wiz-sub">{{range .Steps}}<a href="/admin/wizard?step={{.N}}" class="{{if .Current}}cur{{end}}{{if .Done}} done{{end}}">{{if .Done}}✓ {{end}}{{.Title}}</a>{{end}}</div>{{end}}{{end}}
<p style="text-align:center;margin:-14px 0 18px;font-size:13px"><a href="/admin/">Пропустить мастер и перейти в систему →</a> <span class="muted">— вернуться к нему можно из «Настройка → Мастер настройки»</span></p>
<p style="text-align:center;margin:-14px 0 18px;font-size:13px"><a href="/admin/" title="Вернуться к мастеру — «Настройка → Мастер настройки»">Настроить позже и перейти в систему →</a></p>
{{if .Flash}}<div class="card wiz-card" style="background:var(--accent-weak)"><p style="margin:0">{{.Flash}}</p></div>{{end}}
@@ -52,10 +57,10 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); }
{{/* восстановление из резервной копии — только на первой странице (/first-run), в мастере его нет */}}
<div class="card">
<h2><span class="dot {{if .Done.License}}ok{{else}}err{{end}}"></span>Лицензия</h2>
<div class="help-block"><strong>Что это.</strong> Ключ продукта (BJ-XXXX-XXXX-XXXX) привязывает этот сервер к вашей лицензии.<br>
<strong>Зачем.</strong> От лицензии зависят модули (обмен с DEPO и QUIK, личный кабинет, НПР/УДС) и получение обновлений: bj скачает и поставит модули, входящие в лицензию, автоматически.<br>
<strong>Где взять.</strong> Портал клиента <a href="https://lic.zetit.ru/portal/" target="_blank">lic.zetit.ru/portal</a> или письмо от ZETIT.</div>
{{if .Done.License}}<p>✓ <strong>Ключ продукта введён</strong>, лицензия активна: <strong>{{.Lic.Tenant}}</strong>, {{if eq .Lic.Plan "enterprise"}}полная лицензия (все модули){{else}}базовая лицензия{{end}}, модули: {{if .Lic.Features}}{{range $i, $f := .Lic.Features}}{{if $i}}, {{end}}{{$f}}{{end}}{{else}}только ядро{{end}}; {{.Lic.Message}}</p>{{end}}
{{if .Done.License}}<p style="margin:4px 0 2px;font-size:16px">✓ Лицензия активна — <strong>{{.Lic.Tenant}}</strong></p>
<p class="muted" style="margin:0;font-size:13.5px">{{if eq .Lic.Plan "enterprise"}}Все модули{{else if .Lic.Features}}Входит: {{range $i, $f := .Lic.Features}}{{if $i}}, {{end}}{{modTitle $f}}{{end}}{{else}}Только ядро: обмен с НРД и переводы M2M{{end}} · {{.Lic.Message}}</p>
{{else}}<p style="margin:4px 0 0">Введите ключ продукта — bj сам скачает и поставит модули вашей лицензии.</p>
<p class="muted" style="margin:4px 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}}
{{with .Update}}{{if .HasUpdate}}
<div style="border:1px solid var(--accent);border-radius:8px;padding:12px 14px;margin-top:12px;background:var(--accent-weak)">
<p style="margin:0 0 6px;font-weight:600">На сервере лицензий есть что поставить (выпуск {{.Available}})</p>
@@ -65,7 +70,7 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); }
<button type="submit" class="btn">Обновить сейчас</button>
</form>
</div>
{{else}}<p class="muted" style="font-size:13px">✓ Установлена последняя версия bj ({{.Available}}).</p>{{end}}{{end}}
{{else}}<p class="muted" style="font-size:13px;margin-top:6px">✓ Установлена последняя версия bj</p>{{end}}{{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">
@@ -74,11 +79,10 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); }
<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:12px;display:grid;gap:10px">
<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">
<label>Ключ продукта</label>
<input type="text" name="code" placeholder="BJ-XXXX-XXXX-XXXX" autocomplete="off" style="padding:8px;text-transform:uppercase;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px">
<button type="submit" class="btn" style="justify-self:start">{{if .Done.License}}Сменить ключ{{else}}Активировать{{end}}</button>
<input type="text" name="code" placeholder="BJ-XXXX-XXXX-XXXX" aria-label="Ключ продукта" autocomplete="off" autofocus required style="flex:1 1 260px;padding:10px 12px;font-size:16px;letter-spacing:.04em;text-transform:uppercase;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:8px">
<button type="submit" class="btn">Активировать</button>
</form>{{end}}
{{template "wizNav" .}}
</div>
@@ -93,44 +97,42 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); }
{{else}}
{{$sub := .Sub}}
{{if eq $sub "setup"}}
<div class="help-block"><strong>Что это.</strong> bj подключается к базе учёта DEPO (Fancy): сверяет с учётом каждый перевод (клиент, счёт, остаток, квалификация) и передаёт в DEPO поручения и их исполнение — документы в учёте создаются сами.<br>
<strong>Что понадобится.</strong> Адрес сервера базы DEPO. Имя базы знать не нужно — bj найдёт её сам. Подготовить базу (логин bj, таблицы обмена, задание обработки) можно одним действием: логином администратора SQL — он используется один раз и не сохраняется, — или администратор выполнит один готовый скрипт.</div>
<p style="margin:2px 0 0">bj сверяет каждый перевод с учётом DEPO и сам создаёт в нём поручения. Нужен только адрес сервера — базу DEPO bj найдёт сам.</p>
{{$mode := .Settings.Fancy.DepoJobMode}}
<form method="post" action="/admin/setup/wizard-depo/setup" id="wiz-depo" style="display:grid;gap:12px;max-width:680px;margin-top:10px">
<div style="display:grid;grid-template-columns:1.2fr 2fr .8fr;gap:8px">
<label style="margin:0">СУБД<select name="db_type" onchange="wizDepo()" style="padding:8px;width:100%;box-sizing:border-box;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px;margin-top:4px"><option value="mssql" {{if ne .Settings.Fancy.DBType "postgres"}}selected{{end}}>SQL Server</option><option value="postgres" {{if eq .Settings.Fancy.DBType "postgres"}}selected{{end}}>PostgreSQL</option></select></label>
<label style="margin:0">Сервер DEPO (адрес или host\instance)<input type="text" name="server" value="{{.Settings.Fancy.Server}}" placeholder="адрес сервера" required style="padding:8px;width:100%;box-sizing:border-box;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px;margin-top:4px"></label>
<label style="margin:0">Порт<input type="text" name="port" value="{{if .Settings.Fancy.Port}}{{.Settings.Fancy.Port}}{{end}}" placeholder="1433" style="padding:8px;width:100%;box-sizing:border-box;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px;margin-top:4px"></label>
<div style="display:grid;grid-template-columns:2.2fr .8fr 1.1fr;gap:8px">
<label style="margin:0">Сервер DEPO<input type="text" name="server" value="{{.Settings.Fancy.Server}}" placeholder="адрес или имя\экземпляр" required autofocus style="padding:8px;width:100%;box-sizing:border-box;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:8px;margin-top:4px"></label>
<label style="margin:0">Порт<input type="text" name="port" value="{{if .Settings.Fancy.Port}}{{.Settings.Fancy.Port}}{{end}}" placeholder="1433" style="padding:8px;width:100%;box-sizing:border-box;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:8px;margin-top:4px"></label>
<label style="margin:0">СУБД<select name="db_type" onchange="wizDepo()" style="padding:8px;width:100%;box-sizing:border-box;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:8px;margin-top:4px"><option value="mssql" {{if ne .Settings.Fancy.DBType "postgres"}}selected{{end}}>SQL Server</option><option value="postgres" {{if eq .Settings.Fancy.DBType "postgres"}}selected{{end}}>PostgreSQL</option></select></label>
</div>
{{if .DepoCands}}<label style="margin:0">На сервере несколько баз DEPO — выберите нужную<select name="depo_database" style="padding:8px;width:100%;box-sizing:border-box;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px;margin-top:4px">{{range .DepoCands}}<option>{{.}}</option>{{end}}</select></label>{{end}}
<div id="wiz-depo-pgdb" style="display:none"><label style="margin:0">База DEPO <span class="muted" style="font-weight:400">(для PostgreSQL указывается вручную)</span><input type="text" name="depo_database" value="{{.Settings.Fancy.DepoDatabase}}" pattern="[A-Za-z][A-Za-z0-9_]*" style="padding:8px;width:100%;box-sizing:border-box;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px;margin-top:4px"></label></div>
<div id="wiz-depo-job"><div style="font-weight:600;margin-bottom:4px">Обработку обмена в DEPO запускает</div>
<label style="display:flex;gap:8px;align-items:flex-start;font-weight:400;margin:0 0 4px"><input type="radio" name="depo_job" value="agent" {{if ne $mode "bj"}}checked{{end}} style="width:auto;margin-top:3px"> <span><b>Задание SQL Server Agent</b> <span class="muted" style="font-size:13px">— рекомендуется: работает, даже когда bj выключен. Скрипт создаст его сам.</span></span></label>
<label style="display:flex;gap:8px;align-items:flex-start;font-weight:400;margin:0"><input type="radio" name="depo_job" value="bj" {{if eq $mode "bj"}}checked{{end}} style="width:auto;margin-top:3px"> <span><b>Сервер bj</b> <span class="muted" style="font-size:13px">— bj вызывает обработку каждые {{.DepoJob.Every}} с (если на сервере нет SQL Server Agent, например Express).</span></span></label>
</div>
<div style="border-top:1px solid var(--border);padding-top:10px"><div style="font-weight:600;margin-bottom:6px">Как подготовить базу</div>
<label style="display:flex;gap:8px;align-items:flex-start;font-weight:400;margin:0 0 6px"><input type="radio" name="mode" value="auto" checked onchange="wizDepo()" style="width:auto;margin-top:3px"> <span><b>bj сделает всё сам</b> <span class="muted" style="font-size:13px">— нужен логин администратора SQL (sysadmin) на один раз: bj найдёт базу, создаст свой логин, таблицы обмена и задание. Логин администратора не сохраняется.</span></span></label>
<label class="wiz-opt"><input type="radio" name="mode" value="auto" checked onchange="wizDepo()"> <span><b>bj сделает всё сам</b> <span class="muted">— нужен логин администратора SQL на один раз, bj его не сохраняет</span></span></label>
<div id="wiz-depo-auto" style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin:0 0 8px 26px">
<input type="text" name="admin_user" autocomplete="off" placeholder="логин администратора SQL, например sa" style="padding:8px;width:100%;box-sizing:border-box;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px">
<input type="text" name="admin_user" autocomplete="off" placeholder="логин администратора, например sa" style="padding:8px;width:100%;box-sizing:border-box;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px">
<input type="password" name="admin_password" autocomplete="new-password" placeholder="пароль" style="padding:8px;width:100%;box-sizing:border-box;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px">
</div>
<label style="display:flex;gap:8px;align-items:flex-start;font-weight:400;margin:0 0 6px"><input type="radio" name="mode" value="script" onchange="wizDepo()" style="width:auto;margin-top:3px"> <span><b>Скрипт для администратора</b> <span class="muted" style="font-size:13px">— если логин администратора не дают: один файл делает то же самое (находит базу, создаёт логин bj, таблицы и задание).</span></span></label>
<label class="wiz-opt"><input type="radio" name="mode" value="script" onchange="wizDepo()"> <span><b>Отдать скрипт администратору</b> <span class="muted">— если логин администратора не дают: один файл сделает то же самое</span></span></label>
<div id="wiz-depo-script" style="display:none;margin:0 0 8px 26px">
<button type="submit" class="btn" formaction="/admin/setup/wizard-depo/script.sql" style="background:var(--card);color:var(--text);border:1px solid var(--border-strong,#c9cdd8)">Скачать скрипт</button>
<span class="muted" style="font-size:12.5px">Пароль логина bj придумывает bj и уже вписал в скрипт. Администратор выполняет файл в SSMS — потом нажмите «Проверить».</span>
<span class="muted" style="font-size:12.5px">Пароль для bj уже вписан в скрипт. Администратор выполняет файл в SSMS — потом нажмите «Проверить».</span>
{{template "letterCard" (letter "dba")}}
</div>
<label style="display:flex;gap:8px;align-items:flex-start;font-weight:400;margin:0 0 6px"><input type="radio" name="mode" value="existing" onchange="wizDepo()" style="width:auto;margin-top:3px"> <span><b>Логин для bj уже есть</b> <span class="muted" style="font-size:13px">— база уже подготовлена раньше: только проверить.</span></span></label>
<label class="wiz-opt"><input type="radio" name="mode" value="existing" onchange="wizDepo()"> <span><b>Логин для bj уже есть</b> <span class="muted">— база подготовлена раньше, только проверить</span></span></label>
<div id="wiz-depo-existing" style="display:none;grid-template-columns:1fr 1fr;gap:8px;margin:0 0 8px 26px">
<input type="text" name="user" value="{{.Settings.Fancy.User}}" autocomplete="off" placeholder="логин bj" style="padding:8px;width:100%;box-sizing:border-box;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px">
<input type="password" name="password" autocomplete="new-password" placeholder="{{if .Settings.Fancy.Password}}пароль (пусто — прежний){{else}}пароль{{end}}" style="padding:8px;width:100%;box-sizing:border-box;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px">
</div>
</div>
{{if not .DepoCands}}<details style="font-size:13px"><summary style="cursor:pointer;color:var(--accent)">Дополнительно: указать базу DEPO вручную (обычно не нужно)</summary>
<div id="wiz-depo-job" style="border-top:1px solid var(--border);padding-top:10px"><div style="font-weight:600;margin-bottom:6px">Кто запускает обработку обмена</div>
<label class="wiz-opt"><input type="radio" name="depo_job" value="agent" {{if ne $mode "bj"}}checked{{end}}> <span><b>SQL Server Agent</b> <span class="muted">— рекомендуется: работает, даже когда bj выключен</span></span></label>
<label class="wiz-opt"><input type="radio" name="depo_job" value="bj" {{if eq $mode "bj"}}checked{{end}}> <span><b>Сервер bj</b> <span class="muted">— если Agent нет (например, SQL Server Express)</span></span></label>
</div>
{{if not .DepoCands}}<details style="font-size:13px"><summary style="cursor:pointer;color:var(--accent)">Указать базу DEPO вручную (обычно не нужно)</summary>
<input type="text" name="depo_database" id="wiz-depo-dbman" value="" placeholder="имя базы DEPO на сервере" pattern="[A-Za-z][A-Za-z0-9_]*" style="padding:8px;width:100%;box-sizing:border-box;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px;margin-top:6px;max-width:320px"></details>{{end}}
<div><button type="submit" class="btn" id="wiz-depo-btn">Подготовить базу и проверить →</button></div>
<div><button type="submit" class="btn" id="wiz-depo-btn">Подготовить и проверить</button></div>
</form>
<script>function wizDepo(){var f=document.getElementById('wiz-depo'),pg=f.db_type.value==='postgres',m=f.mode.value;
document.getElementById('wiz-depo-pgdb').style.display=pg?'block':'none';
@@ -141,14 +143,14 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); }
document.getElementById('wiz-depo-auto').style.display=m==='auto'?'grid':'none';
document.getElementById('wiz-depo-script').style.display=m==='script'?'block':'none';
document.getElementById('wiz-depo-existing').style.display=m==='existing'?'grid':'none';
document.getElementById('wiz-depo-btn').textContent=m==='auto'?'Подготовить базу и проверить →':'Проверить →';}
document.getElementById('wiz-depo-btn').textContent=m==='auto'?'Подготовить и проверить':'Проверить';}
wizDepo();</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}}</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.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}}
@@ -200,13 +202,12 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); }
{{if eq .Step 4}}
<div class="card">
<h2><span class="dot {{if .Done.Org}}ok{{else}}err{{end}}"></span>Наша организация</h2>
<div class="help-block"><strong>Что это.</strong> Депозитарий — участник MOEX МОСТ, от имени которого bj отправляет и принимает переводы: код участника, ИНН и наименование попадают в пакеты НРД{{if .DepoLicensed}} и в поручения QUIK{{end}}.<br>
<strong>Откуда взять.</strong> {{if .DepoLicensed}}Если подключена база DEPO (раздел «Учёт DEPO»), bj сам определяет реквизиты по учёту — проверьте их и сохраните. Иначе выберите{{else}}Выберите{{end}} себя из справочника участников НРД или введите вручную.</div>
<p style="margin:2px 0 0">От имени вашей организации bj отправляет и принимает переводы. {{if .DepoLicensed}}Реквизиты bj берёт из DEPO — проверьте и сохраните.{{else}}Найдите себя в справочнике участников НРД.{{end}}</p>
{{if .Suggest}}{{if .Suggest.OrgName}}{{if not .Done.Org}}<p style="margin:8px 0;padding:8px 12px;border-radius:8px;background:var(--accent-weak)">✓ <strong>Определено из DEPO</strong>: {{.Suggest.OrgName}}, ИНН {{.Suggest.OrgINN}}, код {{.Suggest.OrgCode}}{{if .Suggest.AccountID}}, счёт НРД {{.Suggest.AccountID}}{{end}}. Поля ниже заполнены — проверьте и нажмите «Сохранить».</p>{{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>
<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}}
<form method="post" action="/admin/setup/nsd-requisites" style="display:grid;gap:10px;max-width:680px">
<input type="hidden" name="return_to" value="/admin/wizard?step=4">
@@ -218,16 +219,6 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); }
<div style="display:flex;gap:8px;flex-wrap:wrap"><button type="submit" class="btn">Сохранить</button>{{if .Settings.Fancy.Server}}<button type="submit" class="btn" formaction="/admin/setup/fancy/detect" style="background:var(--card);color:var(--text);border:1px solid var(--border-strong,#c9cdd8)">Определить из DEPO</button>{{end}}</div>
</form>
</div>
<div class="card" style="margin-top:12px">
<h3 style="margin:0 0 6px">Логотип организации (необязательно)</h3>
<p class="muted" style="margin:0 0 10px;font-size:13px">Показывается справа в шапке отчётов для регулятора. Можно загрузить сейчас или позже в «Настройка → Оформление».</p>
{{if .Settings.Branding.LogoDataURI}}<p style="margin:0 0 8px"><img src="{{.Settings.Branding.LogoDataURI}}" alt="логотип" style="height:36px;background:#fff;padding:4px 8px;border-radius:6px;border:1px solid var(--border)"></p>{{end}}
<form method="post" action="/admin/setup/logo" enctype="multipart/form-data" style="display:flex;gap:10px;flex-wrap:wrap;align-items:center">
<input type="hidden" name="return_to" value="/admin/wizard?step=4">
<input type="file" name="logo" accept="image/png,image/jpeg,image/gif,image/webp,image/svg+xml,.png,.jpg,.jpeg,.gif,.webp,.svg" required>
<button type="submit" class="btn" style="background:var(--card);color:var(--text);border:1px solid var(--border-strong,#c9cdd8)">{{if .Settings.Branding.LogoDataURI}}Заменить логотип{{else}}Загрузить логотип{{end}}</button>
</form>
</div>
{{template "wizNav" .}}
</div>
{{end}}
@@ -235,16 +226,15 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); }
{{/* ============= ШАГ 5: QUIK МНП ============= */}}
{{if eq .Step 5}}
<div class="card">
<h2><span class="dot {{if .Done.Quik}}ok{{else}}err{{end}}"></span>QUIK — модуль неторговых поручений</h2>
<div class="help-block"><strong>Что это.</strong> Модуль неторговых поручений QUIK (МНП) — продукт компании ARQA Technologies, разработчика QUIK. Через него bj блокирует бумаги клиента в QUIK на время перевода, снимает блокировку при отказе и после исполнения проводит поручение.<br>
<strong>Если МНП у вас ещё нет.</strong> Обратитесь в ARQA Technologies (<a href="https://arqatech.com" target="_blank" rel="noopener">arqatech.com</a>, служба поддержки QUIK) и попросите подключить модуль неторговых поручений для работы с внешней системой. После подключения ARQA выдаст адрес сервера МНП и учётные данные — их и нужно ввести ниже.<br>
<strong>Если МНП уже есть.</strong> Адрес сервера и учётные данные — у вашего администратора QUIK.</div>
<h2><span class="dot {{if .Done.Quik}}ok{{else}}err{{end}}"></span>QUIK</h2>
<p style="margin:2px 0 0">Адрес и логин модуля неторговых поручений — у вашего администратора 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}}
<div class="help-block" style="margin-top:12px"><strong>Справочник депозитариев-получателей.</strong> Чтобы QUIK принимал поручения M2M, в МНП должен быть справочник депозитариев-получателей (<code>M2MDepoRecipients</code>): bj заполняет его сам по справочнику участников НРД. <b>Модуль МНП читает справочник только при запуске</b> — после первого заполнения попросите администратора QUIK перезапустить модуль неторговых поручений, иначе QUIK ответит «Неизвестный депозитарий получателя».<br><span class="muted">{{quikRecipientsInfo}}</span></div>{{end}}
<details style="margin-top:12px;font-size:13px"><summary style="cursor:pointer;color:var(--accent)">Справочник депозитариев-получателей — bj заполняет сам</summary>
<p style="margin:6px 0 0">Чтобы QUIK принимал поручения M2M, bj ведёт в МНП справочник депозитариев-получателей (<code>M2MDepoRecipients</code>) по справочнику участников НРД. <b>МНП читает его только при запуске</b> — после первого заполнения попросите администратора QUIK перезапустить модуль, иначе QUIK ответит «Неизвестный депозитарий получателя».<br><span class="muted">{{quikRecipientsInfo}}</span></p></details>{{end}}
{{template "wizNav" .}}
</div>
{{end}}
@@ -275,14 +265,13 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); }
{{/* ============= ШАГ 7: Шлюз НРД (до ключа: ключ ставится в профиль ИШ) ============= */}}
{{if eq .Step 7}}
<div class="card">
<h2><span class="dot {{if .Done.NSD}}ok{{else}}err{{end}}"></span>Интеграционный шлюз НРД (ИШ)</h2>
<div class="help-block">
<strong>Что это.</strong> Интеграционный шлюз — программа НРД на этом же сервере: через неё bj отправляет пакеты M2M в НРД и получает ответы. bj работает со шлюзом по его адресу на сервере (обычно <code>http://localhost:8090</code>).<br>
<strong>К кому обратиться.</strong> Дистрибутив ИШ выпускает НРД (<a href="https://www.nsd.ru/workflow/system/programs/" target="_blank" rel="noopener">nsd.ru/workflow/system/programs</a>). Доступ к контурам — сначала тестовому TEST3, затем боевому — НРД открывает по заявке: обратитесь к своему менеджеру НРД или в поддержку электронного документооборота.
</div>
<h2><span class="dot {{if .Done.NSD}}ok{{else}}err{{end}}"></span>Шлюз НРД</h2>
<p style="margin:2px 0 0">Через интеграционный шлюз НРД (ИШ) на этом сервере 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")}}
{{template "igateInstall" .}}
</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">
@@ -307,24 +296,17 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); }
{{if eq .Step 8}}
<div class="card">
<h2><span class="dot {{if .Done.Certs}}ok{{else}}err{{end}}"></span>Ключ и сертификаты</h2>
<p>Здесь две части: ключ и сертификат вашей организации (ставите вы) и сертификаты НРД и удостоверяющих центров (bj ведёт их сам).</p>
{{if not .Done.NSD}}<p style="padding:8px 12px;border-radius:8px;background:var(--accent-weak);margin:8px 0">Ключ устанавливается в профиль интеграционного шлюза — сначала установите шлюз в разделе <a href="/admin/wizard?step=7">«Шлюз НРД»</a>.</p>{{end}}
<p style="margin:2px 0 0">Ключ электронной подписи организации для обмена с НРД. Его выдаёт Удостоверяющий центр Московской биржи: вам передадут архив ключа и пароль.</p>
{{if not .Done.NSD}}<p style="padding:8px 12px;border-radius:8px;background:var(--accent-weak);margin:10px 0 0">Ключ ставится в шлюз НРД — сначала установите <a href="/admin/wizard?step=7">шлюз</a>.</p>{{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>
<h3 style="margin-top:14px">1. Ключ и сертификат организации</h3>
<div class="help-block"><strong>Что это.</strong> Ключ электронной подписи вашей организации для обмена с НРД и его сертификат.<br>
<strong>К кому обратиться.</strong> Ключ выдаёт Удостоверяющий центр Московской биржи; оформляется по заявке в рамках подключения к электронному документообороту НРД. Вам передадут архив ключа и пароль к нему — его и загружают в мастере установки ключа.<br>
<strong>Как поставить.</strong> Мастер установки ключа: загрузить архив → запись на носитель → справочник сертификатов → проверка подписи Валидатой.</div>
<a href="/admin/setup/keywizard" class="btn" style="text-decoration:none;display:inline-block">Открыть мастер установки ключа →</a>
<h3 style="margin-top:22px">2. Сертификаты НРД и УЦ — автоматически</h3>
<p>✓ <strong>Автоматическое обновление {{if .Settings.NSDCerts.Manual}}<span style="color:var(--warn)">выключено</span> (включается в «Настройка → Криптография»){{else}}включено{{end}}.</strong> Делать на этом шаге ничего не нужно.</p>
<div class="help-block" style="font-size:13px">
<strong>Как это работает.</strong> Действующие сертификаты НРД и удостоверяющих центров Московской биржи поставляются вместе с bj и приезжают с обновлениями. Каждые 6 часов bj сверяет их со справочником сертификатов ИШ: добавляет недостающие, а заменённые НРД убирает строго после срока, который НРД объявил в уведомлении. Перед каждым изменением делается резервная копия и проверка подписи, при ошибке всё откатывается.<br>
<strong>Тестовые и боевые.</strong> Сертификат попадает только в справочник своего контура: боевые (издатель «Корневой УЦ») — только к боевому ключу, тестовые (издатель «Тестовый УЦ») — только к тестовому. Поэтому смешать контуры нельзя.<br>
<strong>Откуда берутся.</strong> НРД и Московская биржа публикуют сертификаты в постоянных местах: комплект ГОСТ МБ, НКЦ и НРД — <code>https://fs.moex.com/cdp/sert/GOST.zip</code>, сертификаты УЦ — <code>https://fs.moex.com/cdp/</code>. Когда там появляется новый сертификат, мы добавляем его в комплект bj, и он приезжает к вам с обновлением.<br>
Состояние, ручная проверка и выключение — «Настройка → Криптография → Сертификаты НРД в справочнике ИШ».
<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>
{{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}}<p class="muted" style="font-size:13px">Первая сверка пройдёт сразу после установки ключа.</p>{{end}}
<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>
@@ -343,7 +325,7 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); }
</tbody>
</table>
{{else}}
<p class="muted" style="margin-top:12px">Ключ организации ещё не установлен. Можно пропустить этот шаг и вернуться к нему, когда будет архив ключа.</p>
<p class="muted" style="margin-top:12px;font-size:13px">Архива ключа ещё нет — шаг можно пропустить и вернуться позже.</p>
{{end}}
{{template "wizNav" .}}
@@ -353,27 +335,46 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); }
{{/* ============= ШАГ 12: Время и сеть (глава «Сервер») ============= */}}
{{if eq .Step 12}}
<div class="card">
<h2><span class="dot {{if .Done.Server}}ok{{else}}warn{{end}}"></span>Время и сеть сервера</h2>
{{if .AutoDone}}<div class="help-block"><strong>Сделано автоматически.</strong>{{range .AutoDone}}<br>✓ {{.}}{{end}}</div>{{end}}
<h3 style="margin:14px 0 4px">Часовой пояс</h3>
<p class="muted" style="margin:0 0 6px;font-size:13px">По нему — время в журналах, уведомлениях и сроках поручений. Время НРД — московское; если сервер в другом поясе, выберите свой.</p>
<h2><span class="dot {{if .Done.Server}}ok{{else}}warn{{end}}"></span>Время и сеть</h2>
{{$tz := serverTimeZone}}
<form method="post" action="/admin/setup/timezone" style="display:flex;gap:8px;flex-wrap:wrap;align-items:end">
<form method="post" action="/admin/setup/timezone" id="wiz-tz" class="wiz-row">
<input type="hidden" name="return_to" value="/admin/wizard?step=12">
<select name="zone" style="padding:8px;min-width:260px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px">{{range timeZones}}<option value="{{.Zone}}" {{if eq .Zone $tz}}selected{{end}}>{{.Title}}</option>{{end}}</select>
<button type="submit" class="btn">Применить</button>
<span class="muted" style="font-size:12.5px">сейчас: {{$tz}}</span>
<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="padding:8px 10px;min-width:240px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:8px">{{range timeZones}}<option value="{{.Zone}}" {{if eq .Zone $tz}}selected{{end}}>{{.Title}}</option>{{end}}</select>
</form>
<h3 style="margin:16px 0 4px">Сервер времени</h3>
<p style="margin:0 0 6px;font-size:13px">{{if .Done.Server}}<span style="color:var(--ok)">✓ время синхронизируется</span>{{else}}<span style="color:var(--warn)">⚠ время не синхронизируется — укажите сервер времени вашей сети (обычно контроллер домена или маршрутизатор)</span>{{end}}</p>
<form method="post" action="/admin/setup/time" style="display:flex;gap:8px;flex-wrap:wrap;align-items:end">
<p id="wiz-tz-hint" class="muted" style="margin:4px 0 0;font-size:13px">По нему — время в журналах, уведомлениях и сроках поручений. Время НРД — московское.</p>
<div id="wiz-tz-suggest" style="display:none;margin-top:10px;padding:10px 12px;border-radius:8px;background:var(--accent-weak);font-size:13.5px">На вашем компьютере пояс <b id="wiz-tz-name"></b>. <button type="button" class="btn" id="wiz-tz-use" style="margin-left:6px;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.10.10.1 ntp.example.ru" style="padding:8px;min-width:260px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px">
<button type="submit" class="btn" style="background:var(--card);color:var(--text);border:1px solid var(--border-strong,#c9cdd8)">Применить</button>
<input name="servers" value="{{.Settings.Time.Servers}}" placeholder="например, 10.0.0.1" aria-label="Сервер времени" style="padding:8px 10px;min-width:240px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:8px">
<button type="submit" class="btn">Сохранить</button>
</form>
<h3 style="margin:16px 0 4px">Адрес и HTTPS</h3>
<p style="margin:0;font-size:13px">Сервер: <code>{{serverAddrs}}</code>. Вход по HTTPS: <a href="https://{{serverIP}}:8443/admin/">https://{{serverIP}}:8443/</a>.</p>
<p class="muted" style="font-size:12.5px;margin:4px 0 0">Чтобы адрес не менялся, задайте постоянный — на консоли сервера: <code>sudo bj-network</code>. Перенаправление на HTTPS и сертификат организации — в «Настройка → Сервер».</p>
</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}}
@@ -381,13 +382,13 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); }
{{/* ============= ШАГ 11: Модули по лицензии (глава «Запуск») ============= */}}
{{if eq .Step 11}}
<div class="card">
<h2><span class="dot {{if .Done.Modules}}ok{{else}}warn{{end}}"></span>Модули по лицензии</h2>
<p class="muted" style="margin:0 0 8px">Здесь только модули, которые входят в вашу лицензию. Каждый настраивается в своём разделе — кнопка «Настроить» откроет его, а вернуться в мастер можно ссылкой вверху раздела.</p>
<h2><span class="dot {{if .Done.Modules}}ok{{else}}warn{{end}}"></span>Модули</h2>
<p style="margin:2px 0 8px">Модули вашей лицензии уже установлены. Каждый настраивается в своём разделе — сейчас или позже.</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">{{if .Ready}}Открыть{{else}}Настроить{{end}} →</a>
<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>
@@ -401,15 +402,14 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); }
{{if eq .Step 9}}
<div class="card">
<h2><span class="dot {{if .Done.Alerts}}ok{{else}}warn{{end}}"></span>Оповещения и резервные копии</h2>
<div class="help-block"><strong>Зачем.</strong> bj сам следит за обменом: НРД не отвечает, перевод не доставлен{{if .DepoLicensed}}, ошибка обработки в DEPO{{end}}, истекает сертификат ключа. Чтобы об этом узнали сразу, а не открыв bj, — укажите почту. Резервные копии позволят восстановить сервер целиком (настройки, заявки, шлюз НРД) после сбоя или переустановки.<br>
<strong>Можно позже.</strong> Шаг необязательный — обмен работает и без него, но мы настоятельно советуем настроить оба пункта.</div>
<p style="margin:2px 0 0">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}}
<h3 style="margin:16px 0 6px">Почта для оповещений</h3>
{{template "smtpCard" .}}
<details style="margin:10px 0 0"><summary style="cursor:pointer;color:var(--accent);font-size:13px">Разослать уведомления отделам — депозитарий, бэк-офис, мидл-офис (необязательно)</summary>
{{template "notifyGroupsCard" .}}
<h3 style="margin:16px 0 6px">Резервные копии</h3>
</details>
{{template "backupCard" .}}
{{end}}
</div>
+2 -1
View File
@@ -61,6 +61,7 @@ body {
.brand svg { width:26px; height:26px; display:block; flex:none; }
.brand b { font-weight:730; } .brand .brand-j { color:var(--muted); font-weight:600; }
.nav { display:flex; align-items:center; gap:1px; margin-left:6px; flex:1 1 auto; min-width:0; flex-wrap:wrap; }
.nav[hidden] { visibility:hidden; }
.nav::-webkit-scrollbar { display:none; }
.user-chip { display:inline-flex; flex-direction:column; line-height:1.15; margin-right:6px; max-width:180px; }
.user-chip .user-name { font-size:12.5px; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
@@ -336,7 +337,7 @@ details:has(> pre.copyable) { min-width:0; max-width:100%; }
</svg>
<span><b>Bridge</b><span class="brand-j">&amp;Join</span></span>
</a>
<nav class="nav">
<nav class="nav"{{if eq .Active "wizard"}} hidden{{end}}>{{/* мастер и подготовка модулей — без меню системы, как помощник настройки */}}
{{if can "view"}}<a href="/admin/" class="{{if eq .Active "home"}}active{{end}}">Дашборд</a>
<span class="dd{{if or (eq .Active "journal") (eq .Active "claims") (eq .Active "incoming")}} active{{end}}">
<span class="ddbtn">Журнал ▾<span id="nav-journal-badge" title="Входящих ждут решения" style="display:none;margin-left:5px;background:var(--err);color:#fff;border-radius:10px;padding:1px 7px;font-size:11px;font-weight:700"></span></span>
@@ -2,7 +2,7 @@
{{define "smtpCard"}}
<div class="card" id="sec-smtp">
<h2>Почта (SMTP)</h2>
<p class="muted">Отправка уведомлений по e-mail. Пароль хранится в конфигурации сервиса.</p>
<p class="muted">Через какой почтовый сервер bj отправляет письма и кому.</p>
<form method="post" action="/admin/setup/alerts-smtp" style="margin-top:12px">
<label style="display:flex;align-items:center;gap:8px;font-weight:400;margin-bottom:12px"><input type="checkbox" name="enabled" style="width:auto" {{if .Settings.Alerts.SMTP.Enabled}}checked{{end}}> Включить почтовые уведомления</label>
<div class="grid" style="grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px">
@@ -3,7 +3,7 @@
{{$b := .Settings.Backup}}{{$bs := backupStatus}}
<div class="card">
<h2><span class="dot {{if not $b.Enabled}}warn{{else if and $bs $bs.OK}}ok{{else}}err{{end}}"></span>Копии сервера bj на сетевое хранилище</h2>
<p class="muted">Что копируется: база bj (заявки, сделки, кабинет), база и папка шлюза НРД (настройки, сертификаты, полученные от НРД документы), настройки и данные bj. Пароль хранилища bj у себя не хранит — он лежит на сервере в файле, доступном только системе. Восстановить копию можно в мастере настройки (глава «Сервер», «Восстановить из резервной копии»).</p>
<p class="muted">Что копируется: база bj (заявки, сделки, кабинет), база и папка шлюза НРД (настройки, сертификаты, полученные от НРД документы), настройки и данные bj. Пароль хранилища bj у себя не хранит — он лежит на сервере в файле, доступном только системе. Восстановить копию можно на первой странице нового сервера bj — «Восстановить из резервной копии».</p>
<p style="margin:8px 0">{{if $bs}}Последняя копия: <b>{{backupWhen $bs.At}}</b> — {{if $bs.OK}}<span style="color:var(--ok)">✓ {{$bs.Dir}}, {{$bs.Msg}}</span>{{else}}<span style="color:var(--err)">✗ {{$bs.Msg}}</span>{{end}}{{else}}<span class="muted">Копий ещё не было.</span>{{end}}</p>
<style>#bk-form label:not(.chk){display:block;margin:0}#bk-form label:not(.chk) input,#bk-form label:not(.chk) select{width:100%;box-sizing:border-box;margin-top:4px}</style>
<form method="post" action="/admin/setup/backup" id="bk-form" style="display:grid;gap:10px;max-width:640px">
@@ -32,7 +32,7 @@
<div class="card" style="margin:10px 0">
<h3>Контуры НРД</h3>
<p class="muted" style="font-size:13px;margin:0 0 8px">Сначала работают на тестовом контуре TEST3; когда переводы на нём проходят — переходят на боевой. У каждого контура свой ключ (в мастере установки ключа выберите контур) и свой канал в ИШ. bj работает с одним контуром — активным.</p>
<p class="muted" style="font-size:13px;margin:0 0 8px">Сначала — тестовый контур TEST3; когда переводы на нём проходят, переходите на боевой. У каждого контура свой ключ.</p>
<label class="f" style="max-width:220px">Криптография<select id="ig-crypto"><option value="gost">ГОСТ</option><option value="rsa">RSA</option></select></label>
<table id="ig-ct"><tbody></tbody></table>
<div id="ig-prod" style="display:none;border:1px solid var(--warn);border-radius:8px;padding:10px 12px;margin-top:10px">
@@ -61,7 +61,7 @@ function hbBind(inp,box,list,onPick){
<label>Наименование</label>
<input type="text" name="org_name" value="{{if .Settings.NSD.OrgName}}{{.Settings.NSD.OrgName}}{{else if .Suggest}}{{.Suggest.OrgName}}{{end}}">
{{if .Suggest}}{{if .Suggest.OrgName}}<span class="muted" style="font-size:12px">Из DEPO: {{.Suggest.OrgName}}, ИНН {{.Suggest.OrgINN}}, код {{.Suggest.OrgCode}} — подставлено в пустые поля.</span>{{end}}{{end}}
<div><button type="submit" class="btn btn-ok">Сохранить</button></div>
<div><button type="submit" class="btn">Сохранить</button></div>
</form>
</div>
{{end}}
+34 -30
View File
@@ -2,40 +2,43 @@
{{define "quikSection"}}
{{template "verline" (.Versions.Get "quik-mnp")}}
<div class="card">
<h2><span class="dot {{if .Settings.Quik.Verified}}ok{{else if .Settings.Quik.Server}}warn{{else}}err{{end}}"></span>Подключение к БД МНП QUIK (MSSQL)</h2>
<p class="muted">При выводе бумаг клиентом bj блокирует их количество в QUIK через API Модуля неторговых поручений (хранимые процедуры). БД <strong>боевая</strong> — проверка только на чтение (версия + наличие M2M-объектов), без записи. Подключение под логином с ролью <code>InstrEu</code>.<br>
<a href="/admin/help/quik"><strong>Инструкция для администратора QUIK</strong></a> — что настроить в QuikAdmin для M2M · <a href="/admin/docs/">документы ARQA</a>.</p>
<h2><span class="dot {{if .Settings.Quik.Verified}}ok{{else if .Settings.Quik.Server}}warn{{else}}err{{end}}"></span>Подключение к модулю неторговых поручений</h2>
<p class="muted">{{if not .InWizard}}bj блокирует бумаги клиента в QUIK на время перевода через модуль неторговых поручений (МНП). {{end}}Нужен логин SQL с ролью <code>InstrEu</code> — его выдаёт администратор QUIK: <a href="/admin/help/quik">что ему настроить</a> · <a href="/admin/docs/">документы ARQA</a>.</p>
<form method="post" action="/admin/setup/quik" style="margin-top:12px;display:grid;gap:10px;max-width:560px">
<label>Сервер (host или host\instance)
<input type="text" name="server" value="{{.Settings.Quik.Server}}" placeholder="адрес SQL Server QUIK" style="width:100%" required></label>
<label>Порт
<input type="number" name="port" value="{{if .Settings.Quik.Port}}{{.Settings.Quik.Port}}{{else}}1433{{end}}" placeholder="8849" style="width:100%"></label>
<label>Имя БД (если требуется, иначе оставьте пустым)
<input type="text" name="database" value="{{.Settings.Quik.Database}}" placeholder="(по умолчанию)" style="width:100%"></label>
<label>Логин (роль InstrEu)
<input type="text" name="user" value="{{.Settings.Quik.User}}" placeholder="логин SQL" style="width:100%" required></label>
<label>Пароль
<input type="password" name="password" placeholder="{{if .Settings.Quik.Password}}•••••• — оставьте пустым, чтобы не менять{{else}}пароль SQL-логина{{end}}" autocomplete="off" style="width:100%"></label>
<label>Шифрование канала
<select name="encrypt" style="width:100%">
<option value="true" {{if eq .Settings.Quik.Encrypt "true"}}selected{{end}}>true — с TLS (боевой сервер обычно требует)</option>
<option value="disable" {{if eq .Settings.Quik.Encrypt "disable"}}selected{{end}}>disable — без TLS</option>
</select></label>
<label style="display:flex;gap:8px;align-items:center;font-weight:400">
<input type="checkbox" name="trust" value="1" {{if .Settings.Quik.Trust}}checked{{end}}>
Доверять сертификату сервера (TrustServerCertificate — для самоподписанного)</label>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<button type="submit" class="btn btn-ok">Сохранить</button>
<input type="hidden" name="check" value="1">
<div style="display:grid;grid-template-columns:3fr 1fr;gap:10px">
<label>Сервер QUIK МНП
<input type="text" name="server" value="{{.Settings.Quik.Server}}" placeholder="адрес или имя\экземпляр" style="width:100%" required></label>
<label>Порт
<input type="number" name="port" value="{{if .Settings.Quik.Port}}{{.Settings.Quik.Port}}{{else}}1433{{end}}" style="width:100%"></label>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<label>Логин
<input type="text" name="user" value="{{.Settings.Quik.User}}" style="width:100%" required autocomplete="off"></label>
<label>Пароль
<input type="password" name="password" placeholder="{{if .Settings.Quik.Password}}сохранён — пусто, чтобы не менять{{end}}" autocomplete="new-password" style="width:100%"></label>
</div>
<details><summary style="cursor:pointer;color:var(--accent);font-size:13px">Дополнительно: база и шифрование (обычно не нужно)</summary>
<div style="display:grid;gap:10px;margin-top:8px">
<label>База МНП <span class="muted" style="font-weight:400">— пусто: база логина по умолчанию</span>
<input type="text" name="database" value="{{.Settings.Quik.Database}}" style="width:100%"></label>
<label>Шифрование
<select name="encrypt" style="width:100%">
<option value="true" {{if ne .Settings.Quik.Encrypt "disable"}}selected{{end}}>Шифровать соединение (обычно требуется)</option>
<option value="disable" {{if eq .Settings.Quik.Encrypt "disable"}}selected{{end}}>Без шифрования</option>
</select></label>
<label style="display:flex;gap:8px;align-items:center;font-weight:400">
<input type="checkbox" name="trust" value="1" {{if .Settings.Quik.Trust}}checked{{end}}>
Доверять сертификату сервера, даже самоподписанному</label>
</div>
</details>
<div><button type="submit" class="btn">Сохранить и проверить</button> <span class="muted" style="font-size:12.5px;margin-left:6px">проверка только читает — в QUIK ничего не меняется</span></div>
</form>
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:12px">
<form method="post" action="/admin/setup/quik/test" style="margin:0"><button type="submit" class="btn">Проверить подключение (чтение)</button></form>
</div>
<p class="muted" style="margin-top:12px">Статус: {{if .Settings.Quik.Verified}}<span style="color:var(--ok)">✓ подключение проверено</span>{{else}}<span class="muted">не проверено</span>{{end}}</p>
</div>
<div class="card" id="sec-quik-cycle">
<h2><span class="dot {{if .Settings.Quik.Emulate}}warn{{else if not .Settings.Quik.WritesEnabled}}warn{{else if or .Settings.Quik.AutoOut .Settings.Quik.AutoIn}}ok{{else}}warn{{end}}"></span>Автоматический цикл M2M в МНП{{if .Settings.Quik.Emulate}} <span class="hero-status warn">{{if devOn}}имитация{{else}}остановлен{{end}}</span>{{end}}</h2>
{{if .InWizard}}<details><summary style="cursor:pointer;list-style:none"><h2 style="display:inline">{{else}}<h2>{{end}}<span class="dot {{if .Settings.Quik.Emulate}}warn{{else if not .Settings.Quik.WritesEnabled}}warn{{else if or .Settings.Quik.AutoOut .Settings.Quik.AutoIn}}ok{{else}}warn{{end}}"></span>Автоматический цикл M2M в МНП{{if .Settings.Quik.Emulate}} <span class="hero-status warn">{{if devOn}}имитация{{else}}остановлен{{end}}</span>{{end}}</h2>{{if .InWizard}} <span class="muted" style="font-size:13px">— {{if .Settings.Quik.WritesEnabled}}включён{{else}}выключен: bj ничего не пишет в QUIK{{end}} · настроить ▸</span></summary>{{end}}
<p class="muted">Что делает bj в Модуле неторговых поручений по ходу перевода: <strong>исходящий</strong> — при отправке в НРД регистрирует «Вывод ЦБ (M2M)» (QUIK блокирует бумаги), по подтверждению подписывает (списание), по отказу снимает с причиной; <strong>входящий</strong> — после приёма нашего поручения НРД регистрирует «Ввод ЦБ (M2M)» (зачисление сразу). Каждый шаг виден в карточке перевода и на странице <a href="/admin/quik">QUIK МНП</a>.</p>
<form method="post" action="/admin/setup/quik/cycle" style="margin-top:12px;display:grid;gap:10px;max-width:640px">
<input type="hidden" name="section" value="sec-quik-cycle">
@@ -55,7 +58,7 @@
<input type="checkbox" name="poll_terminal" value="1" {{if .Settings.Quik.PollTerminal}}checked{{end}}>
Принимаем поручения M2M из QUIK: клиентам разрешена подача «Вывод ЦБ (M2M)» из терминала / мобильного приложения — bj опрашивает их и создаёт заявку оператору. Если фирма такую подачу не разрешает, оставьте выключенным: раздел «Поручения QUIK» их показывать не будет.</label>
<label>Торговый счёт для регистрации поручений
<input type="text" name="account" value="{{.Settings.Quik.Account}}" placeholder="L01+00000F00" style="width:100%"></label>
<input type="text" name="account" value="{{.Settings.Quik.Account}}" placeholder="торговый счёт фирмы в QUIK" style="width:100%"></label>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<label>МНП-инструмент вывода <span class="muted">(пусто = взять из класса INSTROUTM2M)</span>
<input type="text" name="sec_out" value="{{.Settings.Quik.SecOut}}" placeholder="RUROUTM2M" style="width:100%"></label>
@@ -63,9 +66,10 @@
<input type="text" name="sec_in" value="{{.Settings.Quik.SecIn}}" placeholder="RURINM2M" style="width:100%"></label>
</div>
<label>Исключения кодов клиентов <span class="muted">(по умолчанию «БР-001» → «BR001»; строка «номер портфеля=код QUIK»)</span>
<textarea name="client_code_map" rows="3" style="width:100%;font-family:ui-monospace,monospace;font-size:12px" placeholder="БР-4-4=BR44">{{quikCodeMap .Settings.Quik.ClientCodeMap}}</textarea></label>
<div style="display:flex;gap:8px;flex-wrap:wrap"><button type="submit" class="btn btn-ok">Сохранить</button></div>
<textarea name="client_code_map" rows="3" style="width:100%;font-family:ui-monospace,monospace;font-size:12px" placeholder="БР-12-3=BR123">{{quikCodeMap .Settings.Quik.ClientCodeMap}}</textarea></label>
<div style="display:flex;gap:8px;flex-wrap:wrap"><button type="submit" class="btn">Сохранить</button></div>
</form>
{{if .InWizard}}</details>{{end}}
</div>
{{if .Settings.Quik.LastReport}}