Files
Bridge-and-Join-s/internal/lkgateway/web/templates/layout.html
T
zuevav 6b490986e4 «Обновления» — одна карточка состояния и одна кнопка; проверка при открытии раздела; «Службы» — в «Диагностике»
- Сверху: «Установлены последние версии» (когда проверено) или «Доступно обновление X — Установить обновление» (всё сразу), или «Проверить не удалось»; кнопка «Проверить сейчас».
- При открытии раздела bj проверяет обновления сам, если последней проверке больше 10 минут (и не чаще раза в 10 минут из браузера — без зацикливания, если источник не настроен); check отвечает JSON для экрана.
- «Компоненты и версии» (обновить один компонент) и «Расширенные настройки» (обновление из файла, источник и канал, ключ, автопроверка) — под раскрытием.
- «Службы» с перезапуском перенесены в «Диагностику»; подсказка в письме о службе — туда же.

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

1001 lines
75 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{define "layout"}}<!DOCTYPE html>
<html lang="ru" data-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{.Title}} · Bridge&Join</title>
<style>
/* hidden прячет всегда: иначе display:grid/flex у класса рисовал пустые рамки (полоска под версией ИШ) */
[hidden]{display:none!important}
/* ===================== Дизайн-система ===================== */
/* «Диспетчерская депозитария»: приборная панель оператора НРД/M2M.
Restrained — прохладные нейтрали + один индиго-акцент; MOEX-красный
зарезервирован строго под знак бренда и критику. Все величины —
моноширинными табличными цифрами (приборная подпись). */
/* Светлая тема (дневной свет, рабочее место) */
:root {
--bg:#eef1f6; --bg-elev:#ffffff; --card:#ffffff; --card-2:#f5f7fb;
--border:#e1e6ef; --border-strong:#cbd3e1;
--text:#111725; --text-2:#3d465a; --muted:#69748a;
--accent:#3452d6; --accent-weak:rgba(52,82,214,0.09); --accent-strong:#2740b0;
--ok:#12813f; --ok-weak:rgba(18,129,63,0.11);
--warn:#9a5b06; --warn-weak:rgba(154,91,6,0.12);
--err:#c22a2a; --err-weak:rgba(194,42,42,0.10);
--brand:#c5203e; /* MOEX-красный — только знак бренда и критика */
--grid-line:rgba(17,23,37,0.045);
--shadow:0 1px 2px rgba(16,24,40,0.05), 0 2px 6px rgba(16,24,40,0.06);
--shadow-lg:0 12px 34px rgba(16,24,40,0.14);
--radius:11px; --radius-sm:7px; --radius-lg:15px;
--mono:ui-monospace,"SF Mono","JetBrains Mono",Menlo,Consolas,monospace;
--ease:cubic-bezier(.2,.7,.2,1);
}
/* Тёмная тема (терминал диспетчерской) */
[data-theme="dark"] {
--bg:#0a0d13; --bg-elev:#0f141d; --card:#131926; --card-2:#19212f;
--border:#232c3d; --border-strong:#354057;
--text:#e8edf5; --text-2:#aab6c9; --muted:#828fa6;
--accent:#7089ff; --accent-weak:rgba(112,137,255,0.15); --accent-strong:#94a7ff;
--ok:#3fce74; --ok-weak:rgba(63,206,116,0.15);
--warn:#e2ab41; --warn-weak:rgba(226,171,65,0.16);
--err:#ff7d7d; --err-weak:rgba(255,125,125,0.15);
--brand:#ff5670;
--grid-line:rgba(255,255,255,0.035);
--shadow:0 1px 2px rgba(0,0,0,0.4); --shadow-lg:0 14px 38px rgba(0,0,0,0.55);
}
/* Приборные цифры: величины (ID, счётчики, время, коды) — моно, табличные. */
.num, .stat-value, .clock, td .id, .mono { font-variant-numeric:tabular-nums; }
* { box-sizing: border-box; }
html, body { margin:0; padding:0; }
body {
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
background: var(--bg); color: var(--text); line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
/* ---- Шапка (панель управления) ---- */
.topbar {
display:flex; align-items:center; gap:18px;
padding:0 22px; height:56px;
background: var(--bg-elev); border-bottom:1px solid var(--border);
position:sticky; top:0; z-index:100;
}
.brand { display:flex; align-items:center; gap:10px; font-weight:680; font-size:15.5px; letter-spacing:-0.015em; color:var(--text); flex:none; }
.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; }
.topbar-sub { color:var(--muted); font-size:14px; margin-left:-4px; }
.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; }
.user-chip .user-role { font-size:10px; color:var(--muted); text-transform:uppercase; letter-spacing:.05em; }
.user-logout { font-size:12px; color:var(--muted); text-decoration:none; padding:4px 8px; border:1px solid var(--border); border-radius:8px; margin-right:6px; }
.user-logout:hover { color:var(--text); border-color:var(--border-strong); }
.nav .group-label { font-size:9.5px; text-transform:uppercase; letter-spacing:0.09em; color:var(--muted); padding:0 9px 0 15px; border-left:1px solid var(--border); margin-left:7px; font-weight:600; }
.nav a {
color:var(--text-2); text-decoration:none; font-size:13px; font-weight:500;
padding:7px 11px; border-radius:8px; white-space:nowrap; transition:background .12s var(--ease), color .12s var(--ease);
}
.nav a:hover { background:var(--card-2); color:var(--text); }
.nav a.active { background:var(--accent-weak); color:var(--accent-strong); font-weight:600; }
.nav .dd { position:relative; }
.nav .dd .ddbtn { display:inline-block; cursor:pointer; color:var(--text-2); font-size:13px; font-weight:500;
padding:7px 11px; border-radius:8px; white-space:nowrap; transition:background .12s var(--ease); }
.nav .dd:hover .ddbtn, .nav .dd.active .ddbtn { background:var(--accent-weak); color:var(--accent-strong); font-weight:600; }
.nav .dd:hover .ddmenu, .nav .dd:focus-within .ddmenu { display:block; }
/* невидимый «мост» под кнопкой — чтобы при спуске мыши к меню hover не срывался */
.nav .dd::after { content:''; position:absolute; top:100%; left:0; right:0; height:10px; }
.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; 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; }
.sys-status .pulse { width:8px; height:8px; border-radius:50%; background:var(--ok); box-shadow:0 0 0 0 var(--ok-weak); animation:pulse 2.4s var(--ease) infinite; }
.sys-status.warn .pulse { background:var(--warn); } .sys-status.err .pulse { background:var(--err); }
@keyframes pulse { 0%{box-shadow:0 0 0 0 rgba(18,129,63,.35)} 70%{box-shadow:0 0 0 6px rgba(18,129,63,0)} 100%{box-shadow:0 0 0 0 rgba(18,129,63,0)} }
@media (prefers-reduced-motion: reduce){ .sys-status .pulse{ animation:none } }
.theme-toggle {
background:var(--card-2); border:1px solid var(--border); color:var(--text-2);
width:34px; height:34px; border-radius:9px; cursor:pointer;
display:flex; align-items:center; justify-content:center; padding:0; transition:background .12s var(--ease), color .12s;
}
.theme-toggle svg { width:16px; height:16px; }
.theme-toggle:hover { background:var(--border); color:var(--text); }
.topbar .clock { font-size:11px; color:var(--muted); font-family:var(--mono); font-variant-numeric:tabular-nums; letter-spacing:-0.02em; }
.nsd-chain { text-decoration:none; }
.nsd-chain i { width:8px; height:8px; border-radius:50%; background:var(--muted); display:inline-block; }
.nsd-chain.ok i { background:var(--ok); } .nsd-chain.warn i { background:var(--warn); } .nsd-chain.err i { background:var(--err); }
.nsd-chain.err { background:var(--err-weak); border-color:var(--err); color:var(--err); }
.nsd-chain.warn { background:var(--warn-weak); }
.sys-block { display:inline-flex; flex-direction:column; align-items:center; gap:2px; line-height:1.1; }
.sys-block .sys-status { padding:3px 9px 3px 7px; font-size:11.5px; }
.user-dd .ddbtn.user-chip { display:inline-flex; flex-direction:column; align-items:flex-start; padding:4px 10px; border-radius:8px; margin:0; max-width:220px; }
.user-dd .user-name { font-size:12.5px; }
.nav .ddmenu.ddmenu-right, .user-dd .ddmenu { left:auto; right:0; min-width:190px; }
.user-dd { position:relative; }
.user-dd .ddmenu { display:none; position:absolute; top:100%; right:0; margin-top:8px; background:var(--card); border:1px solid var(--border); border-radius:10px; padding:6px; box-shadow:0 10px 30px rgba(0,0,0,.12); z-index:50; }
.user-dd:hover .ddmenu, .user-dd:focus-within .ddmenu { display:block; }
.user-dd::after { content:''; position:absolute; top:100%; left:0; right:0; height:10px; }
.user-dd .ddmenu a { display:block; padding:8px 12px; border-radius:7px; font-size:13px; color:var(--text-2); text-decoration:none; white-space:nowrap; }
.user-dd .ddmenu a:hover, .user-dd .ddmenu a.active { background:var(--accent-weak); color:var(--accent-strong); }
.user-dd:hover .ddbtn, .user-dd.active .ddbtn { background:var(--accent-weak); }
.ddsep { display:block; height:1px; background:var(--border); margin:6px 4px; }
/* ---- Контент ---- */
main { padding:24px; max-width:1200px; margin:0 auto; }
h1 { font-size:22px; font-weight:680; margin:0 0 4px; letter-spacing:-0.01em; }
h2 { font-size:15px; font-weight:650; margin:0 0 12px; }
h3 { font-size:13.5px; font-weight:600; margin:14px 0 6px; }
p { margin:0 0 10px; }
a { color:var(--accent); text-decoration:none; }
a:hover { text-decoration:underline; }
/* ---- Карточки ---- */
.card {
background:var(--card); border:1px solid var(--border); border-radius:var(--radius);
padding:18px 20px; margin-bottom:16px; box-shadow:var(--shadow);
}
.card h2:first-child { margin-top:0; }
.grid { display:grid; gap:14px; grid-template-columns:repeat(auto-fit, minmax(220px,1fr)); }
/* ---- Статы (приборные читалки) ---- */
.stat { padding:15px 17px; background:var(--card); border:1px solid var(--border); border-radius:var(--radius); box-shadow:var(--shadow); }
.stat-label { display:flex; align-items:center; gap:7px; font-size:10.5px; color:var(--muted); text-transform:uppercase; letter-spacing:.06em; font-weight:600; }
.stat-label::before { content:""; width:7px; height:7px; border-radius:50%; background:var(--border-strong); flex:none; }
.stat.ok .stat-label::before { background:var(--ok); } .stat.warn .stat-label::before { background:var(--warn); } .stat.err .stat-label::before { background:var(--err); } .stat.accent .stat-label::before { background:var(--accent); }
.stat-value { font-size:27px; font-weight:700; margin-top:6px; letter-spacing:-0.03em; font-family:var(--mono); font-variant-numeric:tabular-nums; }
/* ---- Индикаторы ---- */
.dot { display:inline-block; width:8px; height:8px; border-radius:50%; margin-right:7px; vertical-align:middle; }
.dot.ok { background:var(--ok); } .dot.warn { background:var(--warn); } .dot.err { background:var(--err); }
/* ---- Таблицы ---- */
table { width:100%; border-collapse:collapse; font-size:13.5px; }
th, td { text-align:left; padding:9px 12px; border-bottom:1px solid var(--border); }
th { color:var(--muted); font-weight:600; font-size:11px; text-transform:uppercase; letter-spacing:.04em; }
tbody tr:hover td { background:var(--accent-weak); }
/* ---- Бейджи статусов ---- */
.badge { display:inline-block; padding:3px 9px; border-radius:20px; font-size:11px; font-weight:600; }
.live { font-size:11.5px; margin-top:4px; color:var(--muted); display:flex; align-items:center; gap:6px; line-height:1.3 }
.live::before { content:''; width:7px; height:7px; border-radius:50%; background:var(--muted); flex:none }
.live-wait::before { background:var(--accent); animation:pulse 2.4s var(--ease) infinite }
.live-warn { color:var(--warn) } .live-warn::before { background:var(--warn); animation:pulse 2.4s var(--ease) infinite }
.live-ok { color:var(--ok) } .live-ok::before { background:var(--ok) }
.live-bad { color:var(--err) } .live-bad::before { background:var(--err) }
.live-big { font-size:14px; margin:-6px 0 12px; font-weight:500 }
.live[data-active] { color:var(--text-2) } .live-warn[data-active] { color:var(--warn) }
.live-meta { font-size:12px; color:var(--muted); margin:2px 0 10px; display:flex; align-items:center; gap:6px; }
.live-meta .dot { width:7px; height:7px; border-radius:50%; background:var(--ok,#2e7d32); display:inline-block; animation:livedot 2s ease-in-out infinite; }
.live-meta.changed { color:var(--warn,#8a5a00); }
.live-meta.lost { color:var(--err,#b3261e); }
@keyframes livedot { 50% { opacity:.3 } }
@media (prefers-reduced-motion: reduce) { .live-meta .dot { animation:none } }
.badge.draft, .badge.validated, .badge.submitted_to_nsd { background:var(--accent-weak); color:var(--accent); }
.badge.awaiting_decision, .badge.manual_approval { background:var(--warn-weak); color:var(--warn); }
.badge.confirmed, .badge.awaiting_sub16, .badge.done { background:var(--ok-weak); color:var(--ok); }
.badge.rejected, .badge.timed_out, .badge.err { background:var(--err-weak); color:var(--err); }
.badge.ok { background:var(--ok-weak); color:var(--ok); }
/* ---- Код / preformatted ---- */
code { background:var(--card-2); border:1px solid var(--border); padding:1.5px 6px; border-radius:5px; font-size:12px; font-family:ui-monospace,"SF Mono",Menlo,monospace; }
pre { background:var(--card-2); border:1px solid var(--border); border-radius:var(--radius-sm); padding:14px; font-size:12px; overflow:auto; max-height:420px; font-family:ui-monospace,"SF Mono",Menlo,monospace; }
.muted { color:var(--muted); font-size:13px; }
/* ---- Кнопки ---- */
button, .btn {
background:var(--accent); color:#fff; border:1px solid var(--accent);
padding:9px 16px; border-radius:8px; cursor:pointer; font-size:13.5px; font-weight:550;
font-family:inherit; transition:filter .15s;
}
button:hover, .btn:hover { filter:brightness(1.07); text-decoration:none; }
.btn-secondary { background:var(--card-2); color:var(--text); border-color:var(--border-strong); }
.btn-ok { background:var(--ok); border-color:var(--ok); }
.btn-warn { background:var(--warn); border-color:var(--warn); color:#fff; }
.btn-danger { background:var(--err); border-color:var(--err); }
.btn-ghost { background:transparent; color:var(--accent); border-color:transparent; }
/* ---- Формы ---- */
input, select, textarea {
padding:9px 11px; background:var(--bg-elev); border:1px solid var(--border-strong);
color:var(--text); border-radius:8px; font:inherit; font-size:13.5px;
}
input:focus, select:focus, textarea:focus { outline:2px solid var(--accent-weak); border-color:var(--accent); }
label { font-size:13px; font-weight:500; }
/* Поле формы: подпись, ввод и пояснение — каждое со своей строки. Без этого label,
input и подсказка идут в строку и «разъезжаются» на широком экране. */
/* min-width:0 обязателен: иначе длинная строка внутри (ключ, код, ссылка) не даёт
блоку сжаться и растягивает всю страницу — появляется горизонтальная прокрутка. */
.fld { display:grid; gap:4px; align-content:start; min-width:0; max-width:100%; }
.fld > * { min-width:0; }
.fld > label { font-weight:600; }
.fld input[type=text], .fld input[type=password], .fld input[type=email], .fld select { width:100%; max-width:460px; }
.fld .muted { max-width:620px; line-height:1.45; overflow-wrap:anywhere; }
/* ---- Баннер режима эмуляции ---- */
.banner-mock { background:var(--warn-weak); border-bottom:1px solid var(--warn); padding:10px 24px; display:flex; align-items:center; gap:12px; font-size:13px; }
/* ---- Hero (приветствие + статус) ---- */
.hero { padding:8px 0 22px; }
.hero-greeting { font-size:28px; font-weight:720; letter-spacing:-0.02em; margin:0 0 6px; }
.hero-status { display:inline-flex; align-items:center; gap:9px; font-size:15px; font-weight:550; padding:7px 16px; border-radius:24px; }
.hero-status.ok { background:var(--ok-weak); color:var(--ok); }
.hero-status.warn { background:var(--warn-weak); color:var(--warn); }
.hero-status.err { background:var(--err-weak); color:var(--err); }
/* ---- Плитки задач (task tiles) ---- */
.tiles { display:grid; gap:16px; grid-template-columns:repeat(auto-fit, minmax(210px,1fr)); margin:8px 0 24px; }
.tile {
display:flex; flex-direction:column; gap:10px;
padding:22px; background:var(--card); border:1px solid var(--border);
border-radius:16px; box-shadow:var(--shadow); cursor:pointer;
text-decoration:none; color:var(--text); transition:transform .14s, box-shadow .14s, border-color .14s;
min-height:128px;
}
.tile:hover { transform:translateY(-2px); box-shadow:var(--shadow-lg); border-color:var(--border-strong); text-decoration:none; }
.tile .ico { width:42px; height:42px; border-radius:11px; display:flex; align-items:center; justify-content:center; background:var(--accent-weak); color:var(--accent); }
.tile .ico svg { width:21px; height:21px; }
.tile.brand .ico { background:var(--accent); color:#fff; }
.tile .t-title { font-size:15.5px; font-weight:650; letter-spacing:-0.01em; }
.tile .t-sub { font-size:12.5px; color:var(--muted); margin-top:-4px; line-height:1.4; }
.tile .t-arrow { margin-top:auto; color:var(--border-strong); display:flex; }
.tile .t-arrow svg { width:18px; height:18px; }
.tile:hover .t-arrow { color:var(--accent); transform:translateX(2px); transition:transform .15s var(--ease), color .15s; }
/* ---- Секция (заголовок + контент) ---- */
.section-head { display:flex; align-items:baseline; justify-content:space-between; margin:24px 0 12px; }
.section-head h2 { margin:0; font-size:17px; }
.section-head a { font-size:13px; }
/* ---- Админ-центр: боковые разделы + контент (macOS System Settings) ---- */
.settings { display:grid; grid-template-columns:236px 1fr; gap:26px; align-items:start; }
/* Колонка сетки по умолчанию не уже своего содержимого (min-width:auto), поэтому
широкая таблица растягивала всю страницу и внизу появлялся ползунок. */
.settings > * { min-width:0; }
.settings-section, .settings-section .card { min-width:0; }
.settings-section table { max-width:100%; }
.tw { overflow-x:auto; max-width:100%; }
.settings-nav { position:sticky; top:78px; display:flex; flex-direction:column; gap:2px; }
.settings-nav button {
display:flex; align-items:center; gap:11px; justify-content:flex-start;
background:transparent; border:1px solid transparent; color:var(--text-2);
padding:10px 13px; border-radius:9px; cursor:pointer; font-size:14px; font-weight:520;
width:100%; text-align:left; transition:background .12s;
}
.settings-nav button:hover { background:var(--card-2); color:var(--text); }
.settings-nav button.active { background:var(--accent-weak); color:var(--accent); }
.settings-nav button .nico { width:18px; height:18px; flex:none; display:flex; align-items:center; justify-content:center; color:var(--muted); }
.settings-nav button .nico svg { width:18px; height:18px; }
.settings-nav button.active .nico { color:var(--accent); }
.settings-nav button .ind { margin-left:auto; width:8px; height:8px; border-radius:50%; flex:none; }
.settings-nav button .ind.ok { background:var(--ok); }
.settings-nav button .ind.warn { background:var(--warn); }
.settings-nav button .ind.err { background:var(--err); }
.settings-section { display:none; }
.settings-section.active { display:block; }
/* Единый вид «Настройки» и мастера (07.10.2026): раздел настроек устроен как шаг мастера — одна карточка,
заголовок и вводный текст, подблоки через тонкие разделители (не стопка отдельных карточек); поля — подпись
над полем, поле во всю колонку; текст — 14.5px, как в мастере. */
.settings-section.active { background:var(--card); border:1px solid var(--border); border-radius:16px; box-shadow:var(--shadow); padding:28px 32px 24px; }
.settings-section > h1 { font-size:21px; font-weight:680; letter-spacing:-0.015em; margin:0 0 6px; }
.settings-section > h1 + p, .settings-section > h1 + .muted { color:var(--muted); font-size:14.5px !important; margin:0 0 18px !important; max-width:none !important; }
.settings-section .card { border:0; border-top:1px solid var(--border); border-radius:0; box-shadow:none; background:transparent; padding:20px 0 0; margin:20px 0 0; }
.settings-section > h1 + .card, .settings-section > h1 + p + .card, .settings-section > h1 + .muted + .card, .settings-section > h1 + p + p + .card { border-top:0; padding-top:0; margin-top:0; }
.settings-section .card > h2:first-child { font-size:16px; font-weight:650; letter-spacing:-0.005em; margin:0 0 6px; }
.settings-section .card > h2:first-child + p.muted { font-size:13.5px; margin:0 0 12px; }
.settings-section .card .card { padding-top:16px; margin-top:16px; }
/* плашка внутри раздела (ключ не подключён, вход открыт) — остаётся плашкой */
.settings-section .card.callout { border:1px solid color-mix(in srgb, var(--accent) 22%, var(--border)); border-radius:12px; padding:16px 18px; margin:18px 0 0; }
.settings-section > h1 + .card.callout, .settings-section > h1 + p + .card.callout { margin-top:4px; padding-top:16px; border-top:1px solid color-mix(in srgb, var(--accent) 22%, var(--border)); }
.settings-section .card.callout + .card { border-top:0; }
:is(.settings-section, .wiz-card) p { font-size:14.5px; }
:is(.settings-section, .wiz-card) p.muted, :is(.settings-section, .wiz-card) .muted p { font-size:13.5px; }
.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; }
/* поля в сетке: подпись над полем, поле во всю ширину колонки (иначе «Порт [587]» вставал в строку) */
:is(.settings-section, .wiz-card) .grid > div > label:not(:has(input[type=checkbox], input[type=radio])) { display:block; margin:0; }
:is(.settings-section, .wiz-card) .grid > div > :is(input:not([type=checkbox]):not([type=radio]):not([type=hidden]), select, textarea) { display:block; width:100%; box-sizing:border-box; margin-top:5px !important; }
@media (max-width:820px) { .settings-section.active { padding:20px 18px 18px; } }
/* Учётные записи (part_accounts: мастер и Настройка → Пользователи) */
.acc-roles{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin:12px 0 4px}
.acc-role{display:flex;gap:10px;align-items:flex-start;padding:12px 14px;border:1px solid var(--border);border-radius:12px;cursor:pointer;font-weight:400;margin:0}
.acc-role:has(input:checked){border-color:var(--accent);background:var(--accent-weak)}
.acc-role input{margin-top:3px;flex:none;width:auto}
.acc-role b{display:block;font-size:14.5px}
.acc-role small{display:block;color:var(--muted);font-size:13px;line-height:1.4;margin-top:3px}
.acc-role .acc-who{display:block;color:var(--text);font-size:12.5px;margin-top:6px}
.acc-form{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;max-width:640px;margin-top:12px}
.acc-form label{display:block;margin:0}
.acc-form label input{width:100%;box-sizing:border-box;margin-top:4px}
.acc-pw{display:flex;gap:6px;align-items:flex-end}
.ad-st{display:flex;gap:12px;align-items:center;margin:12px 0 4px;padding:12px 16px;border-radius:12px;border:1px solid var(--border);background:var(--card-2);max-width:640px;box-sizing:border-box}
.ad-st b{display:block;font-size:15px;font-weight:650}
.ad-st small{display:block;color:var(--muted);font-size:13px;line-height:1.4;margin-top:2px;overflow-wrap:anywhere}
.ad-st-ic{flex:none;position:relative;width:26px;height:26px;border-radius:50%;background:var(--border-strong)}
.ad-st-ok{background:color-mix(in srgb,var(--ok) 9%,var(--card));border-color:color-mix(in srgb,var(--ok) 40%,var(--border))}
.ad-st-ok .ad-st-ic{background:var(--ok)}
.ad-st-ok .ad-st-ic::after{content:"";position:absolute;left:50%;top:45%;width:6px;height:11px;border:solid #fff;border-width:0 2.2px 2.2px 0;transform:translate(-50%,-50%) rotate(45deg)}
.ad-st-warn{background:var(--warn-weak);border-color:color-mix(in srgb,var(--warn) 45%,var(--border))}
.ad-st-warn .ad-st-ic{background:var(--warn)}
.ad-st-warn .ad-st-ic::after{content:"!";position:absolute;inset:0;display:grid;place-items:center;color:#fff;font-weight:700;font-size:15px}
.ad-st-spin{background:none;border:2.5px solid var(--accent-weak);border-top-color:var(--accent);box-sizing:border-box;animation:dq-spin .8s linear infinite}
.ad-q-l{display:block;margin:14px 0 0;font-weight:600;font-size:14px;max-width:640px}
.ad-q-l input{display:block;width:100%;box-sizing:border-box;margin-top:6px;font-size:15px !important;font-weight:400}
.ad-res{list-style:none;margin:8px 0 0;padding:0;display:grid;gap:6px;max-width:640px}
.ad-hit{display:flex;gap:10px;align-items:baseline;width:100%;text-align:left;padding:10px 14px;border:1px solid var(--border);border-radius:10px;background:var(--card);color:var(--text);font:inherit;font-size:14px;cursor:pointer}
.ad-hit:hover:not(:disabled){border-color:var(--border-strong)}
.ad-hit.is-sel{border-color:var(--accent);background:var(--accent-weak)}
.ad-hit:disabled{opacity:.6;cursor:default}
.ad-kind{flex:none;font-size:11px;font-weight:600;padding:2px 8px;border-radius:999px;background:var(--card-2);color:var(--muted);border:1px solid var(--border)}
.ad-kind-g{background:var(--accent-weak);color:var(--accent);border-color:transparent}
.ad-pick{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:12px;padding:12px 14px;border:1px solid var(--accent);border-radius:12px;background:color-mix(in srgb,var(--accent-weak) 50%,var(--card));max-width:640px;box-sizing:border-box}
.ad-pick[hidden]{display:none}
.ad-groups{margin-top:16px;max-width:640px}
.ad-grp{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:8px 12px;border:1px solid var(--border);border-radius:10px;margin-top:6px;font-size:14px}
.ad-grp form{margin:0}
@media (max-width:600px){.acc-roles,.acc-form{grid-template-columns:minmax(0,1fr)}}
.acc-mods{display:flex;gap:6px 16px;flex-wrap:wrap;align-items:center;margin:12px 0 0;font-size:13.5px}
.acc-mods label{display:inline-flex;gap:6px;align-items:center;font-weight:400;margin:0;font-size:13.5px}
/* сотрудники в Настройка → Пользователи: строка — кто и с какой ролью, «Изменить» раскрывает правку */
.usr-list{display:grid;gap:8px;margin-top:10px}
.usr{border:1px solid var(--border);border-radius:12px;background:var(--card)}
.usr[open]{border-color:color-mix(in srgb, var(--accent) 45%, var(--border))}
.usr>summary{display:flex;align-items:center;gap:12px;padding:10px 14px;cursor:pointer;list-style:none}
.usr>summary::-webkit-details-marker{display:none}
.usr>summary:hover .usr-edit{text-decoration:underline}
.usr-av{flex:none;width:34px;height:34px;border-radius:50%;background:var(--accent-weak);color:var(--accent);display:grid;place-items:center;font-weight:700;font-size:14px}
.usr-main{flex:1;min-width:0}
.usr-main b{display:block;font-size:14.5px;font-weight:600}
.usr-main small{display:block;color:var(--muted);font-size:12.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.usr-role{flex:none;font-size:12.5px;font-weight:600;padding:3px 10px;border-radius:999px;background:var(--card-2);border:1px solid var(--border);color:var(--text-2)}
.usr-edit{flex:none;font-size:13px;color:var(--accent);min-width:64px;text-align:right}
.usr[open] .usr-edit{visibility:hidden}
.usr-form{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;padding:14px;border-top:1px solid var(--border)}
.usr-form label{display:block;margin:0}
.usr-form label input:not([type=checkbox]), .usr-form label select{display:block;width:100%;box-sizing:border-box;margin-top:5px}
.usr-note{align-self:end;color:var(--muted);font-size:13px;line-height:1.4;padding-bottom:10px}
.usr-checks{grid-column:1/-1;display:flex;gap:6px 18px;flex-wrap:wrap}
.usr-checks label{display:inline-flex;gap:6px;align-items:center;font-weight:400;font-size:13.5px}
.usr-act{grid-column:1/-1;display:flex;justify-content:space-between;align-items:center;gap:8px}
.usr-del{background:none !important;border:0 !important;color:var(--err);font-size:13.5px;font-weight:500;padding:6px 2px;cursor:pointer}
.usr-del:hover{text-decoration:underline}
@media (max-width:600px){.usr-form{grid-template-columns:minmax(0,1fr)} .usr-main small{white-space:normal}}
/* «Расширенные настройки» — под раскрытием, заголовком подблока */
.adv-box>summary{display:flex;flex-direction:column;gap:2px;cursor:pointer;list-style:none}
.adv-box>summary::-webkit-details-marker{display:none}
.adv-box>summary .sum-h{font-size:16px;font-weight:650;color:var(--accent)}
.adv-box>summary .sum-h::before{content:"▸ ";color:var(--muted)}
.adv-box[open]>summary .sum-h::before{content:"▾ "}
.adv-box>summary .muted{font-size:13px}
.adv-h{font-size:14.5px;font-weight:650;margin:22px 0 8px}
/* карточка «Как bj работает с QUIK»: поручения клиентов из терминала — одной галочкой */
.quik-term{display:flex;gap:10px;align-items:flex-start;margin:16px 0 0;max-width:680px;line-height:1.45;font-size:13.5px;color:var(--text-2)}
.quik-term b{color:var(--text);font-weight:600}
.quik-term input{margin-top:3px;flex:none}
.org-grid{grid-template-columns:minmax(0,1fr) minmax(0,1fr) minmax(0,2fr);gap:10px;max-width:none}
/* карточка состояния (обновления и др.): значок, что происходит, действие справа */
.stc{display:flex;gap:14px;align-items:center;padding:14px 18px;border-radius:12px;border:1px solid var(--border);background:var(--card-2)}
.stc-ic{flex:none;position:relative;width:28px;height:28px;border-radius:50%;background:var(--border-strong)}
.stc-b{flex:1;min-width:0}
.stc-b b{display:block;font-size:15px;font-weight:650}
.stc-b small{display:block;color:var(--muted);font-size:13px;line-height:1.4;margin-top:2px;overflow-wrap:anywhere}
.stc form, .stc .btn{flex:none}
.stc-ok{background:color-mix(in srgb,var(--ok) 9%,var(--card));border-color:color-mix(in srgb,var(--ok) 40%,var(--border))}
.stc-ok .stc-ic{background:var(--ok)}
.stc-ok .stc-ic::after{content:"";position:absolute;left:50%;top:45%;width:6px;height:12px;border:solid #fff;border-width:0 2.2px 2.2px 0;transform:translate(-50%,-50%) rotate(45deg)}
.stc-warn{background:color-mix(in srgb,var(--accent) 8%,var(--card));border-color:color-mix(in srgb,var(--accent) 40%,var(--border))}
.stc-warn .stc-ic{background:var(--accent)}
.stc-warn .stc-ic::after{content:"↑";position:absolute;inset:0;display:grid;place-items:center;color:#fff;font-weight:700;font-size:15px}
.stc-err{background:var(--warn-weak);border-color:color-mix(in srgb,var(--warn) 45%,var(--border))}
.stc-err .stc-ic{background:var(--warn)}
.stc-err .stc-ic::after{content:"!";position:absolute;inset:0;display:grid;place-items:center;color:#fff;font-weight:700;font-size:15px}
.stc-run .stc-ic{background:none;border:2.5px solid var(--accent-weak);border-top-color:var(--accent);box-sizing:border-box;animation:dq-spin .8s linear infinite}
.stc-run .btn, .stc-run form{visibility:hidden}
@media (max-width:600px){.stc{flex-wrap:wrap}}
@media (max-width:700px){.org-grid{grid-template-columns:minmax(0,1fr)}}
@media (max-width:820px) {
.settings { grid-template-columns:1fr; }
.settings-nav { flex-direction:row; overflow-x:auto; position:static; padding-bottom:8px; }
.settings-nav button { white-space:nowrap; width:auto; }
}
/* ---- Документация / справка (Read) ---- */
.doc-header { margin:6px 0 22px; }
.doc-header h1 { font-size:24px; letter-spacing:-0.02em; }
.doc-header .lede { color:var(--text-2); font-size:15px; max-width:70ch; margin-top:6px; }
.doc { display:grid; grid-template-columns:236px minmax(0,1fr); gap:40px; align-items:start; }
.doc-toc { position:sticky; top:74px; max-height:calc(100vh - 96px); overflow:auto; font-size:13px; padding-right:6px; }
.doc-toc .toc-group { font-size:10px; text-transform:uppercase; letter-spacing:.07em; color:var(--muted); font-weight:700; margin:16px 0 6px; }
.doc-toc .toc-group:first-child { margin-top:0; }
.doc-toc a { display:block; color:var(--text-2); text-decoration:none; padding:5px 11px; border-radius:7px; border-left:2px solid transparent; line-height:1.35; }
.doc-toc a:hover { background:var(--card-2); color:var(--text); }
.doc-toc a.active { color:var(--accent-strong); border-left-color:var(--accent); background:var(--accent-weak); font-weight:600; }
.doc-body { max-width:820px; min-width:0; }
.doc-body > h2 { font-size:19px; letter-spacing:-0.015em; margin:34px 0 12px; padding-top:8px; scroll-margin-top:74px; }
.doc-body > h2:first-child { margin-top:0; }
.doc-body h3 { font-size:15px; margin:22px 0 8px !important; }
.doc-body p, .doc-body li { max-width:74ch; }
.doc-body p { color:var(--text-2); }
.doc-body .card { scroll-margin-top:74px; }
.doc-sep { height:1px; background:var(--border); margin:30px 0; border:0; }
.callout { background:var(--card-2); border:1px solid var(--border); border-radius:var(--radius-sm); padding:13px 15px; margin:14px 0; font-size:14px; line-height:1.55; }
.callout.err { background:var(--err-weak); } .callout.accent { background:var(--accent-weak); } .callout.warn { background:var(--warn-weak); } .callout.ok { background:var(--ok-weak); }
.callout strong:first-child { color:var(--text); }
/* Код-блоки справки — тёмная тема учтена */
.doc-body pre, pre.copyable, pre.code { background:var(--card-2) !important; border:1px solid var(--border); border-radius:var(--radius-sm); color:var(--text); }
/* скрипты и код в карточках: не шире блока — длинные строки переносятся, а не уезжают за край */
pre.copyable { max-width:100%; box-sizing:border-box; white-space:pre-wrap; overflow-wrap:anywhere; overflow:auto; }
details:has(> pre.copyable) { min-width:0; max-width:100%; }
.copy-bar { display:flex; justify-content:flex-end; margin:6px 0 -2px; }
.copy-bar button { font-size:12px; padding:3px 10px; border:1px solid var(--border); border-radius:6px; background:var(--card); color:var(--text); cursor:pointer; }
.copy-bar button.done { color:var(--ok); border-color:var(--ok); }
@media (max-width:900px){ .doc { grid-template-columns:1fr; gap:0; } .doc-toc { position:static; max-height:none; margin-bottom:18px; border-bottom:1px solid var(--border); padding-bottom:12px; } }
/* ---- Toast ---- */
#bj-toast { position:fixed; top:70px; right:22px; z-index:9999; max-width:520px; padding:14px 18px 14px 40px; background:var(--card); border:1px solid var(--border-strong); border-radius:var(--radius-sm); color:var(--text); box-shadow:var(--shadow-lg); font-size:13px; line-height:1.45; opacity:0; transform:translateY(-12px); transition:opacity .25s var(--ease), transform .25s var(--ease); }
#bj-toast::before { content:""; position:absolute; left:16px; top:17px; width:9px; height:9px; border-radius:50%; background:var(--ok); }
#bj-toast.visible { opacity:1; transform:translateY(0); }
#bj-toast .close { position:absolute; top:6px; right:10px; cursor:pointer; color:var(--muted); font-size:15px; }
#bj-toast .close:hover { color:var(--text); }
.banner-dev{margin:0;border-bottom:1px solid var(--border)}
.banner-dev button{width:100%;display:flex;gap:10px;align-items:center;padding:7px 18px;background:transparent;border:0;font:inherit;font-size:13px;color:var(--text);cursor:pointer;text-align:left}
.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; }
button.dq-link, .dq-link { background:none !important; border:0 !important; padding:0 !important; color:var(--accent) !important; font-size:12.5px; font-weight:500; cursor:pointer; border-radius:4px; box-shadow:none !important; }
.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>
<div id="bj-toast"><span class="close" onclick="document.getElementById('bj-toast').classList.remove('visible')">×</span><div id="bj-toast-text"></div></div>
<header class="topbar">
<a class="brand" href="/admin/" title="На дашборд" style="text-decoration:none;color:inherit">
<svg viewBox="0 0 26 26" fill="none" aria-hidden="true">
<path d="M5.5 16 C 9 8.5, 17 8.5, 20.5 16" stroke="var(--accent)" stroke-width="1.9" stroke-linecap="round"/>
<circle cx="5.5" cy="16" r="3.1" fill="var(--accent)"/>
<circle cx="20.5" cy="16" r="3.1" fill="var(--brand)"/>
</svg>
<span><b>Bridge</b><span class="brand-j">&amp;Join</span></span>
</a>
{{if eq .Active "wizard"}}<span class="topbar-sub">Настройка</span>{{end}}
<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>
<span class="ddmenu">
<a href="/admin/journal">Все (вход + исход)</a>
<a href="/admin/claims">Исходящие переводы</a>
<a href="/admin/incoming">Входящие<span id="nav-incoming-badge" style="display:none;margin-left:5px;background:var(--err);color:#fff;border-radius:10px;padding:1px 7px;font-size:11px;font-weight:700"></span></a>
</span>
</span>
{{end}}{{if can "view"}}<a href="/admin/manual" class="{{if eq .Active "manual"}}active{{end}}">Ручной ввод M2M</a>{{end}}{{if and devOn (can "settings")}}<a href="/admin/depo-test" class="{{if eq .Active "depotest"}}active{{end}}">Тест DEPO</a>{{end}}{{if and devOn (can "operate")}}<a href="/admin/m2m-testing" class="{{if eq .Active "m2mtest"}}active{{end}}">Тестирование M2M</a>{{end}}
{{if and .NPR (can "npr")}}<a href="/admin/npr" class="{{if eq .Active "npr"}}active{{end}}">НПР / УДС</a>{{end}}
{{if or (can "view") (can "quik")}}<span class="dd{{if or (eq .Active "fancy") (eq .Active "quik") (eq .Active "news") (eq .Active "handbook")}} active{{end}}">
<span class="ddbtn">Обмен ▾</span>
<span class="ddmenu">
{{if and (can "view") (mod "quik-mnp")}}<a href="/admin/fancy-queue">Обмен с DEPO (Fancy)</a>{{end}}
{{if and (can "quik") (mod "quik-mnp")}}<a href="/admin/quik">Поручения QUIK</a>{{end}}
{{if can "view"}}<a href="/admin/news">События</a>
<a href="/admin/handbook">Справочник НРД</a>{{end}}
</span>
</span>{{end}}
</nav>
<div class="topbar-right">
<span class="sys-block">
{{if and .NSDLink.Configured (not .IsMockMode)}}<a id="nsd-chain" href="/admin/status#nsd" class="sys-status nsd-chain {{.NSDLink.State}}" title="{{nsdSegTip .NSDLink}}"><i></i><span>Шлюз НРД</span></a>{{else}}<span class="sys-status {{if .IsMockMode}}warn{{end}}" title="Состояние обмена с НРД">
<span class="pulse"></span>{{if .IsMockMode}}{{if devOn}}Эмуляция{{else}}НРД не подключён{{end}}{{else}}НРД на связи{{end}}</span>{{end}}
<span class="clock">{{.Now}}</span>
</span>
<span class="dd user-dd{{if or (eq .Active "setup") (eq .Active "status") (eq .Active "help")}} active{{end}}">
<span class="ddbtn user-chip" title="{{with curUser}}{{.RoleTitle}}{{if eq .Source "ad"}} · вход через AD{{end}}{{else}}Пользователи не созданы — вход открыт{{end}}">
<span class="user-name">{{with curUser}}{{if .Name}}{{.Name}}{{else}}{{.Login}}{{end}}{{else}}без входа{{end}} ▾</span>
<span class="user-role">{{with curUser}}{{.RoleTitle}}{{else}}вход открыт{{end}}</span>
</span>
<span class="ddmenu ddmenu-right">
{{if can "settings"}}<a href="/admin/setup" class="{{if eq .Active "setup"}}active{{end}}">Настройка</a>
<a href="/admin/wizard">Мастер настройки</a>
<a href="/admin/status" class="{{if eq .Active "status"}}active{{end}}">Статус системы</a>{{end}}
<a href="/admin/help" class="{{if eq .Active "help"}}active{{end}}">Справка</a>
{{with curUser}}<span class="ddsep"></span><a href="/logout">Выйти</a>{{else}}{{if authOpen}}<span class="ddsep"></span><a href="/admin/setup#users">Создать пользователей</a>{{end}}{{end}}
</span>
</span>
<button class="theme-toggle" id="theme-toggle" title="Светлая/тёмная тема" aria-label="Переключить тему"></button>
</div>
</header>
{{if devLicensed}}
<form method="post" action="/admin/dev-toggle" class="banner-dev {{if devOn}}on{{else}}off{{end}}" title="{{if devOn}}Нажмите, чтобы скрыть у себя служебные разделы и имитации{{else}}Нажмите, чтобы снова показать служебные разделы{{end}}">
<input type="hidden" name="return_to" value="">
<button type="submit"><span class="dot {{if devOn}}warn{{else}}ok{{end}}" style="width:9px;height:9px;margin:0"></span>
{{if devOn}}<strong>Режим разработки включён</strong><span class="muted"> — видны служебные разделы, тестовые сценарии и имитации. Нажмите, чтобы выключить у себя.</span>{{else}}<strong>Режим разработки выключен у вас</strong><span class="muted"> — лицензия его разрешает; нажмите, чтобы включить.</span>{{end}}
</button>
</form>
<script>(function(){var f=document.querySelector('.banner-dev input[name=return_to]');if(f)f.value=location.pathname+location.search;})();</script>
{{end}}
{{if .IsMockMode}}
<div class="banner-mock">
<span class="dot warn" style="width:9px;height:9px;margin:0"></span>
<div><strong style="color:var(--warn)">{{if devOn}}Режим эмуляции{{else}}Обмен с НРД не настроен{{end}}</strong> — {{if devOn}}реального обмена с НРД нет{{else}}поручения в НРД не отправляются{{end}}. <span class="muted">{{.MockReason}}</span></div>
{{if ne .Active "wizard"}}<a href="/admin/wizard" style="margin-left:auto">Открыть мастер настройки →</a>{{end}}
</div>
{{end}}
<main>
{{template "content" .}}
</main>
<script>
(function() {
// --- Тема: light/dark, сохранение в localStorage ---
var root = document.documentElement;
var toggle = document.getElementById('theme-toggle');
var icoMoon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.8A9 9 0 1 1 11.2 3 7 7 0 0 0 21 12.8z"/></svg>';
var icoSun = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>';
function applyTheme(t) {
root.setAttribute('data-theme', t);
toggle.innerHTML = (t === 'dark') ? icoSun : icoMoon;
try { localStorage.setItem('bj-theme', t); } catch(e) {}
}
var saved = 'light';
try { saved = localStorage.getItem('bj-theme') || (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); } catch(e) {}
applyTheme(saved);
toggle.addEventListener('click', function() {
applyTheme(root.getAttribute('data-theme') === 'dark' ? 'light' : 'dark');
});
// --- Toast из ?flash= ---
var toast = document.getElementById('bj-toast');
var toastText = document.getElementById('bj-toast-text');
function showToast(msg) {
toastText.textContent = msg;
toast.classList.add('visible');
var ms = Math.max(5000, Math.min(20000, msg.length * 60));
setTimeout(function() { toast.classList.remove('visible'); }, ms);
}
var params = new URLSearchParams(window.location.search);
var flash = params.get('flash');
if (flash) { showToast(flash); }
// Прокрутка к секции из ?scrollto= (надёжнее фрагмента через редирект).
var anchored = false;
var scrollTarget = params.get('scrollto');
if (scrollTarget) {
var stEl = document.getElementById(scrollTarget);
if (stEl) {
anchored = true;
var doScroll2 = function() { stEl.scrollIntoView({ block: 'start' }); };
doScroll2();
setTimeout(doScroll2, 50);
setTimeout(doScroll2, 200);
window.addEventListener('load', doScroll2);
}
}
if (flash || scrollTarget) {
params.delete('flash');
params.delete('scrollto');
var qs = params.toString();
window.history.replaceState({}, '', window.location.pathname + (qs ? '?' + qs : '') + window.location.hash);
}
// --- Сохранение позиции прокрутки через POST-редиректы ---
// Отключаем авто-восстановление браузера — иначе оно перебивает наше
// (скроллит наверх уже ПОСЛЕ нашего scrollTo).
if ('scrollRestoration' in history) { try { history.scrollRestoration = 'manual'; } catch(e) {} }
// Присваиваем карточкам стабильные id (по порядку рендера) — чтобы вернуться
// к нужной секции по якорю после сохранения.
var cards = document.querySelectorAll('.card');
for (var ci = 0; ci < cards.length; ci++) { if (!cards[ci].id) cards[ci].id = 'sec-' + ci; }
document.addEventListener('submit', function(ev) {
var f = ev.target;
if (f && f.method && f.method.toLowerCase() === 'post') {
try { sessionStorage.setItem('bj-scroll', String(window.scrollY)); } catch(e) {}
// Возврат на ТЕКУЩУЮ страницу (а не на /admin/setup): сервер не всегда
// получает Referer, поэтому говорим путь явно.
if (!f.querySelector('input[name="return_to"]')) {
var rt = document.createElement('input');
rt.type = 'hidden'; rt.name = 'return_to'; rt.value = location.pathname + location.search.replace(/[?&](flash|scrollto)=[^&]*/g, '').replace(/^&/, '?');
f.appendChild(rt);
}
// Подкладываем секцию (ближайшую карточку) — сервер вернёт ?scrollto=.
var card = f.closest ? f.closest('.card') : null;
if (card && card.id && !f.querySelector('input[name="section"]')) {
var inp = document.createElement('input');
inp.type = 'hidden'; inp.name = 'section'; inp.value = card.id;
f.appendChild(inp);
}
}
}, true);
var sc = null;
try { sc = sessionStorage.getItem('bj-scroll'); } catch(e) {}
// При наличии якоря восстановление позиции пропускаем, чтобы не конфликтовать.
if (sc !== null && !anchored) {
var y = parseInt(sc, 10) || 0;
try { sessionStorage.removeItem('bj-scroll'); } catch(e) {}
// Несколько попыток: страница могла ещё не разложиться (шрифты/контент),
// поэтому повторяем сразу, на след. кадре, по таймеру и после полной загрузки.
var restore = function() { window.scrollTo(0, y); };
restore();
window.requestAnimationFrame(restore);
setTimeout(restore, 60);
setTimeout(restore, 200);
window.addEventListener('load', restore);
}
// --- Браузерные уведомления о входящих переводах (сценарий 3333) ---
// Опрашиваем /admin/incoming/feed; на НОВЫЙ входящий показываем Notification,
// клик по которому ведёт в карточку для подтверждения/отказа. Плюс бейдж в меню.
(function() {
var badge = document.getElementById('nav-incoming-badge');
var jbadge = document.getElementById('nav-journal-badge');
function setBadges(n) {
[badge, jbadge].forEach(function(b){
if (!b) return;
if (n > 0) { b.textContent = n; b.style.display = 'inline-block'; }
else { b.style.display = 'none'; }
});
}
function seenSet() {
try { return new Set(JSON.parse(localStorage.getItem('bj-seen-incoming') || '[]')); }
catch(e) { return new Set(); }
}
function saveSeen(s) {
try { localStorage.setItem('bj-seen-incoming', JSON.stringify(Array.from(s))); } catch(e) {}
}
function notify(it) {
// 1) Системное уведомление браузера (только в защищённом контексте: HTTPS
// или localhost; по обычному HTTP браузеры Notification блокируют).
if (('Notification' in window) && Notification.permission === 'granted') {
var n = new Notification('Новый входящий перевод НРД', {
body: 'От ' + it.sender + ' · ' + it.count + ' бумаг · ' + it.received + '\nНажмите, чтобы подтвердить или отказать.',
tag: 'bj-incoming-' + it.id, requireInteraction: true
});
n.onclick = function() { window.focus(); location.href = '/admin/incoming/' + it.id; };
}
// 2) Всегда — заметный баннер на странице (работает и по HTTP).
inPageBanner(it);
}
function bannerStack() {
var c = document.getElementById('bj-incoming-stack');
if (!c) {
c = document.createElement('div');
c.id = 'bj-incoming-stack';
c.style.cssText = 'position:fixed;top:16px;right:16px;z-index:2000;display:flex;flex-direction:column;gap:10px;max-width:360px';
document.body.appendChild(c);
}
return c;
}
function inPageBanner(it) {
var bar = document.createElement('div');
bar.style.cssText = 'background:var(--accent,#2563eb);color:#fff;padding:14px 16px;border-radius:12px;box-shadow:0 12px 40px rgba(0,0,0,.35);cursor:pointer;font-size:14px;line-height:1.4';
bar.innerHTML = '<b>📥 Новый входящий перевод НРД</b><br>От ' + it.sender + ' · ' + it.count + ' бумаг · ' + it.received + '<br><span style="text-decoration:underline">Открыть для подтверждения/отказа →</span>';
bar.onclick = function() { location.href = '/admin/incoming/' + it.id; };
bannerStack().appendChild(bar);
setTimeout(function(){ bar.style.transition='opacity .5s'; bar.style.opacity='0'; setTimeout(function(){bar.remove();}, 600); }, 20000);
}
function poll() {
fetch('/admin/incoming/feed', { cache: 'no-store' }).then(function(r){ return r.json(); }).then(function(d){
setBadges(d.count);
var seen = seenSet(), changed = false;
(d.pending || []).forEach(function(it){
if (!seen.has(it.id)) { notify(it); seen.add(it.id); changed = true; }
});
if (changed) saveSeen(seen);
}).catch(function(){});
}
// Запрашиваем разрешение на уведомления (один раз).
if ('Notification' in window && Notification.permission === 'default') {
Notification.requestPermission().then(function(){ poll(); });
}
poll();
setInterval(poll, 12000); // каждые 12 сек
// Копирование текста: navigator.clipboard работает только по https/localhost, bj часто открыт по http —
// запасной путь через скрытое поле и execCommand. window.bjCopy — для кнопок на страницах.
window.bjCopy = function(text){
function fallback(){
var ta=document.createElement('textarea'); ta.value=text; ta.setAttribute('readonly','');
ta.style.position='fixed'; ta.style.top='-1000px'; document.body.appendChild(ta); ta.select();
var ok=false; try{ ok=document.execCommand('copy'); }catch(e){}
document.body.removeChild(ta); return ok;
}
if (navigator.clipboard && window.isSecureContext) {
return navigator.clipboard.writeText(text).then(function(){return true}, function(){return fallback()});
}
return Promise.resolve(fallback());
};
// Кнопка «Скопировать» над каждым блоком скрипта/кода (pre.copyable): копирует текст целиком.
(function(){
document.querySelectorAll('pre.copyable').forEach(function(pre){
if (pre.previousElementSibling && pre.previousElementSibling.classList.contains('copy-bar')) return;
var bar=document.createElement('div'); bar.className='copy-bar';
var b=document.createElement('button'); b.type='button'; b.textContent='Скопировать';
b.addEventListener('click', function(){
window.bjCopy(pre.textContent).then(function(ok){
b.textContent = ok ? 'Скопировано ✓' : 'Не удалось — выделите и скопируйте вручную';
b.classList.toggle('done', !!ok);
setTimeout(function(){ b.textContent='Скопировать'; b.classList.remove('done'); }, 2000);
});
});
bar.appendChild(b); pre.parentNode.insertBefore(bar, pre);
});
})();
// Цепочка связи с НРД в шапке: обновляется каждые 30 с, чтобы открытая страница не показывала старое.
(function(){
var el = document.getElementById('nsd-chain');
if (!el) return;
function upd(){
fetch('/admin/nsd-health.json', {cache:'no-store'}).then(function(r){ return r.json(); }).then(function(h){
if (!h.configured) return;
var tip = '';
(h.segs||[]).forEach(function(s){ tip += s.title + ': ' + s.text + (s.detail ? ' (' + s.detail + ')' : '') + '\n'; });
tip += (h.ago ? 'Последний ответ НРД ' + h.ago : 'Ответа НРД ещё не было') + '\n';
el.className = 'sys-status nsd-chain ' + h.state;
el.title = tip + 'Подробно — «Статус». Обновляется каждые 30 с.';
}).catch(function(){
el.className = 'sys-status nsd-chain warn';
el.title = 'Нет связи с bj — состояние НРД не обновляется';
});
}
setInterval(upd, 30000);
})();
// Онлайн-статус заявок в работе: строки с data-live и data-active опрашиваются, пока ждут.
// Карточка заявки (live-big с data-sig): изменилось что-то кроме строки статуса — страница
// перерисовывается сама; если оператор уже начал заполнять форму — только предложение обновить.
(function(){
var dirty = false;
document.addEventListener('input', function(e){ if (e.target.closest('main form')) dirty = true; }, true);
document.addEventListener('change', function(e){ if (e.target.closest('main form')) dirty = true; }, true);
try { var u = new URL(location.href); if (u.searchParams.has('flash')) { u.searchParams.delete('flash'); history.replaceState(null, '', u.pathname + u.search + u.hash); } } catch(e){}
function hhmmss(){ var d = new Date(); return ('0'+d.getHours()).slice(-2)+':'+('0'+d.getMinutes()).slice(-2)+':'+('0'+d.getSeconds()).slice(-2); }
function meta(e, text, cls){
var m = e.nextElementSibling;
if (!m || !m.classList.contains('live-meta')) { m = document.createElement('div'); m.className = 'live-meta'; e.parentNode.insertBefore(m, e.nextSibling); }
m.className = 'live-meta' + (cls ? ' ' + cls : ''); m.innerHTML = text;
}
document.querySelectorAll('.live-big[data-live]').forEach(function(e){
meta(e, e.hasAttribute('data-active') ? '<span class="dot"></span>обновляется автоматически' : 'заявка завершена — автообновление не требуется');
});
function tick(){
var els = document.querySelectorAll('.live[data-live][data-active]');
if (!els.length) return;
var out = [], inn = [];
els.forEach(function(e){ var k = e.getAttribute('data-live').split(':'); (k[0]==='out'?out:inn).push(k[1]); });
var q = 'out=' + encodeURIComponent(out.join(',')) + '&in=' + encodeURIComponent(inn.join(','));
fetch('/admin/journal/live?' + q, {cache:'no-store'}).then(function(r){ return r.json(); }).then(function(d){
els.forEach(function(e){
var k = e.getAttribute('data-live').split(':'), st = (d[k[0]]||{})[k[1]];
if (!st) return;
var big = e.classList.contains('live-big'), was = e.getAttribute('data-sig');
if (big && was && st.sig && st.sig !== was) {
if (!dirty) { location.reload(); return; }
meta(e, 'Заявка изменилась. <a href="' + location.pathname + location.search + '">Обновить страницу</a> (отмеченное в форме не сохранится)', 'changed');
e.setAttribute('data-sig', st.sig);
}
e.textContent = st.text;
e.className = 'live ' + (big ? 'live-big ' : '') + 'live-' + st.kind;
if (!st.active) e.removeAttribute('data-active');
var nx = e.nextElementSibling;
if (big && !(nx && nx.classList.contains('changed'))) {
meta(e, st.active ? '<span class="dot"></span>обновляется автоматически · проверено в ' + hhmmss() : 'заявка завершена — автообновление не требуется');
}
});
}).catch(function(){
document.querySelectorAll('.live-big[data-live][data-active]').forEach(function(e){ meta(e, 'нет связи с bj — статус не обновляется, повторим через несколько секунд', 'lost'); });
});
}
setInterval(tick, 8000);
if (document.querySelector('.live[data-live][data-active]')) setTimeout(tick, 1500);
})();
})();
})();
</script>
<div id="upd-modal" style="display:none;position:fixed;inset:0;background:rgba(10,14,24,.55);z-index:9999;align-items:center;justify-content:center;padding:16px">
<div style="background:var(--card,#fff);color:var(--text,#111);border-radius:12px;max-width:520px;width:100%;padding:22px 24px;box-shadow:0 12px 40px rgba(0,0,0,.35)">
<h3 style="margin:0 0 4px;font-size:18px">Обновление bj</h3>
<p id="upd-stage" style="margin:0 0 12px;color:var(--muted,#667)">Подготовка…</p>
<div style="height:12px;background:var(--card-2,#eef0f4);border-radius:6px;overflow:hidden;border:1px solid var(--border,#dde)">
<div id="upd-bar" style="height:100%;width:0;background:var(--accent,#2b50d8);transition:width .4s"></div>
</div>
<p id="upd-detail" style="margin:8px 0 0;font-size:13px;color:var(--muted,#667);min-height:18px"></p>
<div id="upd-done" style="display:none;margin-top:14px;text-align:right"><button type="button" class="btn" onclick="location.reload()">Готово</button></div>
</div>
</div>
<script>
/* Окно с ходом обновления: формы установки обновлений отправляются из скрипта, а страница
опрашивает /admin/setup/update/progress. При обновлении ядра bj перезапускается — связь
пропадает, окно ждёт возвращения и сверяет версию ядра с целевой. */
(function(){
var modal=document.getElementById('upd-modal'); if(!modal) return;
var stageEl=document.getElementById('upd-stage'), bar=document.getElementById('upd-bar'),
detail=document.getElementById('upd-detail'), doneEl=document.getElementById('upd-done');
function mb(n){ return (n/1048576).toFixed(n<10485760?1:0)+' МБ'; }
function show(){ modal.style.display='flex'; }
var finished=false;
function finish(text, ok){
finished=true;
stageEl.textContent=text; bar.style.width='100%';
bar.style.background= ok ? 'var(--ok,#2e8b57)' : 'var(--err,#c0392b)';
doneEl.style.display='block';
if(ok){ // после успешного обновления страница перезагружается сама — с новыми версиями
var left=5, tick=function(){ if(left<=0){ location.reload(); return; }
doneEl.querySelector('button').textContent='Готово (страница обновится через '+left+' с)'; left--; setTimeout(tick,1000); };
tick();
}
}
function run(form){
var lastChange=Date.now(), lastKey='';
// сторож: если отчёт не меняется полторы минуты, перезагружаем страницу — не оставляем «зависшее» окно
(function watch(){ if(finished) return; if(Date.now()-lastChange>90000){ location.reload(); return; } setTimeout(watch,5000); })();
var note='', startVer=null, target='', seenActive=false, postDone=false, lost=false, t0=Date.now(), seq0=null;
show(); stageEl.textContent='Отправка запроса…'; bar.style.width='3%'; detail.textContent='';
// номер последнего отчёта до отправки: отчёт прошлой попытки (например, с ошибкой) не показываем
fetch('/admin/setup/update/progress',{cache:'no-store',credentials:'same-origin'}).then(function(r){ return r.json(); })
.then(function(p){ seq0=p.seq; startVer=p.version; }).catch(function(){ seq0=0; }).then(send);
function send(){
var xhr=new XMLHttpRequest(); xhr.open('POST', form.action);
var file=form.querySelector('input[type=file]');
if(file && file.files.length){
xhr.upload.onprogress=function(e){ if(e.lengthComputable){ stageEl.textContent='Загрузка файла на сервер';
bar.style.width=Math.max(3,Math.round(e.loaded/e.total*25))+'%'; detail.textContent=mb(e.loaded)+' из '+mb(e.total); } };
}
xhr.onloadend=function(){ postDone=true; };
// обычные формы — как браузер (urlencoded), с файлом — multipart
if(file && file.files.length){ xhr.send(new FormData(form)); }
else { xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded'); xhr.send(new URLSearchParams(new FormData(form)).toString()); }
setTimeout(poll, 400);
}
function poll(){
fetch('/admin/setup/update/progress',{cache:'no-store',credentials:'same-origin'}).then(function(r){ if(!r.ok) throw 0; return r.json(); }).then(function(p){
if(startVer===null){ startVer=p.version; }
var key=[p.seq,p.step,p.done,p.finished,p.version].join('/'); if(key!==lastKey){ lastKey=key; lastChange=Date.now(); }
if(p.target) target=p.target;
if(p.note) note=p.note;
// bj перезапустился так быстро, что обрыв связи между опросами не попался: версия ядра
// сменилась или отчёт начался заново (новый процесс) — обновление завершено
if(!lost && startVer!==null && (p.version!==startVer || (seq0>0 && p.seq===0 && !p.active))){ lost=true; }
if(lost){ // bj вернулся после перезапуска
if(!target || p.version===target || p.version!==startVer){ finish('Готово: bj перезапущен, работает версия '+p.version+'.', true); detail.textContent=note ? note.charAt(0).toUpperCase()+note.slice(1)+'.' : ''; return; }
}
var fresh = p.seq!==seq0; // только отчёт этой попытки
if(p.active){ seenActive=true;
var steps=p.steps||4, base=(p.step-1)/steps*100, part=100/steps;
if(p.total>0 && p.step===2){ base+=part*Math.min(1,p.done/p.total); detail.textContent='Скачано '+mb(p.done)+' из '+mb(p.total); }
else if(p.step===3){ detail.textContent='Обычно несколько секунд; при обновлении ядра страница ненадолго пропадёт — это нормально.'; }
bar.style.width=Math.max(5,Math.round(base))+'%';
stageEl.textContent='Шаг '+p.step+' из '+steps+': '+p.stage;
} else if(p.finished && fresh){
if(p.error){ finish('Обновление не выполнено', false); detail.textContent=p.error; return; }
if(target && p.version!==target){ stageEl.textContent='Шаг 4 из 4: перезапуск bj'; bar.style.width='90%'; detail.textContent='Ждём, пока bj поднимется с новой версией…'; }
else { finish('Готово', true); detail.textContent=p.result||''; return; }
} else if(postDone && !fresh && Date.now()-t0>8000){
finish('Готово', true); detail.textContent='Результат — в строке «Последняя проверка».'; return;
}
setTimeout(poll, 800);
}).catch(function(){
lost=true; stageEl.textContent='Шаг 4 из 4: перезапуск bj'; bar.style.width='92%';
detail.textContent='bj перезапускается с новой версией — страница вернётся сама.';
setTimeout(poll, 1500);
});
}
}
document.addEventListener('submit', function(e){
var f=e.target; if(!f || !f.action || e.defaultPrevented) return;
if(!/\/admin\/setup\/update\/(apply|upload)$/.test(new URL(f.action, location.href).pathname)) return;
e.preventDefault(); run(f);
});
})();
</script>
<style>.need-miss{border-color:var(--err)!important;box-shadow:0 0 0 3px color-mix(in srgb,var(--err) 18%,transparent)!important}
.need-msg{color:var(--err);font-size:13.5px;margin:8px 0 0;flex-basis:100%}</style>
<script>
/* Обязательные поля (06.10.2026): кнопка не уходит с пустыми видимыми полями — bj пишет, какие именно не заполнены,
подсвечивает их и ставит курсор в первое. Поля помечены required или data-need="как назвать"; скрытые (другая
вкладка, другой способ) не проверяются. window.bjNeed(form, btn) — то же для кнопок без отправки формы. */
(function(){
function visible(el){ return !el.disabled && el.type!=='hidden' && !!(el.offsetWidth||el.offsetHeight||el.getClientRects().length); }
function name(el){
var n=el.getAttribute('data-need')||el.getAttribute('aria-label')||'';
if(!n){ var l=el.closest('label'); if(l){ var c=l.cloneNode(true); c.querySelectorAll('input,select,textarea,button,.muted,.tooltip').forEach(function(x){x.remove()}); n=c.textContent.replace(/\s+/g,' ').replace(/\(.*?\)/g,'').trim(); } }
return n||el.placeholder||el.name;
}
function msgBox(f, btn){
var host=(btn&&btn.parentElement)||f, m=host.querySelector(':scope > .need-msg');
if(!m){ m=document.createElement('div'); m.className='need-msg'; m.setAttribute('role','alert'); host.appendChild(m); }
return m;
}
window.bjNeed=function(f, btn){
var miss=[], bad=[], first=null;
f.querySelectorAll('[required],[data-need]').forEach(function(el){
el.classList.remove('need-miss');
if(!visible(el)) return;
var empty = el.type==='file' ? !(el.files&&el.files.length) : (el.type==='checkbox'||el.type==='radio') ? false : !String(el.value||'').trim();
if(empty){ miss.push(name(el)); el.classList.add('need-miss'); first=first||el; }
else if(el.checkValidity && !el.checkValidity()){ bad.push(name(el)); el.classList.add('need-miss'); first=first||el; }
});
var old=f.querySelectorAll('.need-msg'); old.forEach(function(x){ x.remove(); });
if(!first) return true;
var t=[]; if(miss.length) t.push('Не заполнено: '+miss.join(', ')); if(bad.length) t.push('Заполнено неверно: '+bad.join(', '));
msgBox(f, btn).textContent=t.join('. ')+'.';
try{ first.focus({preventScroll:false}); first.scrollIntoView({block:'center',behavior:'smooth'}); }catch(e){}
return false;
};
document.addEventListener('DOMContentLoaded',function(){ document.querySelectorAll('form').forEach(function(f){ f.noValidate=true; }); });
document.addEventListener('submit',function(e){
var f=e.target; if(!(f instanceof HTMLFormElement)) return;
var b=e.submitter; if(b&&b.hasAttribute('formnovalidate')) return;
if(!window.bjNeed(f, b)){ e.preventDefault(); e.stopImmediatePropagation(); }
}, true);
document.addEventListener('input',function(e){ var el=e.target; if(el.classList&&el.classList.contains('need-miss')&&String(el.value||'').trim()) el.classList.remove('need-miss'); });
})();
</script>
</body>
</html>
{{end}}