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:
@@ -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 });
|
||||
}
|
||||
|
||||
|
||||
@@ -82,6 +82,32 @@
|
||||
.ab-btn:hover { background: rgba(255,255,255,0.25); }
|
||||
/* ── Hero cards row (Reading · Lab of day · Pet) ── */
|
||||
.hero-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 14px; }
|
||||
|
||||
/* ── Live online-lesson banner ── */
|
||||
.live-lesson {
|
||||
display: flex; align-items: center; gap: 14px; text-decoration: none;
|
||||
background: linear-gradient(100deg, #059652, #06D6A0); color: #fff;
|
||||
border-radius: 16px; padding: 14px 20px; margin-bottom: 18px;
|
||||
box-shadow: 0 6px 22px rgba(5,150,82,0.28); transition: transform .15s, box-shadow .15s;
|
||||
}
|
||||
.live-lesson:hover { transform: translateY(-1px); box-shadow: 0 10px 28px rgba(5,150,82,0.34); }
|
||||
.ll-dot { width: 12px; height: 12px; border-radius: 50%; background: #fff; flex-shrink: 0;
|
||||
box-shadow: 0 0 0 0 rgba(255,255,255,0.7); animation: llPulse 1.6s infinite; }
|
||||
@keyframes llPulse {
|
||||
0% { box-shadow: 0 0 0 0 rgba(255,255,255,0.6); }
|
||||
70% { box-shadow: 0 0 0 10px rgba(255,255,255,0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(255,255,255,0); }
|
||||
}
|
||||
.ll-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
|
||||
.ll-text b { font-family: 'Unbounded', sans-serif; font-size: 0.92rem; font-weight: 800;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.ll-text span { font-size: 0.78rem; opacity: 0.92; }
|
||||
.ll-cta { flex-shrink: 0; background: rgba(255,255,255,0.95); color: #059652;
|
||||
font-weight: 800; font-size: 0.82rem; padding: 8px 16px; border-radius: 10px; white-space: nowrap; }
|
||||
@media (max-width: 480px) {
|
||||
.live-lesson { padding: 12px 14px; gap: 10px; }
|
||||
.ll-cta { padding: 7px 12px; font-size: 0.78rem; }
|
||||
}
|
||||
.hero-card {
|
||||
position: relative; border-radius: 18px; padding: 18px 20px;
|
||||
display: flex; flex-direction: column; min-height: 196px;
|
||||
@@ -1532,6 +1558,13 @@
|
||||
|
||||
<div class="container">
|
||||
|
||||
<!-- Live online-lesson status (student/teacher) -->
|
||||
<a class="live-lesson" id="live-lesson-banner" href="/classroom" style="display:none">
|
||||
<span class="ll-dot"></span>
|
||||
<span class="ll-text"><b id="ll-title">Идёт онлайн-урок</b><span id="ll-sub"></span></span>
|
||||
<span class="ll-cta" id="ll-cta">Присоединиться</span>
|
||||
</a>
|
||||
|
||||
<!-- Gamification Bar (students only) -->
|
||||
<div class="gam-bar" id="gam-bar" style="display:none">
|
||||
<div class="gam-level">
|
||||
@@ -4543,13 +4576,42 @@
|
||||
loadDashboardStats();
|
||||
applyDashboardPrefs();
|
||||
}
|
||||
loadLiveLesson();
|
||||
document.addEventListener('visibilitychange', () => { if (!document.hidden) loadLiveLesson(); });
|
||||
LS.notif.init();
|
||||
|
||||
// Статус онлайн-урока: показываем баннер, если у ученика/учителя идёт активная сессия.
|
||||
async function loadLiveLesson() {
|
||||
const el = document.getElementById('live-lesson-banner');
|
||||
if (!el) return;
|
||||
let data;
|
||||
try { data = await LS.crGetMySession(); } catch { el.style.display = 'none'; return; }
|
||||
const s = data && data.session;
|
||||
if (!s) { el.style.display = 'none'; return; }
|
||||
const title = (s.title && s.title.trim()) ? s.title.trim() : 'Онлайн-урок';
|
||||
document.getElementById('ll-title').textContent = (isTeacher ? 'Ваш урок идёт: ' : 'Идёт урок: ') + title;
|
||||
let sub;
|
||||
if (isTeacher) {
|
||||
const online = Array.isArray(s.attendance) ? s.attendance.filter(a => !a.left_at).length : 0;
|
||||
sub = online ? (online + ' онлайн') : 'ожидание учеников';
|
||||
} else {
|
||||
sub = data.wasJoined ? 'Вы участник — вернуться к доске' : 'Нажмите, чтобы присоединиться';
|
||||
}
|
||||
document.getElementById('ll-sub').textContent = sub;
|
||||
document.getElementById('ll-cta').textContent = isTeacher
|
||||
? 'Вернуться к доске'
|
||||
: (data.wasJoined ? 'Вернуться' : 'Присоединиться');
|
||||
el.style.display = '';
|
||||
}
|
||||
|
||||
// Real-time SSE for page-specific events (notif handled by notifications.js)
|
||||
LS.connectSSE(ev => {
|
||||
if (ev.type === 'assignment') {
|
||||
LS.toast(ev.message, 'info');
|
||||
isTeacher ? loadAdminAssignments() : loadAssignments();
|
||||
} else if (ev.type === 'classroom_live') {
|
||||
loadLiveLesson();
|
||||
if (ev.state === 'started' && !isTeacher && window.LS && LS.sfx) LS.sfx.play('user_joined');
|
||||
} else if (ev.type === 'session') {
|
||||
LS.toast(ev.message, 'info');
|
||||
if (isTeacher) loadAdminSessions();
|
||||
|
||||
Reference in New Issue
Block a user