stopwatch / server.js
krishgokul92's picture
Update server.js
4790603 verified
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}`);
});