Files
Bridge-and-Join-s/internal/lkgateway/web/templates/layout.html
T

577 lines
38 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{define "layout"}}<!DOCTYPE html>
<html lang="ru" data-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{.Title}} · Bridge&Join</title>
<style>
/* ===================== Дизайн-система ===================== */
/* «Диспетчерская депозитария»: приборная панель оператора НРД/M2M.
Restrained — прохладные нейтрали + один индиго-акцент; MOEX-красный
зарезервирован строго под знак бренда и критику. Все величины —
моноширинными табличными цифрами (приборная подпись). */
/* Светлая тема (дневной свет, рабочее место) */
:root {
--bg:#eef1f6; --bg-elev:#ffffff; --card:#ffffff; --card-2:#f5f7fb;
--border:#e1e6ef; --border-strong:#cbd3e1;
--text:#111725; --text-2:#3d465a; --muted:#69748a;
--accent:#3452d6; --accent-weak:rgba(52,82,214,0.09); --accent-strong:#2740b0;
--ok:#12813f; --ok-weak:rgba(18,129,63,0.11);
--warn:#9a5b06; --warn-weak:rgba(154,91,6,0.12);
--err:#c22a2a; --err-weak:rgba(194,42,42,0.10);
--brand:#c5203e; /* MOEX-красный — только знак бренда и критика */
--grid-line:rgba(17,23,37,0.045);
--shadow:0 1px 2px rgba(16,24,40,0.05), 0 2px 6px rgba(16,24,40,0.06);
--shadow-lg:0 12px 34px rgba(16,24,40,0.14);
--radius:11px; --radius-sm:7px; --radius-lg:15px;
--mono:ui-monospace,"SF Mono","JetBrains Mono",Menlo,Consolas,monospace;
--ease:cubic-bezier(.2,.7,.2,1);
}
/* Тёмная тема (терминал диспетчерской) */
[data-theme="dark"] {
--bg:#0a0d13; --bg-elev:#0f141d; --card:#131926; --card-2:#19212f;
--border:#232c3d; --border-strong:#354057;
--text:#e8edf5; --text-2:#aab6c9; --muted:#828fa6;
--accent:#7089ff; --accent-weak:rgba(112,137,255,0.15); --accent-strong:#94a7ff;
--ok:#3fce74; --ok-weak:rgba(63,206,116,0.15);
--warn:#e2ab41; --warn-weak:rgba(226,171,65,0.16);
--err:#ff7d7d; --err-weak:rgba(255,125,125,0.15);
--brand:#ff5670;
--grid-line:rgba(255,255,255,0.035);
--shadow:0 1px 2px rgba(0,0,0,0.4); --shadow-lg:0 14px 38px rgba(0,0,0,0.55);
}
/* Приборные цифры: величины (ID, счётчики, время, коды) — моно, табличные. */
.num, .stat-value, .clock, td .id, .mono { font-variant-numeric:tabular-nums; }
* { box-sizing: border-box; }
html, body { margin:0; padding:0; }
body {
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
background: var(--bg); color: var(--text); line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
/* ---- Шапка (панель управления) ---- */
.topbar {
display:flex; align-items:center; gap:18px;
padding:0 22px; height:56px;
background: var(--bg-elev); border-bottom:1px solid var(--border);
position:sticky; top:0; z-index:100;
}
.brand { display:flex; align-items:center; gap:10px; font-weight:680; font-size:15.5px; letter-spacing:-0.015em; color:var(--text); flex:none; }
.brand svg { width:26px; height:26px; display:block; flex:none; }
.brand b { font-weight:730; } .brand .brand-j { color:var(--muted); font-weight:600; }
.nav { display:flex; align-items:center; gap:1px; margin-left:6px; flex:1 1 auto; min-width:0; flex-wrap:wrap; }
.nav::-webkit-scrollbar { display:none; }
.user-chip { display:inline-flex; flex-direction:column; line-height:1.15; margin-right:6px; max-width:180px; }
.user-chip .user-name { font-size:12.5px; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.user-chip .user-role { font-size:10px; color:var(--muted); text-transform:uppercase; letter-spacing:.05em; }
.user-logout { font-size:12px; color:var(--muted); text-decoration:none; padding:4px 8px; border:1px solid var(--border); border-radius:8px; margin-right:6px; }
.user-logout:hover { color:var(--text); border-color:var(--border-strong); }
.nav .group-label { font-size:9.5px; text-transform:uppercase; letter-spacing:0.09em; color:var(--muted); padding:0 9px 0 15px; border-left:1px solid var(--border); margin-left:7px; font-weight:600; }
.nav a {
color:var(--text-2); text-decoration:none; font-size:13px; font-weight:500;
padding:7px 11px; border-radius:8px; white-space:nowrap; transition:background .12s var(--ease), color .12s var(--ease);
}
.nav a:hover { background:var(--card-2); color:var(--text); }
.nav a.active { background:var(--accent-weak); color:var(--accent-strong); font-weight:600; }
.nav .dd { position:relative; }
.nav .dd .ddbtn { display:inline-block; cursor:pointer; color:var(--text-2); font-size:13px; font-weight:500;
padding:7px 11px; border-radius:8px; white-space:nowrap; transition:background .12s var(--ease); }
.nav .dd:hover .ddbtn, .nav .dd.active .ddbtn { background:var(--accent-weak); color:var(--accent-strong); font-weight:600; }
.nav .dd:hover .ddmenu, .nav .dd:focus-within .ddmenu { display:block; }
/* невидимый «мост» под кнопкой — чтобы при спуске мыши к меню hover не срывался */
.nav .dd::after { content:''; position:absolute; top:100%; left:0; right:0; height:10px; }
.nav .ddmenu { display:none; position:absolute; top:100%; left:0; margin-top:8px; background:var(--card); border:1px solid var(--border);
border-radius:10px; box-shadow:0 8px 30px rgba(0,0,0,.14); min-width:210px; z-index:60; padding:6px; }
.nav .ddmenu a { display:block; padding:8px 12px; border-radius:7px; }
.topbar-right { display:flex; align-items:center; gap:12px; flex:none; padding-left:10px; }
@media (max-width:1100px){ .sys-status span:not(.pulse){ display:none } .sys-status{ padding:5px 8px } .topbar .clock{ display:none } .user-dd .user-role{ display:none } }
/* Строка системного статуса — состояние читается всегда */
.sys-status { display:inline-flex; align-items:center; gap:7px; font-size:12px; font-weight:600; color:var(--text-2); padding:5px 11px 5px 9px; border:1px solid var(--border); border-radius:20px; background:var(--card-2); white-space:nowrap; }
.sys-status .pulse { width:8px; height:8px; border-radius:50%; background:var(--ok); box-shadow:0 0 0 0 var(--ok-weak); animation:pulse 2.4s var(--ease) infinite; }
.sys-status.warn .pulse { background:var(--warn); } .sys-status.err .pulse { background:var(--err); }
@keyframes pulse { 0%{box-shadow:0 0 0 0 rgba(18,129,63,.35)} 70%{box-shadow:0 0 0 6px rgba(18,129,63,0)} 100%{box-shadow:0 0 0 0 rgba(18,129,63,0)} }
@media (prefers-reduced-motion: reduce){ .sys-status .pulse{ animation:none } }
.theme-toggle {
background:var(--card-2); border:1px solid var(--border); color:var(--text-2);
width:34px; height:34px; border-radius:9px; cursor:pointer;
display:flex; align-items:center; justify-content:center; padding:0; transition:background .12s var(--ease), color .12s;
}
.theme-toggle svg { width:16px; height:16px; }
.theme-toggle:hover { background:var(--border); color:var(--text); }
.topbar .clock { font-size:11px; color:var(--muted); font-family:var(--mono); font-variant-numeric:tabular-nums; letter-spacing:-0.02em; }
.sys-block { display:inline-flex; flex-direction:column; align-items:center; gap:2px; line-height:1.1; }
.sys-block .sys-status { padding:3px 9px 3px 7px; font-size:11.5px; }
.user-dd .ddbtn.user-chip { display:inline-flex; flex-direction:column; align-items:flex-start; padding:4px 10px; border-radius:8px; margin:0; max-width:220px; }
.user-dd .user-name { font-size:12.5px; }
.nav .ddmenu.ddmenu-right, .user-dd .ddmenu { left:auto; right:0; min-width:190px; }
.user-dd { position:relative; }
.user-dd .ddmenu { display:none; position:absolute; top:100%; right:0; margin-top:8px; background:var(--card); border:1px solid var(--border); border-radius:10px; padding:6px; box-shadow:0 10px 30px rgba(0,0,0,.12); z-index:50; }
.user-dd:hover .ddmenu, .user-dd:focus-within .ddmenu { display:block; }
.user-dd::after { content:''; position:absolute; top:100%; left:0; right:0; height:10px; }
.user-dd .ddmenu a { display:block; padding:8px 12px; border-radius:7px; font-size:13px; color:var(--text-2); text-decoration:none; white-space:nowrap; }
.user-dd .ddmenu a:hover, .user-dd .ddmenu a.active { background:var(--accent-weak); color:var(--accent-strong); }
.user-dd:hover .ddbtn, .user-dd.active .ddbtn { background:var(--accent-weak); }
.ddsep { display:block; height:1px; background:var(--border); margin:6px 4px; }
/* ---- Контент ---- */
main { padding:24px; max-width:1200px; margin:0 auto; }
h1 { font-size:22px; font-weight:680; margin:0 0 4px; letter-spacing:-0.01em; }
h2 { font-size:15px; font-weight:650; margin:0 0 12px; }
h3 { font-size:13.5px; font-weight:600; margin:14px 0 6px; }
p { margin:0 0 10px; }
a { color:var(--accent); text-decoration:none; }
a:hover { text-decoration:underline; }
/* ---- Карточки ---- */
.card {
background:var(--card); border:1px solid var(--border); border-radius:var(--radius);
padding:18px 20px; margin-bottom:16px; box-shadow:var(--shadow);
}
.card h2:first-child { margin-top:0; }
.grid { display:grid; gap:14px; grid-template-columns:repeat(auto-fit, minmax(220px,1fr)); }
/* ---- Статы (приборные читалки) ---- */
.stat { padding:15px 17px; background:var(--card); border:1px solid var(--border); border-radius:var(--radius); box-shadow:var(--shadow); }
.stat-label { display:flex; align-items:center; gap:7px; font-size:10.5px; color:var(--muted); text-transform:uppercase; letter-spacing:.06em; font-weight:600; }
.stat-label::before { content:""; width:7px; height:7px; border-radius:50%; background:var(--border-strong); flex:none; }
.stat.ok .stat-label::before { background:var(--ok); } .stat.warn .stat-label::before { background:var(--warn); } .stat.err .stat-label::before { background:var(--err); } .stat.accent .stat-label::before { background:var(--accent); }
.stat-value { font-size:27px; font-weight:700; margin-top:6px; letter-spacing:-0.03em; font-family:var(--mono); font-variant-numeric:tabular-nums; }
/* ---- Индикаторы ---- */
.dot { display:inline-block; width:8px; height:8px; border-radius:50%; margin-right:7px; vertical-align:middle; }
.dot.ok { background:var(--ok); } .dot.warn { background:var(--warn); } .dot.err { background:var(--err); }
/* ---- Таблицы ---- */
table { width:100%; border-collapse:collapse; font-size:13.5px; }
th, td { text-align:left; padding:9px 12px; border-bottom:1px solid var(--border); }
th { color:var(--muted); font-weight:600; font-size:11px; text-transform:uppercase; letter-spacing:.04em; }
tbody tr:hover td { background:var(--accent-weak); }
/* ---- Бейджи статусов ---- */
.badge { display:inline-block; padding:3px 9px; border-radius:20px; font-size:11px; font-weight:600; }
.live { font-size:11.5px; margin-top:4px; color:var(--muted); display:flex; align-items:center; gap:6px; line-height:1.3 }
.live::before { content:''; width:7px; height:7px; border-radius:50%; background:var(--muted); flex:none }
.live-wait::before { background:var(--accent); animation:pulse 2.4s var(--ease) infinite }
.live-warn { color:var(--warn) } .live-warn::before { background:var(--warn); animation:pulse 2.4s var(--ease) infinite }
.live-ok { color:var(--ok) } .live-ok::before { background:var(--ok) }
.live-bad { color:var(--err) } .live-bad::before { background:var(--err) }
.live-big { font-size:14px; margin:-6px 0 12px; font-weight:500 }
.live[data-active] { color:var(--text-2) } .live-warn[data-active] { color:var(--warn) }
.badge.draft, .badge.validated, .badge.submitted_to_nsd { background:var(--accent-weak); color:var(--accent); }
.badge.awaiting_decision, .badge.manual_approval { background:var(--warn-weak); color:var(--warn); }
.badge.confirmed, .badge.awaiting_sub16, .badge.done { background:var(--ok-weak); color:var(--ok); }
.badge.rejected, .badge.timed_out, .badge.err { background:var(--err-weak); color:var(--err); }
.badge.ok { background:var(--ok-weak); color:var(--ok); }
/* ---- Код / preformatted ---- */
code { background:var(--card-2); border:1px solid var(--border); padding:1.5px 6px; border-radius:5px; font-size:12px; font-family:ui-monospace,"SF Mono",Menlo,monospace; }
pre { background:var(--card-2); border:1px solid var(--border); border-radius:var(--radius-sm); padding:14px; font-size:12px; overflow:auto; max-height:420px; font-family:ui-monospace,"SF Mono",Menlo,monospace; }
.muted { color:var(--muted); font-size:13px; }
/* ---- Кнопки ---- */
button, .btn {
background:var(--accent); color:#fff; border:1px solid var(--accent);
padding:9px 16px; border-radius:8px; cursor:pointer; font-size:13.5px; font-weight:550;
font-family:inherit; transition:filter .15s;
}
button:hover, .btn:hover { filter:brightness(1.07); text-decoration:none; }
.btn-secondary { background:var(--card-2); color:var(--text); border-color:var(--border-strong); }
.btn-ok { background:var(--ok); border-color:var(--ok); }
.btn-warn { background:var(--warn); border-color:var(--warn); color:#fff; }
.btn-danger { background:var(--err); border-color:var(--err); }
.btn-ghost { background:transparent; color:var(--accent); border-color:transparent; }
/* ---- Формы ---- */
input, select, textarea {
padding:9px 11px; background:var(--bg-elev); border:1px solid var(--border-strong);
color:var(--text); border-radius:8px; font:inherit; font-size:13.5px;
}
input:focus, select:focus, textarea:focus { outline:2px solid var(--accent-weak); border-color:var(--accent); }
label { font-size:13px; font-weight:500; }
/* Поле формы: подпись, ввод и пояснение — каждое со своей строки. Без этого label,
input и подсказка идут в строку и «разъезжаются» на широком экране. */
/* min-width:0 обязателен: иначе длинная строка внутри (ключ, код, ссылка) не даёт
блоку сжаться и растягивает всю страницу — появляется горизонтальная прокрутка. */
.fld { display:grid; gap:4px; align-content:start; min-width:0; max-width:100%; }
.fld > * { min-width:0; }
.fld > label { font-weight:600; }
.fld input[type=text], .fld input[type=password], .fld input[type=email], .fld select { width:100%; max-width:460px; }
.fld .muted { max-width:620px; line-height:1.45; overflow-wrap:anywhere; }
/* ---- Баннер режима эмуляции ---- */
.banner-mock { background:var(--warn-weak); border-bottom:1px solid var(--warn); padding:10px 24px; display:flex; align-items:center; gap:12px; font-size:13px; }
/* ---- Hero (приветствие + статус) ---- */
.hero { padding:8px 0 22px; }
.hero-greeting { font-size:28px; font-weight:720; letter-spacing:-0.02em; margin:0 0 6px; }
.hero-status { display:inline-flex; align-items:center; gap:9px; font-size:15px; font-weight:550; padding:7px 16px; border-radius:24px; }
.hero-status.ok { background:var(--ok-weak); color:var(--ok); }
.hero-status.warn { background:var(--warn-weak); color:var(--warn); }
.hero-status.err { background:var(--err-weak); color:var(--err); }
/* ---- Плитки задач (task tiles) ---- */
.tiles { display:grid; gap:16px; grid-template-columns:repeat(auto-fit, minmax(210px,1fr)); margin:8px 0 24px; }
.tile {
display:flex; flex-direction:column; gap:10px;
padding:22px; background:var(--card); border:1px solid var(--border);
border-radius:16px; box-shadow:var(--shadow); cursor:pointer;
text-decoration:none; color:var(--text); transition:transform .14s, box-shadow .14s, border-color .14s;
min-height:128px;
}
.tile:hover { transform:translateY(-2px); box-shadow:var(--shadow-lg); border-color:var(--border-strong); text-decoration:none; }
.tile .ico { width:42px; height:42px; border-radius:11px; display:flex; align-items:center; justify-content:center; background:var(--accent-weak); color:var(--accent); }
.tile .ico svg { width:21px; height:21px; }
.tile.brand .ico { background:var(--accent); color:#fff; }
.tile .t-title { font-size:15.5px; font-weight:650; letter-spacing:-0.01em; }
.tile .t-sub { font-size:12.5px; color:var(--muted); margin-top:-4px; line-height:1.4; }
.tile .t-arrow { margin-top:auto; color:var(--border-strong); display:flex; }
.tile .t-arrow svg { width:18px; height:18px; }
.tile:hover .t-arrow { color:var(--accent); transform:translateX(2px); transition:transform .15s var(--ease), color .15s; }
/* ---- Секция (заголовок + контент) ---- */
.section-head { display:flex; align-items:baseline; justify-content:space-between; margin:24px 0 12px; }
.section-head h2 { margin:0; font-size:17px; }
.section-head a { font-size:13px; }
/* ---- Админ-центр: боковые разделы + контент (macOS System Settings) ---- */
.settings { display:grid; grid-template-columns:236px 1fr; gap:26px; align-items:start; }
/* Колонка сетки по умолчанию не уже своего содержимого (min-width:auto), поэтому
широкая таблица растягивала всю страницу и внизу появлялся ползунок. */
.settings > * { min-width:0; }
.settings-section, .settings-section .card { min-width:0; }
.settings-section table { max-width:100%; }
.tw { overflow-x:auto; max-width:100%; }
.settings-nav { position:sticky; top:78px; display:flex; flex-direction:column; gap:2px; }
.settings-nav button {
display:flex; align-items:center; gap:11px; justify-content:flex-start;
background:transparent; border:1px solid transparent; color:var(--text-2);
padding:10px 13px; border-radius:9px; cursor:pointer; font-size:14px; font-weight:520;
width:100%; text-align:left; transition:background .12s;
}
.settings-nav button:hover { background:var(--card-2); color:var(--text); }
.settings-nav button.active { background:var(--accent-weak); color:var(--accent); }
.settings-nav button .nico { width:18px; height:18px; flex:none; display:flex; align-items:center; justify-content:center; color:var(--muted); }
.settings-nav button .nico svg { width:18px; height:18px; }
.settings-nav button.active .nico { color:var(--accent); }
.settings-nav button .ind { margin-left:auto; width:8px; height:8px; border-radius:50%; flex:none; }
.settings-nav button .ind.ok { background:var(--ok); }
.settings-nav button .ind.warn { background:var(--warn); }
.settings-nav button .ind.err { background:var(--err); }
.settings-section { display:none; }
.settings-section.active { display:block; }
.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; }
}
/* ---- Документация / справка (Read) ---- */
.doc-header { margin:6px 0 22px; }
.doc-header h1 { font-size:24px; letter-spacing:-0.02em; }
.doc-header .lede { color:var(--text-2); font-size:15px; max-width:70ch; margin-top:6px; }
.doc { display:grid; grid-template-columns:236px minmax(0,1fr); gap:40px; align-items:start; }
.doc-toc { position:sticky; top:74px; max-height:calc(100vh - 96px); overflow:auto; font-size:13px; padding-right:6px; }
.doc-toc .toc-group { font-size:10px; text-transform:uppercase; letter-spacing:.07em; color:var(--muted); font-weight:700; margin:16px 0 6px; }
.doc-toc .toc-group:first-child { margin-top:0; }
.doc-toc a { display:block; color:var(--text-2); text-decoration:none; padding:5px 11px; border-radius:7px; border-left:2px solid transparent; line-height:1.35; }
.doc-toc a:hover { background:var(--card-2); color:var(--text); }
.doc-toc a.active { color:var(--accent-strong); border-left-color:var(--accent); background:var(--accent-weak); font-weight:600; }
.doc-body { max-width:820px; min-width:0; }
.doc-body > h2 { font-size:19px; letter-spacing:-0.015em; margin:34px 0 12px; padding-top:8px; scroll-margin-top:74px; }
.doc-body > h2:first-child { margin-top:0; }
.doc-body h3 { font-size:15px; margin:22px 0 8px !important; }
.doc-body p, .doc-body li { max-width:74ch; }
.doc-body p { color:var(--text-2); }
.doc-body .card { scroll-margin-top:74px; }
.doc-sep { height:1px; background:var(--border); margin:30px 0; border:0; }
.callout { background:var(--card-2); border:1px solid var(--border); border-radius:var(--radius-sm); padding:13px 15px; margin:14px 0; font-size:14px; line-height:1.55; }
.callout.err { background:var(--err-weak); } .callout.accent { background:var(--accent-weak); } .callout.warn { background:var(--warn-weak); } .callout.ok { background:var(--ok-weak); }
.callout strong:first-child { color:var(--text); }
/* Код-блоки справки — тёмная тема учтена */
.doc-body pre, pre.copyable, pre.code { background:var(--card-2) !important; border:1px solid var(--border); border-radius:var(--radius-sm); color:var(--text); }
@media (max-width:900px){ .doc { grid-template-columns:1fr; gap:0; } .doc-toc { position:static; max-height:none; margin-bottom:18px; border-bottom:1px solid var(--border); padding-bottom:12px; } }
/* ---- Toast ---- */
#bj-toast { position:fixed; top:70px; right:22px; z-index:9999; max-width:520px; padding:14px 18px 14px 40px; background:var(--card); border:1px solid var(--border-strong); border-radius:var(--radius-sm); color:var(--text); box-shadow:var(--shadow-lg); font-size:13px; line-height:1.45; opacity:0; transform:translateY(-12px); transition:opacity .25s var(--ease), transform .25s var(--ease); }
#bj-toast::before { content:""; position:absolute; left:16px; top:17px; width:9px; height:9px; border-radius:50%; background:var(--ok); }
#bj-toast.visible { opacity:1; transform:translateY(0); }
#bj-toast .close { position:absolute; top:6px; right:10px; cursor:pointer; color:var(--muted); font-size:15px; }
#bj-toast .close:hover { color:var(--text); }
.banner-dev{margin:0;border-bottom:1px solid var(--border)}
.banner-dev button{width:100%;display:flex;gap:10px;align-items:center;padding:7px 18px;background:transparent;border:0;font:inherit;font-size:13px;color:var(--text);cursor:pointer;text-align:left}
.banner-dev.on{background:color-mix(in srgb,var(--warn) 12%,var(--card))}
.banner-dev.off{background:color-mix(in srgb,var(--ok) 10%,var(--card))}
.banner-dev button:hover{filter:brightness(.97)}
</style>
</head>
<body>
<div id="bj-toast"><span class="close" onclick="document.getElementById('bj-toast').classList.remove('visible')">×</span><div id="bj-toast-text"></div></div>
<header class="topbar">
<a class="brand" href="/admin/" title="На дашборд" style="text-decoration:none;color:inherit">
<svg viewBox="0 0 26 26" fill="none" aria-hidden="true">
<path d="M5.5 16 C 9 8.5, 17 8.5, 20.5 16" stroke="var(--accent)" stroke-width="1.9" stroke-linecap="round"/>
<circle cx="5.5" cy="16" r="3.1" fill="var(--accent)"/>
<circle cx="20.5" cy="16" r="3.1" fill="var(--brand)"/>
</svg>
<span><b>Bridge</b><span class="brand-j">&amp;Join</span></span>
</a>
<nav class="nav">
{{if can "view"}}<a href="/admin/" class="{{if eq .Active "home"}}active{{end}}">Дашборд</a>
<span class="dd{{if or (eq .Active "journal") (eq .Active "claims") (eq .Active "incoming")}} active{{end}}">
<span class="ddbtn">Журнал ▾<span id="nav-journal-badge" title="Входящих ждут решения" style="display:none;margin-left:5px;background:var(--err);color:#fff;border-radius:10px;padding:1px 7px;font-size:11px;font-weight:700"></span></span>
<span class="ddmenu">
<a href="/admin/journal">Все (вход + исход)</a>
<a href="/admin/claims">Исходящие переводы</a>
<a href="/admin/incoming">Входящие<span id="nav-incoming-badge" style="display:none;margin-left:5px;background:var(--err);color:#fff;border-radius:10px;padding:1px 7px;font-size:11px;font-weight:700"></span></a>
</span>
</span>
{{end}}{{if can "view"}}<a href="/admin/manual" class="{{if eq .Active "manual"}}active{{end}}">Ручной ввод M2M</a>{{end}}{{if can "settings"}}<a href="/admin/depo-test" class="{{if eq .Active "depotest"}}active{{end}}">Тест DEPO</a>{{end}}{{if and devOn (can "operate")}}<a href="/admin/m2m-testing" class="{{if eq .Active "m2mtest"}}active{{end}}">Тестирование M2M</a>{{end}}
{{if and .NPR (can "npr")}}<a href="/admin/npr" class="{{if eq .Active "npr"}}active{{end}}">НПР / УДС</a>{{end}}
{{if or (can "view") (can "quik")}}<span class="dd{{if or (eq .Active "fancy") (eq .Active "quik") (eq .Active "news") (eq .Active "handbook")}} active{{end}}">
<span class="ddbtn">Обмен ▾</span>
<span class="ddmenu">
{{if can "view"}}<a href="/admin/fancy-queue">Обмен с DEPO (Fancy)</a>{{end}}
{{if can "quik"}}<a href="/admin/quik">Поручения QUIK</a>{{end}}
{{if can "view"}}<a href="/admin/news">События</a>
<a href="/admin/handbook">Справочник НРД</a>{{end}}
</span>
</span>{{end}}
</nav>
<div class="topbar-right">
<span class="sys-block">
<span class="sys-status {{if .IsMockMode}}warn{{end}}" title="Состояние обмена с НРД">
<span class="pulse"></span>{{if .IsMockMode}}{{if devOn}}Эмуляция{{else}}Нет связи с НРД{{end}}{{else}}НРД на связи{{end}}</span>
<span class="clock">{{.Now}}</span>
</span>
<span class="dd user-dd{{if or (eq .Active "setup") (eq .Active "status") (eq .Active "help")}} active{{end}}">
<span class="ddbtn user-chip" title="{{with curUser}}{{.RoleTitle}}{{if eq .Source "ad"}} · вход через AD{{end}}{{else}}Пользователи не созданы — вход открыт{{end}}">
<span class="user-name">{{with curUser}}{{if .Name}}{{.Name}}{{else}}{{.Login}}{{end}}{{else}}без входа{{end}} ▾</span>
<span class="user-role">{{with curUser}}{{.RoleTitle}}{{else}}вход открыт{{end}}</span>
</span>
<span class="ddmenu ddmenu-right">
{{if can "settings"}}<a href="/admin/setup" class="{{if eq .Active "setup"}}active{{end}}">Настройка</a>
<a href="/admin/status" class="{{if eq .Active "status"}}active{{end}}">Статус системы</a>{{end}}
<a href="/admin/help" class="{{if eq .Active "help"}}active{{end}}">Справка</a>
{{with curUser}}<span class="ddsep"></span><a href="/logout">Выйти</a>{{else}}{{if authOpen}}<span class="ddsep"></span><a href="/admin/setup#users">Создать пользователей</a>{{end}}{{end}}
</span>
</span>
<button class="theme-toggle" id="theme-toggle" title="Светлая/тёмная тема" aria-label="Переключить тему"></button>
</div>
</header>
{{if devLicensed}}
<form method="post" action="/admin/dev-toggle" class="banner-dev {{if devOn}}on{{else}}off{{end}}" title="{{if devOn}}Нажмите, чтобы скрыть у себя служебные разделы и имитации{{else}}Нажмите, чтобы снова показать служебные разделы{{end}}">
<input type="hidden" name="return_to" value="">
<button type="submit"><span class="dot {{if devOn}}warn{{else}}ok{{end}}" style="width:9px;height:9px;margin:0"></span>
{{if devOn}}<strong>Режим разработки включён</strong><span class="muted"> — видны служебные разделы, тестовые сценарии и имитации. Нажмите, чтобы выключить у себя.</span>{{else}}<strong>Режим разработки выключен у вас</strong><span class="muted"> — лицензия его разрешает; нажмите, чтобы включить.</span>{{end}}
</button>
</form>
<script>(function(){var f=document.querySelector('.banner-dev input[name=return_to]');if(f)f.value=location.pathname+location.search;})();</script>
{{end}}
{{if .IsMockMode}}
<div class="banner-mock">
<span class="dot warn" style="width:9px;height:9px;margin:0"></span>
<div><strong style="color:var(--warn)">{{if devOn}}Режим эмуляции{{else}}Обмен с НРД не настроен{{end}}</strong> — {{if devOn}}реального обмена с НРД нет{{else}}поручения в НРД не отправляются{{end}}. <span class="muted">{{.MockReason}}</span></div>
<a href="/admin/wizard" style="margin-left:auto">Открыть мастер настройки →</a>
</div>
{{end}}
<main>
{{template "content" .}}
</main>
<script>
(function() {
// --- Тема: light/dark, сохранение в localStorage ---
var root = document.documentElement;
var toggle = document.getElementById('theme-toggle');
var icoMoon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.8A9 9 0 1 1 11.2 3 7 7 0 0 0 21 12.8z"/></svg>';
var icoSun = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>';
function applyTheme(t) {
root.setAttribute('data-theme', t);
toggle.innerHTML = (t === 'dark') ? icoSun : icoMoon;
try { localStorage.setItem('bj-theme', t); } catch(e) {}
}
var saved = 'light';
try { saved = localStorage.getItem('bj-theme') || (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); } catch(e) {}
applyTheme(saved);
toggle.addEventListener('click', function() {
applyTheme(root.getAttribute('data-theme') === 'dark' ? 'light' : 'dark');
});
// --- Toast из ?flash= ---
var toast = document.getElementById('bj-toast');
var toastText = document.getElementById('bj-toast-text');
function showToast(msg) {
toastText.textContent = msg;
toast.classList.add('visible');
var ms = Math.max(5000, Math.min(20000, msg.length * 60));
setTimeout(function() { toast.classList.remove('visible'); }, ms);
}
var params = new URLSearchParams(window.location.search);
var flash = params.get('flash');
if (flash) { showToast(flash); }
// Прокрутка к секции из ?scrollto= (надёжнее фрагмента через редирект).
var anchored = false;
var scrollTarget = params.get('scrollto');
if (scrollTarget) {
var stEl = document.getElementById(scrollTarget);
if (stEl) {
anchored = true;
var doScroll2 = function() { stEl.scrollIntoView({ block: 'start' }); };
doScroll2();
setTimeout(doScroll2, 50);
setTimeout(doScroll2, 200);
window.addEventListener('load', doScroll2);
}
}
if (flash || scrollTarget) {
params.delete('flash');
params.delete('scrollto');
var qs = params.toString();
window.history.replaceState({}, '', window.location.pathname + (qs ? '?' + qs : '') + window.location.hash);
}
// --- Сохранение позиции прокрутки через POST-редиректы ---
// Отключаем авто-восстановление браузера — иначе оно перебивает наше
// (скроллит наверх уже ПОСЛЕ нашего scrollTo).
if ('scrollRestoration' in history) { try { history.scrollRestoration = 'manual'; } catch(e) {} }
// Присваиваем карточкам стабильные id (по порядку рендера) — чтобы вернуться
// к нужной секции по якорю после сохранения.
var cards = document.querySelectorAll('.card');
for (var ci = 0; ci < cards.length; ci++) { if (!cards[ci].id) cards[ci].id = 'sec-' + ci; }
document.addEventListener('submit', function(ev) {
var f = ev.target;
if (f && f.method && f.method.toLowerCase() === 'post') {
try { sessionStorage.setItem('bj-scroll', String(window.scrollY)); } catch(e) {}
// Возврат на ТЕКУЩУЮ страницу (а не на /admin/setup): сервер не всегда
// получает Referer, поэтому говорим путь явно.
if (!f.querySelector('input[name="return_to"]')) {
var rt = document.createElement('input');
rt.type = 'hidden'; rt.name = 'return_to'; rt.value = location.pathname;
f.appendChild(rt);
}
// Подкладываем секцию (ближайшую карточку) — сервер вернёт ?scrollto=.
var card = f.closest ? f.closest('.card') : null;
if (card && card.id && !f.querySelector('input[name="section"]')) {
var inp = document.createElement('input');
inp.type = 'hidden'; inp.name = 'section'; inp.value = card.id;
f.appendChild(inp);
}
}
}, true);
var sc = null;
try { sc = sessionStorage.getItem('bj-scroll'); } catch(e) {}
// При наличии якоря восстановление позиции пропускаем, чтобы не конфликтовать.
if (sc !== null && !anchored) {
var y = parseInt(sc, 10) || 0;
try { sessionStorage.removeItem('bj-scroll'); } catch(e) {}
// Несколько попыток: страница могла ещё не разложиться (шрифты/контент),
// поэтому повторяем сразу, на след. кадре, по таймеру и после полной загрузки.
var restore = function() { window.scrollTo(0, y); };
restore();
window.requestAnimationFrame(restore);
setTimeout(restore, 60);
setTimeout(restore, 200);
window.addEventListener('load', restore);
}
// --- Браузерные уведомления о входящих переводах (сценарий 3333) ---
// Опрашиваем /admin/incoming/feed; на НОВЫЙ входящий показываем Notification,
// клик по которому ведёт в карточку для подтверждения/отказа. Плюс бейдж в меню.
(function() {
var badge = document.getElementById('nav-incoming-badge');
var jbadge = document.getElementById('nav-journal-badge');
function setBadges(n) {
[badge, jbadge].forEach(function(b){
if (!b) return;
if (n > 0) { b.textContent = n; b.style.display = 'inline-block'; }
else { b.style.display = 'none'; }
});
}
function seenSet() {
try { return new Set(JSON.parse(localStorage.getItem('bj-seen-incoming') || '[]')); }
catch(e) { return new Set(); }
}
function saveSeen(s) {
try { localStorage.setItem('bj-seen-incoming', JSON.stringify(Array.from(s))); } catch(e) {}
}
function notify(it) {
// 1) Системное уведомление браузера (только в защищённом контексте: HTTPS
// или localhost; по обычному HTTP браузеры Notification блокируют).
if (('Notification' in window) && Notification.permission === 'granted') {
var n = new Notification('Новый входящий перевод НРД', {
body: 'От ' + it.sender + ' · ' + it.count + ' бумаг · ' + it.received + '\nНажмите, чтобы подтвердить или отказать.',
tag: 'bj-incoming-' + it.id, requireInteraction: true
});
n.onclick = function() { window.focus(); location.href = '/admin/incoming/' + it.id; };
}
// 2) Всегда — заметный баннер на странице (работает и по HTTP).
inPageBanner(it);
}
function bannerStack() {
var c = document.getElementById('bj-incoming-stack');
if (!c) {
c = document.createElement('div');
c.id = 'bj-incoming-stack';
c.style.cssText = 'position:fixed;top:16px;right:16px;z-index:2000;display:flex;flex-direction:column;gap:10px;max-width:360px';
document.body.appendChild(c);
}
return c;
}
function inPageBanner(it) {
var bar = document.createElement('div');
bar.style.cssText = 'background:var(--accent,#2563eb);color:#fff;padding:14px 16px;border-radius:12px;box-shadow:0 12px 40px rgba(0,0,0,.35);cursor:pointer;font-size:14px;line-height:1.4';
bar.innerHTML = '<b>📥 Новый входящий перевод НРД</b><br>От ' + it.sender + ' · ' + it.count + ' бумаг · ' + it.received + '<br><span style="text-decoration:underline">Открыть для подтверждения/отказа →</span>';
bar.onclick = function() { location.href = '/admin/incoming/' + it.id; };
bannerStack().appendChild(bar);
setTimeout(function(){ bar.style.transition='opacity .5s'; bar.style.opacity='0'; setTimeout(function(){bar.remove();}, 600); }, 20000);
}
function poll() {
fetch('/admin/incoming/feed', { cache: 'no-store' }).then(function(r){ return r.json(); }).then(function(d){
setBadges(d.count);
var seen = seenSet(), changed = false;
(d.pending || []).forEach(function(it){
if (!seen.has(it.id)) { notify(it); seen.add(it.id); changed = true; }
});
if (changed) saveSeen(seen);
}).catch(function(){});
}
// Запрашиваем разрешение на уведомления (один раз).
if ('Notification' in window && Notification.permission === 'default') {
Notification.requestPermission().then(function(){ poll(); });
}
poll();
setInterval(poll, 12000); // каждые 12 сек
// Онлайн-статус заявок в работе: строки с data-live и data-active опрашиваются, пока ждут.
(function(){
function tick(){
var els = document.querySelectorAll('.live[data-live][data-active]');
if (!els.length) return;
var out = [], inn = [];
els.forEach(function(e){ var k = e.getAttribute('data-live').split(':'); (k[0]==='out'?out:inn).push(k[1]); });
var q = 'out=' + encodeURIComponent(out.join(',')) + '&in=' + encodeURIComponent(inn.join(','));
fetch('/admin/journal/live?' + q, {cache:'no-store'}).then(function(r){ return r.json(); }).then(function(d){
els.forEach(function(e){
var k = e.getAttribute('data-live').split(':'), st = (d[k[0]]||{})[k[1]];
if (!st) return;
e.textContent = st.text;
e.className = 'live ' + (e.classList.contains('live-big') ? 'live-big ' : '') + 'live-' + st.kind;
if (!st.active) e.removeAttribute('data-active');
});
}).catch(function(){});
}
setInterval(tick, 8000);
if (document.querySelector('.live[data-live][data-active]')) setTimeout(tick, 1500);
})();
})();
})();
</script>
</body>
</html>
{{end}}