File size: 2,783 Bytes
a4ce88a
 
 
 
 
f40632a
 
 
 
 
 
 
 
 
 
 
 
 
a4ce88a
f40632a
 
a4ce88a
52f8019
f40632a
a4ce88a
f40632a
 
 
a4ce88a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f40632a
a4ce88a
 
 
 
 
 
f40632a
a4ce88a
f40632a
a4ce88a
 
 
 
f40632a
 
 
 
 
f40c2cc
a4ce88a
 
f40632a
 
 
f07c935
a4ce88a
 
 
 
 
 
 
 
 
f07c935
a4ce88a
 
 
f07c935
f40632a
a4ce88a
f40632a
 
 
a4ce88a
f40632a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// 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}`);
});