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
1001 lines
75 KiB
HTML
1001 lines
75 KiB
HTML
{{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">&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}}
|