krishgokul92 commited on
Commit
f520256
·
verified ·
1 Parent(s): bdd3dc8

Upload 10 files

Browse files
.dockerignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ node_modules
2
+ .git
3
+ .gitignore
4
+ Dockerfile
5
+ README.md
6
+ npm-debug.log*
Dockerfile ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ FROM node:20-alpine
2
+ WORKDIR /app
3
+ COPY package*.json ./
4
+ RUN npm ci --only=production || npm i --only=production
5
+ COPY . .
6
+ EXPOSE 7860
7
+ CMD ["node", "server.js"]
package.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "server-stopwatch",
3
+ "version": "1.0.0",
4
+ "description": "Server-authoritative stopwatch mirrored to clients",
5
+ "main": "server.js",
6
+ "scripts": {
7
+ "start": "node server.js"
8
+ },
9
+ "dependencies": {
10
+ "express": "^4.19.2",
11
+ "socket.io": "^4.7.5"
12
+ }
13
+ }
public/admin.html ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset='utf-8'/>
5
+ <title>Stopwatch Admin</title>
6
+ <link rel='stylesheet' href='/style.css'/>
7
+ </head>
8
+ <body>
9
+ <div class='container'>
10
+ <h2>Stopwatch Admin</h2>
11
+ <div id='display' class='time'>00:00:00.000</div>
12
+ <div class='controls'>
13
+ <button id='start' class='primary'>Start</button>
14
+ <button id='stop'>Stop</button>
15
+ <button id='reset' class='danger'>Reset</button>
16
+ </div>
17
+ </div>
18
+ <script src='/socket.io/socket.io.js'></script>
19
+ <script type='module' src='/admin.js'></script>
20
+ </body>
21
+ </html>
public/admin.js ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ import { createSmoothRenderer } from './shared.js';
2
+ const socket = io();
3
+ const display = document.getElementById('display');
4
+ const renderer = createSmoothRenderer({ setText: t => display.textContent = t });
5
+ socket.on('state', s => renderer.applyServerState(s));
6
+ document.getElementById('start').onclick = () => socket.emit('cmd:start');
7
+ document.getElementById('stop').onclick = () => socket.emit('cmd:stop');
8
+ document.getElementById('reset').onclick = () => socket.emit('cmd:reset');
public/client.html ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset='utf-8'/>
5
+ <title>Stopwatch Client</title>
6
+ <link rel='stylesheet' href='/style.css'/>
7
+ </head>
8
+ <body>
9
+ <div class='container'>
10
+ <h2>Stopwatch Client</h2>
11
+ <div id='display' class='time'>00:00:00.000</div>
12
+ </div>
13
+ <script src='/socket.io/socket.io.js'></script>
14
+ <script type='module' src='/client.js'></script>
15
+ </body>
16
+ </html>
public/client.js ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import { createSmoothRenderer } from './shared.js';
2
+ const socket = io();
3
+ const display = document.getElementById('display');
4
+ const renderer = createSmoothRenderer({ setText: t => display.textContent = t });
5
+ socket.on('state', s => renderer.applyServerState(s));
public/shared.js ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function formatTime(ms) {
2
+ const totalMs = Math.max(0, Math.round(ms));
3
+ const hours = Math.floor(totalMs / 3600000);
4
+ const minutes = Math.floor((totalMs % 3600000) / 60000);
5
+ const seconds = Math.floor((totalMs % 60000) / 1000);
6
+ const millis = totalMs % 1000;
7
+ const H = hours.toString().padStart(2, "0");
8
+ const M = minutes.toString().padStart(2, "0");
9
+ const S = seconds.toString().padStart(2, "0");
10
+ const m = millis.toString().padStart(3, "0");
11
+ return `${H}:${M}:${S}.${m}`;
12
+ }
13
+ export function createSmoothRenderer({ setText }) {
14
+ let baseElapsedMs = 0;
15
+ let t0 = performance.now();
16
+ let running = false;
17
+ function applyServerState({ running: run, elapsedMs }) {
18
+ running = run;
19
+ baseElapsedMs = elapsedMs;
20
+ t0 = performance.now();
21
+ }
22
+ function loop() {
23
+ const now = performance.now();
24
+ const ms = baseElapsedMs + (running ? now - t0 : 0);
25
+ setText(formatTime(ms));
26
+ requestAnimationFrame(loop);
27
+ }
28
+ requestAnimationFrame(loop);
29
+ return { applyServerState };
30
+ }
public/style.css ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --bg: #0f1115;
3
+ --card: #181b22;
4
+ --text: #e8eef8;
5
+ --muted: #a9b4c2;
6
+ --accent: #46c2ff;
7
+ --accent-2: #8efbbf;
8
+ }
9
+ * { box-sizing: border-box; }
10
+ html, body { height: 100%; }
11
+ body {
12
+ margin: 0;
13
+ font-family: system-ui, sans-serif;
14
+ background: radial-gradient(80% 80% at 50% 30%, #12151c 0%, var(--bg) 70%);
15
+ color: var(--text);
16
+ display: grid;
17
+ place-items: center;
18
+ }
19
+ .container {
20
+ width: min(720px, 92vw);
21
+ background: var(--card);
22
+ border: 1px solid #242a35;
23
+ border-radius: 16px;
24
+ padding: 24px;
25
+ box-shadow: 0 12px 40px rgba(0,0,0,0.45);
26
+ }
27
+ .time {
28
+ font-size: clamp(40px, 10vw, 84px);
29
+ text-align: center;
30
+ font-variant-numeric: tabular-nums;
31
+ }
32
+ .controls { display: flex; gap: 10px; justify-content: center; margin-top: 10px; }
33
+ button {
34
+ background: #232a36;
35
+ color: var(--text);
36
+ border: 1px solid #2f3746;
37
+ padding: 10px 16px;
38
+ font-size: 1rem;
39
+ border-radius: 10px;
40
+ cursor: pointer;
41
+ }
42
+ button.primary { background: linear-gradient(135deg, var(--accent), var(--accent-2)); color: #111; border: none; }
43
+ button.danger { background: #3a2020; border-color: #5a2a2a; }
server.js ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const path = require("path");
2
+ const express = require("express");
3
+ const http = require("http");
4
+ const { Server } = require("socket.io");
5
+
6
+ const app = express();
7
+ const server = http.createServer(app);
8
+ const io = new Server(server, { cors: { origin: "*" } });
9
+
10
+ app.use(express.static(path.join(__dirname, "public")));
11
+
12
+ app.get("/", (_req, res) => {
13
+ res.sendFile(path.join(__dirname, "public", "admin.html"));
14
+ });
15
+
16
+ app.get("/client", (_req, res) => {
17
+ res.sendFile(path.join(__dirname, "public", "client.html"));
18
+ });
19
+
20
+ let running = false;
21
+ let startNs = 0n;
22
+ let accumulatedNs = 0n;
23
+
24
+ const nowNs = () => process.hrtime.bigint();
25
+ const nsToMillis = (ns) => Number(ns) / 1e6;
26
+
27
+ function currentElapsedNs() {
28
+ if (!running) return accumulatedNs;
29
+ return nowNs() - startNs;
30
+ }
31
+
32
+ function broadcastState() {
33
+ const payload = {
34
+ running,
35
+ elapsedMs: nsToMillis(currentElapsedNs()),
36
+ serverSentAtMs: Date.now()
37
+ };
38
+ io.emit("state", payload);
39
+ }
40
+
41
+ io.on("connection", (socket) => {
42
+ socket.emit("state", {
43
+ running,
44
+ elapsedMs: nsToMillis(currentElapsedNs()),
45
+ serverSentAtMs: Date.now()
46
+ });
47
+
48
+ socket.on("cmd:start", () => {
49
+ if (!running) {
50
+ startNs = nowNs() - accumulatedNs;
51
+ running = true;
52
+ broadcastState();
53
+ }
54
+ });
55
+
56
+ socket.on("cmd:stop", () => {
57
+ if (running) {
58
+ accumulatedNs = nowNs() - startNs;
59
+ running = false;
60
+ broadcastState();
61
+ }
62
+ });
63
+
64
+ socket.on("cmd:reset", () => {
65
+ running = false;
66
+ accumulatedNs = 0n;
67
+ startNs = 0n;
68
+ broadcastState();
69
+ });
70
+ });
71
+
72
+ setInterval(broadcastState, 200);
73
+
74
+ const PORT = process.env.PORT || 7860;
75
+ server.listen(PORT, "0.0.0.0", () => {
76
+ console.log(`Stopwatch server running on http://0.0.0.0:${PORT}`);
77
+ });