Files
Bridge-and-Join-s/s.html
zuevav 414227796b Настройка: состояние служб ядра и модулей с перезапуском из админки
Модули работают отдельными службами (bj-npr, bj-quikexport, bj-lk), и когда
служба лежит, раздел модуля отвечает ошибкой связи, а администратору
предлагалось идти в консоль за systemctl status. Теперь в разделе «Обновления»
карточка «Службы»: состояние каждой службы, с какого времени, причина отказа
и кнопка «Перезапустить».

Состояние читаем обычным systemctl show — прав не требует. Перезапуск делает
root-демон bj-keymedia (тот же канал, что у установки обновлений) по закрытому
списку служб: чужую службу и сам демон отсюда не тронуть.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL
2026-09-18 19:28:58 +03:00

1006 lines
65 KiB
HTML
Raw Permalink 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.
<!DOCTYPE html>
<html lang="ru" data-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Настройка · Bridge&Join</title>
<style>
: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;
--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);
}
.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; overflow-x:auto; scrollbar-width:none; -ms-overflow-style:none; }
.nav::-webkit-scrollbar { display:none; }
.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; }
.topbar-right { display:flex; align-items:center; gap:12px; flex:none; padding-left:10px; }
@media (max-width:1200px){ .sys-status span:not(.pulse){ display:none } .sys-status{ padding:5px 8px } .topbar .clock{ 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:12px; color:var(--muted); font-family:var(--mono); font-variant-numeric:tabular-nums; letter-spacing:-0.02em; }
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; }
.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); }
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; }
.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 { 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); }
.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; }
.settings { display:grid; grid-template-columns:236px 1fr; gap:26px; align-items:start; }
.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; }
.settings-section > h1 { margin-bottom:18px; }
@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; }
}
.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); }
@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; } }
#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); }
</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">
<span class="brand">
<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>
</span>
<nav class="nav">
<span class="group-label">Оператор</span>
<a href="/admin/" class="">Дашборд</a>
<a href="/admin/claims" class="">Переводы</a>
<a href="/admin/incoming" class="">Входящие<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>
<a href="/admin/fancy-queue" class="">Очередь Fancy</a>
<a href="/admin/quik" class="">Поручения QUIK</a>
<a href="/admin/news" class="">События</a>
<span class="group-label">Администратор</span>
<a href="/admin/setup" class="active">Настройка</a>
<a href="/admin/status" class="">Статус</a>
<a href="/admin/help" class="">Справка</a>
</nav>
<div class="topbar-right">
<span class="sys-status " title="Состояние обмена с НРД">
<span class="pulse"></span>НРД на связи</span>
<span class="clock">14.08.2026 13:17:32</span>
<button class="theme-toggle" id="theme-toggle" title="Светлая/тёмная тема" aria-label="Переключить тему"></button>
</div>
</header>
<main>
<div class="settings">
<nav class="settings-nav">
<button data-sec="overview" class="active"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="3"/></svg></span>Обзор</button>
<button data-sec="db"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5.5" rx="7" ry="2.6"/><path d="M5 5.5v13c0 1.4 3.1 2.6 7 2.6s7-1.2 7-2.6v-13"/><path d="M5 12c0 1.4 3.1 2.6 7 2.6s7-1.2 7-2.6"/></svg></span>База данных<span class="ind ok"></span></button>
<button data-sec="fancy"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M4 8h12M12 4l4 4-4 4"/><path d="M20 16H8M12 20l-4-4 4-4"/></svg></span>Обмен Fancy<span class="ind ok"></span></button>
<button data-sec="quik"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M4 20V4M4 20h16"/><rect x="7" y="12" width="2.6" height="5"/><rect x="12" y="8" width="2.6" height="9"/><rect x="17" y="14" width="2.6" height="3"/></svg></span>QUIK МНП<span class="ind ok"></span></button>
<button data-sec="crypto"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/></svg></span>Криптография<span class="ind ok"></span></button>
<button data-sec="nsd"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21h18M4 21V10M20 21V10M3 10l9-6 9 6M8 21v-7M12 21v-7M16 21v-7"/></svg></span>НРД<span class="ind ok"></span></button>
<button data-sec="lk"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"/><circle cx="9" cy="11" r="2"/><path d="M6 16c.4-1.4 1.6-2.2 3-2.2s2.6.8 3 2.2"/><path d="M15 10h4M15 13h3"/></svg></span>Личный кабинет <span class="muted" style="font-size:10px;font-weight:500">ЕСИА Финанс</span><span class="ind ok"></span></button>
<button data-sec="tests"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M9 3h6M10 3v6l-5 8.5A2 2 0 0 0 6.7 21h10.6a2 2 0 0 0 1.7-3.5L14 9V3"/><path d="M8 15h8"/></svg></span>Тесты</button>
<button data-sec="update"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20V8M6 12l6-6 6 6"/><path d="M5 4h14"/></svg></span>Обновления</button>
<button data-sec="license"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="12" r="4"/><path d="M12 12h9M18 12v3M15 12v2"/></svg></span>Лицензия</button>
</nav>
<div class="settings-body">
<section class="settings-section active" id="sec-overview">
<h1>Обзор</h1>
<div class="card">
<h2>Готовность системы: 3 из 4</h2>
<div class="grid" style="margin-top:12px">
<div class="stat">
<div><span class="dot ok"></span><strong>postgres</strong></div>
<div class="muted" style="font-size:12px;margin-top:4px">настроено</div>
</div>
<div class="stat">
<div><span class="dot ok"></span><strong>crypto-service</strong></div>
<div class="muted" style="font-size:12px;margin-top:4px">настроено</div>
</div>
<div class="stat">
<div><span class="dot ok"></span><strong>nsd-adapter</strong></div>
<div class="muted" style="font-size:12px;margin-top:4px">настроено</div>
</div>
<div class="stat">
<div><span class="dot err"></span><strong>lk-callback</strong></div>
<div class="muted" style="font-size:12px;margin-top:4px">не настроено</div>
</div>
</div>
</div>
</section>
<section class="settings-section" id="sec-db">
<h1>База данных</h1>
<div class="card">
<h2><span class="dot ok"></span>PostgreSQL</h2>
<p class="muted">Журнал сделок m2m-core и принимающая БД. Сейчас: <code>подключено</code>.</p>
<details style="margin-top:14px">
<summary style="cursor:pointer;color:var(--accent);font-size:13px">Изменить подключение</summary>
<form method="post" action="/admin/setup/postgres" style="margin-top:12px;display:grid;gap:10px;max-width:640px">
<label>DSN</label>
<input type="text" name="dsn" value="postgres://bj:bj_dev@127.0.0.1:5432/bj?sslmode=disable" placeholder="postgres://bj:secret@127.0.0.1:5432/bj?sslmode=disable">
<p class="muted" style="margin:0">При сохранении выполняется Ping.</p>
<div><button type="submit" class="btn">Сохранить и проверить</button></div>
</form>
</details>
</div>
</section>
<section class="settings-section" id="sec-fancy">
<h1>Обмен Fancy (MSSQL / PostgreSQL)</h1>
<div class="card" id="sec-fancy-conn">
<h2><span class="dot ok"></span>Подключение к серверу БД клиента</h2>
<p class="muted">Канал обмена с Fancy: запрос из ЛК → таблица в БД → «кран» Fancy проверяет по своей БД и пишет ответ → bj-server забирает. Эта же БД — журнал обмена с НРД. Поддерживаются <strong>Microsoft SQL Server</strong> и <strong>PostgreSQL</strong>.<br>
<a href="/admin/help/fancy">Справка для разработчика Fancy</a> — как подготовить сервер БД (создать БД, пользователя, права), структура таблиц, контракт «крана», копируемый SQL.</p>
<form method="post" action="/admin/setup/fancy" style="margin-top:12px;display:grid;gap:10px;max-width:560px">
<label>Тип СУБД
<select name="db_type" id="fancy-dbtype" style="width:100%" onchange="var p=document.getElementById('fancy-port');if(!p.value||p.value=='1433'||p.value=='5432'){p.value=this.value=='postgres'?'5432':'1433';}">
<option value="mssql" selected>Microsoft SQL Server</option>
<option value="postgres" >PostgreSQL</option>
</select></label>
<label>Сервер (host или host\instance)
<input type="text" name="server" value="10.10.10.21" placeholder="10.10.10.x" style="width:100%" required></label>
<label>Порт
<input type="number" id="fancy-port" name="port" value="1433" style="width:100%"></label>
<label>Имя БД (латиница/цифры/_, с буквы)
<input type="text" name="database" value="bj_exchange" placeholder="bj_exchange" pattern="[A-Za-z][A-Za-z0-9_]*" style="width:100%" required></label>
<label>Пользователь (логин БД)
<input type="text" name="user" value="bj_user" placeholder="bj_user" style="width:100%" required></label>
<label>Пароль
<input type="password" name="password" placeholder="•••••• — оставьте пустым, чтобы не менять" autocomplete="off" style="width:100%"></label>
<label>Шифрование канала
<select name="encrypt" style="width:100%">
<option value="disable" selected>disable — без TLS (типично для LAN)</option>
<option value="true" >true — с TLS</option>
</select></label>
<div><button type="submit" class="btn btn-ok">Сохранить</button></div>
</form>
</div>
<div class="card">
<h2>Подготовка базы данных</h2>
<p class="muted">Порядок: 1) проверить соединение → 2) создать БД (или создайте её на сервере руками) → 3) налить схему (таблицы). После этого БД готова к обмену.</p>
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:10px">
<form method="post" action="/admin/setup/fancy/test" style="margin:0"><button type="submit" class="btn">① Проверить соединение</button></form>
<form method="post" action="/admin/setup/fancy/create-db" style="margin:0"><button type="submit" class="btn">② Создать БД</button></form>
<form method="post" action="/admin/setup/fancy/apply-schema" style="margin:0"><button type="submit" class="btn btn-ok">③ Налить схему</button></form>
</div>
<p class="muted" style="margin-top:12px">Статус схемы:
<span style="color:var(--ok)">✓ налита — БД готова</span></p>
<details style="margin-top:8px">
<summary style="cursor:pointer;font-weight:600">Что будет создано (черновик схемы, согласуем с Fancy)</summary>
<ul class="muted" style="font-size:13px;margin-top:8px">
<li><code>bj_transfer_requests</code> — шапка запроса из ЛК: колонки 1:1 с XML НРД (guid, sender/receiver_code, cost_code, инвестор, документ, ИИС, transferring/receiving INN)</li>
<li><code>bj_transfer_securities</code> — бумаги запроса, по строке на бумагу: reference_id, security_code, isin, quantity, settlement_inn, deponent_code, account_id, section_id, isolation_status</li>
<li><code>bj_transfer_responses</code> — ответ «крана» после проверки по БД Fancy (verdict, reason)</li>
<li><code>bj_nsd_log</code> — журнал обмена с НРД (direction, doc_type, коды, raw XML)</li>
</ul>
</details>
</div>
</section>
<section class="settings-section" id="sec-quik">
<h1>QUIK — Модуль неторговых поручений</h1>
<div class="card">
<h2><span class="dot ok"></span>Подключение к БД МНП QUIK (MSSQL)</h2>
<p class="muted">При выводе бумаг клиентом bj блокирует их количество в QUIK через API Модуля неторговых поручений (хранимые процедуры). БД <strong>боевая</strong> — тест только на чтение (версия + наличие M2M-объектов), без записи. Подключение под логином с ролью <code>InstrEu</code>.<br>
<a href="/admin/help/fancy">Про архитектуру M2M</a> · документация QUIK МНП 21.4.0.</p>
<form method="post" action="/admin/setup/quik" style="margin-top:12px;display:grid;gap:10px;max-width:560px">
<label>Сервер (host или host\instance)
<input type="text" name="server" value="91.209.122.88" placeholder="91.209.122.88" style="width:100%" required></label>
<label>Порт
<input type="number" name="port" value="8849" placeholder="8849" style="width:100%"></label>
<label>Имя БД (если требуется, иначе оставьте пустым)
<input type="text" name="database" value="88_INSTR" placeholder="(по умолчанию)" style="width:100%"></label>
<label>Логин (роль InstrEu)
<input type="text" name="user" value="BROKER_88" placeholder="BROKER_88" style="width:100%" required></label>
<label>Пароль
<input type="password" name="password" placeholder="•••••• — оставьте пустым, чтобы не менять" autocomplete="off" style="width:100%"></label>
<label>Шифрование канала
<select name="encrypt" style="width:100%">
<option value="true" selected>true — с TLS (боевой сервер обычно требует)</option>
<option value="disable" >disable — без TLS</option>
</select></label>
<label style="display:flex;gap:8px;align-items:center;font-weight:400">
<input type="checkbox" name="trust" value="1" checked>
Доверять сертификату сервера (TrustServerCertificate — для самоподписанного)</label>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<button type="submit" class="btn btn-ok">Сохранить</button>
</div>
</form>
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:12px">
<form method="post" action="/admin/setup/quik/test" style="margin:0"><button type="submit" class="btn">Проверить подключение (чтение)</button></form>
</div>
<p class="muted" style="margin-top:12px">Статус: <span style="color:var(--ok)">✓ подключение проверено</span></p>
</div>
<div class="card">
<h2>Полный ответ сервера (последний тест)</h2>
<p class="muted">Можно выделить и скопировать целиком — для отправки коллегам/аутсорсеру QUIK.
Тест от 20.07.2026 09:50:50 UTC.</p>
<div style="display:flex;gap:8px;margin:8px 0">
<button type="button" class="btn" onclick="var t=document.getElementById('quik-report').textContent;navigator.clipboard&&navigator.clipboard.writeText(t);this.textContent='Скопировано ✓';setTimeout(()=>this.textContent='Скопировать отчёт',1500)">Скопировать отчёт</button>
</div>
<pre id="quik-report" style="white-space:pre-wrap;word-break:break-word;background:var(--surface-2,#0d1117);color:var(--code-fg,#c9d1d9);padding:14px;border-radius:8px;font-size:12.5px;line-height:1.5;overflow:auto;max-height:420px;user-select:all">=== Тест подключения к БД МНП QUIK ===
Время: 2026-07-20 12:50:50 MSK
Сервер: 91.209.122.88:8849
БД: 88_INSTR
Логин: BROKER_88
Шифрование: encrypt=true, TrustServerCertificate=true
Клиент (bj): исходящий IP 195.239.71.22, драйвер go-mssqldb
----------------------------------------
РЕЗУЛЬТАТ: ПОДКЛЮЧЕНИЕ OK
Версия сервера: Microsoft SQL Server 2016 (SP3-GDR) (KB5029186) - 13.0.6435.1 (X64)
Текущая БД: 88_INSTR
Логин на серв.: BROKER_88
M2M-готовность: true — БД похожа на МНП с поддержкой M2M — ключевые объекты найдены.
Объекты МНП/M2M (видимость под логином):
proc:sp_CancelInstr нет
proc:sp_NewInstrSec ДА
proc:sp_SignInstr нет
table:Contracts ДА
table:Export ДА
table:Export2 ДА
table:Instructions ДА
table:M2MDepoRecipients ДА
table:QuikSecurities ДА
</pre>
</div>
</section>
<section class="settings-section" id="sec-crypto">
<h1>Криптография</h1>
<div class="card" style="background:var(--accent-weak)">
<h2>Установка ключа на флешку</h2>
<p class="muted">Пошаговый мастер: загрузить архив НРД с паролем → запись на флешку → справочник сертификатов → проверка Валидаты → готово.</p>
<a href="/admin/setup/keywizard" class="btn btn-ok" style="margin-top:10px;display:inline-block">Открыть мастер установки ключа →</a>
</div>
<div class="card">
<h2><span class="dot ok"></span>СКЗИ «Валидата Клиент L»</h2>
<p class="muted">Активный профиль: <code>moex</code> · провайдер <code>validata</code>. Подробно — <a href="/admin/help/crypto">справка</a>.</p>
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:12px">
<form method="post" action="/admin/setup/crypto/check" style="margin:0"><button type="submit" class="btn btn-ok">✓ Проверить СКЗИ</button></form>
<form method="post" action="/admin/setup/crypto/test-sign" style="margin:0"><button type="submit" class="btn">✎ Тестовая подпись</button></form>
<form method="post" action="/admin/setup/restart-crypto" style="margin:0" onsubmit="return confirm('Перезапустить crypto-service? Поднимется через ~5 сек.');"><button type="submit" class="btn btn-warn">↻ crypto-service</button></form>
<form method="post" action="/admin/setup/restart-server" style="margin:0" onsubmit="return confirm('Перезапустить bj-server? Через 5-10 сек страница вернётся.');"><button type="submit" class="btn btn-secondary">↻ bj-server</button></form>
</div>
<details style="margin-top:14px">
<summary style="cursor:pointer;color:var(--accent);font-size:13px">Параметры провайдера (для совместимости)</summary>
<form method="post" action="/admin/setup/crypto" style="margin-top:12px;display:grid;gap:10px;max-width:640px">
<label>Провайдер</label>
<select name="provider">
<option value="stub" >stub — без криптографии (демо)</option>
<option value="validata" selected>Валидата Клиент L</option>
</select>
<label>Путь к модулю PKCS#11</label>
<input type="text" name="module_path" value="" placeholder="/opt/Validata/VDCSP/lib/amd64/libvdpkcs11.so">
<label>UDS-сокет</label>
<input type="text" name="socket_path" value="/run/bj/crypto.sock" placeholder="/run/bj/crypto.sock">
<div><button type="submit" class="btn">Сохранить</button></div>
</form>
</details>
</div>
<div class="card" id="sec-cacerts">
<h2><span class="dot warn"></span>Сертификаты УЦ (авто-загрузка)</h2>
<p class="muted">URL .cer-файлов УЦ НРД (<a href="https://www.nsd.ru/workflow/system/cryptography/" target="_blank" rel="noopener">nsd.ru</a>). Скачиваются, парсятся и сохраняются в <code>/var/lib/bj/ca-certs/</code>.</p>
<form method="post" action="/admin/setup/cacerts" style="margin-top:10px;display:grid;gap:10px;max-width:720px">
<input type="hidden" name="section" value="sec-cacerts">
<label>URL'ы (один на строку)</label>
<textarea name="urls" rows="3" style="font-family:ui-monospace,monospace;font-size:12px"></textarea>
<label style="display:flex;align-items:center;gap:8px;cursor:pointer"><input type="checkbox" name="auto_update" checked style="width:auto"> Авто-обновление раз в сутки</label>
<div style="display:flex;gap:8px"><button type="submit" class="btn">Сохранить</button></div>
</form>
<form method="post" action="/admin/setup/cacerts/fetch" style="margin-top:8px"><input type="hidden" name="section" value="sec-cacerts"><button type="submit" class="btn btn-ok">⬇ Скачать сейчас</button></form>
</div>
</section>
<section class="settings-section" id="sec-nsd">
<h1>НРД</h1>
<div class="card">
<h2><span class="dot ok"></span>Интеграционный шлюз (ИШ)</h2>
<p class="muted">Профиль <code>test3-gost</code>, ИШ <code>http://localhost:8090</code>. Стенды и установка — <a href="/admin/help/systems">справка</a>.</p>
<form method="post" action="/admin/setup/nsd" style="margin-top:12px;display:grid;gap:10px;max-width:680px">
<label>Профиль / контур</label>
<select name="profile" id="nsd-profile">
<option value="" >— mock (демо) —</option>
<option value="guest-gost" >guest-gost — GUEST, ГОСТ</option>
<option value="guest-rsa" >guest-rsa — GUEST, RSA</option>
<option value="test3-gost" selected>test3-gost — TEST3, ГОСТ</option>
<option value="test3-rsa" >test3-rsa — TEST3, RSA</option>
<option value="prod-gost" >prod-gost — ПРОМ, ГОСТ</option>
<option value="prod-rsa" >prod-rsa — ПРОМ, RSA</option>
</select>
<label>URL ONYX (WSL) НРД</label>
<input type="text" name="igw_base_url" id="nsd-url" value="http://localhost:8090" placeholder="автозаполнится по профилю">
<label>Ключевой контейнер</label>
<input type="text" name="key_container" id="nsd-container" value="TEST3_GOST_CONTAINER" placeholder="напр. TEST3_GOST_CONTAINER">
<hr style="border:none;border-top:1px solid var(--border);margin:6px 0">
<p class="muted" style="margin:0">Депозитарные реквизиты (откуда списываются бумаги) — из договора/письма НРД. Нужны для формирования заявки на перевод.</p>
<label>Депозитарный код</label>
<input type="text" name="deponent_code" value="MC0413600000" placeholder="напр. MC0413600000">
<label>Депозитарный счёт</label>
<input type="text" name="account_id" value="HL171004001C" placeholder="депозитарный счёт">
<label>Раздел счёта</label>
<input type="text" name="section_id" value="36MC0413600000F00" placeholder="раздел депозитарного счёта">
<div><button type="submit" class="btn">Сохранить и проверить</button></div>
</form>
<script>
(function() {
var urls = {
"guest-gost": ["https://gost-gt.nsd.ru/onyx-ms/OnyxEdoWSService/OnyxEdo", "GUEST_GOST_CONTAINER"],
"guest-rsa": ["https://rsa-gt.nsd.ru/onyx-ms/OnyxEdoWSService/OnyxEdo", "GUEST_RSA_CONTAINER"],
"test3-gost": ["https://gost-t3.nsd.ru/onyx-ms/OnyxEdoWSService/OnyxEdo", "TEST3_GOST_CONTAINER"],
"test3-rsa": ["https://rsa-t3.nsd.ru/onyx-ms/OnyxEdoWSService/OnyxEdo", "TEST3_RSA_CONTAINER"],
"prod-gost": ["https://gost.nsd.ru/onyx-ms/OnyxEdoWSService/OnyxEdo", "PROD_GOST_CONTAINER"],
"prod-rsa": ["https://rsa.nsd.ru/onyx-ms/OnyxEdoWSService/OnyxEdo", "PROD_RSA_CONTAINER"]
};
var p = document.getElementById("nsd-profile"), u = document.getElementById("nsd-url"), c = document.getElementById("nsd-container");
if (p) p.addEventListener("change", function() {
var v = p.value;
if (urls[v] && (!u.value || confirm("Заменить URL и контейнер на дефолт для " + v + "?"))) { u.value = urls[v][0]; c.value = urls[v][1]; }
});
})();
</script>
</div>
</section>
<section class="settings-section" id="sec-lk">
<h1>Личный кабинет <span class="muted" style="font-size:16px;font-weight:500">· ЕСИА Финанс</span></h1>
<p class="muted" style="margin:-8px 0 16px">Интеграция с платформой ЛК <strong>ЕСИА Финанс</strong>: адрес для клиента, приём заявок M2M, возврат статуса и подключение к API (тест/бой). НРД настраивается отдельно — в разделе «НРД».</p>
<div class="card">
<h2><span class="dot ok"></span>Личный кабинет — интеграция</h2>
<p class="muted">Адрес ЛК (стенд/бой), куда клиент подаёт заявку M2M. Заявка приходит к нам по контракту <code>POST /api/v1/back_office/claims/</code>, а мы возвращаем статус через <strong>Callback URL</strong>.</p>
<form method="post" action="/admin/setup/lk" style="margin-top:12px;display:grid;gap:12px;max-width:640px">
<div>
<label>Адрес ЛК</label>
<div style="display:flex;gap:8px;align-items:center;margin-top:4px">
<input type="text" name="lk_public_url" value="https://fontvielle.ef.winr.pro" placeholder="https://fontvielle.ef.winr.pro" style="flex:1">
<a class="btn btn-secondary" href="https://fontvielle.ef.winr.pro" target="_blank" rel="noopener">Открыть →</a>
</div>
<span class="muted" style="font-size:12px">Показывается ссылкой на дашборде для первичного теста.</span>
</div>
<div>
<label>Callback URL <span class="muted" style="font-weight:400">(куда bj шлёт статус заявки)</span></label>
<input type="text" name="callback_url" value="" placeholder="https://fontvielle.ef.winr.pro" style="margin-top:4px">
<span class="muted" style="font-size:12px">Не настроен — уведомления в ЛК отключены. Для обмена с НРД не требуется.</span>
</div>
<div><button type="submit" class="btn">Сохранить и проверить</button></div>
</form>
</div>
<div class="card">
<h2>Подключение к API ЛК — тест и бой</h2>
<p class="muted">Реквизиты, по которым bj обращается к API платформы ЛК ЕСИА Финанс (HTTP Basic). Заполните нужные окружения и нажмите <strong>«Проверить связь»</strong> — сразу видно, есть подключение или нет.</p>
<div class="grid" style="grid-template-columns:repeat(auto-fit,minmax(300px,1fr));margin-top:12px">
<div style="border:1px solid var(--border);border-radius:10px;padding:16px;background:var(--card-2)">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:10px">
<strong>Тестовый стенд</strong>
<span class="badge confirmed">связь есть</span>
</div>
<form method="post" action="/admin/setup/lk-integration" style="display:grid;gap:9px">
<input type="hidden" name="env" value="test">
<div><label>Адрес API</label><input type="text" name="base_url" value="https://fontvielle.ef.winr.pro" placeholder="https://fontvielle.ef.winr.pro/manager/api" style="margin-top:3px"></div>
<div><label>Логин</label><input type="text" name="login" value="fontvielle" autocomplete="off" style="margin-top:3px"></div>
<div><label>Пароль</label><input type="password" name="password" value="" placeholder="•••••• (не менять — оставьте пустым)" autocomplete="new-password" style="margin-top:3px"></div>
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:2px">
<button type="submit" class="btn btn-secondary">Сохранить</button>
<button type="submit" formaction="/admin/setup/lk-check" class="btn">Проверить связь</button>
</div>
</form>
<p class="muted" style="font-size:12px;margin-top:8px">Последняя проверка: связь есть (HTTP 200) · 14.08 12:28</p>
</div>
<div style="border:1px solid var(--border);border-radius:10px;padding:16px;background:var(--card-2)">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:10px">
<strong>Боевой контур</strong>
<span class="badge confirmed">связь есть</span>
</div>
<form method="post" action="/admin/setup/lk-integration" style="display:grid;gap:9px">
<input type="hidden" name="env" value="prod">
<div><label>Адрес API</label><input type="text" name="base_url" value="https://lk.fontvielle.ru" placeholder="https://lk.example.ru/manager/api" style="margin-top:3px"></div>
<div><label>Логин</label><input type="text" name="login" value="fontvielle" autocomplete="off" style="margin-top:3px"></div>
<div><label>Пароль</label><input type="password" name="password" value="" placeholder="•••••• (не менять — оставьте пустым)" autocomplete="new-password" style="margin-top:3px"></div>
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:2px">
<button type="submit" class="btn btn-secondary">Сохранить</button>
<button type="submit" formaction="/admin/setup/lk-check" class="btn">Проверить связь</button>
</div>
</form>
<p class="muted" style="font-size:12px;margin-top:8px">Последняя проверка: связь есть (HTTP 200) · 14.08 12:21</p>
</div>
</div>
<form method="post" action="/admin/setup/lk-integration" style="margin-top:14px;display:flex;gap:10px;align-items:center;flex-wrap:wrap">
<input type="hidden" name="env" value="test">
<label style="font-weight:500">Активное окружение:</label>
<select name="active_env" onchange="this.form.submit()">
<option value="test" >Тест</option>
<option value="prod" >Бой</option>
</select>
<span class="muted" style="font-size:12px">bj работает с ЛК через выбранное окружение.</span>
</form>
</div>
<div class="card">
<h2>Симулятор заявки M2M</h2>
<p class="muted">Пока подача через веб-форму у ЕСИА Финанс не готова — bj сам собирает заявку <code>depo_m2m</code> и <strong>отправляет её в API ЛК</strong> (<code>POST /claims/</code>) через активное окружение. Ответ показывается ниже; затем заявку ловим опросом в «Переводы». Поля предзаполнены рабочим тестовым договором.</p>
<form method="post" action="/admin/setup/lk-simulate" style="margin-top:12px">
<div class="grid" style="grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px">
<div><label>ID договора (owner_id)</label><input type="text" name="contract_id" value="1335" style="margin-top:3px"></div>
<div><label>ID клиента (user_id)</label><input type="text" name="user_id" value="771" style="margin-top:3px"></div>
<div><label>Счёт депо (account)</label><input type="text" name="account" value="123" style="margin-top:3px"></div>
<div><label>Раздел (section)</label><input type="text" name="section" value="124" style="margin-top:3px"></div>
</div>
<div class="grid" style="grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px;margin-top:12px">
<div><label>Название бумаги</label><input type="text" name="paper_name" value="ВК ао" style="margin-top:3px"></div>
<div><label>ISIN</label><input type="text" name="paper_isin" value="RU000A106YF0" style="margin-top:3px"></div>
<div><label>Вид бумаги</label>
<select name="paper_type" style="margin-top:3px">
<option value="share">Акция (share)</option>
<option value="bond">Облигация (bond)</option>
<option value="fund">Фонд (fund)</option>
<option value="depositary_receipt">Депозитарная расписка</option>
</select></div>
<div><label>№ госрегистрации</label><input type="text" name="paper_reg" value="1-01-16453-A" style="margin-top:3px"></div>
<div><label>Количество</label><input type="text" name="paper_count" value="1" style="margin-top:3px"></div>
</div>
<div class="grid" style="grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px;margin-top:12px">
<div><label>ИНН контрагента</label><input type="text" name="contragent_inn" value="7702165310" style="margin-top:3px"></div>
<div><label>Наименование контрагента</label><input type="text" name="contragent_name" value="Тест-депозитарий" style="margin-top:3px"></div>
<div><label>Место расчётов</label>
<select name="place_of_payment" style="margin-top:3px">
<option value="nrd">НРД</option>
<option value="spb">СПБ</option>
</select></div>
<div style="display:flex;align-items:flex-end"><label style="display:flex;align-items:center;gap:8px;font-weight:400"><input type="checkbox" name="expense_information" style="width:auto"> Передать информацию о расходах</label></div>
</div>
<div style="margin-top:16px"><button type="submit" class="btn">Собрать и отправить в ЛК</button></div>
</form>
<p class="muted" style="font-size:12px;margin-top:10px">Результат (сырой ответ API) появится вверху страницы. Если «Раздел счёта не доступен для быстрых переводов» — Финансы должны включить признак M2M на разделе счёта.</p>
</div>
</section>
<section class="settings-section" id="sec-tests">
<h1>Тесты</h1>
<div class="card">
<h2>Тестовый пакет роботу НРД</h2>
<p class="muted">Робот <code>MC0012500000</code> эмулирует вторую сторону перевода. Выберите сценарий — bj-server отправит эталонный запрос через ИШ, ответ придёт во входящие. Требуется настроенный ИШ + профиль Валидаты.</p>
<form method="post" action="/admin/setup/test-nsd" style="margin-top:12px;display:flex;gap:8px;flex-wrap:wrap;align-items:center">
<select name="scenario">
<option value="2001">2001 — Принять все бумаги</option>
<option value="2002">2002 — Принять частично</option>
<option value="1111">1111 — Ответ с отказом</option>
<option value="3333">3333 — Робот принимающая сторона</option>
</select>
<button type="submit" class="btn">Отправить роботу</button>
</form>
<p class="muted" style="margin-top:6px;font-size:12px">Ответ робота — асинхронно (~30-60 сек) во входящие ИШ.</p>
</div>
<div class="card">
<h2>Сквозной тестовый прогон (mock/реальный)</h2>
<p class="muted">Заявка с предзаполненными данными через всю цепочку до финального статуса.</p>
<form method="post" action="/admin/setup/test-run" style="margin-top:12px"><button type="submit" class="btn">Запустить тестовую заявку</button></form>
<div style="margin-top:16px;padding:14px;background:var(--card-2);border:1px solid var(--border);border-radius:10px">
<strong>Последний прогон</strong>
<table style="margin-top:8px">
<tbody>
<tr><td style="width:160px" class="muted">Статус</td><td><span style="color:var(--ok)">✓ успешно</span></td></tr>
<tr><td class="muted">FSM-статус</td><td><code>awaiting_decision</code></td></tr>
<tr><td class="muted">ClaimID</td><td><code>ff99dc31-d5a2-44e7-9595-1170cf2a821a</code> <a href="/admin/claims/ff99dc31-d5a2-44e7-9595-1170cf2a821a">→ карточка</a></td></tr>
<tr><td class="muted">Сообщение</td><td>Сценарий 3333 «Робот — принимающая сторона»: пакет отправлен роботу MC0012500000 (GUID ff99dc31-d5a2-44e7-9595-1170cf2a821a). Ждём ответ робота во входящих ИШ.</td></tr>
</tbody>
</table>
</div>
</div>
</section>
<section class="settings-section" id="sec-update">
<h1>Обновления</h1>
<div class="card">
<h2>Версия bj-server</h2>
<div style="display:flex;align-items:center;gap:14px;flex-wrap:wrap;margin-top:6px">
<span class="stat-value" style="font-size:20px">0.1.0</span>
<form method="post" action="/admin/setup/update/check" style="margin:0"><button type="submit" class="btn btn-secondary">Проверить обновления</button></form>
</div>
<p class="muted" style="margin-top:6px;font-size:12px">Последняя проверка: 17.06.2026 09:45 — проверка не удалась: release: запрос манифеста: Get &#34;http://127.0.0.1:8099/v1/stable/manifest.json&#34;: dial tcp 127.0.0.1:8099: connect: connection refused</p>
</div>
<div class="card">
<h2><span class="dot warn"></span>Источник обновлений</h2>
<p class="muted">Артефактория раздаёт подписанные релизы. Обновления проверяются по подписи Ed25519 и sha256 — без валидной подписи установка не выполняется.</p>
<form method="post" action="/admin/setup/update" style="margin-top:12px;display:grid;gap:10px;max-width:680px">
<label>URL артефактории</label>
<input type="text" name="base_url" value="" placeholder="https://updates.example.com">
<label>Канal</label>
<select name="channel">
<option value="stable" selected>stable — стабильный</option>
<option value="beta" >beta — предварительный</option>
</select>
<label>Публичный ключ издателя (base64 Ed25519)</label>
<input type="text" name="public_key" value="" placeholder="зашит в релиз; переопределить здесь">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer"><input type="checkbox" name="auto_check" style="width:auto"> Проверять автоматически (раз в 6 часов)</label>
<div><button type="submit" class="btn">Сохранить</button></div>
</form>
</div>
</section>
<section class="settings-section" id="sec-license">
<h1>Лицензия</h1>
<div class="card">
<h2>
<span class="dot warn"></span>Не активирована
</h2>
<p class="muted">Лицензионный ключ не введён. Без лицензии сервис работает, но автообновления заблокированы.</p>
</div>
<div class="card">
<h2>Активация</h2>
<p class="muted">Вставьте лицензионный ключ, полученный от поставщика. Проверка офлайн по подписи — связь с сервером лицензий не требуется.</p>
<form method="post" action="/admin/setup/license" style="margin-top:12px;display:grid;gap:10px;max-width:720px">
<label>Лицензионный ключ</label>
<textarea name="key" rows="3" style="font-family:ui-monospace,monospace;font-size:11px" placeholder="payload.signature.keyid"></textarea>
<details>
<summary style="cursor:pointer;color:var(--accent);font-size:13px">Публичный ключ издателя (если не зашит)</summary>
<input type="text" name="public_key" value="/2AA/Sqr07zXfXPR&#43;3zHoGyvZBk55GGeDNX4CvCuwXQ=" placeholder="base64 Ed25519" style="margin-top:8px;width:100%">
</details>
<div><button type="submit" class="btn">Активировать</button></div>
</form>
</div>
</section>
</div>
</div>
<script>
(function() {
var navs = document.querySelectorAll('.settings-nav button');
var secs = document.querySelectorAll('.settings-section');
function show(id) {
navs.forEach(function(b){ b.classList.toggle('active', b.dataset.sec === id); });
secs.forEach(function(s){ s.classList.toggle('active', s.id === 'sec-' + id); });
try { sessionStorage.setItem('bj-setup-tab', id); } catch(e) {}
try { history.replaceState(null, '', '#' + id); } catch(e) {}
}
navs.forEach(function(b){ b.addEventListener('click', function(){ show(b.dataset.sec); }); });
var h = (location.hash || '').replace('#','');
var afterSave = /[?&](flash|scrollto)=/.test(location.search);
if (h && document.getElementById('sec-' + h)) {
show(h);
} else if (afterSave) {
var saved = null;
try { saved = sessionStorage.getItem('bj-setup-tab'); } catch(e) {}
if (saved && document.getElementById('sec-' + saved)) show(saved);
}
})();
</script>
</main>
<script>
(function() {
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');
});
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); }
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);
}
if ('scrollRestoration' in history) { try { history.scrollRestoration = 'manual'; } catch(e) {} }
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) {}
if (!f.querySelector('input[name="return_to"]')) {
var rt = document.createElement('input');
rt.type = 'hidden'; rt.name = 'return_to'; rt.value = location.pathname;
f.appendChild(rt);
}
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);
}
(function() {
var badge = document.getElementById('nav-incoming-badge');
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) {
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; };
}
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){
if (badge) {
if (d.count > 0) { badge.textContent = d.count; badge.style.display = 'inline-block'; }
else { badge.style.display = 'none'; }
}
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, 25000);
})();
})();
</script>
</body>
</html>