Spaces:
Sleeping
Sleeping
| // server.js — HF Spaces / Docker ready (PORT + 0.0.0.0) | |
| // - Robust room/admin/client stats | |
| // - Commands: start/stop/reset/blackout | |
| // - NTP-style sync (sync:ping -> sync:pong) | |
| const path = require('path'); | |
| const express = require('express'); | |
| const http = require('http'); | |
| const { Server } = require('socket.io'); | |
| const app = express(); | |
| const server = http.createServer(app); | |
| const io = new Server(server, { | |
| pingInterval: 10000, | |
| pingTimeout: 5000, | |
| cors: { origin: true } | |
| }); | |
| // Serve static files | |
| app.use(express.static(path.join(__dirname, 'public'))); | |
| // Single entry page (index.html handles role=admin|client) | |
| app.get('/', (req, res) => res.sendFile(path.join(__dirname, 'public', 'index.html'))); | |
| // HF Spaces / Docker | |
| const HOST = '0.0.0.0'; | |
| const PORT = process.env.PORT || 7860; | |
| // ---------- Helpers ---------- | |
| function normRoom(x) { return String(x || 'default').trim().toLowerCase(); } | |
| function normRole(x) { return String(x || 'client').trim().toLowerCase(); } | |
| // Emit counts for a room using socket.data.role | |
| function emitStats(room) { | |
| const ids = io.sockets.adapter.rooms.get(room) || new Set(); | |
| let numAdmins = 0, numClients = 0; | |
| for (const id of ids) { | |
| const s = io.sockets.sockets.get(id); | |
| if (!s) continue; | |
| (s.data?.role === 'admin') ? numAdmins++ : numClients++; | |
| } | |
| io.to(room).emit('stats', { numAdmins, numClients }); | |
| } | |
| // ---------- Socket.io ---------- | |
| io.on('connection', (socket) => { | |
| const room = normRoom(socket.handshake.query.room); | |
| const role = normRole(socket.handshake.query.role); | |
| socket.data.room = room; | |
| socket.data.role = role; | |
| socket.join(room); | |
| emitStats(room); | |
| // Optional: let any client/admin force a refresh | |
| socket.on('stats:refresh', () => emitStats(room)); | |
| // NTP-like sync | |
| socket.on('sync:ping', (msg = {}) => { | |
| const t1 = Date.now(); | |
| socket.emit('sync:pong', { t0: msg.t0, t1, t2: Date.now() }); | |
| }); | |
| // Admin commands | |
| socket.on('admin:start', ({ delayMs = 3000, label = '' } = {}) => { | |
| if (socket.data.role !== 'admin') return; | |
| const startAt = Date.now() + Math.max(500, Number(delayMs)); | |
| io.to(room).emit('cmd', { type: 'start', startAt, label }); | |
| }); | |
| socket.on('admin:stop', () => { | |
| if (socket.data.role !== 'admin') return; | |
| io.to(room).emit('cmd', { type: 'stop' }); | |
| }); | |
| socket.on('admin:reset', () => { | |
| if (socket.data.role !== 'admin') return; | |
| io.to(room).emit('cmd', { type: 'reset' }); | |
| }); | |
| socket.on('admin:blackout', ({ on = true } = {}) => { | |
| if (socket.data.role !== 'admin') return; | |
| io.to(room).emit('cmd', { type: 'blackout', on: !!on }); | |
| }); | |
| socket.on('disconnect', () => emitStats(room)); | |
| }); | |
| server.listen(PORT, HOST, () => { | |
| console.log(`Timer server on http://${HOST}:${PORT}`); | |
| }); | |