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:
@@ -30,6 +30,58 @@ function getStats(_req, res) {
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Overview (Phase 3 dashboard) — prepared statements ───────────────── */
|
||||
const overviewStmts = {
|
||||
newUsers24h: db.prepare("SELECT COUNT(*) AS n FROM users WHERE created_at >= datetime('now', '-24 hours')"),
|
||||
newSessions24h: db.prepare("SELECT COUNT(*) AS n FROM test_sessions WHERE started_at >= datetime('now', '-24 hours')"),
|
||||
activeUsers24h: db.prepare("SELECT COUNT(*) AS n FROM users WHERE last_login IS NOT NULL AND last_login >= datetime('now', '-24 hours')"),
|
||||
failedSessions24h: db.prepare("SELECT COUNT(*) AS n FROM test_sessions WHERE started_at >= datetime('now', '-24 hours') AND status != 'completed'"),
|
||||
activeClasses: db.prepare('SELECT COUNT(*) AS n FROM classes'),
|
||||
// No banned_at column — fall back to audit log for recent bans (last 7 days)
|
||||
bannedThisWeek: db.prepare(`
|
||||
SELECT u.id, u.name, u.email, al.created_at AS banned_at
|
||||
FROM admin_audit_log al
|
||||
JOIN users u ON u.id = CAST(SUBSTR(al.target, 6) AS INTEGER)
|
||||
WHERE al.action = 'user.ban'
|
||||
AND al.created_at >= datetime('now', '-7 days')
|
||||
AND u.is_banned = 1
|
||||
GROUP BY u.id
|
||||
ORDER BY al.created_at DESC
|
||||
LIMIT 10
|
||||
`),
|
||||
topSessions24h: db.prepare(`
|
||||
SELECT ts.id, u.name AS user_name, s.name AS subject_name,
|
||||
ts.score, ts.total,
|
||||
ROUND(CAST(ts.score AS REAL) / ts.total * 100, 1) AS percent,
|
||||
ts.finished_at
|
||||
FROM test_sessions ts
|
||||
JOIN users u ON u.id = ts.user_id
|
||||
LEFT JOIN subjects s ON s.id = ts.subject_id
|
||||
WHERE ts.status = 'completed'
|
||||
AND ts.finished_at >= datetime('now', '-24 hours')
|
||||
AND ts.total > 0
|
||||
ORDER BY (CAST(ts.score AS REAL) / ts.total) DESC, ts.finished_at DESC
|
||||
LIMIT 5
|
||||
`),
|
||||
};
|
||||
|
||||
/* ── GET /api/admin/overview ──────────────────────────────────────────── */
|
||||
function getOverview(_req, res) {
|
||||
try {
|
||||
res.json({
|
||||
newUsers24h: overviewStmts.newUsers24h.get().n,
|
||||
newSessions24h: overviewStmts.newSessions24h.get().n,
|
||||
activeUsers24h: overviewStmts.activeUsers24h.get().n,
|
||||
activeClasses: overviewStmts.activeClasses.get().n,
|
||||
failedSessions24h: overviewStmts.failedSessions24h.get().n,
|
||||
bannedThisWeek: overviewStmts.bannedThisWeek.all(),
|
||||
topSessions24h: overviewStmts.topSessions24h.all(),
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
/* ── GET /api/admin/users?page=1&limit=50&role=student&q=name ─────────── */
|
||||
function getUsers(req, res) {
|
||||
const limit = Math.min(200, Math.max(1, Number(req.query.limit) || 50));
|
||||
@@ -539,7 +591,8 @@ function broadcast(req, res) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getStats, getUsers, updateRole, getUserSessions, getAllSessions, getSessionDetail,
|
||||
getStats, getOverview,
|
||||
getUsers, updateRole, getUserSessions, getAllSessions, getSessionDetail,
|
||||
clearUserSessions, updateUser, banUser, deleteUser,
|
||||
getFeatures, updateFeatures, getFreeStudentFeatures, updateFreeStudentFeatures,
|
||||
getAuditLog, clearAuditLog, getErrorLog, clearErrorLog, getHealth,
|
||||
|
||||
@@ -14,6 +14,7 @@ router.patch('/free-student-features', requireRole('admin'), ctrl.updateF
|
||||
router.use(requireRole('admin'));
|
||||
|
||||
router.get('/stats', ctrl.getStats);
|
||||
router.get('/overview', ctrl.getOverview);
|
||||
router.get('/users', ctrl.getUsers);
|
||||
router.patch('/users/:id/role', ctrl.updateRole);
|
||||
router.get('/users/:id/sessions', ctrl.getUserSessions);
|
||||
|
||||
Reference in New Issue
Block a user