Мастер: шапка на месте; домен — только логин и пароль; модуль «1С: база мидл-офиса» (тест)

- Шапка мастера: правый блок (шлюз НРД, пользователь, тема) снова справа — после правила [hidden] скрытое меню перестало его распирать.
- Домен в шаге «Учётные записи»: bj сам находит домен и контроллеры (логин с @домен, домены поиска DNS, rootDSE DNS-серверов, SRV _ldap._tcp.dc._msdcs); адрес и домен — в расширенных настройках, открываются сами, если не нашлось. Контроллеры по очереди, LDAPS → LDAP+StartTLS.
- Неудачная попытка ничего не теряет: введённое (и пароль) — черновиком, форма заполнена; причины словами (неизвестный ЦС, имя в сертификате, IP вместо имени, порт закрыт, нет в DNS).
- Сертификат неизвестного центра: карточка «кем выдан, отпечаток (SHA-1 как в Windows и SHA-256)» и «Доверять и подключить»; после входа bj берёт из домена сертификат центра (AIA / Certification Authorities), подписавший сертификат контроллера, — подходят все контроллеры. То же в Настройка → Пользователи при «Проверить».
- Лицензия: модуль onec-middle «1С: база мидл-офиса (тест)»; раздел настроек — адрес публикации, пользователь, пароль, проверка через OData $metadata; в мастере — в «Модулях».
- ЛК (callback, API, готовность) — только при модуле «ЕСИА Финанс»; карточка режима QUIK и стили настроек как в мастере.

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-07 01:03:16 +03:00
parent 005053294e
commit 773fa8e0bf
21 changed files with 1064 additions and 118 deletions
@@ -11,7 +11,7 @@
<p class="muted">PostgreSQL: подключение, схемы <code>fansy</code> / <code>fansy_staging</code> / <code>m2m_core</code>, что подгружается извне (ETL Fansy) и что пишет сама система. Учётные записи, гранты, миграции.</p>
</div>
</a>{{end}}
{{if or (mod "web-cabinet") devOn}}<a href="/admin/help/lk-api" style="text-decoration:none">
{{if or (mod "esia-finance") devOn}}<a href="/admin/help/lk-api" style="text-decoration:none">
<div class="card" style="height:100%">
<h2 style="color:var(--accent)">API личного кабинета →</h2>
<p class="muted">REST-контракт ESIA Finance V1: <code>POST /api/v1/back_office/claims/</code>, GET/PATCH-операции, формат callback'ов, аутентификация Basic, примеры запросов curl.</p>
@@ -15,6 +15,7 @@
<button data-sec="nsd"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21h18M4 21V10M20 21V10M3 10l9-6 9 6M8 21v-7M12 21v-7M16 21v-7"/></svg></span>НРД<span class="ind {{if .Settings.NSD.IGWBaseURL}}ok{{else}}warn{{end}}"></span></button>
{{if mod "web-cabinet"}}<button data-sec="cabinet"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="9" r="3.2"/><path d="M6 19c.6-3 3-4.6 6-4.6s5.4 1.6 6 4.6"/><rect x="3" y="4" width="18" height="16" rx="2"/></svg></span>Кабинет клиента<span class="ind {{if .Settings.LK.APIKey}}ok{{else}}warn{{end}}"></span></button>{{end}}
{{if mod "esia-finance"}}<button data-sec="lk"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"/><circle cx="9" cy="11" r="2"/><path d="M6 16c.4-1.4 1.6-2.2 3-2.2s2.6.8 3 2.2"/><path d="M15 10h4M15 13h3"/></svg></span>ЕСИА Финанс<span class="ind {{if .Settings.LK.PublicURL}}ok{{else}}warn{{end}}"></span></button>{{end}}
{{if mod "onec-middle"}}<button data-sec="onec"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="10" cy="6" rx="6" ry="2.5"/><path d="M4 6v10c0 1.4 2.7 2.5 6 2.5"/><path d="M16 6v4"/><path d="M4 11c0 1.4 2.7 2.5 6 2.5"/><path d="M14 16h7M18 13l3 3-3 3"/></svg></span>1С: мидл-офис<span class="ind {{if .Settings.OneC.CheckOK}}ok{{else}}warn{{end}}"></span></button>{{end}}
<button data-sec="tests"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M9 3h6M10 3v6l-5 8.5A2 2 0 0 0 6.7 21h10.6a2 2 0 0 0 1.7-3.5L14 9V3"/><path d="M8 15h8"/></svg></span>Диагностика</button>
<button data-sec="alerts"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.7 21a2 2 0 0 1-3.4 0"/></svg></span>Оповещения<span class="ind {{if or .Settings.Alerts.SMTP.Enabled .Settings.Alerts.Zabbix.Enabled}}ok{{end}}"></span></button>
<button data-sec="update"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20V8M6 12l6-6 6 6"/><path d="M5 4h14"/></svg></span>Обновления{{if .UpdatesAvailable}}<span class="ind warn"></span>{{end}}</button>
@@ -36,7 +37,7 @@
<div class="grid" style="margin-top:12px">
{{range .Readiness}}
<div class="stat">
<div><span class="dot {{if .Configured}}ok{{else}}err{{end}}"></span><strong>{{.Name}}</strong></div>
<div><span class="dot {{if .Configured}}ok{{else}}err{{end}}"></span><strong>{{if .Title}}{{.Title}}{{else}}{{.Name}}{{end}}</strong></div>
<div class="muted" style="font-size:12px;margin-top:4px">{{if .Configured}}настроено{{else}}не настроено{{end}}</div>
</div>
{{end}}
@@ -182,6 +183,7 @@
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap"><button class="btn btn-ok" type="submit">Сохранить</button></div>
</form>
{{with adDraft}}{{template "adTrustCard" (withRet . "")}}{{end}}
<form method="post" action="/admin/setup/auth/test" style="display:grid;grid-template-columns:1fr 1fr auto;gap:10px;max-width:820px;margin-top:12px;align-items:end">
<label>Проверка: логин домена <input type="text" name="test_login" placeholder="ivanov" style="width:100%" autocomplete="off"></label>
<label>Пароль <input type="password" name="test_password" style="width:100%" autocomplete="off"></label>
@@ -783,6 +785,32 @@
</section>
{{end}}
{{/* ============ 1С: БАЗА МИДЛ-ОФИСА (модуль onec-middle, тестовый) ============ */}}
{{if mod "onec-middle"}}
<section class="settings-section" id="sec-onec">
<h1>1С: база мидл-офиса</h1>
<p class="muted" style="margin:-8px 0 16px">Связь bj с базой 1С мидл-офиса. Модуль в разработке: сейчас здесь задаётся и проверяется подключение к базе, обмен данными появится в следующих выпусках.</p>
<div class="card">
<h2><span class="dot {{if .Settings.OneC.CheckOK}}ok{{else}}warn{{end}}"></span>Подключение к базе 1С</h2>
<p class="muted">bj обращается к базе через стандартный интерфейс OData. Его включают при публикации базы: конфигуратор 1С → Администрирование → Публикация на веб-сервере → «Публиковать стандартный интерфейс OData».</p>
<form method="post" action="/admin/setup/onec" style="margin-top:12px;display:grid;gap:12px;max-width:640px">
<div class="fld">
<label>Адрес публикации базы</label>
<input type="text" name="onec_url" value="{{.Settings.OneC.URL}}" placeholder="http://1c-server/middle" data-need="адрес публикации базы" style="margin-top:4px">
<span class="muted" style="font-size:12px">Адрес, по которому база открывается в браузере, без /odata — например http://1c-server/middle.</span>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px">
<div class="fld"><label>Пользователь 1С</label><input type="text" name="onec_user" value="{{.Settings.OneC.User}}" data-need="пользователь 1С" autocomplete="off" style="margin-top:4px"></div>
<div class="fld"><label>Пароль</label><input type="password" name="onec_password" {{if .Settings.OneC.Password}}placeholder="сохранён — не меняйте, если тот же"{{else}}data-need="пароль"{{end}} autocomplete="new-password" style="margin-top:4px"></div>
</div>
<span class="muted" style="font-size:12px">Лучше завести в 1С отдельного пользователя для bj — с правом только на нужные данные.</span>
<div><button type="submit" class="btn">Сохранить и проверить</button></div>
</form>
{{with .Settings.OneC}}{{if not .CheckedAt.IsZero}}<p style="margin:14px 0 0;font-size:13.5px"><b style="color:var(--{{if .CheckOK}}ok{{else}}err{{end}})">{{if .CheckOK}}✓ Подключение работает{{else}}✗ Подключение не работает{{end}}</b> — {{.CheckMsg}} <span class="muted">(проверено {{.CheckedAt.Format "02.01.2006 15:04"}})</span></p>{{end}}{{end}}
</div>
</section>
{{end}}
{{/* ============ ДИАГНОСТИКА (id sec-tests сохранён для ссылок) ============ */}}
<section class="settings-section" id="sec-tests">
<h1>Диагностика</h1>
@@ -52,40 +52,8 @@
.wiz-row-l { color:var(--muted); font-weight:400; font-size:14.5px; min-width:170px; margin:0; }
.wiz-opt { display:flex; gap:8px; align-items:flex-start; font-weight:400; margin:0 0 6px; }
.wiz-opt .muted { font-size:13px; }
/* Учёт DEPO: вопросы по порядку (номер — счётчиком), выбор — плитками, под ними — панель выбранного способа */
.dq { counter-reset:dq; }
.dq-s { padding:18px 0 20px; border-top:1px solid var(--border); }
.dq-s:first-child { border-top:0; padding-top:2px; }
.dq-h { display:flex; align-items:center; gap:10px; font-size:15.5px; font-weight:650; margin:0 0 12px; }
.dq-h::before { counter-increment:dq; content:counter(dq); flex:none; width:22px; height:22px; border-radius:50%; background:var(--accent-weak); color:var(--accent); display:grid; place-items:center; font-size:12px; font-weight:700; }
.dq-tiles { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:10px; }
.dq-tile { display:flex; gap:10px; align-items:flex-start; padding:12px 14px; border:1px solid var(--border); border-radius:12px; cursor:pointer; font-weight:400; margin:0; transition:border-color .15s var(--ease), background .15s var(--ease); }
.dq-tile:hover { border-color:var(--border-strong); }
.dq-tile:has(input:checked) { border-color:var(--accent); background:var(--accent-weak); }
.dq-tile b { display:block; font-weight:600; font-size:14.5px; }
.dq-tile small { display:block; color:var(--muted); font-size:13px; line-height:1.4; margin-top:3px; }
.dq-alt { display:inline-flex; gap:8px; align-items:flex-start; margin:12px 0 0; font-weight:400; font-size:13.5px; color:var(--text-2); cursor:pointer; }
.dq-grid { display:grid; gap:10px 12px; }
.dq-grid label { display:block; margin:0; font-size:13.5px; font-weight:500; color:var(--text-2); }
.dq-grid input, .dq-grid select { display:block; width:100%; box-sizing:border-box; margin-top:5px; }
.dq-srv { grid-template-columns:minmax(0,1.5fr) minmax(0,.55fr) minmax(0,1.25fr); }
.dq-2 { grid-template-columns:repeat(2,minmax(0,1fr)); }
.dq-opt { color:var(--muted); font-weight:400; }
.dq-note { color:var(--muted); font-size:13px; margin:8px 0 0; }
.dq-panel { margin-top:18px; }
.dq-panel + .dq-panel { margin-top:14px; }
.wiz-card .dq-go { margin-top:16px; border-radius:999px; padding:10px 22px; font-size:14.5px; font-weight:600; }
.wiz-card .dq-mono { font-family:var(--mono); }
.dq-sql { margin-top:14px; }
.dq-sql-head { display:flex; align-items:center; gap:8px; margin:0 0 6px; justify-content:flex-start; }
.dq-sql-st { flex:1; font-size:12.5px; color:var(--muted); }
.dq-sql pre:empty::before { content:attr(data-msg); color:var(--muted); font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif; font-size:13px; }
.dq-sql pre { max-height:380px; min-height:140px; font-size:12px; margin:0; transition:opacity .2s; }
.dq-sql pre.dq-busy { opacity:.5; }
.dq.is-pg [data-ms], .dq:not(.is-pg) [data-pg] { display:none !important; }
.dq-jobname { margin-top:14px; max-width:520px; }
.dq-jobname label { display:block; margin:0; font-size:13.5px; font-weight:500; color:var(--text-2); }
.dq-jobname input { display:block; width:100%; box-sizing:border-box; margin-top:5px; }
/* установка ключа (общий блок part_keywiz) — в виде мастера */
.keywiz ol { gap:4px !important; }
.keywiz ol li > div > div:first-child { font-weight:500 !important; font-size:14.5px; }
@@ -93,37 +61,8 @@
.keywiz .card h2 { font-size:16px; margin:0 0 6px; }
.keywiz form .btn-ok, .keywiz form .btn-secondary { justify-self:start; border-radius:999px; padding:10px 22px; font-weight:600; }
.keywiz form .btn-ok { background:var(--accent); border-color:var(--accent); }
.dq-box { border:1px solid var(--border); border-radius:12px; padding:14px 16px 16px; }
.dq-box-bj { background:color-mix(in srgb, var(--accent-weak) 45%, transparent); }
.dq-box-h { font-weight:600; font-size:14.5px; margin:0 0 10px; }
.dq-box-h span { color:var(--muted); font-weight:400; font-size:13px; }
.dq-lbl-row { display:flex; justify-content:space-between; align-items:baseline; gap:8px; }
.wiz-card .dq-link { background:none; border:0; padding:0; color:var(--accent); font-size:12.5px; font-weight:500; cursor:pointer; border-radius:4px; }
.dq-link:hover { text-decoration:underline; }
.dq-warn { margin:10px 0 0; padding:8px 12px; border-radius:8px; background:var(--warn-weak); color:var(--text-2); font-size:13px; }
/* ход подготовки: строки шагов со значком состояния */
.dq-run { margin-top:16px; border:1px solid var(--border); border-radius:12px; padding:12px 16px; }
.dq-steps { list-style:none; margin:0; padding:0; display:grid; gap:2px; }
.dq-st { display:flex; gap:10px; align-items:flex-start; padding:6px 0; font-size:14px; }
.dq-st-t { min-width:0; }
.dq-st-d { color:var(--muted); font-size:12.5px; margin-top:2px; overflow-wrap:anywhere; }
.dq-st-ic { flex:none; position:relative; width:18px; height:18px; border-radius:50%; margin-top:1px; box-sizing:border-box; }
.dq-st.is-run .dq-st-ic { border:2px solid var(--accent-weak); border-top-color:var(--accent); animation:dq-spin .8s linear infinite; }
.dq-st.is-run { color:var(--text); font-weight:500; }
.dq-st.is-ok .dq-st-ic { background:var(--ok); }
.dq-st.is-ok .dq-st-ic::after { content:""; position:absolute; left:50%; top:45%; width:4px; height:8px; border:solid #fff; border-width:0 1.8px 1.8px 0; transform:translate(-50%,-50%) rotate(45deg); }
.dq-st.is-ok:not(.is-final) { color:var(--text-2); }
.dq-st.is-final { font-weight:600; color:var(--ok); }
.dq-st.is-warn .dq-st-ic, .dq-st.is-err .dq-st-ic { color:#fff; font-size:12px; font-weight:700; line-height:18px; text-align:center; }
.dq-st.is-warn .dq-st-ic { background:var(--warn); }
.dq-st.is-warn .dq-st-ic::after { content:"!"; }
.dq-st.is-err .dq-st-ic { background:var(--err); }
.dq-st.is-err .dq-st-ic::after { content:"×"; }
.dq-st.is-err { color:var(--err); }
.dq-run-msg { margin-top:8px; padding:10px 12px; border-radius:8px; background:var(--err-weak, rgba(200,40,40,.08)); font-size:13.5px; overflow-wrap:anywhere; white-space:pre-wrap; }
@keyframes dq-spin { to { transform:rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .dq-st.is-run .dq-st-ic { animation:none; border-color:var(--accent); } }
@media (max-width:600px) { .dq-tiles, .dq-srv, .dq-2 { grid-template-columns:minmax(0,1fr); } .dq-sql-head { flex-wrap:wrap; } .dq-sql-st { flex-basis:100%; } }
.tooltip { display:inline-block; background:var(--border); color:var(--muted); border-radius:50%; width:16px; height:16px; line-height:16px; text-align:center; font-size:11px; cursor:help; margin-left:4px; }
.help-block { background:var(--accent-weak); padding:10px 14px; margin:10px 0; font-size:13px; border-radius:10px; }
.help-block strong { color:var(--accent); }
@@ -748,8 +687,8 @@
<p class="muted" style="font-size:12.5px;margin:8px 0 0">Пароль передайте сотруднику лично; сменить его можно в разделе Настройка → Пользователи.</p>
<div style="margin-top:12px"><button type="submit" class="btn btn-ok">Создать учётную запись</button></div>
</form>
{{$A := .Settings.Auth}}
<details class="acc-ad" style="margin-top:20px;padding-top:14px;border-top:1px solid var(--border)">
{{$A := .Settings.Auth}}{{$D := adDraft}}
<details class="acc-ad" {{if $D}}open{{end}} style="margin-top:20px;padding-top:14px;border-top:1px solid var(--border)">
<summary style="cursor:pointer;color:var(--accent);font-size:14px">Добавить сотрудника из домена (Active Directory) — необязательно</summary>
{{if and $A.ADEnabled $A.ADBindUser}}
<p class="muted" style="font-size:13.5px;margin:10px 0 0">Домен <b>{{$A.ADDomain}}</b> подключён. Найдите сотрудника — он будет входить в bj своим доменным логином и паролем; пароль проверяет домен.</p>
@@ -777,16 +716,40 @@
},350); });
})();</script>
{{end}}
<details {{if not (and $A.ADEnabled $A.ADBindUser)}}open{{end}} style="margin-top:12px"><summary style="cursor:pointer;font-size:13px;color:var(--accent)">{{if and $A.ADEnabled $A.ADBindUser}}Другой домен или учётная запись{{else}}Подключить домен{{end}}</summary>
<p class="muted" style="font-size:13px;margin:8px 0 0">Нужны адрес контроллера домена и учётная запись для поиска сотрудников — подойдёт обычная, без прав администратора домена. bj сразу проверит связь.</p>
<form method="post" action="/admin/setup/users/ad-connect" class="acc-form">
<details id="ad-connect" {{if or $D (not (and $A.ADEnabled $A.ADBindUser))}}open{{end}} style="margin-top:12px"><summary style="cursor:pointer;font-size:13px;color:var(--accent)">{{if and $A.ADEnabled $A.ADBindUser}}Другой домен или учётная запись{{else}}Подключить домен{{end}}</summary>
{{$bu := $A.ADBindUser}}{{$pw := $A.ADBindPassword}}{{$fs := ""}}{{$fd := ""}}{{$adv := false}}
{{if $D}}{{$bu = $D.BindUser}}{{$pw = ""}}{{if $D.HasPassword}}{{$pw = "введён"}}{{end}}{{$fs = $D.Form.Server}}{{$fd = $D.Form.Domain}}{{$adv = $D.Form.Adv}}{{end}}
<p class="muted" style="font-size:13px;margin:8px 0 0">Введите учётную запись домена и пароль — домен и его контроллеры bj найдёт сам. Прав администратора домена не нужно; лучше отдельная служебная запись (например svc-bj): bj хранит её пароль, чтобы искать сотрудников.</p>
<div id="ad-found" class="muted" style="font-size:13px;margin-top:8px" aria-live="polite"></div>
<form method="post" action="/admin/setup/users/ad-connect" class="acc-form" id="ad-form">
<input type="hidden" name="return_to" value="/admin/wizard?step=13">
<label>Контроллер домена<input type="text" name="ad_server" data-need="контроллер домена" value="{{$A.ADServer}}" placeholder="dc1.corp.local или ldaps://dc1.corp.local:636" autocomplete="off"></label>
<label>Домен<input type="text" name="ad_domain" data-need="домен" value="{{$A.ADDomain}}" placeholder="corp.local" autocomplete="off"></label>
<label>Учётная запись для поиска<input type="text" name="ad_bind_user" data-need="учётная запись для поиска" value="{{$A.ADBindUser}}" placeholder="svc-bj" autocomplete="off"></label>
<label>Пароль{{if $A.ADBindPassword}} <span class="muted">(задан; пусто — оставить)</span>{{end}}<input type="password" name="ad_bind_password" {{if not $A.ADBindPassword}}data-need="пароль учётной записи"{{end}} autocomplete="new-password"></label>
<div><button type="submit" class="btn">Подключить и проверить связь</button></div>
<label>Учётная запись домена<input type="text" name="ad_bind_user" data-need="учётная запись домена" value="{{$bu}}" placeholder="svc-bj или svc-bj@corp.example.ru" autocomplete="off"></label>
<label>Пароль{{if $pw}} <span class="muted">({{$pw}}; пусто — оставить)</span>{{end}}<input type="password" name="ad_bind_password" {{if not $pw}}data-need="пароль"{{end}} autocomplete="new-password"></label>
<details id="ad-adv" {{if $adv}}open{{end}} style="grid-column:1/-1;margin-top:2px"><summary style="cursor:pointer;font-size:13px;color:var(--muted)">Расширенные настройки — если домен не найдётся сам</summary>
<div class="acc-form" style="margin-top:8px">
<label>Домен<input type="text" name="ad_domain" value="{{$fd}}" placeholder="найдётся сам, например corp.example.ru" autocomplete="off"></label>
<label>Контроллер домена<input type="text" name="ad_server" value="{{$fs}}" placeholder="найдётся сам, например dc1.corp.example.ru" autocomplete="off"></label>
</div>
<p class="muted" style="font-size:12.5px;margin:6px 0 0">Пусто — bj ищет сам. Контроллер можно указать именем (bj попробует LDAPS 636, затем LDAP 389 со StartTLS) или адресом со схемой: ldaps://dc1.corp.example.ru:636.</p>
</details>
<div><button type="submit" class="btn">Подключить</button></div>
</form>
{{if $D}}<p style="margin:12px 0 0;padding:10px 12px;border-radius:10px;background:var(--card-2);font-size:13.5px;line-height:1.45"><b style="color:var(--err)">Не подключено:</b> {{$D.Err}}{{if not $D.Cert}} Поправьте и нажмите «Подключить» ещё раз — введённое сохранено.{{end}}</p>
{{template "adTrustCard" (withRet $D "/admin/wizard?step=13")}}{{end}}
<script>(function(){
var box=document.getElementById('ad-connect'), outer=box.closest('details.acc-ad'), out=document.getElementById('ad-found'), adv=document.getElementById('ad-adv'), f=document.getElementById('ad-form'), done=false;
function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,function(c){return{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]})}
function look(){ if(done||!box.open||(outer&&!outer.open)) return; done=true; out.textContent='Ищу домен…';
fetch('/admin/setup/users/ad-discover',{cache:'no-store',headers:{'Accept':'application/json'}}).then(function(r){return r.json()}).then(function(d){
var x=d.found;
if(d.ok){ out.innerHTML='<span style="color:var(--ok)">✓</span> Найден домен <b>'+esc(x.domain)+'</b>, контроллеры: '+x.dcs.map(esc).join(', ')+' <span class="muted">('+esc(x.from)+')</span>';
f.ad_domain.placeholder=x.domain+' — найден сам'; f.ad_server.placeholder=x.dcs[0]+' — найден сам'; return; }
out.textContent=x&&x.domain?'Домен '+x.domain+' найден, но его контроллеры не нашлись в DNS — укажите контроллер в расширенных настройках.':'Домен сам не нашёлся — укажите его в расширенных настройках или впишите учётную запись с доменом: svc-bj@corp.example.ru.';
adv.open=true;
}).catch(function(){ out.textContent=''; done=false; });
}
box.addEventListener('toggle',look); if(outer) outer.addEventListener('toggle',look); look();
})();</script>
</details>
</details>
<script>(function(){
+73 -2
View File
@@ -89,7 +89,7 @@ body {
.nav .ddmenu { display:none; position:absolute; top:100%; left:0; margin-top:8px; background:var(--card); border:1px solid var(--border);
border-radius:10px; box-shadow:0 8px 30px rgba(0,0,0,.14); min-width:210px; z-index:60; padding:6px; }
.nav .ddmenu a { display:block; padding:8px 12px; border-radius:7px; }
.topbar-right { display:flex; align-items:center; gap:12px; flex:none; padding-left:10px; }
.topbar-right { display:flex; align-items:center; gap:12px; flex:none; padding-left:10px; margin-left:auto; }
@media (max-width:1100px){ .sys-status span:not(.pulse){ display:none } .sys-status{ padding:5px 8px } .topbar .clock{ display:none } .user-dd .user-role{ display:none } }
/* Строка системного статуса — состояние читается всегда */
.sys-status { display:inline-flex; align-items:center; gap:7px; font-size:12px; font-weight:600; color:var(--text-2); padding:5px 11px 5px 9px; border:1px solid var(--border); border-radius:20px; background:var(--card-2); white-space:nowrap; }
@@ -276,7 +276,15 @@ label { font-size:13px; font-weight:500; }
.settings-nav button .ind.err { background:var(--err); }
.settings-section { display:none; }
.settings-section.active { display:block; }
.settings-section > h1 { margin-bottom:18px; }
.settings-section > h1 { margin-bottom:18px; font-size:24px; font-weight:700; letter-spacing:-0.015em; }
/* «Настройка» в стиле мастера (07.10.2026): скруглённые карточки, спокойные поля, крупные заголовки */
.settings-section .card { padding:24px 28px 20px; border-radius:16px; }
.settings-section .card > h2:first-child { font-size:17px; font-weight:660; letter-spacing:-0.01em; margin:0 0 6px; }
.settings-section .card > h2:first-child + p.muted { font-size:14px; margin:0 0 14px; }
.settings-section input:not([type=radio]):not([type=checkbox]):not([type=hidden]):not([type=file]):not([type=submit]), .settings-section select, .settings-section 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); box-sizing:border-box; }
.settings-section input:focus, .settings-section select:focus, .settings-section textarea:focus { background:var(--card) !important; border-color:var(--accent) !important; outline:3px solid var(--accent-weak); }
.settings-section .btn, .settings-section button { border-radius:10px; }
.settings-section input[type=radio], .settings-section input[type=checkbox] { width:auto; }
@media (max-width:820px) {
.settings { grid-template-columns:1fr; }
.settings-nav { flex-direction:row; overflow-x:auto; position:static; padding-bottom:8px; }
@@ -327,6 +335,69 @@ details:has(> pre.copyable) { min-width:0; max-width:100%; }
.banner-dev.on{background:color-mix(in srgb,var(--warn) 12%,var(--card))}
.banner-dev.off{background:color-mix(in srgb,var(--ok) 10%,var(--card))}
.banner-dev button:hover{filter:brightness(.97)}
/* Компоненты мастера настройки (07.10.2026 — общие: те же плитки, панели и ход по этапам в «Настройке») */
/* Учёт DEPO: вопросы по порядку (номер — счётчиком), выбор — плитками, под ними — панель выбранного способа */
.dq { counter-reset:dq; }
.dq-s { padding:18px 0 20px; border-top:1px solid var(--border); }
.dq-s:first-child { border-top:0; padding-top:2px; }
.dq-h { display:flex; align-items:center; gap:10px; font-size:15.5px; font-weight:650; margin:0 0 12px; }
.dq-h::before { counter-increment:dq; content:counter(dq); flex:none; width:22px; height:22px; border-radius:50%; background:var(--accent-weak); color:var(--accent); display:grid; place-items:center; font-size:12px; font-weight:700; }
.dq-tiles { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:10px; }
.dq-tile { display:flex; gap:10px; align-items:flex-start; padding:12px 14px; border:1px solid var(--border); border-radius:12px; cursor:pointer; font-weight:400; margin:0; transition:border-color .15s var(--ease), background .15s var(--ease); }
.dq-tile:hover { border-color:var(--border-strong); }
.dq-tile:has(input:checked) { border-color:var(--accent); background:var(--accent-weak); }
.dq-tile b { display:block; font-weight:600; font-size:14.5px; }
.dq-tile small { display:block; color:var(--muted); font-size:13px; line-height:1.4; margin-top:3px; }
.dq-alt { display:inline-flex; gap:8px; align-items:flex-start; margin:12px 0 0; font-weight:400; font-size:13.5px; color:var(--text-2); cursor:pointer; }
.dq-grid { display:grid; gap:10px 12px; }
.dq-grid label { display:block; margin:0; font-size:13.5px; font-weight:500; color:var(--text-2); }
.dq-grid input, .dq-grid select { display:block; width:100%; box-sizing:border-box; margin-top:5px; }
.dq-srv { grid-template-columns:minmax(0,1.5fr) minmax(0,.55fr) minmax(0,1.25fr); }
.dq-2 { grid-template-columns:repeat(2,minmax(0,1fr)); }
.dq-opt { color:var(--muted); font-weight:400; }
.dq-note { color:var(--muted); font-size:13px; margin:8px 0 0; }
.dq-panel { margin-top:18px; }
.dq-panel + .dq-panel { margin-top:14px; }
.dq-sql { margin-top:14px; }
.dq-sql-head { display:flex; align-items:center; gap:8px; margin:0 0 6px; justify-content:flex-start; }
.dq-sql-st { flex:1; font-size:12.5px; color:var(--muted); }
.dq-sql pre:empty::before { content:attr(data-msg); color:var(--muted); font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif; font-size:13px; }
.dq-sql pre { max-height:380px; min-height:140px; font-size:12px; margin:0; transition:opacity .2s; }
.dq-sql pre.dq-busy { opacity:.5; }
.dq.is-pg [data-ms], .dq:not(.is-pg) [data-pg] { display:none !important; }
.dq-jobname { margin-top:14px; max-width:520px; }
.dq-jobname label { display:block; margin:0; font-size:13.5px; font-weight:500; color:var(--text-2); }
.dq-jobname input { display:block; width:100%; box-sizing:border-box; margin-top:5px; }
.dq-box { border:1px solid var(--border); border-radius:12px; padding:14px 16px 16px; }
.dq-box-bj { background:color-mix(in srgb, var(--accent-weak) 45%, transparent); }
.dq-box-h { font-weight:600; font-size:14.5px; margin:0 0 10px; }
.dq-box-h span { color:var(--muted); font-weight:400; font-size:13px; }
.dq-lbl-row { display:flex; justify-content:space-between; align-items:baseline; gap:8px; }
.dq-link:hover { text-decoration:underline; }
.dq-warn { margin:10px 0 0; padding:8px 12px; border-radius:8px; background:var(--warn-weak); color:var(--text-2); font-size:13px; }
.dq-run { margin-top:16px; border:1px solid var(--border); border-radius:12px; padding:12px 16px; }
.dq-steps { list-style:none; margin:0; padding:0; display:grid; gap:2px; }
.dq-st { display:flex; gap:10px; align-items:flex-start; padding:6px 0; font-size:14px; }
.dq-st-t { min-width:0; }
.dq-st-d { color:var(--muted); font-size:12.5px; margin-top:2px; overflow-wrap:anywhere; }
.dq-st-ic { flex:none; position:relative; width:18px; height:18px; border-radius:50%; margin-top:1px; box-sizing:border-box; }
.dq-st.is-run .dq-st-ic { border:2px solid var(--accent-weak); border-top-color:var(--accent); animation:dq-spin .8s linear infinite; }
.dq-st.is-run { color:var(--text); font-weight:500; }
.dq-st.is-ok .dq-st-ic { background:var(--ok); }
.dq-st.is-ok .dq-st-ic::after { content:""; position:absolute; left:50%; top:45%; width:4px; height:8px; border:solid #fff; border-width:0 1.8px 1.8px 0; transform:translate(-50%,-50%) rotate(45deg); }
.dq-st.is-ok:not(.is-final) { color:var(--text-2); }
.dq-st.is-final { font-weight:600; color:var(--ok); }
.dq-st.is-warn .dq-st-ic, .dq-st.is-err .dq-st-ic { color:#fff; font-size:12px; font-weight:700; line-height:18px; text-align:center; }
.dq-st.is-warn .dq-st-ic { background:var(--warn); }
.dq-st.is-warn .dq-st-ic::after { content:"!"; }
.dq-st.is-err .dq-st-ic { background:var(--err); }
.dq-st.is-err .dq-st-ic::after { content:"×"; }
.dq-st.is-err { color:var(--err); }
.dq-run-msg { margin-top:8px; padding:10px 12px; border-radius:8px; background:var(--err-weak, rgba(200,40,40,.08)); font-size:13.5px; overflow-wrap:anywhere; white-space:pre-wrap; }
@keyframes dq-spin { to { transform:rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .dq-st.is-run .dq-st-ic { animation:none; border-color:var(--accent); } }
@media (max-width:600px) { .dq-tiles, .dq-srv, .dq-2 { grid-template-columns:minmax(0,1fr); } .dq-sql-head { flex-wrap:wrap; } .dq-sql-st { flex-basis:100%; } }
</style>
</head>
<body>
@@ -0,0 +1,19 @@
{{/* Сертификат контроллера домена, которого bj не знает: кто выдал, отпечаток и «Доверять». Вход: withRet <adDraftView> <return_to>. */}}
{{define "adTrustCard"}}{{with .D.Cert}}
<div class="ad-trust" style="margin-top:14px;padding:14px 16px;border:1px solid color-mix(in srgb,var(--warn) 55%,var(--border));border-radius:12px;background:var(--warn-weak)">
<div style="font-weight:600;font-size:14.5px">Сертификат контроллера {{$.D.Server}}</div>
<div style="display:grid;grid-template-columns:auto minmax(0,1fr);gap:4px 14px;margin-top:8px;font-size:13.5px">
<span class="muted">{{if .IsCA}}Центр сертификации{{else}}Выдан на{{end}}</span><span>{{.Subject}}</span>
<span class="muted">Кем выдан</span><span>{{.Issuer}}</span>
<span class="muted">Действует до</span><span>{{.Until}}</span>
{{if .SHA1}}<span class="muted">Отпечаток (как в Windows)</span><code style="font-size:12px;overflow-wrap:anywhere">{{.SHA1}}</code>{{end}}
<span class="muted">Отпечаток SHA-256</span><code style="font-size:12px;overflow-wrap:anywhere">{{.SHA256}}</code>
</div>
{{if .IsCA}}<p class="muted" style="font-size:12.5px;margin:10px 0 0">Если это центр сертификации вашего домена — доверьтесь ему: bj запомнит его и будет проверять по нему все контроллеры. Сверить отпечаток можно на контроллере: certlm.msc → Доверенные корневые центры сертификации → сертификат → Состав → Отпечаток.</p>
{{else}}<p class="muted" style="font-size:12.5px;margin:10px 0 0">Контроллер не прислал сертификат центра, поэтому показан его собственный. После «Доверять» bj войдёт в домен, возьмёт оттуда сертификат центра «{{.Issuer}}» (только если именно он подписал этот сертификат) — и дальше подойдут все контроллеры домена и их новые сертификаты. Сверить отпечаток можно на контроллере: certlm.msc → Личное → Сертификаты → сертификат {{.Subject}} → Состав → Отпечаток.</p>{{end}}
<form method="post" action="/admin/setup/users/ad-trust" style="margin-top:10px">
{{if $.Ret}}<input type="hidden" name="return_to" value="{{$.Ret}}">{{end}}<input type="hidden" name="fp" value="{{.SHA256}}">
<button type="submit" class="btn btn-ok">Доверять и подключить</button>
</form>
</div>
{{end}}{{end}}
@@ -141,7 +141,7 @@
<div class="dq-jobname" id="quik-acc">
<label>Торговый счёт для поручений<input type="text" name="account" value="{{.D.Settings.Quik.Account}}" placeholder="торговый счёт фирмы в QUIK" spellcheck="false" class="dq-mono"></label>
</div>
<p class="dq-note" id="quik-mode-st" aria-live="polite">Выбор сохраняется сразу. Тонкая настройка — инструменты МНП, коды клиентов, поручения из терминала — в «Настройка → QUIK».</p>
<p class="dq-note" id="quik-mode-st" aria-live="polite">Выбор сохраняется сразу.{{if .Ret}} Тонкая настройка — инструменты МНП, коды клиентов, поручения из терминала — в «Настройка → QUIK».{{end}}</p>
<noscript><button type="submit" class="btn">Сохранить</button></noscript>
</form>
<p class="dq-note">{{quikRecipientsInfo}} После первого заполнения справочника депозитариев-получателей попросите администратора QUIK перезапустить модуль неторговых поручений — QUIK читает справочник только при запуске.</p>