feat(admin-dash): P1 — sparklines, content inventory, subject distribution, worst-5 sessions
- 7d sparkline per 3 main metric cards (inline SVG polyline, renderSparkline helper) - "Контент проекта" row: questions/tests/courses/classes totals (compact .ov-inv-grid) - Per-subject stacked bar (24h) with hue-cycle colors and legend below - "Худшие 5 сегодня" mirrors top-5 table; both side-by-side ≥1100px via .ov-results-grid - renderSessionRows() shared helper for top/worst table rows Backend: 5 new prepared statements (worstSessions24h, sparkUsers7d, sparkSessions7d, sparkActiveUsers7d, inventory, sessionsBySubject24h) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -73,6 +73,48 @@ const overviewStmts = {
|
||||
ORDER BY (CAST(ts.score AS REAL) / ts.total) DESC, ts.finished_at DESC
|
||||
LIMIT 5
|
||||
`),
|
||||
worstSessions24h: 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) ASC, ts.finished_at DESC
|
||||
LIMIT 5
|
||||
`),
|
||||
sparkUsers7d: db.prepare(`
|
||||
SELECT date(created_at) AS d, COUNT(*) AS n FROM users
|
||||
WHERE created_at >= date('now', '-6 days')
|
||||
GROUP BY d ORDER BY d
|
||||
`),
|
||||
sparkSessions7d: db.prepare(`
|
||||
SELECT date(started_at) AS d, COUNT(*) AS n FROM test_sessions
|
||||
WHERE started_at >= date('now', '-6 days')
|
||||
GROUP BY d ORDER BY d
|
||||
`),
|
||||
sparkActiveUsers7d: db.prepare(`
|
||||
SELECT date(last_login) AS d, COUNT(DISTINCT id) AS n FROM users
|
||||
WHERE last_login IS NOT NULL AND last_login >= date('now', '-6 days')
|
||||
GROUP BY d ORDER BY d
|
||||
`),
|
||||
inventory: db.prepare(`
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM questions) AS questions,
|
||||
(SELECT COUNT(*) FROM tests) AS tests,
|
||||
(SELECT COUNT(*) FROM courses) AS courses,
|
||||
(SELECT COUNT(*) FROM classes) AS classes
|
||||
`),
|
||||
sessionsBySubject24h: db.prepare(`
|
||||
SELECT s.slug, s.name, COUNT(*) AS n FROM test_sessions ts
|
||||
JOIN subjects s ON s.id = ts.subject_id
|
||||
WHERE ts.started_at >= datetime('now', '-24 hours')
|
||||
GROUP BY s.id ORDER BY n DESC
|
||||
`),
|
||||
};
|
||||
|
||||
/* ── GET /api/admin/overview ──────────────────────────────────────────── */
|
||||
@@ -87,6 +129,14 @@ function getOverview(_req, res) {
|
||||
stuckSessions: overviewStmts.stuckSessions.all(),
|
||||
bannedThisWeek: overviewStmts.bannedThisWeek.all(),
|
||||
topSessions24h: overviewStmts.topSessions24h.all(),
|
||||
worstSessions24h: overviewStmts.worstSessions24h.all(),
|
||||
inventory: overviewStmts.inventory.get(),
|
||||
sessionsBySubject24h: overviewStmts.sessionsBySubject24h.all(),
|
||||
sparks: {
|
||||
users: overviewStmts.sparkUsers7d.all(),
|
||||
sessions: overviewStmts.sparkSessions7d.all(),
|
||||
active: overviewStmts.sparkActiveUsers7d.all(),
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
|
||||
Reference in New Issue
Block a user