Spaces:
Sleeping
Sleeping
| 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, { cors: { origin: "*" } }); | |
| app.use(express.static(path.join(__dirname, "public"))); | |
| app.get("/", (_req, res) => { | |
| res.sendFile(path.join(__dirname, "public", "admin.html")); | |
| }); | |
| app.get("/client", (_req, res) => { | |
| res.sendFile(path.join(__dirname, "public", "client.html")); | |
| }); | |
| /** ---------------- Stopwatch state ---------------- */ | |
| let running = false; | |
| let startNs = 0n; | |
| let accumulatedNs = 0n; | |
| let color = "#E8EEF8"; // shared color for all displays | |
| let blackout = false; // shared blackout flag | |
| const nowNs = () => process.hrtime.bigint(); | |
| const nsToMillis = (ns) => Number(ns) / 1e6; | |
| function currentElapsedNs() { | |
| if (!running) return accumulatedNs; | |
| return nowNs() - startNs; | |
| } | |
| function broadcastState() { | |
| const payload = { | |
| running, | |
| elapsedMs: nsToMillis(currentElapsedNs()), | |
| serverSentAtMs: Date.now(), | |
| color, | |
| blackout, | |
| }; | |
| io.emit("state", payload); | |
| } | |
| /** --------------- Client tracking for Admin --------------- */ | |
| const clients = new Map(); // socketId -> {id, ip, ua, url, connectedAt, latencyMs, tz, lang, screen} | |
| function getIp(socket) { | |
| const xf = socket.handshake.headers["x-forwarded-for"]; | |
| if (xf && typeof xf === "string") return xf.split(",")[0].trim(); | |
| return (socket.handshake.address || "").replace("::ffff:", "") || "unknown"; | |
| } | |
| function broadcastClients() { | |
| const list = Array.from(clients.values()).sort((a, b) => a.connectedAt - b.connectedAt); | |
| io.to("admins").emit("clients:list", { count: list.length, clients: list }); | |
| } | |
| function startLatencyProbe(socket) { | |
| const interval = setInterval(() => { | |
| const sentTs = Date.now(); | |
| socket.emit("srv:ping", { sentTs }); | |
| }, 5000); | |
| socket.data.latencyInterval = interval; | |
| } | |
| function stopLatencyProbe(socket) { | |
| if (socket.data.latencyInterval) { | |
| clearInterval(socket.data.latencyInterval); | |
| socket.data.latencyInterval = null; | |
| } | |
| } | |
| /** ----------------- Socket handling ----------------- */ | |
| io.on("connection", (socket) => { | |
| // Send current state immediately | |
| socket.emit("state", { | |
| running, | |
| elapsedMs: nsToMillis(currentElapsedNs()), | |
| serverSentAtMs: Date.now(), | |
| color, | |
| blackout, | |
| }); | |
| // Admin subscribes to client list updates | |
| socket.on("admin:join", () => { | |
| socket.join("admins"); | |
| broadcastClients(); | |
| }); | |
| // Client announces itself for admin dashboard | |
| socket.on("client:hello", (info = {}) => { | |
| const rec = { | |
| id: socket.id, | |
| ip: getIp(socket), | |
| ua: String(info.ua || socket.handshake.headers["user-agent"] || "unknown"), | |
| url: String(info.url || "unknown"), | |
| connectedAt: Date.now(), | |
| latencyMs: null, | |
| tz: info.tz || "", | |
| lang: info.lang || "", | |
| screen: info.screen || "" | |
| }; | |
| clients.set(socket.id, rec); | |
| startLatencyProbe(socket); | |
| broadcastClients(); | |
| }); | |
| // Latency update | |
| socket.on("srv:pong", ({ sentTs }) => { | |
| if (!clients.has(socket.id)) return; | |
| const rtt = Date.now() - (sentTs || Date.now()); | |
| const half = Math.max(0, Math.round(rtt / 2)); | |
| const rec = clients.get(socket.id); | |
| rec.latencyMs = half; | |
| broadcastClients(); | |
| }); | |
| /** Stopwatch admin commands */ | |
| socket.on("cmd:start", () => { | |
| if (!running) { | |
| startNs = nowNs() - accumulatedNs; | |
| running = true; | |
| broadcastState(); | |
| } | |
| }); | |
| socket.on("cmd:stop", () => { | |
| if (running) { | |
| accumulatedNs = nowNs() - startNs; | |
| running = false; | |
| broadcastState(); | |
| } | |
| }); | |
| socket.on("cmd:reset", () => { | |
| running = false; | |
| accumulatedNs = 0n; | |
| startNs = 0n; | |
| broadcastState(); | |
| }); | |
| // Color (hex) from admin | |
| socket.on("cmd:color", (hex) => { | |
| if (typeof hex !== "string") return; | |
| const ok = /^#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$/.test(hex.trim()); | |
| if (!ok) return; | |
| color = hex.trim(); | |
| broadcastState(); | |
| }); | |
| // Blackout toggle from admin | |
| socket.on("cmd:blackout", (enabled) => { | |
| blackout = Boolean(enabled); | |
| broadcastState(); | |
| }); | |
| // 🚀 Wake command from admin: tell all clients to visually "wake up" | |
| socket.on("cmd:wake", () => { | |
| io.emit("wake"); | |
| }); | |
| socket.on("disconnect", () => { | |
| stopLatencyProbe(socket); | |
| clients.delete(socket.id); | |
| broadcastClients(); | |
| }); | |
| }); | |
| // Heartbeat: periodically rebroadcast state so late-joining clients resync | |
| setInterval(broadcastState, 200); | |
| const PORT = process.env.PORT || 7860; | |
| server.listen(PORT, "0.0.0.0", () => { | |
| console.log(`Stopwatch server running on http://0.0.0.0:${PORT}`); | |
| }); | |