Files
Learn_System/frontend/js/dashboard-admin-center.js
T
Maxim Dolgolyov 4b5be8442b fix(admin): «Открыть» зависшей сессии ведёт на её детали, а не в пустой список
Алерт «Зависла» (in_progress >1ч) вёл на /admin#sessions, но список сессий показывает
ТОЛЬКО completed (getAllSessions: WHERE status='completed') — поэтому зависшей сессии там
не было (симптом: «показывает зависший тест, но в списке его нет»). Теперь «Открыть»
делает deep-link на детали конкретной сессии /admin#sessions/<id> — страница деталей
открывает сессию при любом статусе (getSessionDetail без фильтра по статусу) и позволяет
её посмотреть и удалить.

node --check OK; id присутствует в payload overview (stuckSessions → ts.id).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 20:02:53 +03:00

763 lines
47 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
'use strict';
/* ════════════════════════════════════════════════════════════════
DASHBOARD · ADMIN COMMAND CENTER
Рендерит «командный центр администратора» в /dashboard для роли admin.
Дизайн — в стиле дизайн-системы LearnSpace (/css/ls.css):
палитра violet #9B5DE5 / cyan #06D6E0, шрифты Unbounded + Manrope,
карточки-«стекло» 20px, градиентные акценты. На реальных данных
GET /api/admin/overview.
Весь CSS заскоуплен под #admin-command-center, чтобы не конфликтовать
с ls.css / dashboard.html. Деструктивные действия не выполняются
inline — кнопки ведут в соответствующие разделы /admin.
Точка входа: window.DashAdminCenter.mount(rootEl).
════════════════════════════════════════════════════════════════ */
(function () {
'use strict';
let _root = null;
let _data = null;
let _tab = 'all';
let _clockTimer = null;
/* ── subject hue cycle (совпадает с overview.js) ──────────────── */
const SUBJ_COLORS = [
'#9B5DE5', '#06D6E0', '#06D664', '#FFB347', '#F15BB5',
'#7C3AED', '#4FC3F7', '#FFD54F', '#FF8A65', '#BA68C8',
];
/* ── one-time font + CSS injection ────────────────────────────── */
function ensureAssets() {
if (!document.getElementById('acc-font')) {
const l = document.createElement('link');
l.id = 'acc-font';
l.rel = 'stylesheet';
l.href = 'https://fonts.googleapis.com/css2?family=Unbounded:wght@400;700;800&family=Manrope:wght@400;500;600;700&display=swap';
document.head.appendChild(l);
}
if (document.getElementById('acc-style')) return;
const s = document.createElement('style');
s.id = 'acc-style';
s.textContent = CSS;
document.head.appendChild(s);
}
/* ── helpers ──────────────────────────────────────────────────── */
const e = (str) => (window.LS && LS.esc ? LS.esc(str) : String(str == null ? '' : str));
function initials(name) {
if (!name) return '?';
return name.trim().split(/\s+/).slice(0, 2)
.map((w) => (w[0] ? w[0].toUpperCase() : '')).join('') || '?';
}
function hashHue(str) {
let h = 0;
for (let i = 0; i < (str || '').length; i++) h = ((h << 5) - h + str.charCodeAt(i)) | 0;
return Math.abs(h) % 360;
}
function pctClass(p) {
if (p == null) return 'mid';
return p >= 75 ? 'hi' : p >= 50 ? 'mid' : 'lo';
}
function fmtNum(n) {
if (n == null) return '0';
return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, '');
}
function parseTs(s) {
if (!s) return null;
try { return new Date(s.replace(' ', 'T') + (s.endsWith('Z') ? '' : 'Z')); }
catch (_) { return null; }
}
function fmtAgo(s) {
const d = parseTs(s);
if (!d) return '';
const sec = Math.floor((Date.now() - d.getTime()) / 1000);
if (sec < 60) return 'только что';
const min = Math.floor(sec / 60);
if (min < 60) return min + ' мин';
const hr = Math.floor(min / 60);
if (hr < 24) return hr + ' ч';
return Math.floor(hr / 24) + ' дн';
}
function fmtSince(s) {
const d = parseTs(s);
if (!d) return '—';
let min = Math.floor((Date.now() - d.getTime()) / 60000);
if (min < 0) min = 0;
const hr = Math.floor(min / 60);
if (hr >= 48) return Math.floor(hr / 24) + 'д ' + (hr % 24) + 'ч';
return hr > 0 ? hr + 'ч ' + (min % 60) + 'м' : min + 'м';
}
function fmtBannedDate(s) {
const d = parseTs(s);
if (!d) return '';
return d.toLocaleDateString('ru', { day: 'numeric', month: 'short' });
}
function go(hash) { window.location.href = hash; }
/* ── sparkline path (array of {d, n} → 7-day path) ────────────── */
function sparkPath(raw, w, h) {
const map = {};
(raw || []).forEach((r) => { map[r.d] = r.n; });
const pts = [];
for (let i = 6; i >= 0; i--) {
const dt = new Date();
dt.setDate(dt.getDate() - i);
pts.push(map[dt.toISOString().slice(0, 10)] || 0);
}
const max = Math.max.apply(null, pts) || 1;
const pad = 2;
const xs = pts.map((_, i) => pad + (i / 6) * (w - 2 * pad));
const ys = pts.map((v) => h - pad - (v / max) * (h - 2 * pad));
return xs.map((x, i) => x.toFixed(1) + ' ' + ys[i].toFixed(1)).join(' L ');
}
/* ── greeting by hour ─────────────────────────────────────────── */
function greeting() {
const hr = new Date().getHours();
if (hr < 6) return 'Доброй ночи';
if (hr < 12) return 'Доброе утро';
if (hr < 18) return 'Добрый день';
return 'Добрый вечер';
}
function clockStr() {
const d = new Date();
return d.toLocaleTimeString('ru', { hour: '2-digit', minute: '2-digit' });
}
function dateStr() {
const d = new Date();
const s = d.toLocaleDateString('ru', { weekday: 'long', day: 'numeric', month: 'long' });
return s.charAt(0).toUpperCase() + s.slice(1);
}
/* ── KPI pulse row ────────────────────────────────────────────── */
function kpiRow(d) {
const sp = d.sparks || {};
const sessSpark = sparkPath(sp.sessions, 120, 34);
return `
<section class="acc-pulse">
<div class="acc-kpi hero">
<div class="acc-kpi-top">
<div class="acc-kpi-ic b"><svg class="acc-ic sm" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg></div>
<span class="acc-lbl">Сессий запущено · 24ч</span>
<span class="acc-live"><span class="acc-dot"></span>LIVE</span>
</div>
<div class="acc-num">${fmtNum(d.newSessions24h)}</div>
<div class="acc-kpi-foot">за последние сутки</div>
<svg class="acc-spark" viewBox="0 0 120 34" preserveAspectRatio="none">
<path class="acc-line" d="M ${sessSpark}" stroke="#9B5DE5"/>
</svg>
</div>
<div class="acc-kpi">
<div class="acc-kpi-top">
<div class="acc-kpi-ic g"><svg class="acc-ic sm" viewBox="0 0 24 24"><path d="M4 18V7M9 18V4M14 18v-7M19 18V9"/></svg></div>
<span class="acc-lbl">Активных юзеров</span>
</div>
<div class="acc-num">${fmtNum(d.activeUsers24h)}</div>
<svg class="acc-spark" viewBox="0 0 74 26" preserveAspectRatio="none">
<path class="acc-line" d="M ${sparkPath(sp.active, 74, 26)}" stroke="#06D664"/>
</svg>
</div>
<div class="acc-kpi">
<div class="acc-kpi-top">
<div class="acc-kpi-ic c"><svg class="acc-ic sm" viewBox="0 0 24 24"><circle cx="9" cy="8" r="3.4"/><path d="M3.5 20a5.5 5.5 0 0 1 11 0"/><path d="M18 7v6M21 10h-6"/></svg></div>
<span class="acc-lbl">Новых за 24ч</span>
</div>
<div class="acc-num">${fmtNum(d.newUsers24h)}</div>
<svg class="acc-spark" viewBox="0 0 74 26" preserveAspectRatio="none">
<path class="acc-line" d="M ${sparkPath(sp.users, 74, 26)}" stroke="#06D6E0"/>
</svg>
</div>
<div class="acc-kpi">
<div class="acc-kpi-top">
<div class="acc-kpi-ic v"><svg class="acc-ic sm" viewBox="0 0 24 24"><rect x="3" y="4.5" width="18" height="15" rx="2.5"/><path d="M3 9h18M8 4.5v3M16 4.5v3"/></svg></div>
<span class="acc-lbl">Всего классов</span>
</div>
<div class="acc-num">${fmtNum(d.classesTotal)}</div>
<div class="acc-kpi-foot">активных учебных групп</div>
</div>
</section>`;
}
/* ── attention inbox items (unified queue) ────────────────────── */
function buildAttnItems(d) {
const items = [];
(d.bannedThisWeek || []).forEach((u) => {
items.push({
sev: 'rose', kind: 'block', kindLabel: 'Блокировка',
title: u.name || '—',
meta: `<span class="acc-mono">${e(u.email || '')}</span> · ${fmtBannedDate(u.banned_at)}`,
act: 'Разблокировать', actHash: '/admin#users', solid: false,
});
});
(d.stuckSessions || []).forEach((s) => {
items.push({
sev: 'amber', kind: 'stuck', kindLabel: 'Зависла',
title: s.user_name || '—',
meta: `${e(s.subject_name || '—')} · висит <span class="acc-mono">${fmtSince(s.started_at)}</span>`,
// Глубокая ссылка на ДЕТАЛИ конкретной сессии (открывается при любом статусе):
// список /admin#sessions показывает только completed, поэтому зависшая (in_progress)
// там не находилась. На странице деталей её можно посмотреть и удалить.
act: 'Открыть', actHash: '/admin#sessions/' + s.id, solid: true,
});
});
const ab = d.abandonedSessions24h || 0;
if (ab > 0) {
items.push({
sev: 'amber', kind: 'stuck', kindLabel: 'Брошено',
title: 'Всплеск брошенных сессий',
meta: `<span class="acc-mono">${ab}</span> сессий прервано за 24ч`,
act: 'Разобрать', actHash: '/admin#sessions', solid: false,
});
}
return items;
}
function attnRowHtml(it) {
const icon = it.sev === 'rose'
? '<svg class="acc-ic sm" viewBox="0 0 24 24"><circle cx="12" cy="8" r="3.4"/><path d="M5 20a7 7 0 0 1 14 0"/><path d="M18 5l3 3M21 5l-3 3"/></svg>'
: '<svg class="acc-ic sm" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 7.5v5l3 2"/></svg>';
return `
<div class="acc-attn-row">
<div class="acc-sev ${it.sev}">${icon}</div>
<div class="acc-attn-main">
<div class="acc-a-row1"><span class="acc-kind ${it.sev}">${e(it.kindLabel)}</span><h4>${e(it.title)}</h4></div>
<div class="acc-attn-meta">${it.meta}</div>
</div>
<button class="acc-attn-act${it.solid ? ' solid' : ''}" data-go="${it.actHash}">${e(it.act)}</button>
</div>`;
}
function attnCard(d) {
const items = buildAttnItems(d);
const blocks = items.filter((i) => i.kind === 'block');
const stuck = items.filter((i) => i.kind === 'stuck');
let shown = items;
if (_tab === 'block') shown = blocks;
else if (_tab === 'stuck') shown = stuck;
const body = shown.length
? `<div class="acc-attn-list">${shown.map(attnRowHtml).join('')}</div>`
: `<div class="acc-attn-empty">
<svg class="acc-ic" viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg>
<b>Всё в норме</b><span>нет событий, требующих внимания</span>
</div>`;
return `
<section class="acc-card acc-attn">
<div class="acc-card-head">
<div class="acc-ttl-ic"><svg class="acc-ic sm" viewBox="0 0 24 24"><path d="M12 3l9 16H3z"/><path d="M12 10v4M12 17h.01"/></svg></div>
<h2>Требует внимания</h2>
<span class="acc-count">${items.length} ${items.length === 1 ? 'событие' : 'событий'}</span>
<span class="acc-more" data-go="/admin#sessions">все алерты <svg class="acc-ic xs" viewBox="0 0 24 24"><path d="M5 12h14M13 6l6 6-6 6"/></svg></span>
</div>
<div class="acc-attn-tabs">
<button class="acc-attn-tab${_tab === 'all' ? ' on' : ''}" data-tab="all">Все <span class="acc-tag">${items.length}</span></button>
<button class="acc-attn-tab${_tab === 'block' ? ' on' : ''}" data-tab="block">Блокировки <span class="acc-tag rose">${blocks.length}</span></button>
<button class="acc-attn-tab${_tab === 'stuck' ? ' on' : ''}" data-tab="stuck">Зависшие <span class="acc-tag amber">${stuck.length}</span></button>
</div>
${body}
<div class="acc-attn-foot">
<span>Единая очередь действий вместо разрозненных карточек.</span>
<span><b>${items.length}</b> в очереди</span>
</div>
</section>`;
}
/* ── live feed (top sessions) ─────────────────────────────────── */
function feedCard(d) {
const rows = (d.topSessions24h || []).slice(0, 8);
const subj = d.sessionsBySubject24h || [];
const total = subj.reduce((a, r) => a + r.n, 0) || 1;
const feedHtml = rows.length ? rows.map((s) => {
const name = s.user_name || '—';
const pc = s.percent;
return `
<div class="acc-feed-row">
<div class="acc-feed-av" style="background:hsl(${hashHue(name)},55%,55%)">${e(initials(name))}</div>
<div class="acc-feed-main">
<b>${e(name)}</b>
<div class="acc-f-meta">${e(s.subject_name || '—')} · ${fmtAgo(s.finished_at)}</div>
</div>
<div class="acc-feed-right">
<div class="acc-feed-pct ${pctClass(pc)}">${pc != null ? pc : '—'}%</div>
<div class="acc-feed-ago">${(s.score != null ? s.score : 0)}/${(s.total != null ? s.total : 0)}</div>
</div>
</div>`;
}).join('') : '<div class="acc-attn-empty" style="padding:30px 16px"><span>Нет завершённых сессий за 24ч</span></div>';
let segs = '', legend = '';
subj.forEach((r, i) => {
const pct = (r.n / total * 100).toFixed(1);
const col = SUBJ_COLORS[i % SUBJ_COLORS.length];
segs += `<div class="acc-seg" style="width:${pct}%;background:${col}" title="${e(r.name)}: ${r.n}"></div>`;
legend += `<span><span class="acc-subj-dot" style="background:${col}"></span>${e(r.name)} <b>${r.n}</b></span>`;
});
const subjBlock = subj.length ? `
<div class="acc-subj-mini">
<div class="acc-sm-head"><span>Сессии по предметам · 24ч</span><b>${total}</b></div>
<div class="acc-subj-track">${segs}</div>
<div class="acc-subj-legend">${legend}</div>
</div>` : '';
return `
<section class="acc-card">
<div class="acc-card-head">
<div class="acc-ttl-ic" style="background:var(--acc-green-50)"><svg class="acc-ic sm" style="stroke:var(--acc-green)" viewBox="0 0 24 24"><path d="M4 18V7M9 18V4M14 18v-7M19 18V9"/></svg></div>
<h2>Топ сегодня</h2>
<span class="acc-more" data-go="/admin#sessions">все сессии <svg class="acc-ic xs" viewBox="0 0 24 24"><path d="M5 12h14M13 6l6 6-6 6"/></svg></span>
</div>
<div class="acc-feed">${feedHtml}</div>
${subjBlock}
</section>`;
}
/* ── content health ───────────────────────────────────────────── */
function healthRow(d) {
const inv = d.inventory || {};
const card = (icon, n, lbl) => `
<div class="acc-hcard">
<div class="acc-hcard-top">
<div class="acc-hcard-ic">${icon}</div>
<span class="acc-lbl">${lbl}</span>
</div>
<div class="acc-hn">${fmtNum(n != null ? n : 0)}</div>
</div>`;
return `
<div class="acc-sec-title"><span>Здоровье контента</span><span class="acc-ln"></span></div>
<div class="acc-health">
${card('<svg class="acc-ic sm" viewBox="0 0 24 24"><path d="M9.5 9a2.5 2.5 0 1 1 3.4 2.3c-.8.4-1.4.9-1.4 1.7v.4"/><path d="M11.5 17h.01"/><circle cx="12" cy="12" r="9"/></svg>', inv.questions, 'вопросов')}
${card('<svg class="acc-ic sm" viewBox="0 0 24 24"><path d="M9 5h7a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2z"/><path d="M9 9h6M9 13h6M9 17h4"/></svg>', inv.tests, 'тестов')}
${card('<svg class="acc-ic sm" viewBox="0 0 24 24"><path d="M4 5a2 2 0 0 1 2-2h6v17H6a2 2 0 0 0-2 2z"/><path d="M20 5a2 2 0 0 0-2-2h-6v17h6a2 2 0 0 1 2 2z"/></svg>', inv.courses, 'курсов')}
${card('<svg class="acc-ic sm" viewBox="0 0 24 24"><rect x="3" y="4.5" width="18" height="15" rx="2.5"/><path d="M3 9h18M8 4.5v3M16 4.5v3"/></svg>', inv.classes, 'классов')}
</div>`;
}
/* ── results tables (top / worst) ─────────────────────────────── */
function resTable(rows) {
if (!rows || !rows.length) return '<div class="acc-attn-empty" style="padding:26px 14px"><span>Нет данных за 24ч</span></div>';
const body = rows.slice(0, 5).map((s) => {
const name = s.user_name || '—';
return `<tr>
<td><div class="acc-rt-user"><span class="acc-rt-av" style="background:hsl(${hashHue(name)},55%,55%)">${e(initials(name))}</span>${e(name)}</div></td>
<td><span class="acc-rt-subj">${e(s.subject_name || '—')}</span></td>
<td class="r"><span class="acc-rt-score">${(s.score != null ? s.score : 0)}/${(s.total != null ? s.total : 0)}</span></td>
<td class="r"><span class="acc-rt-pct ${pctClass(s.percent)}">${s.percent != null ? s.percent : '—'}%</span></td>
</tr>`;
}).join('');
return `<table class="acc-rtable">
<thead><tr><th>Ученик</th><th>Предмет</th><th class="r">Счёт</th><th class="r">%</th></tr></thead>
<tbody>${body}</tbody></table>`;
}
function resultsRow(d) {
return `
<div class="acc-sec-title"><span>Результаты · 24ч</span><span class="acc-ln"></span></div>
<div class="acc-results">
<section class="acc-card">
<div class="acc-card-head"><h2>Топ-5 сегодня</h2></div>
${resTable(d.topSessions24h)}
</section>
<section class="acc-card">
<div class="acc-card-head"><h2>Худшие 5 сегодня</h2></div>
${resTable(d.worstSessions24h)}
</section>
</div>`;
}
/* ── quick actions ────────────────────────────────────────────── */
function quickRow() {
const btn = (icon, title, sub, hash) => `
<button class="acc-qbtn" data-go="${hash}">
<div class="acc-qbtn-ic">${icon}</div>
<b>${title}</b><span>${sub}</span>
</button>`;
return `
<div class="acc-sec-title"><span>Быстрые действия</span><span class="acc-ln"></span></div>
<div class="acc-quick">
${btn('<svg class="acc-ic sm" viewBox="0 0 24 24"><circle cx="9" cy="8" r="3.4"/><path d="M3.5 20a5.5 5.5 0 0 1 11 0"/><circle cx="17.5" cy="9" r="2.6"/><path d="M16 14.6A4.6 4.6 0 0 1 21 19"/></svg>', 'Пользователи', '#users', '/admin#users')}
${btn('<svg class="acc-ic sm" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 7.5v5l3.5 2"/></svg>', 'Сессии', '#sessions', '/admin#sessions')}
${btn('<svg class="acc-ic sm" viewBox="0 0 24 24"><path d="M9 5h7a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2z"/><path d="M9 9h6M9 13h6"/></svg>', 'Тесты', '#tests', '/admin#tests')}
${btn('<svg class="acc-ic sm" viewBox="0 0 24 24"><rect x="3" y="4.5" width="18" height="15" rx="2.5"/><path d="M3 9h18M8 4.5v3M16 4.5v3"/></svg>', 'Классы', '#classes', '/classes')}
${btn('<svg class="acc-ic sm" viewBox="0 0 24 24"><rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/></svg>', 'Права', '#permissions', '/admin#permissions')}
${btn('<svg class="acc-ic sm" viewBox="0 0 24 24"><path d="M6 3h9l3 3v15H6a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z"/><path d="M9 9h6M9 13h6M9 17h4"/></svg>', 'Аудит-лог', '#sublog', '/admin#sublog')}
</div>`;
}
/* ── header (page-head + topbar strip) ────────────────────────── */
function headHtml(d) {
const items = buildAttnItems(d);
const blocks = items.filter((i) => i.kind === 'block').length;
const stuck = (d.stuckSessions || []).length;
const ab = d.abandonedSessions24h || 0;
let sub;
if (!items.length) {
sub = 'Сегодня всё спокойно — событий, требующих внимания, нет.';
} else {
const parts = [];
if (blocks) parts.push(`<b>${blocks}</b> ${blocks === 1 ? 'блокировка' : 'блокировок'}`);
if (stuck) parts.push(`<b>${stuck}</b> ${stuck === 1 ? 'зависшая сессия' : 'зависших'}`);
if (ab) parts.push(`<b>${ab}</b> брошенных`);
sub = 'Требует внимания: ' + parts.join(', ') + '.';
}
return `
<header class="acc-topbar">
<div class="acc-crumbs">
<span>Дашборд</span>
<svg class="acc-ic xs" viewBox="0 0 24 24"><path d="M9 6l6 6-6 6"/></svg>
<span class="here">Командный центр</span>
</div>
<div class="acc-clock"><span id="acc-date">${dateStr()}</span> · <b id="acc-clock">${clockStr()}</b></div>
<div class="acc-tb-right">
<button class="acc-tb-icon" data-act="refresh" title="Обновить">
<svg class="acc-ic" viewBox="0 0 24 24"><path d="M20 11a8 8 0 1 0-1.5 5.5"/><path d="M20 5v6h-6"/></svg>
</button>
<button class="acc-btn" data-go="/admin">
<svg class="acc-ic sm" viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h10"/></svg>
Полная админка
</button>
</div>
</header>
<section class="acc-page-head">
<div>
<div class="acc-kicker">
<svg class="acc-ic sm" viewBox="0 0 24 24"><path d="M4 14l5-5 3 3 4-6 4 5"/><path d="M4 19h16"/></svg>
<span>${greeting()}</span><span class="muted">· сводка за 24 часа</span>
</div>
<h1>Командный <span>центр</span></h1>
<p class="acc-sub">${sub}</p>
</div>
</section>`;
}
/* ── full render ──────────────────────────────────────────────── */
function render() {
if (!_root || !_data) return;
const d = _data;
_root.innerHTML = `
<div class="acc-shell">
${headHtml(d)}
${kpiRow(d)}
<div class="acc-grid">
${attnCard(d)}
<div class="acc-col">${feedCard(d)}</div>
</div>
${healthRow(d)}
${resultsRow(d)}
${quickRow()}
<div class="acc-foot">
<span>LearnSpace · командный центр администратора</span>
<span>обновлено ${clockStr()}</span>
</div>
</div>`;
wire();
}
function wire() {
_root.querySelectorAll('[data-go]').forEach((el) => {
el.addEventListener('click', () => go(el.getAttribute('data-go')));
});
_root.querySelectorAll('[data-tab]').forEach((el) => {
el.addEventListener('click', () => { _tab = el.getAttribute('data-tab'); render(); });
});
const rb = _root.querySelector('[data-act="refresh"]');
if (rb) rb.addEventListener('click', () => load());
}
function renderSkeleton() {
_root.innerHTML = `
<div class="acc-shell">
<div class="acc-skel-cards">
<div class="acc-skel hero"></div><div class="acc-skel"></div>
<div class="acc-skel"></div><div class="acc-skel"></div>
</div>
<div class="acc-skel-rows">
<div class="acc-skel row"></div><div class="acc-skel row"></div>
<div class="acc-skel row"></div><div class="acc-skel row"></div>
</div>
</div>`;
}
function startClock() {
if (_clockTimer) return;
_clockTimer = setInterval(() => {
const c = document.getElementById('acc-clock');
if (c) c.textContent = clockStr();
}, 30000);
}
async function load() {
if (!_root) return;
renderSkeleton();
try {
_data = await LS.adminGetOverview();
render();
startClock();
} catch (err) {
_root.innerHTML = `<div class="acc-shell"><div class="acc-attn-empty" style="padding:60px 20px">
<b>Не удалось загрузить данные</b>
<span>${e(err && err.message ? err.message : 'Ошибка сети')}</span>
<button class="acc-btn" style="margin-top:14px" data-act="retry">Повторить</button>
</div></div>`;
const rb = _root.querySelector('[data-act="retry"]');
if (rb) rb.addEventListener('click', () => load());
}
}
function mount(rootEl) {
ensureAssets();
_root = rootEl;
load();
}
window.DashAdminCenter = { mount, reload: load };
/* ════════════════════ SCOPED CSS ════════════════════ */
const CSS = `
#admin-command-center{
/* совпадает с дизайн-системой /css/ls.css (стекло, border, тени, радиусы) */
--surface:rgba(255,255,255,0.82); --surface-2:#faf8ff; --surface-3:rgba(155,93,229,.06);
--border:rgba(15,23,42,.10); --border-2:rgba(15,23,42,.20);
--tx:#0F172A; --tx2:#3D4F6B; --tx3:#56687A; --tx4:#8693A6;
--acc:#9B5DE5; --acc-600:#8a4fd0; --acc-700:#7a3bc4; --acc-50:rgba(155,93,229,.10); --acc-100:rgba(155,93,229,.22);
--acc-green:#0BA85A; --acc-green-50:rgba(6,214,100,.14); --acc-amber:#D9831A; --acc-amber-50:rgba(255,179,71,.16);
--acc-rose:#E23C8E; --acc-rose-50:rgba(241,91,181,.13); --acc-cyan:#0AA6B2; --acc-cyan-50:rgba(6,214,224,.14);
--acc-violet:#9B5DE5; --acc-violet-50:rgba(155,93,229,.12);
--grad-1:linear-gradient(135deg,#06D6E0,#9B5DE5);
--acc-sh-xs:var(--shadow); --acc-sh-sm:var(--shadow); --acc-sh:var(--shadow-h);
--acc-sh-accent:0 2px 14px rgba(155,93,229,.30);
--r-sm:8px; --r:12px; --r-lg:20px;
--sans:'Manrope',system-ui,-apple-system,sans-serif;
--mono:'Manrope',system-ui,sans-serif;
--display:'Unbounded',sans-serif;
font-family:var(--sans); color:var(--tx); font-size:14px; line-height:1.5;
-webkit-font-smoothing:antialiased;
}
#admin-command-center *{ box-sizing:border-box; }
#admin-command-center .acc-shell{ max-width:1380px; margin:0 auto; }
#admin-command-center .acc-mono{ font-family:var(--mono); font-variant-numeric:tabular-nums; color:var(--tx2); }
#admin-command-center .acc-ic{ width:18px; height:18px; flex:0 0 auto; display:inline-block;
stroke:currentColor; fill:none; stroke-width:1.7; stroke-linecap:round; stroke-linejoin:round; }
#admin-command-center .acc-ic.sm{ width:15px; height:15px; stroke-width:1.8; }
#admin-command-center .acc-ic.xs{ width:13px; height:13px; stroke-width:1.9; }
/* topbar */
#admin-command-center .acc-topbar{ display:flex; align-items:center; gap:14px; margin-bottom:18px; flex-wrap:wrap; }
#admin-command-center .acc-crumbs{ display:flex; align-items:center; gap:8px; font-size:13px; color:var(--tx3); font-weight:500; }
#admin-command-center .acc-crumbs .here{ color:var(--tx); font-weight:700; }
#admin-command-center .acc-crumbs .acc-ic{ stroke:var(--tx4); }
#admin-command-center .acc-clock{ font-family:var(--mono); font-variant-numeric:tabular-nums; font-size:12px; color:var(--tx3);
padding-left:14px; border-left:1px solid var(--border); }
#admin-command-center .acc-clock b{ color:var(--tx2); font-weight:700; }
#admin-command-center .acc-tb-right{ margin-left:auto; display:flex; align-items:center; gap:8px; }
#admin-command-center .acc-tb-icon{ width:36px; height:36px; display:grid; place-items:center;
border-radius:var(--r-sm); border:1px solid var(--border); background:var(--surface); backdrop-filter:var(--blur);
color:var(--tx2); box-shadow:var(--acc-sh-xs); transition:border-color .16s,color .16s,background .16s; }
#admin-command-center .acc-tb-icon:hover{ border-color:var(--acc-100); color:var(--acc); background:var(--acc-50); }
#admin-command-center .acc-btn{ display:inline-flex; align-items:center; gap:7px; height:36px; padding:0 16px;
border-radius:var(--r-sm); font-size:13px; font-weight:700; border:none; font-family:var(--sans);
background:var(--grad-1); color:#fff;
box-shadow:var(--acc-sh-accent); transition:filter .16s,transform .12s,box-shadow .16s; }
#admin-command-center .acc-btn:hover{ filter:brightness(1.05); box-shadow:0 10px 26px rgba(155,93,229,.42); }
#admin-command-center .acc-btn:active{ transform:translateY(1px); }
#admin-command-center .acc-btn .acc-ic{ stroke:#fff; }
/* page head */
#admin-command-center .acc-page-head{ display:flex; align-items:flex-end; justify-content:space-between; gap:24px; margin-bottom:22px; }
#admin-command-center .acc-kicker{ display:inline-flex; align-items:center; gap:8px; font-family:var(--sans);
font-size:11px; font-weight:700; letter-spacing:.08em; text-transform:uppercase; color:var(--acc); margin-bottom:10px; }
#admin-command-center .acc-kicker .acc-ic{ stroke:var(--acc); }
#admin-command-center .acc-kicker .muted{ color:var(--tx3); }
#admin-command-center .acc-page-head h1{ font-family:var(--display); font-size:30px; font-weight:800; letter-spacing:-.02em; line-height:1.08; margin:0; }
#admin-command-center .acc-page-head h1 span{ background:var(--grad-1); -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent; color:transparent; }
#admin-command-center .acc-sub{ margin:9px 0 0; font-size:14px; color:var(--tx2); max-width:60ch; }
#admin-command-center .acc-sub b{ color:var(--tx); font-weight:700; }
/* pulse kpi */
#admin-command-center .acc-pulse{ display:grid; grid-template-columns:1.4fr 1fr 1fr 1fr; gap:16px; margin-bottom:24px; }
#admin-command-center .acc-kpi{ background:var(--surface); backdrop-filter:var(--blur); border:1.5px solid var(--border); border-radius:var(--r-lg);
padding:18px; box-shadow:var(--acc-sh-xs); position:relative; overflow:hidden; transition:transform .18s var(--ease-out),box-shadow .18s,border-color .18s; }
#admin-command-center .acc-kpi:hover{ transform:translateY(-3px); box-shadow:var(--acc-sh); border-color:var(--acc-100); }
#admin-command-center .acc-kpi.hero{ background:linear-gradient(180deg,rgba(155,93,229,.06),var(--surface)); border-color:rgba(155,93,229,.20); }
#admin-command-center .acc-kpi-top{ display:flex; align-items:center; gap:9px; margin-bottom:12px; }
#admin-command-center .acc-kpi-ic{ width:32px; height:32px; border-radius:10px; display:grid; place-items:center; flex:0 0 auto; }
#admin-command-center .acc-kpi-ic.b{ background:var(--acc-50); } #admin-command-center .acc-kpi-ic.b .acc-ic{ stroke:var(--acc); }
#admin-command-center .acc-kpi-ic.c{ background:var(--acc-cyan-50); } #admin-command-center .acc-kpi-ic.c .acc-ic{ stroke:var(--acc-cyan); }
#admin-command-center .acc-kpi-ic.g{ background:var(--acc-green-50); } #admin-command-center .acc-kpi-ic.g .acc-ic{ stroke:var(--acc-green); }
#admin-command-center .acc-kpi-ic.v{ background:var(--acc-rose-50); } #admin-command-center .acc-kpi-ic.v .acc-ic{ stroke:var(--acc-rose); }
#admin-command-center .acc-lbl{ font-size:12px; font-weight:700; color:var(--tx3); }
#admin-command-center .acc-live{ margin-left:auto; display:inline-flex; align-items:center; gap:5px;
font-family:var(--mono); font-size:9px; font-weight:700; letter-spacing:.08em; color:var(--acc-green); }
#admin-command-center .acc-dot{ width:6px; height:6px; border-radius:50%; background:var(--acc-green); position:relative; }
#admin-command-center .acc-dot::after{ content:""; position:absolute; inset:-3px; border-radius:50%;
border:1.4px solid var(--acc-green); animation:acc-ping 1.8s ease-out infinite; }
@keyframes acc-ping{ 0%{ transform:scale(.5); opacity:.85 } 100%{ transform:scale(1.8); opacity:0 } }
#admin-command-center .acc-num{ font-family:var(--display); font-size:28px; font-weight:800; letter-spacing:-.02em;
line-height:1; font-variant-numeric:tabular-nums; }
#admin-command-center .acc-kpi.hero .acc-num{ font-size:36px; }
#admin-command-center .acc-kpi-foot{ margin-top:11px; font-size:12px; color:var(--tx3); }
#admin-command-center .acc-spark{ position:absolute; right:14px; bottom:14px; width:74px; height:26px; opacity:.9; pointer-events:none; }
#admin-command-center .acc-kpi.hero .acc-spark{ width:120px; height:34px; }
#admin-command-center .acc-line{ fill:none; stroke-width:2.2; stroke-linecap:round; stroke-linejoin:round; }
/* main grid */
#admin-command-center .acc-grid{ display:grid; grid-template-columns:minmax(0,1.55fr) minmax(0,1fr); gap:20px; align-items:start; margin-bottom:24px; }
#admin-command-center .acc-col{ display:flex; flex-direction:column; gap:20px; }
#admin-command-center .acc-card{ background:var(--surface); backdrop-filter:var(--blur); border:1.5px solid var(--border); border-radius:var(--r-lg); box-shadow:var(--acc-sh-xs); overflow:hidden; transition:box-shadow .2s; }
#admin-command-center .acc-card:hover{ box-shadow:var(--acc-sh); }
#admin-command-center .acc-card-head{ display:flex; align-items:center; gap:10px; padding:15px 18px; border-bottom:1px solid var(--border); }
#admin-command-center .acc-ttl-ic{ width:30px; height:30px; border-radius:9px; display:grid; place-items:center; flex:0 0 auto; background:var(--acc-50); }
#admin-command-center .acc-ttl-ic .acc-ic{ stroke:var(--acc); }
#admin-command-center .acc-card-head h2{ font-family:var(--display); font-size:14px; font-weight:800; letter-spacing:-.01em; margin:0; }
#admin-command-center .acc-count{ font-family:var(--mono); font-variant-numeric:tabular-nums; font-size:11px; font-weight:700; color:var(--tx2);
background:var(--surface-3); border:1px solid var(--border); border-radius:99px; padding:3px 10px; }
#admin-command-center .acc-more{ margin-left:auto; display:inline-flex; align-items:center; gap:5px;
font-size:12.5px; font-weight:700; color:var(--tx3); cursor:pointer; transition:color .14s; }
#admin-command-center .acc-more:hover{ color:var(--acc); }
/* attention */
#admin-command-center .acc-attn{ border-color:rgba(255,179,71,.45); box-shadow:0 0 0 1px rgba(255,179,71,.06),var(--acc-sh-xs); }
#admin-command-center .acc-attn .acc-card-head{ background:linear-gradient(180deg,rgba(255,179,71,.08),var(--surface)); border-bottom-color:rgba(255,179,71,.28); }
#admin-command-center .acc-attn .acc-ttl-ic{ background:var(--acc-amber-50); }
#admin-command-center .acc-attn .acc-ttl-ic .acc-ic{ stroke:var(--acc-amber); }
#admin-command-center .acc-attn-tabs{ display:flex; gap:5px; padding:11px 12px 0; flex-wrap:wrap; }
#admin-command-center .acc-attn-tab{ display:inline-flex; align-items:center; gap:7px; height:32px; padding:0 13px; border:none;
background:none; border-radius:999px; font-size:12.5px; font-weight:700; color:var(--tx3); cursor:pointer; transition:background .14s,color .14s,box-shadow .14s; font-family:var(--sans); }
#admin-command-center .acc-attn-tab:hover{ background:var(--surface-3); color:var(--tx); }
#admin-command-center .acc-attn-tab.on{ background:var(--acc); color:#fff; box-shadow:var(--acc-sh-accent); }
#admin-command-center .acc-tag{ font-family:var(--mono); font-variant-numeric:tabular-nums; font-size:10px; font-weight:700; padding:1px 7px; border-radius:99px; background:var(--surface-3); color:var(--tx2); }
#admin-command-center .acc-attn-tab.on .acc-tag{ background:rgba(255,255,255,.22); color:#fff; }
#admin-command-center .acc-attn-list{ padding:8px; }
#admin-command-center .acc-attn-row{ display:grid; grid-template-columns:40px minmax(0,1fr) auto; gap:12px; align-items:center;
padding:11px 10px; border-radius:var(--r); transition:background .14s; }
#admin-command-center .acc-attn-row+.acc-attn-row{ margin-top:1px; }
#admin-command-center .acc-attn-row:hover{ background:var(--surface-3); }
#admin-command-center .acc-sev{ width:40px; height:40px; border-radius:12px; display:grid; place-items:center; }
#admin-command-center .acc-sev.rose{ background:var(--acc-rose-50); } #admin-command-center .acc-sev.rose .acc-ic{ stroke:var(--acc-rose); }
#admin-command-center .acc-sev.amber{ background:var(--acc-amber-50); } #admin-command-center .acc-sev.amber .acc-ic{ stroke:var(--acc-amber); }
#admin-command-center .acc-attn-main{ min-width:0; }
#admin-command-center .acc-a-row1{ display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
#admin-command-center .acc-attn-main h4{ font-size:13.5px; font-weight:700; letter-spacing:-.01em; margin:0; font-family:var(--sans); }
#admin-command-center .acc-kind{ font-family:var(--mono); font-size:9.5px; font-weight:700; letter-spacing:.04em; text-transform:uppercase; padding:2px 7px; border-radius:6px; }
#admin-command-center .acc-kind.rose{ background:var(--acc-rose-50); color:var(--acc-rose); }
#admin-command-center .acc-kind.amber{ background:var(--acc-amber-50); color:var(--acc-amber); }
#admin-command-center .acc-attn-meta{ font-size:11.5px; color:var(--tx3); margin-top:3px; }
#admin-command-center .acc-attn-meta .acc-mono{ color:var(--tx2); }
#admin-command-center .acc-attn-act{ display:inline-flex; align-items:center; gap:6px; height:32px; padding:0 13px; white-space:nowrap;
border:1px solid var(--border); border-radius:var(--r-sm); background:var(--surface); font-size:12px; font-weight:700; color:var(--tx2);
box-shadow:var(--acc-sh-xs); cursor:pointer; transition:border-color .14s,color .14s,background .14s; font-family:var(--sans); }
#admin-command-center .acc-attn-act:hover{ border-color:var(--acc-100); color:var(--acc-700); background:var(--acc-50); }
#admin-command-center .acc-attn-act.solid{ background:var(--acc); color:#fff; border-color:var(--acc); box-shadow:var(--acc-sh-accent); }
#admin-command-center .acc-attn-act.solid:hover{ background:var(--acc-600); color:#fff; }
#admin-command-center .acc-attn-foot{ display:flex; align-items:center; justify-content:space-between; gap:12px;
padding:12px 18px; border-top:1px solid var(--border); background:var(--surface-2); font-size:12px; color:var(--tx3); }
#admin-command-center .acc-attn-foot b{ color:var(--tx); font-weight:700; }
#admin-command-center .acc-attn-empty{ display:flex; flex-direction:column; align-items:center; justify-content:center; gap:4px;
padding:46px 16px; text-align:center; color:var(--tx3); }
#admin-command-center .acc-attn-empty .acc-ic{ stroke:var(--acc-green); width:30px; height:30px; margin-bottom:6px; }
#admin-command-center .acc-attn-empty b{ color:var(--tx); font-size:14px; }
#admin-command-center .acc-attn-empty span{ font-size:12.5px; }
/* feed */
#admin-command-center .acc-feed{ padding:6px 8px 8px; max-height:430px; overflow-y:auto; }
#admin-command-center .acc-feed-row{ display:grid; grid-template-columns:32px minmax(0,1fr) auto; gap:11px; align-items:center; padding:9px 10px; border-radius:var(--r); transition:background .14s; }
#admin-command-center .acc-feed-row:hover{ background:var(--surface-3); }
#admin-command-center .acc-feed-av{ width:32px; height:32px; border-radius:50%; display:grid; place-items:center; color:#fff; font-size:11px; font-weight:700; font-family:var(--sans); }
#admin-command-center .acc-feed-main{ min-width:0; }
#admin-command-center .acc-feed-main b{ font-size:13px; font-weight:700; display:block; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
#admin-command-center .acc-f-meta{ font-size:11px; color:var(--tx3); margin-top:1px; }
#admin-command-center .acc-feed-right{ text-align:right; }
#admin-command-center .acc-feed-pct{ font-family:var(--display); font-size:14px; font-weight:800; line-height:1; font-variant-numeric:tabular-nums; }
#admin-command-center .acc-feed-pct.hi{ color:var(--acc-green); } #admin-command-center .acc-feed-pct.mid{ color:var(--acc-amber); } #admin-command-center .acc-feed-pct.lo{ color:var(--acc-rose); }
#admin-command-center .acc-feed-ago{ font-size:10px; color:var(--tx4); font-family:var(--mono); font-variant-numeric:tabular-nums; margin-top:2px; }
/* subject mini */
#admin-command-center .acc-subj-mini{ padding:14px 18px; border-top:1px solid var(--border); background:var(--surface-2); }
#admin-command-center .acc-sm-head{ display:flex; align-items:center; justify-content:space-between; margin-bottom:9px; font-size:11.5px; color:var(--tx3); font-weight:700; }
#admin-command-center .acc-sm-head b{ font-family:var(--mono); font-variant-numeric:tabular-nums; color:var(--tx2); }
#admin-command-center .acc-subj-track{ height:9px; border-radius:5px; overflow:hidden; display:flex; background:var(--surface-3); margin-bottom:9px; }
#admin-command-center .acc-seg{ height:100%; transition:width .6s cubic-bezier(.22,.72,.28,1); }
#admin-command-center .acc-subj-legend{ display:flex; flex-wrap:wrap; gap:6px 13px; font-size:11px; color:var(--tx3); }
#admin-command-center .acc-subj-legend span{ display:inline-flex; align-items:center; gap:5px; }
#admin-command-center .acc-subj-legend b{ font-family:var(--mono); font-variant-numeric:tabular-nums; color:var(--tx2); font-weight:700; }
#admin-command-center .acc-subj-dot{ width:7px; height:7px; border-radius:50%; }
/* section title */
#admin-command-center .acc-sec-title{ display:flex; align-items:center; gap:9px; margin:6px 0 14px;
font-family:var(--sans); font-size:11px; font-weight:700; letter-spacing:.1em; text-transform:uppercase; color:var(--tx3); }
#admin-command-center .acc-sec-title::before{ content:''; width:3px; height:13px; border-radius:99px; background:var(--grad-1); flex:0 0 auto; }
#admin-command-center .acc-ln{ flex:1; height:1px; background:linear-gradient(90deg,var(--border),transparent); }
/* health */
#admin-command-center .acc-health{ display:grid; grid-template-columns:repeat(4,1fr); gap:16px; margin-bottom:26px; }
#admin-command-center .acc-hcard{ background:var(--surface); backdrop-filter:var(--blur); border:1.5px solid var(--border); border-radius:var(--r-lg); padding:16px 17px;
box-shadow:var(--acc-sh-xs); transition:transform .18s var(--ease-out),box-shadow .18s,border-color .18s; }
#admin-command-center .acc-hcard:hover{ transform:translateY(-3px); box-shadow:var(--acc-sh); border-color:var(--acc-100); }
#admin-command-center .acc-hcard-top{ display:flex; align-items:center; gap:9px; margin-bottom:11px; }
#admin-command-center .acc-hcard-ic{ width:30px; height:30px; border-radius:9px; display:grid; place-items:center; background:var(--acc-50); color:var(--acc); }
#admin-command-center .acc-hcard-ic .acc-ic{ stroke:currentColor; }
#admin-command-center .acc-hn{ font-family:var(--display); font-size:24px; font-weight:800; letter-spacing:-.02em; line-height:1; font-variant-numeric:tabular-nums; }
/* results */
#admin-command-center .acc-results{ display:grid; grid-template-columns:1fr 1fr; gap:20px; margin-bottom:26px; }
#admin-command-center .acc-rtable{ width:100%; border-collapse:collapse; }
#admin-command-center .acc-rtable th{ text-align:left; font-family:var(--sans); font-size:10px; text-transform:uppercase; letter-spacing:.06em;
color:var(--tx3); font-weight:700; padding:10px 15px; border-bottom:1px solid var(--border); background:none; position:static; }
#admin-command-center .acc-rtable th.r,#admin-command-center .acc-rtable td.r{ text-align:right; }
#admin-command-center .acc-rtable td{ padding:11px 15px; font-size:13px; border-bottom:1px solid var(--border); }
#admin-command-center .acc-rtable tr:last-child td{ border-bottom:none; }
#admin-command-center .acc-rtable tbody tr{ transition:background .12s; }
#admin-command-center .acc-rtable tbody tr:hover{ background:var(--surface-3); }
#admin-command-center .acc-rt-user{ display:flex; align-items:center; gap:9px; font-weight:700; }
#admin-command-center .acc-rt-av{ width:26px; height:26px; border-radius:50%; display:grid; place-items:center; color:#fff; font-size:10px; font-weight:700; flex:0 0 auto; }
#admin-command-center .acc-rt-subj{ color:var(--tx2); font-size:12.5px; }
#admin-command-center .acc-rt-score{ font-family:var(--mono); font-variant-numeric:tabular-nums; color:var(--tx3); font-size:12px; }
#admin-command-center .acc-rt-pct{ font-family:var(--display); font-weight:800; font-size:13px; font-variant-numeric:tabular-nums; }
#admin-command-center .acc-rt-pct.hi{ color:var(--acc-green); } #admin-command-center .acc-rt-pct.mid{ color:var(--acc-amber); } #admin-command-center .acc-rt-pct.lo{ color:var(--acc-rose); }
/* quick */
#admin-command-center .acc-quick{ display:grid; grid-template-columns:repeat(6,1fr); gap:14px; }
#admin-command-center .acc-qbtn{ display:flex; flex-direction:column; gap:11px; padding:16px 15px; background:var(--surface); backdrop-filter:var(--blur);
border:1.5px solid var(--border); border-radius:var(--r-lg); box-shadow:var(--acc-sh-xs); text-align:left; cursor:pointer;
transition:transform .16s var(--ease-out),box-shadow .16s,border-color .16s; font-family:var(--sans); }
#admin-command-center .acc-qbtn:hover{ transform:translateY(-3px); box-shadow:var(--acc-sh); border-color:var(--acc-100); }
#admin-command-center .acc-qbtn-ic{ width:36px; height:36px; border-radius:10px; display:grid; place-items:center; background:var(--acc-50); color:var(--acc); }
#admin-command-center .acc-qbtn-ic .acc-ic{ stroke:currentColor; }
#admin-command-center .acc-qbtn b{ font-size:13px; font-weight:700; letter-spacing:-.01em; }
#admin-command-center .acc-qbtn span{ font-size:11px; color:var(--tx3); font-family:var(--mono); }
/* foot */
#admin-command-center .acc-foot{ margin-top:28px; display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:10px;
font-size:12px; color:var(--tx4); font-family:var(--mono); }
/* skeleton */
@keyframes acc-shimmer{ 0%{ background-position:-400px 0 } 100%{ background-position:400px 0 } }
#admin-command-center .acc-skel{ border-radius:var(--r-lg); border:1.5px solid var(--border);
background:linear-gradient(90deg,var(--surface-3) 25%,var(--surface) 50%,var(--surface-3) 75%); background-size:400px 100%;
animation:acc-shimmer 1.4s infinite linear; }
#admin-command-center .acc-skel-cards{ display:grid; grid-template-columns:1.4fr 1fr 1fr 1fr; gap:16px; margin-bottom:24px; }
#admin-command-center .acc-skel-cards .acc-skel{ height:118px; }
#admin-command-center .acc-skel-rows{ display:flex; flex-direction:column; gap:12px; }
#admin-command-center .acc-skel.row{ height:60px; }
/* responsive */
@media (max-width:1160px){
#admin-command-center .acc-grid{ grid-template-columns:1fr; }
#admin-command-center .acc-pulse{ grid-template-columns:1fr 1fr; }
#admin-command-center .acc-kpi.hero{ grid-column:span 2; }
#admin-command-center .acc-quick{ grid-template-columns:repeat(3,1fr); }
#admin-command-center .acc-health{ grid-template-columns:1fr 1fr; }
}
@media (max-width:760px){
#admin-command-center .acc-pulse{ grid-template-columns:1fr; }
#admin-command-center .acc-kpi.hero{ grid-column:auto; }
#admin-command-center .acc-results{ grid-template-columns:1fr; }
#admin-command-center .acc-quick{ grid-template-columns:1fr 1fr; }
#admin-command-center .acc-page-head h1{ font-size:25px; }
#admin-command-center .acc-clock{ display:none; }
}
`;
})();