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:
+11
-6
@@ -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`);
|
||||
|
||||
Reference in New Issue
Block a user