feat(admin): phase 3 — dashboard #overview landing
GET /api/admin/overview returns 24h digest (~0.08ms/call).
- adminController.getOverview: 7 prepared statements (users 24h, sessions 24h, active users, classes count, failed sessions, banned this week, top-5 sessions)
- new section frontend/js/admin/sections/overview.js (~205L): bento-grid cards, alerts (only when >0), top-5 table, quick-links
- nav-item + tab-pane reordered: #overview is now default; #stats remains routable
Auth: admin-only (inside requireRole('admin') block, sibling of /stats).
Backward compat: all 13 existing routes unchanged.
Known follow-ups (post-merge polish):
- activeClasses counts all (label could be 'Всего классов')
- failedSessions24h includes in_progress (could tighten to abandoned only)
- topSessions24h drops NULL-score completed rows
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+11
-10
@@ -52,6 +52,7 @@
|
||||
/* ─── Tabs → section bridge ─── */
|
||||
// Routes that map 1:1 to a section module (Phase 2-extracted).
|
||||
const ROUTE_TO_SECTION = {
|
||||
overview: 'overview',
|
||||
stats: 'stats',
|
||||
questions: 'questions',
|
||||
tests: 'tests',
|
||||
@@ -660,8 +661,8 @@
|
||||
window.avatarReject = avatarReject;
|
||||
|
||||
/* ─── init ─── */
|
||||
// Initial #stats tab is .active in markup — section module will lazy-load on first switchTab.
|
||||
AdminSections.stats.init();
|
||||
// Initial #overview tab is .active in markup — section module will lazy-load on first switchTab.
|
||||
AdminSections.overview.init();
|
||||
loadAvatarRequests(); // load badge count on page open
|
||||
if (window.lucide) lucide.createIcons();
|
||||
|
||||
@@ -670,19 +671,19 @@
|
||||
if (!window.AdminRouter) return;
|
||||
|
||||
function activate(route) {
|
||||
const name = route || 'stats';
|
||||
const name = route || 'overview';
|
||||
const btn = document.querySelector('.admin-nav-item[data-tab="' + name + '"]');
|
||||
if (!btn) {
|
||||
console.warn('AdminRouter: unknown route', name);
|
||||
AdminRouter.navigate('#stats', { replace: true, silent: true });
|
||||
const fallback = document.querySelector('.admin-nav-item[data-tab="stats"]');
|
||||
AdminRouter.navigate('#overview', { replace: true, silent: true });
|
||||
const fallback = document.querySelector('.admin-nav-item[data-tab="overview"]');
|
||||
if (fallback) switchTab(fallback, { fromRouter: true });
|
||||
return;
|
||||
}
|
||||
if (btn.classList.contains('locked')) {
|
||||
LS.toast('Этот раздел доступен только администраторам', 'warn');
|
||||
AdminRouter.navigate('#stats', { replace: true, silent: true });
|
||||
const fallback = document.querySelector('.admin-nav-item[data-tab="stats"]');
|
||||
AdminRouter.navigate('#overview', { replace: true, silent: true });
|
||||
const fallback = document.querySelector('.admin-nav-item[data-tab="overview"]');
|
||||
if (fallback) switchTab(fallback, { fromRouter: true });
|
||||
return;
|
||||
}
|
||||
@@ -691,11 +692,11 @@
|
||||
|
||||
AdminRouter.on('change', (r) => activate(r.route));
|
||||
|
||||
// Initial dispatch: respect existing hash, else default to #stats.
|
||||
// Initial dispatch: respect existing hash, else default to #overview.
|
||||
const initial = AdminRouter.current();
|
||||
if (!initial.route) {
|
||||
AdminRouter.navigate('#stats', { replace: true, silent: true });
|
||||
} else if (initial.route !== 'stats') {
|
||||
AdminRouter.navigate('#overview', { replace: true, silent: true });
|
||||
} else if (initial.route !== 'overview') {
|
||||
activate(initial.route);
|
||||
}
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user