feat(dashboard): статус «идёт онлайн-урок» с присоединением

На дашборде ученика/учителя — баннер активной classroom-сессии: заголовок урока,
для учителя «N онлайн», для ученика «Присоединиться/Вернуться», ссылка на /classroom
(там сессия подхватывается автоматически). Данные — LS.crGetMySession (учитель → своя
сессия, ученик → сессия его класса/приглашения). Нет активной сессии → баннер скрыт.

Доска работает по WebSocket, дашборд — по SSE, поэтому добавлен отдельный SSE-сигнал
classroom_live (state started/ended) ученикам класса/приглашённым/учителю в createSession
и endSession (аддитивно, в try/catch — не ломает создание/завершение сессии). Баннер
живо появляется/исчезает по этому событию + обновляется при возврате на вкладку.

Verified: рендер баннера 10/10 (ученик/учитель/нет сессии, online-счёт без вышедших,
пустой title→«Онлайн-урок»); node --check sessions.js + инлайна dashboard; sse-путь резолвится.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-23 14:24:14 +03:00
parent 0d4c658d93
commit 758e1bf6cb
2 changed files with 82 additions and 0 deletions
@@ -41,6 +41,15 @@ function createSession(req, res) {
const session = db.prepare('SELECT * FROM classroom_sessions WHERE id=?').get(sessionId);
emitToSession(sessionId, { type: 'classroom_started', sessionId, title, classId: class_id || null, teacherName: teacher.name });
// Баннер «идёт онлайн-урок» на дашбордах — через SSE-канал (доска работает по WS,
// дашборд по SSE, поэтому нужен отдельный сигнал ученикам класса / приглашённым / учителю).
try {
const sse = require('../../sse');
const payload = { type: 'classroom_live', state: 'started', sessionId, title, classId: class_id || null };
if (class_id) sse.emitToClass(class_id, payload);
else if (user_ids) for (const uid of user_ids) sse.emit(uid, payload);
sse.emit(teacher.id, payload);
} catch { /* SSE недоступен — не критично */ }
res.json(session);
}
@@ -74,6 +83,17 @@ function endSession(req, res) {
db.prepare('DELETE FROM classroom_draw_permissions WHERE session_id=?').run(sessionId);
db.prepare('DELETE FROM classroom_muted WHERE session_id=?').run(sessionId);
emitToSession(sessionId, { type: 'classroom_ended', sessionId });
// Снять баннер «идёт онлайн-урок» с дашбордов (SSE-канал).
try {
const sse = require('../../sse');
const payload = { type: 'classroom_live', state: 'ended', sessionId };
if (session.class_id) sse.emitToClass(session.class_id, payload);
else {
const invited = db.prepare('SELECT user_id FROM classroom_invites WHERE session_id=?').all(sessionId);
for (const r of invited) sse.emit(r.user_id, payload);
}
sse.emit(session.teacher_id, payload);
} catch { /* SSE недоступен — не критично */ }
res.json({ ok: true });
}