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
+31 -3
View File
@@ -30,9 +30,16 @@ const auth = [authMiddleware];
const chatLimiter = rateLimit({ windowMs: 5_000, max: 5, message: 'Слишком много сообщений, подождите' });
// Template library — MUST be before /:id to avoid shadowing
router.get('/admin/active', ...teacher, c.adminGetActiveSessions);
router.get('/admin/sessions', ...teacher, c.adminGetAllSessions);
router.get('/admin/teachers-list', ...teacher, c.adminGetTeachersList);
router.get('/templates', ...teacher, c.getTemplates);
router.delete('/templates/:tid', ...teacher, c.deleteTemplate);
// History — MUST be before /:id to avoid shadowing
router.get('/my/history', ...auth, c.getMyHistory);
router.get('/class/:classId/history', ...auth, c.getClassHistory);
// Session lifecycle
router.post('/', ...teacher, c.createSession);
router.get('/online-students', ...teacher, c.getOnlineStudents);
@@ -65,9 +72,13 @@ router.patch('/:id/strokes/:strokeId', ...auth, c.updateStroke);
router.post('/:id/stroke-preview', ...auth, c.previewStroke);
// Multi-page
router.post('/:id/pages', ...teacher, c.addPage);
router.put('/:id/page', ...teacher, c.changePage);
router.patch('/:id/page-template', ...teacher, c.updatePageTemplate);
router.get('/:id/pages', ...auth, c.getPages);
router.post('/:id/pages', ...teacher, c.addPage);
router.put('/:id/page', ...teacher, c.changePage);
router.patch('/:id/page-template', ...teacher, c.updatePageTemplate);
router.patch('/:id/pages/:pageNum/name', ...teacher, c.renamePage);
router.post('/:id/pages/:pageNum/duplicate', ...teacher, c.duplicatePage);
router.delete('/:id/pages/:pageNum', ...teacher, c.deletePage);
// Hand raise
router.post('/:id/hand', ...auth, c.raiseHand);
@@ -82,6 +93,12 @@ router.post('/:id/mute', ...teacher, c.mutePeer);
router.post('/:id/screen', ...teacher, c.screenStart);
router.delete('/:id/screen', ...teacher, c.screenStop);
// Simulation: open/close/state/mode for all participants
router.post('/:id/sim', ...teacher, c.simOpen);
router.delete('/:id/sim', ...teacher, c.simClose);
router.post('/:id/sim/state', ...teacher, c.simState);
router.post('/:id/sim/mode', ...teacher, c.simMode);
// Cursor broadcast (all participants)
router.post('/:id/cursor', ...auth, c.broadcastCursor);
@@ -95,10 +112,21 @@ router.delete('/:id/allow-draw/:userId', ...teacher, c.revokeDraw);
// Session notes (per user)
router.get('/:id/notes', ...auth, c.getNotes);
router.put('/:id/notes', ...auth, c.saveNotes);
router.get('/:id/notes/all', ...teacher, c.getAllNotes);
// Session summary & history detail
router.get('/:id/summary', ...auth, c.getSessionSummary);
router.get('/:id/chat/export', ...teacher, c.exportChat);
router.delete('/:id/history', ...teacher, c.deleteHistorySession);
// Save current session as template
router.post('/:id/save-template', ...teacher, c.saveTemplate);
// Load template into current session
router.post('/:id/load-template', ...teacher, c.loadTemplate);
// Guest token (generate/revoke/get)
router.get('/:id/guest-token', ...teacher, c.getGuestToken);
router.post('/:id/guest-token', ...teacher, c.generateGuestToken);
router.delete('/:id/guest-token', ...teacher, c.revokeGuestToken);
module.exports = router;