9737c787f9
Инфраструктура M2M (живой обмен с НРД через ИШ): - обработка M2MTransferResponse: ERROR(M2Mxx) → заявка Отклонена, сохранение ответа; INFO → ждём Decision; идемпотентность поллера - fallback-корреляция ответов с нулевым GUID (M2M14/M2M17) по FIFO - сырой XML ответа НРД в карточке заявки (для пересылки в ТП) - тестовый пакет роботу приведён к эталону m2m_robot_samples (CostInfo=Yes, 4 бумаги, IsolationStatus, DocumentSeries=сценарий); override паспорта - редирект из теста сразу в карточку заявки Мастер установки ключа Валидаты на флешку (admin/setup/keywizard): - пошаговый: загрузка .7z+пароль → выбор флешки → запись → справочник сертификатов (CRL) → перезапуск+проверка ИШ → готово - привилегированный воркер (bj-keymedia) в host-namespace через файл-обмен, bj-server остаётся в песочнице - сохранение структуры профиля архива (spr<N>), перечисление съёмных USB Прочее: - пакет-доказательство для ТП НРД + форма регистрации участника M2M - эталонные образцы робота (DOC/m2m_robot_samples) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
288 lines
15 KiB
HTML
288 lines
15 KiB
HTML
{{define "layout"}}<!DOCTYPE html>
|
||
<html lang="ru" data-theme="light">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>{{.Title}} · Bridge&Join</title>
|
||
<style>
|
||
/* ===================== Дизайн-система ===================== */
|
||
/* Светлая тема (по умолчанию) */
|
||
:root {
|
||
--bg:#f5f6f8; --bg-elev:#ffffff; --card:#ffffff; --card-2:#fafbfc;
|
||
--border:#e4e7ec; --border-strong:#d0d5dd;
|
||
--text:#1a1f29; --text-2:#475067; --muted:#7a8499;
|
||
--accent:#2563eb; --accent-weak:rgba(37,99,235,0.10); --accent-strong:#1d4ed8;
|
||
--ok:#16a34a; --ok-weak:rgba(22,163,74,0.12);
|
||
--warn:#d97706; --warn-weak:rgba(217,119,6,0.12);
|
||
--err:#dc2626; --err-weak:rgba(220,38,38,0.12);
|
||
--brand:#c5203e; /* MOEX-красный для акцентов бренда */
|
||
--shadow:0 1px 2px rgba(16,24,40,0.06), 0 1px 3px rgba(16,24,40,0.10);
|
||
--shadow-lg:0 8px 24px rgba(16,24,40,0.12);
|
||
--radius:10px; --radius-sm:6px;
|
||
}
|
||
/* Тёмная тема */
|
||
[data-theme="dark"] {
|
||
--bg:#0f1115; --bg-elev:#161922; --card:#1a1d24; --card-2:#20242e;
|
||
--border:#2a2f3a; --border-strong:#3a4150;
|
||
--text:#e8eaed; --text-2:#b4bcc9; --muted:#8b94a3;
|
||
--accent:#5b9dff; --accent-weak:rgba(91,157,255,0.14); --accent-strong:#7db0ff;
|
||
--ok:#3fbf6c; --ok-weak:rgba(63,191,108,0.16);
|
||
--warn:#e8b13a; --warn-weak:rgba(232,177,58,0.16);
|
||
--err:#e85a5a; --err-weak:rgba(232,90,90,0.16);
|
||
--brand:#ff5a78;
|
||
--shadow:0 1px 2px rgba(0,0,0,0.3); --shadow-lg:0 8px 28px rgba(0,0,0,0.5);
|
||
}
|
||
* { 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:20px;
|
||
padding:0 24px; height:58px;
|
||
background: var(--bg-elev); border-bottom:1px solid var(--border);
|
||
position:sticky; top:0; z-index:100;
|
||
}
|
||
.brand { display:flex; align-items:center; gap:9px; font-weight:700; font-size:16px; letter-spacing:-0.01em; }
|
||
.brand .logo { width:26px; height:26px; border-radius:7px; background:linear-gradient(135deg,var(--accent),var(--brand)); display:inline-block; }
|
||
.nav { display:flex; align-items:center; gap:2px; margin-left:8px; }
|
||
.nav .group-label { font-size:10px; text-transform:uppercase; letter-spacing:0.06em; color:var(--muted); padding:0 8px 0 14px; border-left:1px solid var(--border); margin-left:6px; }
|
||
.nav a {
|
||
color:var(--text-2); text-decoration:none; font-size:13.5px; font-weight:500;
|
||
padding:7px 11px; border-radius:7px; white-space:nowrap;
|
||
}
|
||
.nav a:hover { background:var(--card-2); color:var(--text); }
|
||
.nav a.active { background:var(--accent-weak); color:var(--accent); }
|
||
.topbar-right { margin-left:auto; display:flex; align-items:center; gap:14px; }
|
||
.theme-toggle {
|
||
background:var(--card-2); border:1px solid var(--border); color:var(--text-2);
|
||
width:34px; height:34px; border-radius:8px; cursor:pointer; font-size:15px;
|
||
display:flex; align-items:center; justify-content:center; padding:0;
|
||
}
|
||
.theme-toggle:hover { background:var(--border); color:var(--text); }
|
||
.topbar .clock { font-size:12.5px; color:var(--muted); font-variant-numeric:tabular-nums; }
|
||
|
||
/* ---- Контент ---- */
|
||
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:16px 18px; background:var(--card); border:1px solid var(--border); border-radius:var(--radius); box-shadow:var(--shadow); }
|
||
.stat-label { font-size:11px; color:var(--muted); text-transform:uppercase; letter-spacing:.05em; font-weight:600; }
|
||
.stat-value { font-size:26px; font-weight:700; margin-top:6px; letter-spacing:-0.02em; }
|
||
|
||
/* ---- Индикаторы ---- */
|
||
.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); }
|
||
|
||
/* ---- Код / 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; }
|
||
|
||
/* ---- Баннер режима эмуляции ---- */
|
||
.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(-3px); box-shadow:var(--shadow-lg); border-color:var(--accent); text-decoration:none; }
|
||
.tile .ico { width:46px; height:46px; border-radius:12px; display:flex; align-items:center; justify-content:center; font-size:24px; background:var(--accent-weak); }
|
||
.tile.brand .ico { background:linear-gradient(135deg,var(--accent),var(--brand)); }
|
||
.tile .t-title { font-size:16px; font-weight:640; letter-spacing:-0.01em; }
|
||
.tile .t-sub { font-size:12.5px; color:var(--muted); margin-top:-4px; }
|
||
.tile .t-arrow { margin-top:auto; color:var(--muted); font-size:18px; }
|
||
.tile:hover .t-arrow { color:var(--accent); }
|
||
|
||
/* ---- Секция (заголовок + контент) ---- */
|
||
.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; }
|
||
.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 { font-size:16px; width:20px; text-align:center; }
|
||
.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; }
|
||
}
|
||
|
||
/* ---- Toast ---- */
|
||
#bj-toast { position:fixed; top:72px; right:24px; z-index:9999; max-width:520px; padding:14px 18px; background:var(--card); border-left:4px solid var(--ok); 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, transform .25s; }
|
||
#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"><span class="logo"></span>Bridge&Join</span>
|
||
<nav class="nav">
|
||
<span class="group-label">Оператор</span>
|
||
<a href="/admin/" class="{{if eq .Active "home"}}active{{end}}">Дашборд</a>
|
||
<a href="/admin/claims" class="{{if eq .Active "claims"}}active{{end}}">Переводы</a>
|
||
<a href="/admin/news" class="{{if eq .Active "news"}}active{{end}}">События</a>
|
||
<span class="group-label">Администратор</span>
|
||
<a href="/admin/setup" class="{{if eq .Active "setup"}}active{{end}}">Настройка</a>
|
||
<a href="/admin/status" class="{{if eq .Active "status"}}active{{end}}">Статус</a>
|
||
<a href="/admin/help" class="{{if eq .Active "help"}}active{{end}}">Справка</a>
|
||
</nav>
|
||
<div class="topbar-right">
|
||
<span class="clock">{{.Now}}</span>
|
||
<button class="theme-toggle" id="theme-toggle" title="Светлая/тёмная тема" aria-label="Переключить тему">🌙</button>
|
||
</div>
|
||
</header>
|
||
|
||
{{if .IsMockMode}}
|
||
<div class="banner-mock">
|
||
<span style="font-size:16px">🟡</span>
|
||
<div><strong style="color:var(--warn)">Режим эмуляции</strong> — реального обмена с НРД нет. <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');
|
||
function applyTheme(t) {
|
||
root.setAttribute('data-theme', t);
|
||
toggle.textContent = (t === 'dark') ? '☀️' : '🌙';
|
||
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);
|
||
params.delete('flash');
|
||
var qs = params.toString();
|
||
window.history.replaceState({}, '', window.location.pathname + (qs ? '?' + qs : '') + window.location.hash);
|
||
}
|
||
|
||
// --- Сохранение позиции прокрутки через POST-редиректы ---
|
||
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) {}
|
||
}
|
||
}, true);
|
||
var sc = null;
|
||
try { sc = sessionStorage.getItem('bj-scroll'); } catch(e) {}
|
||
if (sc !== null) {
|
||
window.requestAnimationFrame(function() {
|
||
window.scrollTo(0, parseInt(sc, 10));
|
||
try { sessionStorage.removeItem('bj-scroll'); } catch(e) {}
|
||
});
|
||
}
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|
||
{{end}}
|