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
1006 lines
65 KiB
HTML
1006 lines
65 KiB
HTML
<!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">&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 "http://127.0.0.1:8099/v1/stable/manifest.json": 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+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>
|