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}`); });