feat: WebSocket real-time + rAF render gate + guest board + screen picker

Classroom performance:
- WebSocket server (ws-server.js) for low-latency cursor & stroke preview
  Replaces HTTP POST per event → eliminates per-message auth overhead
  Session member cache (30s TTL) avoids SQLite query per WS message
  Fallback to HTTP POST when WS not connected
- Cursor throttle reduced 100ms → 33ms (~30fps)
- Stroke preview throttle reduced 50ms → 20ms
- whiteboard.js: render() is now rAF-gated (_doRender/_rafPending)
  Multiple render() calls within one frame collapse into one repaint
  document.hidden check — zero CPU when tab is in background
  visibilitychange listener restores canvas on tab focus

Guest board:
- guestClassroom.js route: public token-based read-only access
- guest-board.html: name entry + read-only whiteboard + SSE
- SSE: addGuestClient/removeGuestClient/emitToGuests

Screen share picker:
- Discord-style modal with tab switching (screen/window/tab)
- Live video preview before confirming share
- useExistingScreenStream() in ClassroomRTC

Fullscreen exit overlay:
- #cr-fs-exit-overlay button inside cr-board-wrap
- Visible only via CSS :fullscreen selector (touchpad users)

File sharing from library:
- Teacher picks file from library, sends as styled card in chat
- crDownloadLibraryFile() fetches with Bearer auth

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-04-13 18:04:59 +03:00
parent 074ee5687b
commit fd29acbbdd
70 changed files with 12231 additions and 498 deletions
+11 -6
View File
@@ -32,8 +32,9 @@ const flashcardRoutes = require('./routes/flashcards');
const settingsRoutes = require('./routes/settings');
const analyticsRoutes = require('./routes/analytics');
const liveRoutes = require('./routes/live');
const classroomRoutes = require('./routes/classroom');
const gamesRoutes = require('./routes/games');
const classroomRoutes = require('./routes/classroom');
const guestClassroomRoutes = require('./routes/guestClassroom');
const gamesRoutes = require('./routes/games');
const knowledgeMapRoutes = require('./routes/knowledgeMap');
const petRoutes = require('./routes/pet');
const collectionRoutes = require('./routes/collection');
@@ -77,8 +78,8 @@ app.use((_req, res, next) => {
"font-src 'self' https://fonts.gstatic.com https://cdn.jsdelivr.net; " +
"img-src 'self' data: blob: https:; " +
"connect-src 'self' https://cdn.jsdelivr.net https://stun.l.google.com; " +
"frame-src https://www.youtube.com https://rutube.ru https://player.vimeo.com; " +
"frame-ancestors 'none'" +
"frame-src 'self' https://www.youtube.com https://rutube.ru https://player.vimeo.com; " +
"frame-ancestors 'self'" +
(isProd ? "; upgrade-insecure-requests" : "")
);
if (isProd) res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
@@ -143,8 +144,9 @@ app.use('/api/search', searchRoutes);
app.use('/api/flashcards', flashcardRoutes);
app.use('/api/settings', settingsRoutes);
app.use('/api/analytics', analyticsRoutes);
app.use('/api/live', liveRoutes);
app.use('/api/classroom', classroomRoutes);
app.use('/api/live', liveRoutes);
app.use('/api/classroom/guest', guestClassroomRoutes); // public — MUST be before /api/classroom
app.use('/api/classroom', classroomRoutes);
app.use('/api/games', gamesRoutes);
app.use('/api/knowledge-map', knowledgeMapRoutes);
app.use('/api/pet', petRoutes);
@@ -308,6 +310,9 @@ app.use((_req, res) => res.status(404).sendFile(path.join(frontendDir, '404.html
const server = app.listen(PORT, () => logger.info(`Server running on port ${PORT}`, { env: config.NODE_ENV }));
/* ── WebSocket server for low-latency classroom events (cursor + preview) ── */
require('./ws-server').attach(server);
/* ── Graceful shutdown ── */
function shutdown(signal) {
logger.info(`${signal} received — shutting down gracefully`);