krishgokul92 commited on
Commit
2dccdd6
·
verified ·
1 Parent(s): c5d4931

Update public/index.html

Browse files
Files changed (1) hide show
  1. public/index.html +271 -84
public/index.html CHANGED
@@ -2,46 +2,129 @@
2
  <html lang="en">
3
  <head>
4
  <meta charset="utf-8" />
 
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- <title>Timer Interface</title>
7
  <style>
8
- /* Shared Styles for Admin and Client */
9
- body { font-family: Arial, sans-serif; background: #121820; color: #e6edf3; }
10
- .wrapper { margin: 0 auto; padding: 20px; max-width: 1200px; text-align: center; }
11
- .btn { border: none; border-radius: 8px; padding: 10px 15px; font-size: 16px; cursor: pointer; }
12
- .btn:hover { background: #333; }
13
- .start-btn { background-color: #2261ff; }
14
- .stop-btn { background-color: #f43f5e; }
15
- .reset-btn { background-color: #0ea5e9; }
16
- .blackout-btn { background-color: #000; color: #fff; }
17
- .btn-group { display: flex; gap: 10px; margin-bottom: 20px; justify-content: center; }
18
- .time-display { font-size: 100px; margin-bottom: 20px; }
19
- /* Ensure proper centering and consistency */
20
- .wrapper { display: flex; flex-direction: column; justify-content: center; align-items: center; }
21
- /* Fullscreen blackout overlay */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  .blackout { position: fixed; inset: 0; background: #000; display: none; z-index: 999999; }
 
 
 
23
  </style>
24
  </head>
25
  <body>
26
 
27
- <div class="wrapper" id="app">
28
- <!-- The Timer will dynamically show the Admin or Client UI based on role -->
29
- <div id="admin" style="display: none;">
30
- <h2>Admin Controls</h2>
31
- <div class="btn-group">
32
- <button class="btn start-btn" id="start">Start</button>
33
- <button class="btn stop-btn" id="stop">Stop</button>
34
- <button class="btn reset-btn" id="reset">Reset</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  </div>
36
- <div class="btn-group">
37
- <button class="btn blackout-btn" id="blackoutOn">Blackout ON</button>
38
- <button class="btn blackout-btn" id="blackoutOff">Blackout OFF</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  </div>
40
  </div>
41
 
42
- <div id="client" style="display: none;">
43
- <h2>Client Timer</h2>
44
- <div class="time-display" id="time">00:00</div>
 
 
 
45
  </div>
46
  </div>
47
 
@@ -49,71 +132,175 @@
49
 
50
  <script src="/socket.io/socket.io.js"></script>
51
  <script>
52
- const params = new URLSearchParams(window.location.search);
53
- const role = params.get('role') || 'client'; // Default to 'client'
54
- const socket = io({ query: { role, room: 'default' } });
 
55
 
56
- // Show either admin or client interface
 
 
 
 
 
 
 
 
57
  if (role === 'admin') {
58
- document.getElementById('admin').style.display = 'block';
59
- document.getElementById('client').style.display = 'none';
60
  } else {
61
- document.getElementById('admin').style.display = 'none';
62
- document.getElementById('client').style.display = 'block';
 
63
  }
64
 
65
- // Admin controls
66
- document.getElementById('start').onclick = () => socket.emit('admin:start', { delayMs: 3000, label: 'Start Timer' });
67
- document.getElementById('stop').onclick = () => socket.emit('admin:stop');
68
- document.getElementById('reset').onclick = () => socket.emit('admin:reset');
69
- document.getElementById('blackoutOn').onclick = () => socket.emit('admin:blackout', { on: true });
70
- document.getElementById('blackoutOff').onclick = () => socket.emit('admin:blackout', { on: false });
71
-
72
- // Client timer
73
- let startTime = 0;
74
- let isRunning = false;
75
- let interval;
76
-
77
- socket.on('cmd', (cmd) => {
78
- if (cmd.type === 'start') {
79
- startTime = Date.now() + (cmd.startAt - Date.now());
80
- isRunning = true;
81
- startTimer();
82
- }
83
- if (cmd.type === 'stop') {
84
- clearInterval(interval);
85
- isRunning = false;
86
- }
87
- if (cmd.type === 'reset') {
88
- startTime = 0;
89
- isRunning = false;
90
- clearInterval(interval);
91
- updateTime();
92
- }
93
- if (cmd.type === 'blackout') {
94
- if (cmd.on) {
95
- document.body.style.background = 'black';
96
- } else {
97
- document.body.style.background = '#121820';
98
- }
99
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  });
 
 
 
 
 
 
 
 
 
 
 
 
101
 
102
- function startTimer() {
103
- interval = setInterval(() => {
104
- if (isRunning) {
105
- const elapsed = Date.now() - startTime;
106
- const seconds = Math.floor(elapsed / 1000);
107
- const milliseconds = Math.floor((elapsed % 1000) / 10);
108
- document.getElementById('time').textContent = `${String(seconds).padStart(2, '0')}:${String(milliseconds).padStart(2, '0')}`;
109
- }
110
- }, 50); // Update every 50ms
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
- function updateTime() {
114
- document.getElementById('time').textContent = '00:00';
 
 
 
115
  }
116
  </script>
117
-
118
  </body>
119
  </html>
 
2
  <html lang="en">
3
  <head>
4
  <meta charset="utf-8" />
5
+ <title>LAN Timer</title>
6
  <meta name="viewport" content="width=device-width, initial-scale=1" />
 
7
  <style>
8
+ :root { color-scheme: dark; }
9
+ /* Base */
10
+ body {
11
+ margin:0; background:#0b0f14; color:#e6edf3;
12
+ font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
13
+ }
14
+ a { color:#7fb3ff; text-decoration:none; }
15
+ .wrap { max-width: 1080px; margin: 40px auto; padding: 0 20px; }
16
+ .card {
17
+ background:#121820; border:1px solid #1f2933; border-radius:16px;
18
+ padding:20px; box-shadow:0 6px 24px rgba(0,0,0,.35);
19
+ }
20
+ h1 { margin:0 0 6px; font-weight:800; letter-spacing:.2px; font-size: clamp(28px, 3.5vw, 44px); }
21
+ p.sub { margin:0 0 20px; color:#8ea0b5; }
22
+
23
+ /* Controls / admin */
24
+ .row { display:flex; gap:12px; flex-wrap:wrap; align-items:center; margin-bottom:16px; }
25
+ label { display:flex; gap:8px; align-items:center; color:#9fb4cc; }
26
+ .input {
27
+ background:#0d141c; color:#e6edf3; border:1px solid #1f2933;
28
+ border-radius:12px; padding:10px 12px; min-width: 180px;
29
+ }
30
+ .btn {
31
+ border:0; border-radius:12px; padding:12px 18px; font-weight:700; cursor:pointer;
32
+ background:#1e2936; color:#fff; transition: transform .02s ease, background .2s ease
33
+ }
34
+ .btn:active { transform: translateY(1px); }
35
+ .btn.start { background:#2261ff; }
36
+ .btn.stop { background:#f43f5e; }
37
+ .btn.reset { background:#0ea5e9; }
38
+ .btn.blackon { background:#000; border:1px solid #2c3a4a; }
39
+ .btn.blackoff { background:#16a34a; }
40
+ .badge {
41
+ background:#10161f; border:1px solid #223041; padding:6px 10px; border-radius:999px;
42
+ color:#9fb4cc; display:inline-flex; gap:8px; align-items:center;
43
+ }
44
+ .grid { display:grid; grid-template-columns: repeat(3, 1fr); gap:12px; }
45
+ .stat { background:#0d141c; border:1px solid #1f2933; border-radius:12px; padding:14px; }
46
+ .stat b { font-size: 28px; }
47
+
48
+ /* Client timer */
49
+ .stage {
50
+ min-height: 70svh; display:grid; place-items:center;
51
+ background:#05080d; border:1px solid #121820; border-radius:16px;
52
+ }
53
+ .time {
54
+ font-size: clamp(120px, 20vw, 260px); font-weight: 900; letter-spacing: 1px; line-height: 1.0;
55
+ font-variant-numeric: tabular-nums;
56
+ -webkit-font-feature-settings: "tnum" 1; font-feature-settings: "tnum" 1;
57
+ }
58
+ .state {
59
+ position: fixed; top:12px; right:12px; background:#0d141c; border:1px solid #1f2933;
60
+ padding:6px 10px; border-radius:999px; color:#9fb4cc; font-size:13px;
61
+ }
62
+ .room {
63
+ position: fixed; top:12px; left:12px; background:#0d141c; border:1px solid #1f2933;
64
+ padding:6px 10px; border-radius:999px; color:#9fb4cc; font-size:13px;
65
+ }
66
+ .hint { margin-top:10px; color:#7f93a8; font-size:12px; text-align:center; }
67
  .blackout { position: fixed; inset: 0; background: #000; display: none; z-index: 999999; }
68
+
69
+ /* Role blocks */
70
+ .hidden { display:none; }
71
  </style>
72
  </head>
73
  <body>
74
 
75
+ <!-- Header badges (client view) -->
76
+ <div class="room hidden" id="roomBadge">room: default</div>
77
+ <div class="state hidden" id="stateBadge">IDLE</div>
78
+
79
+ <div class="wrap">
80
+ <!-- ADMIN VIEW -->
81
+ <div id="adminView" class="card hidden">
82
+ <h1>Timer Admin</h1>
83
+ <p class="sub">Controls all clients in the same room over WebSockets. Use a small start delay for tight sync.</p>
84
+
85
+ <div class="row">
86
+ <label>Start delay (ms)
87
+ <input id="delay" class="input" type="number" value="3000" min="1" step="1">
88
+ </label>
89
+ <label>Label
90
+ <input id="label" class="input" type="text" placeholder="e.g. Heat 1">
91
+ </label>
92
+ <button id="start" class="btn start">Start</button>
93
+ <button id="stop" class="btn stop">Stop</button>
94
+ <button id="reset" class="btn reset">Reset</button>
95
+ <span class="badge" id="stats">Clients: 0 • Admins: 1</span>
96
+ </div>
97
+
98
+ <div class="row">
99
+ <button id="blackOn" class="btn blackon">Blackout ON</button>
100
+ <button id="blackOff" class="btn blackoff">Show Timer</button>
101
  </div>
102
+
103
+ <div class="grid">
104
+ <div class="stat">
105
+ <div>Room</div>
106
+ <b id="roomName">default</b>
107
+ </div>
108
+ <div class="stat">
109
+ <div>Server time (ms)</div>
110
+ <b id="serverNow">--</b>
111
+ </div>
112
+ <div class="stat">
113
+ <div>Hint</div>
114
+ <div>
115
+ Open <code>/?role=client</code> on each device.
116
+ Use <code>?room=yourcode</code> on both admin and clients to isolate groups.
117
+ </div>
118
+ </div>
119
  </div>
120
  </div>
121
 
122
+ <!-- CLIENT VIEW -->
123
+ <div id="clientView" class="hidden">
124
+ <div class="stage">
125
+ <div class="time" id="time">00:00</div>
126
+ </div>
127
+ <div class="hint">Keep this page visible for best accuracy. Screen is kept awake when possible.</div>
128
  </div>
129
  </div>
130
 
 
132
 
133
  <script src="/socket.io/socket.io.js"></script>
134
  <script>
135
+ // --- URL params ---
136
+ const qs = new URLSearchParams(location.search);
137
+ const role = (qs.get('role') || 'client').toLowerCase(); // 'admin' | 'client'
138
+ const room = qs.get('room') || 'default';
139
 
140
+ // --- Elements ---
141
+ const adminView = document.getElementById('adminView');
142
+ const clientView = document.getElementById('clientView');
143
+ const roomBadge = document.getElementById('roomBadge');
144
+ const stateBadge = document.getElementById('stateBadge');
145
+ const blackoutEl = document.getElementById('blackout');
146
+ const timeEl = document.getElementById('time');
147
+
148
+ // Show correct view
149
  if (role === 'admin') {
150
+ adminView.classList.remove('hidden');
 
151
  } else {
152
+ clientView.classList.remove('hidden');
153
+ roomBadge.classList.remove('hidden');
154
+ stateBadge.classList.remove('hidden');
155
  }
156
 
157
+ // --- Socket.IO connection (rooms + role passed as query) ---
158
+ const socket = io({ query: { role, room } });
159
+
160
+ // --- Wake lock (client) ---
161
+ let wakeLock;
162
+ async function keepAwake() {
163
+ try { if ('wakeLock' in navigator) wakeLock = await navigator.wakeLock.request('screen'); }
164
+ catch(_) {}
165
+ }
166
+ if (role === 'client') {
167
+ keepAwake();
168
+ document.addEventListener('visibilitychange', () => {
169
+ if (document.visibilityState === 'visible') keepAwake();
170
+ });
171
+ }
172
+
173
+ // --- Admin: stats + server clock via NTP-style sync ---
174
+ const roomNameEl = document.getElementById('roomName');
175
+ const statsEl = document.getElementById('stats');
176
+ const serverNowEl = document.getElementById('serverNow');
177
+ if (roomNameEl) roomNameEl.textContent = room;
178
+
179
+ function syncOnce() { socket.emit('sync:ping', { t0: Date.now() }); }
180
+ if (role === 'admin') {
181
+ setInterval(syncOnce, 1000);
182
+ syncOnce();
183
+ }
184
+ socket.on('sync:pong', ({ t0, t1, t2 }) => {
185
+ if (role !== 'admin') return;
186
+ const t3 = Date.now();
187
+ const offset = ((t1 - t0) + (t2 - t3)) / 2;
188
+ serverNowEl.textContent = String((Date.now() + offset)|0);
189
+ });
190
+
191
+ socket.on('stats', ({ numAdmins, numClients }) => {
192
+ if (statsEl) statsEl.textContent = `Clients: ${numClients} • Admins: ${numAdmins}`;
193
+ });
194
+
195
+ // --- Admin buttons ---
196
+ const delayEl = document.getElementById('delay');
197
+ const labelEl = document.getElementById('label');
198
+ const startBtn = document.getElementById('start');
199
+ const stopBtn = document.getElementById('stop');
200
+ const resetBtn = document.getElementById('reset');
201
+ const blackOnBtn = document.getElementById('blackOn');
202
+ const blackOffBtn = document.getElementById('blackOff');
203
+
204
+ if (startBtn) startBtn.onclick = () => {
205
+ socket.emit('admin:start', {
206
+ delayMs: Number(delayEl.value || 3000),
207
+ label: labelEl.value || ''
208
+ });
209
+ };
210
+ if (stopBtn) stopBtn.onclick = () => socket.emit('admin:stop');
211
+ if (resetBtn) resetBtn.onclick = () => socket.emit('admin:reset');
212
+ if (blackOnBtn) blackOnBtn.onclick = () => socket.emit('admin:blackout', { on: true });
213
+ if (blackOffBtn) blackOffBtn.onclick = () => socket.emit('admin:blackout', { on: false });
214
+
215
+ // --- Client clock sync (best-of samples) ---
216
+ const offsets = []; // {delay, offset}
217
+ function clientSyncOnce() { socket.emit('sync:ping', { t0: Date.now() }); }
218
+ if (role === 'client') {
219
+ for (let i=0;i<10;i++) setTimeout(clientSyncOnce, i*150);
220
+ setInterval(clientSyncOnce, 3000);
221
+ }
222
+ socket.on('sync:pong', ({ t0, t1, t2 }) => {
223
+ if (role !== 'client') return;
224
+ const t3 = Date.now();
225
+ const delay = (t3 - t0) - (t2 - t1);
226
+ const offset = ((t1 - t0) + (t2 - t3)) / 2; // server - client
227
+ offsets.push({ delay, offset, ts: t3 });
228
+ if (offsets.length > 40) offsets.shift();
229
  });
230
+ function bestOffset() {
231
+ if (!offsets.length) return 0;
232
+ const best = [...offsets].sort((a,b)=>a.delay-b.delay).slice(0,7).map(x=>x.offset);
233
+ return Math.round(best.reduce((s,v)=>s+v,0)/best.length);
234
+ }
235
+
236
+ // --- Client timer state machine ---
237
+ const State = { IDLE:'IDLE', RUNNING:'RUNNING', STOPPED:'STOPPED' };
238
+ let state = State.IDLE;
239
+ let label = '';
240
+ let zeroPerfTs = null; // when timer hits 00:00 (in performance.now() space)
241
+ let rafId = 0;
242
 
243
+ function setState(s){ state=s; if (stateBadge) stateBadge.textContent = s; }
244
+ function serverMsToLocalPerf(msServer) {
245
+ const off = bestOffset(); // server - client
246
+ const localNowWall = Date.now();
247
+ const localNowPerf = performance.now();
248
+ const whenLocalWall = msServer - off;
249
+ const delta = whenLocalWall - localNowWall;
250
+ return localNowPerf + delta;
251
+ }
252
+ // Fixed 5-char format "SS:CC"
253
+ function fmt(ms) {
254
+ if (ms < 0) ms = 0;
255
+ const totalCs = Math.floor(ms / 10);
256
+ const secs = Math.floor(totalCs / 100) % 100; // wrap at 100s
257
+ const cs = totalCs % 100;
258
+ return `${String(secs).padStart(2,'0')}:${String(cs).padStart(2,'0')}`;
259
+ }
260
+ function renderElapsed(ms){ if (timeEl) timeEl.textContent = fmt(ms); }
261
+ function loop(){
262
+ const now = performance.now();
263
+ const ms = now - zeroPerfTs;
264
+ renderElapsed(ms);
265
+ rafId = requestAnimationFrame(loop);
266
+ }
267
+ function startAtServerTime(startAt){
268
+ zeroPerfTs = serverMsToLocalPerf(startAt);
269
+ setState(State.RUNNING);
270
+ cancelAnimationFrame(rafId);
271
+ rafId = requestAnimationFrame(loop);
272
  }
273
+ function stopPause(){
274
+ if (state !== State.RUNNING) return;
275
+ setState(State.STOPPED);
276
+ cancelAnimationFrame(rafId);
277
+ }
278
+ function resetAll(){
279
+ cancelAnimationFrame(rafId);
280
+ setState(State.IDLE);
281
+ renderElapsed(0);
282
+ }
283
+
284
+ // --- Command handling (both roles listen so admin preview works too) ---
285
+ socket.on('cmd', (msg) => {
286
+ if (!msg || !msg.type) return;
287
+ switch (msg.type) {
288
+ case 'start': startAtServerTime(msg.startAt); break;
289
+ case 'stop': stopPause(); break;
290
+ case 'reset': resetAll(); break;
291
+ case 'blackout':
292
+ blackoutEl.style.display = msg.on ? 'block' : 'none';
293
+ document.documentElement.style.cursor = msg.on ? 'none' : 'auto';
294
+ break;
295
+ }
296
+ });
297
 
298
+ // Initial UI
299
+ if (role === 'client') {
300
+ document.getElementById('roomBadge').textContent = `room: ${room}`;
301
+ setState(State.IDLE);
302
+ renderElapsed(0);
303
  }
304
  </script>
 
305
  </body>
306
  </html>