1/** 2 * Scheduled Sessions UI 3 * Loaded after app.js in direct-reference.html 4 * Adds: schedule button, create modal, my sessions panel, upcoming session banner 5 */ 6 7(function() { 8 'use strict'; 9 10 // ========================================== 11 // HELPERS 12 // ========================================== 13 14 function escapeHtml(str) { 15 if (!str) return ''; 16 var div = document.createElement('div'); 17 div.textContent = String(str); 18 return div.innerHTML; 19 } 20 21 function getAuthToken() { 22 // LingoAuth is the real token store (lv_access_token) â the old keys 23 // were never written, which made this whole feature claim "signed out". 24 return (window.LingoAuth && LingoAuth.getAccessToken && LingoAuth.getAccessToken()) 25 || window.authToken || localStorage.getItem('authToken'); 26 } 27 28 function showNotification(message, type) { 29 if (window.showToast) { window.showToast(message, type); return; } 30 var notif = document.createElement('div'); 31 var bgColor = type === 'success' ? '#10B981' : type === 'warning' ? '#f59e0b' : '#2563EB'; 32 notif.style.cssText = 'position:fixed;top:60px;left:50%;transform:translateX(-50%);background:' + bgColor + ';color:#fff;padding:10px 20px;border-radius:8px;font-size:14px;font-weight:500;z-index:10002;box-shadow:0 4px 20px rgba(0,0,0,0.3);'; 33 notif.textContent = message; 34 document.body.appendChild(notif); 35 setTimeout(function() { notif.remove(); }, 4000); 36 } 37 38 function formatDate(dateStr) { 39 var d = new Date(dateStr); 40 return d.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' }); 41 } 42 43 function formatTime(dateStr) { 44 var d = new Date(dateStr); 45 return d.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' }); 46 } 47 48 function timeUntil(dateStr) { 49 var diff = new Date(dateStr).getTime() - Date.now(); 50 if (diff <= 0) return 'now'; 51 var mins = Math.floor(diff / 60000); 52 if (mins < 60) return mins + 'min'; 53 var hrs = Math.floor(mins / 60); 54 if (hrs < 24) return hrs + 'h ' + (mins % 60) + 'min'; 55 return Math.floor(hrs / 24) + 'd ' + (hrs % 24) + 'h'; 56 } 57 58 // ========================================== 59 // SCHEDULE BUTTON (pre-join screen) 60 // ========================================== 61 62 function injectScheduleButton() { 63 if (document.getElementById('scheduleBtn')) return; 64 65 // Look for the join room form area or pre-room controls. 66 // #roomSetup / #joinBtn are what direct-reference.html actually uses â 67 // the original selectors matched nothing, so the whole Schedule 68 // feature was invisible (2026-06-10 audit). 69 var joinArea = document.querySelector('#roomSetup .setup-card, #roomSetup, .pre-room-actions, .join-form, #joinRoomForm, #preRoomScreen'); 70 if (!joinArea) { 71 // Try finding by the join button 72 var joinBtn = document.querySelector('#joinBtn, button[data-action="join"], #joinRoomBtn, .join-room-btn'); 73 if (joinBtn) joinArea = joinBtn.parentElement; 74 } 75 if (!joinArea) return; 76 77 var btn = document.createElement('button'); 78 btn.id = 'scheduleBtn'; 79 // var(--primary-text) is #60A5FA in dark mode and unset in light, so the 80 // fallback gives readable brand blue on the light join screen â the old 81 // hardcoded #60A5FA was a dark-theme colour washing out on white. 82 btn.style.cssText = 'background:rgba(37,99,235,0.08);border:1px solid rgba(37,99,235,0.3);color:var(--primary-text, #2563EB);padding:10px 20px;border-radius:8px;font-size:14px;font-weight:600;cursor:pointer;display:flex;align-items:center;gap:8px;margin-top:8px;width:100%;justify-content:center;transition:all 0.2s;'; 83 84 var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); 85 svg.setAttribute('width', '16'); 86 svg.setAttribute('height', '16'); 87 svg.setAttribute('viewBox', '0 0 24 24'); 88 svg.setAttribute('fill', 'none'); 89 svg.setAttribute('stroke', 'currentColor'); 90 svg.setAttribute('stroke-width', '2'); 91 var rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); 92 rect.setAttribute('x', '3'); 93 rect.setAttribute('y', '4'); 94 rect.setAttribute('width', '18'); 95 rect.setAttribute('height', '18'); 96 rect.setAttribute('rx', '2'); 97 svg.appendChild(rect); 98 var line1 = document.createElementNS('http://www.w3.org/2000/svg', 'line'); 99 line1.setAttribute('x1', '16'); 100 line1.setAttribute('y1', '2'); 101 line1.setAttribute('x2', '16'); 102 line1.setAttribute('y2', '6'); 103 svg.appendChild(line1); 104 var line2 = document.createElementNS('http://www.w3.org/2000/svg', 'line'); 105 line2.setAttribute('x1', '8'); 106 line2.setAttribute('y1', '2'); 107 line2.setAttribute('x2', '8'); 108 line2.setAttribute('y2', '6'); 109 svg.appendChild(line2); 110 var line3 = document.createElementNS('http://www.w3.org/2000/svg', 'line'); 111 line3.setAttribute('x1', '3'); 112 line3.setAttribute('y1', '10'); 113 line3.setAttribute('x2', '21'); 114 line3.setAttribute('y2', '10'); 115 svg.appendChild(line3); 116 117 btn.appendChild(svg); 118 var label = document.createElement('span'); 119 label.textContent = 'Schedule Session'; 120 btn.appendChild(label); 121 122 btn.onmouseover = function() { btn.style.background = 'rgba(37,99,235,0.16)'; }; 123 btn.onmouseout = function() { btn.style.background = 'rgba(37,99,235,0.08)'; }; 124 btn.onclick = function() { showCreateModal(); }; 125 126 joinArea.appendChild(btn); 127 } 128 129 // ========================================== 130 // CREATE MODAL 131 // ========================================== 132 133 function showCreateModal() { 134 var existing = document.getElementById('scheduleModal'); 135 if (existing) existing.remove(); 136
137 var overlay = document.createElement('div'); 138 overlay.id = 'scheduleModal'; 139 overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);display:flex;align-items:center;justify-content:center;z-index:10001;'; 140 overlay.onclick = function(e) { if (e.target === overlay) overlay.remove(); }; 141 142 var card = document.createElement('div'); 143 // Theme tokens, not a hard-coded dark slab: this modal was a dark island 144 // over the default light room (2026-08-09 audit, CR-03). 145 card.style.cssText = 'background:var(--surface-primary, #141824);border:1px solid var(--border-primary, rgba(255,255,255,0.1));border-radius:16px;padding:32px;max-width:480px;width:90%;max-height:90vh;overflow-y:auto;'; 146 147 // Title 148 var title = document.createElement('h3'); 149 title.style.cssText = 'color:var(--text-primary, #F7F9FC);margin:0 0 20px;font-size:20px;'; 150 title.textContent = 'Schedule Interpreting Session'; 151 card.appendChild(title); 152 153 // Form fields 154 var fields = [ 155 { id: 'schedTitle', label: 'Session Title', type: 'text', placeholder: 'e.g. NHS Patient Consultation', required: true }, 156 { id: 'schedDate', label: 'Date & Time', type: 'datetime-local', required: true }, 157 { id: 'schedDuration', label: 'Estimated Duration (min)', type: 'number', value: '30' }, 158 { id: 'schedInvitees', label: 'Invitee Emails', type: 'text', placeholder: '[email protected], [email protected]' }, 159 { id: 'schedNotes', label: 'Notes (optional)', type: 'textarea', placeholder: 'Any context for participants...' } 160 ]; 161 162 fields.forEach(function(f) { 163 var group = document.createElement('div'); 164 group.style.cssText = 'margin-bottom:16px;'; 165 166 var lbl = document.createElement('label'); 167 lbl.style.cssText = 'display:block;color:var(--text-secondary, #B4B4C7);font-size:13px;margin-bottom:6px;'; 168 lbl.textContent = f.label; 169 lbl.setAttribute('for', f.id); 170 group.appendChild(lbl); 171 172 var input; 173 if (f.type === 'textarea') { 174 input = document.createElement('textarea'); 175 input.rows = 3; 176 } else { 177 input = document.createElement('input'); 178 input.type = f.type; 179 } 180 input.id = f.id; 181 input.style.cssText = 'width:100%;background:var(--surface-secondary, rgba(255,255,255,0.04));border:1px solid var(--border-primary, rgba(255,255,255,0.1));color:var(--text-primary, #F7F9FC);padding:10px 12px;border-radius:8px;font-size:14px;box-sizing:border-box;'; 182 if (f.placeholder) input.placeholder = f.placeholder; 183 if (f.value) input.value = f.value; 184 if (f.required) input.required = true; 185 group.appendChild(input); 186 187 card.appendChild(group); 188 }); 189 190 // Set default datetime to 1 hour from now 191 var now = new Date(); 192 now.setHours(now.getHours() + 1, 0, 0, 0); 193 var dateInput = card.querySelector('#schedDate'); 194 if (dateInput) { 195 dateInput.value = now.toISOString().slice(0, 16); 196 } 197 198 // Actions 199 var actions = document.createElement('div'); 200 actions.style.cssText = 'display:flex;gap:12px;margin-top:24px;'; 201 202 var cancelBtn = document.createElement('button'); 203 cancelBtn.style.cssText = 'flex:1;background:var(--btn-neutral-bg, rgba(255,255,255,0.06));color:var(--text-secondary, #B4B4C7);border:1px solid var(--btn-neutral-border, rgba(255,255,255,0.1));padding:12px;border-radius:8px;font-size:14px;font-weight:600;cursor:pointer;'; 204 cancelBtn.textContent = 'Cancel'; 205 cancelBtn.onclick = function() { overlay.remove(); }; 206 actions.appendChild(cancelBtn); 207 208 var createBtn = document.createElement('button'); 209 createBtn.style.cssText = 'flex:1;background:#2563EB;color:#fff;border:none;padding:12px;border-radius:8px;font-size:14px;font-weight:600;cursor:pointer;'; 210 createBtn.textContent = 'Schedule'; 211 createBtn.onclick = function() { handleCreateSession(overlay); }; 212 actions.appendChild(createBtn); 213 214 card.appendChild(actions); 215 overlay.appendChild(card); 216 document.body.appendChild(overlay); 217 } 218 219 function handleCreateSession(overlay) { 220 var token = getAuthToken(); 221 if (!token) { showNotification('Please sign in first', 'warning'); return; } 222 223 var title = document.getElementById('schedTitle')?.value?.trim(); 224 var scheduledAt = document.getElementById('schedDate')?.value; 225 var duration = parseInt(document.getElementById('schedDuration')?.value) || 30; 226 var inviteeStr = document.getElementById('schedInvitees')?.value?.trim(); 227 var notes = document.getElementById('schedNotes')?.value?.trim(); 228 229 if (!title) { showNotification('Title is required', 'warning'); return; } 230 if (!scheduledAt) { showNotification('Date & time is required', 'warning'); return; } 231 232 var invitees = inviteeStr ? inviteeStr.split(',').map(function(e) { return e.trim(); }).filter(function(e) { return e.includes('@'); }) : []; 233 234 var body = { 235 title: title, 236 scheduledAt: new Date(scheduledAt).toISOString(), 237 estimatedDurationMin: duration, 238 invitees: invitees, 239 notes: notes || undefined 240 }; 241 242 fetch('/api/schedule/create', { 243 method: 'POST', 244 headers: { 245 'Authorization': 'Bearer ' + token, 246 'Content-Type': 'application/json' 247 }, 248 body: JSON.stringify(body) 249 }) 250 .then(function(r) { return r.json(); }) 251 .then(function(data) { 252 if (data.error) { 253 showNotification(data.error, 'warning'); 254 return; 255 } 256 overlay.remove(); 257 showNotification('Session scheduled! Room: ' + data.roomCode + (data.invitesSent > 0 ? ' (' + data.invitesSent + ' invite(s) sent)' : ''), 'success'); 258 }) 259 .catch(function() { 260 showNotification('Failed to create session', 'warning'); 261 }); 262 } 263 264 // ========================================== 265 // MY SESSIONS PANEL 266 // ========================================== 267 268 function showSessionsPanel() { 269 var existing = document.getElementById('sessionsPanel'); 270 if (existing) { existing.remove(); return; } 271
272 var overlay = document.createElement('div'); 273 overlay.id = 'sessionsPanel'; 274 overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.6);display:flex;justify-content:flex-end;z-index:10000;'; 275 overlay.onclick = function(e) { if (e.target === overlay) overlay.remove(); }; 276 277 var panel = document.createElement('div'); 278 panel.style.cssText = 'width:420px;max-width:90vw;height:100%;background:var(--surface-primary, #0F1219);border-left:1px solid var(--border-primary, rgba(255,255,255,0.08));display:flex;flex-direction:column;overflow:hidden;'; 279 280 // Header 281 var header = document.createElement('div'); 282 header.style.cssText = 'padding:20px;border-bottom:1px solid var(--border-primary, rgba(255,255,255,0.08));display:flex;justify-content:space-between;align-items:center;'; 283 284 var headerTitle = document.createElement('h3'); 285 headerTitle.style.cssText = 'color:var(--text-primary, #F7F9FC);margin:0;font-size:18px;'; 286 headerTitle.textContent = 'Scheduled Sessions'; 287 header.appendChild(headerTitle); 288 289 var closeBtn = document.createElement('button'); 290 closeBtn.style.cssText = 'background:none;border:none;color:var(--text-tertiary, #6B6B80);font-size:24px;cursor:pointer;padding:0 4px;'; 291 closeBtn.textContent = '\u00D7'; 292 closeBtn.onclick = function() { overlay.remove(); }; 293 header.appendChild(closeBtn); 294 295 panel.appendChild(header); 296 297 // New session button 298 var newBtnWrap = document.createElement('div'); 299 newBtnWrap.style.cssText = 'padding:12px 20px;'; 300 301 var newBtn = document.createElement('button'); 302 newBtn.style.cssText = 'width:100%;background:rgba(37,99,235,0.1);border:1px solid rgba(37,99,235,0.3);color:#60A5FA;padding:10px;border-radius:8px;font-size:14px;font-weight:600;cursor:pointer;'; 303 newBtn.textContent = '+ New Session'; 304 newBtn.onclick = function() { overlay.remove(); showCreateModal(); }; 305 newBtnWrap.appendChild(newBtn); 306 panel.appendChild(newBtnWrap); 307 308 var listEl = document.createElement('div'); 309 listEl.id = 'sessionsList'; 310 listEl.style.cssText = 'flex:1;overflow-y:auto;padding:0 16px 16px;'; 311 panel.appendChild(listEl); 312 313 overlay.appendChild(panel); 314 document.body.appendChild(overlay); 315 316 loadSessions(); 317 } 318 319 function loadSessions() { 320 var listEl = document.getElementById('sessionsList'); 321 if (!listEl) return; 322 323 listEl.textContent = ''; 324 var loadingMsg = document.createElement('div'); 325 loadingMsg.style.cssText = 'text-align:center;padding:40px;color:var(--text-tertiary, #6B6B80);'; 326 loadingMsg.textContent = 'Loading...'; 327 listEl.appendChild(loadingMsg); 328 329 var token = getAuthToken(); 330 if (!token) { 331 listEl.textContent = ''; 332 var msg = document.createElement('div'); 333 msg.style.cssText = 'text-align:center;padding:40px;color:var(--text-tertiary, #6B6B80);'; 334 msg.textContent = 'Please sign in to view sessions.'; 335 listEl.appendChild(msg); 336 return; 337 } 338 339 fetch('/api/schedule/my-sessions', { 340 headers: { 'Authorization': 'Bearer ' + token } 341 }) 342 .then(function(r) { if (!r.ok) throw new Error(r.status); return r.json(); }) 343 .then(function(data) { 344 listEl.textContent = ''; 345 346 if (!data.sessions || data.sessions.length === 0) { 347 var emptyMsg = document.createElement('div'); 348 emptyMsg.style.cssText = 'text-align:center;padding:40px;color:var(--text-tertiary, #6B6B80);'; 349 emptyMsg.textContent = 'No scheduled sessions yet.'; 350 listEl.appendChild(emptyMsg); 351 return; 352 } 353 354 data.sessions.forEach(function(session) { 355 var item = document.createElement('div'); 356 item.style.cssText = 'background:var(--surface-secondary, rgba(255,255,255,0.03));border:1px solid var(--border-primary, rgba(255,255,255,0.06));border-radius:12px;padding:16px;margin-bottom:12px;'; 357 358 var statusColors = { 359 pending: '#60A5FA', reminded: '#60A5FA', active: '#10B981', 360 completed: '#6B6B80', cancelled: '#ef4444', expired: '#6B6B80' 361 }; 362 var statusColor = statusColors[session.status] || '#6B6B80'; 363 364 // Title row 365 var titleRow = document.createElement('div'); 366 titleRow.style.cssText = 'display:flex;justify-content:space-between;align-items:start;margin-bottom:8px;'; 367 368 var titleDiv = document.createElement('div'); 369 var titleH4 = document.createElement('h4'); 370 titleH4.style.cssText = 'color:var(--text-primary, #F7F9FC);margin:0;font-size:14px;'; 371 titleH4.textContent = session.title; 372 titleDiv.appendChild(titleH4); 373 374 var dateP = document.createElement('p'); 375 dateP.style.cssText = 'color:var(--text-tertiary, #6B6B80);font-size:12px;margin:4px 0 0;'; 376 dateP.textContent = formatDate(session.scheduled_at) + ' at ' + formatTime(session.scheduled_at); 377 titleDiv.appendChild(dateP); 378 titleRow.appendChild(titleDiv); 379 380 var badge = document.createElement('span'); 381 badge.style.cssText = 'font-size:11px;padding:2px 8px;border-radius:4px;font-weight:600;text-transform:uppercase;color:' + statusColor + ';background:' + statusColor + '20;'; 382 badge.textContent = session.status; 383 titleRow.appendChild(badge); 384 385 item.appendChild(titleRow); 386 387 // Meta 388 var meta = document.createElement('div'); 389 meta.style.cssText = 'display:flex;gap:12px;color:var(--text-tertiary, #6B6B80);font-size:12px;margin-bottom:12px;'; 390 391 var roomSpan = document.createElement('span'); 392 roomSpan.textContent = 'Room: ' + session.room_code; 393 meta.appendChild(roomSpan); 394 395 if (session.status === 'pending' || session.status === 'reminded') { 396 var countdownSpan = document.createElement('span'); 397 countdownSpan.style.color = '#60A5FA'; 398 countdownSpan.textContent = 'In ' + timeUntil(session.scheduled_at); 399 meta.appendChild(countdownSpan); 400 } 401
402 var guestsSpan = document.createElement('span'); 403 guestsSpan.textContent = (session.guest_tokens?.length || 0) + ' invitee(s)'; 404 meta.appendChild(guestsSpan); 405 406 item.appendChild(meta); 407 408 // Actions for pending/reminded sessions 409 if (session.status === 'pending' || session.status === 'reminded') { 410 var actions = document.createElement('div'); 411 actions.style.cssText = 'display:flex;gap:8px;'; 412 413 var startBtn = document.createElement('button'); 414 startBtn.style.cssText = 'flex:1;background:#10B981;color:#fff;border:none;padding:8px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;'; 415 startBtn.textContent = 'Start Now'; 416 startBtn.setAttribute('data-id', session.id); 417 startBtn.setAttribute('data-room', session.room_code); 418 startBtn.onclick = function() { 419 var sid = this.getAttribute('data-id'); 420 var room = this.getAttribute('data-room'); 421 startSession(sid, room); 422 }; 423 actions.appendChild(startBtn); 424 425 var cancelSessionBtn = document.createElement('button'); 426 cancelSessionBtn.style.cssText = 'background:none;border:1px solid rgba(239,68,68,0.3);color:#ef4444;padding:8px 16px;border-radius:6px;font-size:12px;cursor:pointer;'; 427 cancelSessionBtn.textContent = 'Cancel'; 428 cancelSessionBtn.setAttribute('data-id', session.id); 429 cancelSessionBtn.onclick = function() { cancelSession(this.getAttribute('data-id')); }; 430 actions.appendChild(cancelSessionBtn); 431 432 item.appendChild(actions); 433 } 434 435 listEl.appendChild(item); 436 }); 437 }) 438 .catch(function() { 439 listEl.textContent = ''; 440 var errMsg = document.createElement('div'); 441 errMsg.style.cssText = 'text-align:center;padding:40px;color:#ef4444;'; 442 errMsg.textContent = 'Failed to load sessions.'; 443 listEl.appendChild(errMsg); 444 }); 445 } 446 447 function startSession(sessionId, roomCode) { 448 var token = getAuthToken(); 449 fetch('/api/schedule/' + encodeURIComponent(sessionId) + '/start', { 450 method: 'POST', 451 headers: { 'Authorization': 'Bearer ' + token } 452 }) 453 .then(function(r) { return r.json(); }) 454 .then(function(data) { 455 if (data.error) { showNotification(data.error, 'warning'); return; } 456 // Close panel and join room 457 var panel = document.getElementById('sessionsPanel'); 458 if (panel) panel.remove(); 459 showNotification('Session started! Joining room ' + roomCode, 'success'); 460 // Auto-join. The old code looked for #joinRoomBtn and 461 // button[data-action="join"] â neither id exists anywhere in the app, 462 // so the toast said "Joining room X" and nothing ever happened 463 // (2026-07-29). The real control is #joinBtn, and precall-check.js 464 // intercepts it in the capture phase to run its mic/speaker wizard, 465 // which is the correct destination: the user confirms their devices 466 // and the wizard's own Join takes them in. 467 var roomInput = document.getElementById('roomCode'); 468 var joinBtn = document.getElementById('joinBtn'); 469 if (roomInput && joinBtn) { 470 roomInput.value = roomCode; 471 roomInput.dispatchEvent(new Event('input', { bubbles: true })); 472 joinBtn.click(); 473 } else { 474 showNotification('Room ' + roomCode + ' is ready â enter it on the join screen.', 'info'); 475 } 476 }) 477 .catch(function() { showNotification('Failed to start session', 'warning'); }); 478 } 479 480 function cancelSession(sessionId) { 481 if (!confirm('Cancel this scheduled session?')) return; 482 var token = getAuthToken(); 483 fetch('/api/schedule/' + encodeURIComponent(sessionId), { 484 method: 'DELETE', 485 headers: { 'Authorization': 'Bearer ' + token } 486 }) 487 .then(function() { loadSessions(); showNotification('Session cancelled', 'success'); }) 488 .catch(function() { showNotification('Failed to cancel session', 'warning'); }); 489 } 490 491 window.showSessionsPanel = showSessionsPanel; 492 window.showScheduleCreate = showCreateModal; 493 494 // ========================================== 495 // UPCOMING SESSION BANNER 496 // ========================================== 497 498 function checkUpcomingSessions() { 499 var token = getAuthToken(); 500 if (!token) return; 501 502 fetch('/api/schedule/upcoming', { 503 headers: { 'Authorization': 'Bearer ' + token } 504 }) 505 .then(function(r) { return r.json(); }) 506 .then(function(data) { 507 if (!data.sessions || data.sessions.length === 0) return; 508 509 // Show banner for session within 15 minutes 510 var soonSession = data.sessions.find(function(s) { 511 var diff = new Date(s.scheduled_at).getTime() - Date.now(); 512 return diff > 0 && diff <= 15 * 60 * 1000; 513 }); 514 515 if (soonSession) { 516 showUpcomingBanner(soonSession); 517 } 518 }) 519 .catch(function() { /* ignore */ }); 520 } 521 522 function showUpcomingBanner(session) { 523 if (document.getElementById('upcomingBanner')) return; 524 525 var banner = document.createElement('div'); 526 banner.id = 'upcomingBanner'; 527 banner.style.cssText = 'position:fixed;top:0;left:0;right:0;background:linear-gradient(90deg,#1e3a5f,#2563EB);color:#fff;padding:10px 20px;font-size:14px;display:flex;align-items:center;justify-content:center;gap:12px;z-index:9998;box-shadow:0 2px 10px rgba(0,0,0,0.3);'; 528 529 var text = document.createElement('span'); 530 text.textContent = 'Upcoming: "' + session.title + '" starts in ' + timeUntil(session.scheduled_at); 531 banner.appendChild(text); 532 533 var joinBtn = document.createElement('button'); 534 joinBtn.style.cssText = 'background:#10B981;color:#fff;border:none;padding:6px 16px;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;'; 535 joinBtn.textContent = 'Join Now'; 536 joinBtn.onclick = function() { 537 startSession(session.id, session.room_code); 538 banner.remove(); 539 }; 540 banner.appendChild(joinBtn); 541 542 var dismissBtn = document.createElement('button');
543 dismissBtn.style.cssText = 'background:none;border:none;color:rgba(255,255,255,0.6);font-size:18px;cursor:pointer;margin-left:4px;'; 544 dismissBtn.textContent = '\u00D7'; 545 dismissBtn.onclick = function() { banner.remove(); }; 546 banner.appendChild(dismissBtn); 547 548 document.body.appendChild(banner); 549 } 550 551 // ========================================== 552 // INITIALIZATION 553 // ========================================== 554 555 // Inject schedule button when pre-room screen is visible 556 var scheduleBtnInjected = false; 557 var scheduleObserver = new MutationObserver(function() { 558 if (scheduleBtnInjected) return; 559 injectScheduleButton(); 560 if (document.getElementById('scheduleBtn')) { 561 scheduleBtnInjected = true; 562 scheduleObserver.disconnect(); 563 } 564 }); 565 scheduleObserver.observe(document.body, { childList: true, subtree: true }); 566 567 // Check for upcoming sessions every 2 minutes 568 setTimeout(checkUpcomingSessions, 3000); 569 var upcomingInterval = setInterval(checkUpcomingSessions, 120000); 570 window.addEventListener('beforeunload', function() { clearInterval(upcomingInterval); }); 571 572 console.log('Schedule UI loaded'); 573})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.