const els = { adminStatus: document.querySelector('#adminStatus'), credentialsSource: document.querySelector('#credentialsSource'), tokenStatus: document.querySelector('#tokenStatus'), steamStatus: document.querySelector('#steamStatus'), gcStatus: document.querySelector('#gcStatus'), loginPanel: document.querySelector('#loginPanel'), notificationPanel: document.querySelector('#notificationPanel'), telegramPanel: document.querySelector('#telegramPanel'), lobbyPanel: document.querySelector('#lobbyPanel'), credentialsPanel: document.querySelector('#credentialsPanel'), guardPanel: document.querySelector('#guardPanel'), tokenPanel: document.querySelector('#tokenPanel'), adminPassword: document.querySelector('#adminPassword'), unlockBtn: document.querySelector('#unlockBtn'), adminError: document.querySelector('#adminError'), adminMessage: document.querySelector('#adminMessage'), steamAccount: document.querySelector('#steamAccount'), steamPassword: document.querySelector('#steamPassword'), clearToken: document.querySelector('#clearToken'), reloadBtn: document.querySelector('#reloadBtn'), saveBtn: document.querySelector('#saveBtn'), steamGuardCode: document.querySelector('#steamGuardCode'), submitGuardBtn: document.querySelector('#submitGuardBtn'), guardHint: document.querySelector('#guardHint'), tokenSteamId: document.querySelector('#tokenSteamId'), tokenAccount: document.querySelector('#tokenAccount'), tokenUpdated: document.querySelector('#tokenUpdated'), tokenExpires: document.querySelector('#tokenExpires'), clearTokenBtn: document.querySelector('#clearTokenBtn'), enableNotificationsBtn: document.querySelector('#enableNotificationsBtn'), disableNotificationsBtn: document.querySelector('#disableNotificationsBtn'), testNotificationBtn: document.querySelector('#testNotificationBtn'), notificationStatus: document.querySelector('#notificationStatus'), telegramToken: document.querySelector('#telegramToken'), telegramRecipientIds: document.querySelector('#telegramRecipientIds'), saveTelegramBtn: document.querySelector('#saveTelegramBtn'), testTelegramBtn: document.querySelector('#testTelegramBtn'), disableTelegramBtn: document.querySelector('#disableTelegramBtn'), telegramStatus: document.querySelector('#telegramStatus'), savedSteamAccount: document.querySelector('#savedSteamAccount'), activateAccountBtn: document.querySelector('#activateAccountBtn'), lobbyName: document.querySelector('#lobbyName'), lobbyRegion: document.querySelector('#lobbyRegion'), customGameId: document.querySelector('#customGameId'), customGameMode: document.querySelector('#customGameMode'), customMapName: document.querySelector('#customMapName'), lobbyPassKey: document.querySelector('#lobbyPassKey'), customMinPlayers: document.querySelector('#customMinPlayers'), customMaxPlayers: document.querySelector('#customMaxPlayers'), notifyAtPlayers: document.querySelector('#notifyAtPlayers'), leaveAtPlayers: document.querySelector('#leaveAtPlayers'), adminNames: document.querySelector('#adminNames'), customGameCrc: document.querySelector('#customGameCrc'), customGameTimestamp: document.querySelector('#customGameTimestamp'), allowSpectating: document.querySelector('#allowSpectating'), saveLobbySettingsBtn: document.querySelector('#saveLobbySettingsBtn'), createLobbyBtn: document.querySelector('#createLobbyBtn'), stopLobbyBtn: document.querySelector('#stopLobbyBtn'), automationPhase: document.querySelector('#automationPhase'), activeLobbyId: document.querySelector('#activeLobbyId'), activePlayerCount: document.querySelector('#activePlayerCount'), publicationStatus: document.querySelector('#publicationStatus'), arcadePublicationStatus: document.querySelector('#arcadePublicationStatus'), lobbyProtocolStatus: document.querySelector('#lobbyProtocolStatus'), dotaClientVersion: document.querySelector('#dotaClientVersion'), leaveReason: document.querySelector('#leaveReason'), lobbyMembers: document.querySelector('#lobbyMembers'), }; let adminPassword = sessionStorage.getItem('dokaAdminPassword') || ''; let credentialsDirty = false; let lobbySettingsDirty = false; let telegramDirty = false; let regionsReady = false; let lastAdminState = null; function readPendingLobbyTemplate() { try { const saved = sessionStorage.getItem('dokaLobbyTemplate'); if (saved) { const parsed = JSON.parse(saved); if (parsed && typeof parsed === 'object') return parsed; } } catch { // Fall through to the query-string compatibility path. } const query = new URLSearchParams(window.location.search); return query.get('template') === 'lobby' ? Object.fromEntries(query.entries()) : null; } const pendingLobbyTemplate = readPendingLobbyTemplate(); let lobbyTemplateApplied = false; const dateTimeFormat = new Intl.DateTimeFormat('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', }); function formatDate(iso) { if (!iso) return '-'; const date = new Date(iso); if (Number.isNaN(date.getTime())) return '-'; return dateTimeFormat.format(date); } function setBadge(label, klass) { els.adminStatus.textContent = label; els.adminStatus.className = `badge ${klass}`; } function showError(message) { els.adminError.hidden = !message; els.adminError.textContent = message || ''; } function showMessage(message) { els.adminMessage.hidden = !message; els.adminMessage.textContent = message || ''; } async function adminFetch(url, options = {}) { const res = await fetch(url, { ...options, cache: 'no-store', headers: { 'Content-Type': 'application/json', 'X-Admin-Password': adminPassword, ...(options.headers || {}), }, }); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`); return data; } function unlocked() { return Boolean(adminPassword); } function initRegions(regions) { if (regionsReady || !regions) return; for (const [id, name] of Object.entries(regions).sort(([a], [b]) => Number(a) - Number(b))) { const option = document.createElement('option'); option.value = id; option.textContent = `${name} (${id})`; els.lobbyRegion.append(option); } regionsReady = true; } function setLobbyInputs(settings) { els.lobbyName.value = settings.lobbyName || ''; els.lobbyRegion.value = String(settings.serverRegion ?? 3); els.customGameId.value = settings.customGameId || ''; els.customGameMode.value = settings.customGameMode || ''; els.customMapName.value = settings.customMapName || ''; els.lobbyPassKey.value = settings.passKey || ''; els.customMinPlayers.value = String(settings.customMinPlayers ?? 2); els.customMaxPlayers.value = String(settings.customMaxPlayers ?? 10); els.notifyAtPlayers.value = String(settings.notifyAtPlayers ?? 8); els.leaveAtPlayers.value = String(settings.leaveAtPlayers ?? 9); els.adminNames.value = (settings.adminNames || []).join(', '); els.customGameCrc.value = settings.customGameCrc || ''; els.customGameTimestamp.value = settings.customGameTimestamp || ''; els.allowSpectating.checked = settings.allowSpectating !== false; lobbySettingsDirty = false; } async function resolveMissingTemplateMetadata() { if (els.customGameMode.value || !pendingLobbyTemplate?.sourceLobbyId) return; const controller = new AbortController(); let timedOut = false; const timeout = setTimeout(() => { timedOut = true; controller.abort(); }, 15000); try { showMessage('Resolving the internal addon name (local Workshop first, Dota GC fallback; up to 15 seconds)...'); const data = await adminFetch('/api/admin/lobby-template', { method: 'POST', signal: controller.signal, body: JSON.stringify({ lobbyId: pendingLobbyTemplate.sourceLobbyId, serverRegion: Number(pendingLobbyTemplate.serverRegion || 0), customGameId: pendingLobbyTemplate.customGameId, }), }); els.customGameMode.value = data.templateMetadata?.customGameMode || ''; if (!els.customGameTimestamp.value && data.templateMetadata?.customGameTimestamp) { els.customGameTimestamp.value = data.templateMetadata.customGameTimestamp; } if (!els.customGameMode.value) throw new Error('The source lobby has no internal addon name.'); lobbySettingsDirty = true; showError(''); showMessage(`Lobby template is complete: Workshop ${els.customGameId.value}, addon ${els.customGameMode.value}, map ${els.customMapName.value}. Review the thresholds, then create it.`); } catch (err) { showError(`Could not complete lobby template: ${timedOut ? 'metadata lookup timed out after 15 seconds' : err.message}`); } finally { clearTimeout(timeout); } } function applyLobbyTemplate() { if (!pendingLobbyTemplate || lobbyTemplateApplied) return; const setFromTemplate = (element, key) => { const value = pendingLobbyTemplate[key]; if (value !== undefined && value !== null) element.value = String(value); }; setFromTemplate(els.lobbyName, 'lobbyName'); setFromTemplate(els.lobbyRegion, 'serverRegion'); setFromTemplate(els.customGameId, 'customGameId'); setFromTemplate(els.customGameMode, 'customGameMode'); setFromTemplate(els.customMapName, 'customMapName'); setFromTemplate(els.customMinPlayers, 'customMinPlayers'); setFromTemplate(els.customMaxPlayers, 'customMaxPlayers'); setFromTemplate(els.customGameCrc, 'customGameCrc'); setFromTemplate(els.customGameTimestamp, 'customGameTimestamp'); // The source feed never exposes the lobby password. Clear any stale value. els.lobbyPassKey.value = ''; const maxPlayers = Number(els.customMaxPlayers.value); if (Number.isInteger(maxPlayers) && maxPlayers > 0) { const highestSafeThreshold = Math.max(1, maxPlayers - 1); const leaveAt = Math.min(Number(els.leaveAtPlayers.value) || highestSafeThreshold, highestSafeThreshold); els.leaveAtPlayers.value = String(leaveAt); els.notifyAtPlayers.value = String(Math.min(Number(els.notifyAtPlayers.value) || leaveAt, leaveAt)); } lobbyTemplateApplied = true; lobbySettingsDirty = true; try { sessionStorage.removeItem('dokaLobbyTemplate'); } catch { // The query-string fallback is still removed below. } window.history.replaceState({}, '', '/admin'); showMessage(`Lobby template loaded: Workshop ${els.customGameId.value}, map ${els.customMapName.value}. Review the notification and leave thresholds, then create it.`); if (!els.customGameMode.value) { showError('The GC did not return the internal addon name for this row. Refresh the lobby list and create the template again; an incomplete template cannot be published.'); resolveMissingTemplateMetadata(); } } function lobbyPayload() { return { lobbyName: els.lobbyName.value, serverRegion: Number(els.lobbyRegion.value), customGameId: els.customGameId.value, customGameMode: els.customGameMode.value, customMapName: els.customMapName.value, passKey: els.lobbyPassKey.value, customMinPlayers: Number(els.customMinPlayers.value), customMaxPlayers: Number(els.customMaxPlayers.value), notifyAtPlayers: Number(els.notifyAtPlayers.value), leaveAtPlayers: Number(els.leaveAtPlayers.value), adminNames: els.adminNames.value, customGameCrc: els.customGameCrc.value, customGameTimestamp: els.customGameTimestamp.value, allowSpectating: els.allowSpectating.checked, }; } function renderLobbyAutomation(automation) { const status = automation.status || {}; const settings = automation.settings || {}; const busy = ['creating', 'leaving'].includes(status.phase); const hasLobby = Boolean(status.lobbyId); if (!lobbySettingsDirty && !busy) setLobbyInputs(settings); if (unlocked() && regionsReady && automation.settings && !busy) applyLobbyTemplate(); els.automationPhase.textContent = status.phase || 'idle'; els.activeLobbyId.textContent = status.lobbyId || '-'; els.activePlayerCount.textContent = String(status.humanCount ?? 0); els.publicationStatus.textContent = status.practicePublished === true ? 'listed' : status.practicePublished === false ? 'not listed' : status.armed ? 'checking' : '-'; els.arcadePublicationStatus.textContent = status.arcadePublished === true ? 'listed' : status.arcadePublished === false ? 'not listed' : status.armed ? 'checking' : '-'; const protocol = status.lobbyProtocol; els.lobbyProtocolStatus.textContent = protocol ? `type=${protocol.lobbyType}, visibility=${protocol.visibility}, auto=${protocol.autoCreated ? 'yes' : 'no'}, bot team=${protocol.botTeam ?? '-'}` : '-'; els.leaveReason.textContent = status.leaveReason || '-'; els.createLobbyBtn.disabled = busy || hasLobby || Boolean(status.armed); els.stopLobbyBtn.disabled = busy || (!hasLobby && !status.armed); els.saveLobbySettingsBtn.disabled = busy; const members = status.members || []; if (members.length) { els.lobbyMembers.textContent = members .map((member) => member.name ? `${member.name} (${member.steamId})` : member.steamId) .join(', '); } else { els.lobbyMembers.textContent = 'No real players in the active lobby.'; } if (status.lastError) showError(status.lastError); } function pushSupported() { return 'serviceWorker' in navigator && 'PushManager' in window && 'Notification' in window; } async function currentPushSubscription() { if (!pushSupported()) return null; const registration = await navigator.serviceWorker.register('/sw.js'); return registration.pushManager.getSubscription(); } async function updateNotificationStatus() { if (!pushSupported()) { els.notificationStatus.textContent = 'not supported'; els.enableNotificationsBtn.disabled = true; els.disableNotificationsBtn.disabled = true; els.testNotificationBtn.disabled = true; return; } try { const subscription = await currentPushSubscription(); els.notificationStatus.textContent = subscription && Notification.permission === 'granted' ? 'enabled on this browser' : Notification.permission; els.disableNotificationsBtn.disabled = !subscription; els.testNotificationBtn.disabled = !subscription; } catch (err) { els.notificationStatus.textContent = err.message; } } function urlBase64ToUint8Array(value) { const padding = '='.repeat((4 - (value.length % 4)) % 4); const base64 = (value + padding).replace(/-/g, '+').replace(/_/g, '/'); const raw = atob(base64); return Uint8Array.from([...raw].map((char) => char.charCodeAt(0))); } function renderSavedAccounts(credentials) { const accounts = Array.isArray(credentials.savedAccounts) ? credentials.savedAccounts : []; const currentSelection = els.savedSteamAccount.value; els.savedSteamAccount.replaceChildren(); if (!accounts.length) { const option = document.createElement('option'); option.value = ''; option.textContent = 'No saved accounts'; els.savedSteamAccount.append(option); els.savedSteamAccount.disabled = true; els.activateAccountBtn.disabled = true; return; } for (const account of accounts) { const option = document.createElement('option'); option.value = account.accountName; option.textContent = account.accountName === credentials.activeAccountName ? `${account.accountName} (active)` : account.accountName; els.savedSteamAccount.append(option); } els.savedSteamAccount.disabled = false; els.activateAccountBtn.disabled = false; const preferred = accounts.some((account) => account.accountName === currentSelection) ? currentSelection : credentials.activeAccountName; els.savedSteamAccount.value = preferred || accounts[0].accountName; } function renderTelegram(telegram = {}) { const bot = telegram.botUsername ? `@${telegram.botUsername}` : 'bot'; if (telegram.configured) { els.telegramStatus.textContent = `${bot}, ${telegram.recipientCount || 0} recipient(s)${telegram.lastError ? `; error: ${telegram.lastError}` : ''}`; } else { els.telegramStatus.textContent = 'not configured'; } els.testTelegramBtn.disabled = !telegram.configured; els.disableTelegramBtn.disabled = !telegram.configured; const editing = document.activeElement === els.telegramToken || document.activeElement === els.telegramRecipientIds || telegramDirty; if (!editing) { els.telegramToken.value = ''; els.telegramToken.placeholder = telegram.configured ? 'configured; leave blank to keep saved token' : 'paste token from @BotFather'; els.telegramRecipientIds.value = (telegram.recipientIds || []).join(', '); telegramDirty = false; } } function render(data, options = {}) { lastAdminState = data; const preserveEditedInputs = options.preserveEditedInputs !== false; const connection = data.connection || {}; const credentials = data.credentials || {}; const token = data.refreshToken || {}; const steamGuard = connection.steamGuard || {}; els.loginPanel.hidden = unlocked(); els.notificationPanel.hidden = !unlocked(); els.telegramPanel.hidden = !unlocked(); els.lobbyPanel.hidden = !unlocked(); els.credentialsPanel.hidden = !unlocked(); els.tokenPanel.hidden = !unlocked(); els.guardPanel.hidden = !unlocked() || !steamGuard.required; setBadge(unlocked() ? 'unlocked' : 'locked', unlocked() ? 'badge-ok' : 'badge-muted'); els.credentialsSource.textContent = credentials.source || '-'; els.tokenStatus.textContent = token.exists ? 'saved' : 'missing'; els.steamStatus.textContent = connection.steam || '-'; els.gcStatus.textContent = connection.gc || '-'; els.dotaClientVersion.textContent = connection.dotaClientVersion ? `${connection.dotaClientVersion}${connection.dotaClientVersionSource ? ` (${connection.dotaClientVersionSource})` : ''}` : 'not detected; set DOTA_CLIENT_VERSION'; initRegions(data.regions); renderLobbyAutomation(data.lobbyAutomation || {}); renderTelegram(data.lobbyAutomation?.telegram || {}); renderSavedAccounts(credentials); const editingCredentials = document.activeElement === els.steamAccount || document.activeElement === els.steamPassword || credentialsDirty; if (!preserveEditedInputs || !editingCredentials) { els.steamAccount.value = credentials.accountName || ''; els.steamPassword.value = ''; els.steamPassword.placeholder = credentials.passwordSaved ? 'saved; leave blank to keep the saved password' : 'password required for a new account'; credentialsDirty = false; } els.tokenSteamId.textContent = token.steamID || '-'; els.tokenAccount.textContent = token.accountName || '-'; els.tokenUpdated.textContent = formatDate(token.updatedAt); els.tokenExpires.textContent = formatDate(token.expiresAt); if (steamGuard.required) { const where = steamGuard.domain ? `Email domain: ${steamGuard.domain}` : 'Mobile authenticator code required'; els.guardHint.textContent = steamGuard.lastCodeWrong ? `Last code was rejected. ${where}.` : where; } else { els.guardHint.textContent = ''; } } async function loadAuth(options = {}) { if (!unlocked()) { render({}); return; } try { showError(''); const data = await adminFetch('/api/admin/auth'); render(data, options); } catch (err) { showError(err.message); if (err.message.includes('Invalid admin password')) { adminPassword = ''; sessionStorage.removeItem('dokaAdminPassword'); } render({}); } } async function saveCredentials() { try { showError(''); showMessage(''); const data = await adminFetch('/api/admin/auth', { method: 'PUT', body: JSON.stringify({ accountName: els.steamAccount.value, password: els.steamPassword.value, clearRefreshToken: els.clearToken.checked, }), }); credentialsDirty = false; render(data); showMessage('Credentials saved. Reconnect started.'); } catch (err) { showError(err.message); } } async function submitSteamGuard() { try { showError(''); showMessage(''); const data = await adminFetch('/api/admin/steam-guard', { method: 'POST', body: JSON.stringify({ code: els.steamGuardCode.value }), }); els.steamGuardCode.value = ''; render(data); showMessage('Steam Guard code submitted.'); } catch (err) { showError(err.message); } } async function clearRefreshToken() { try { showError(''); showMessage(''); const data = await adminFetch('/api/admin/clear-token', { method: 'POST', body: '{}' }); render(data); showMessage('Refresh token cleared.'); } catch (err) { showError(err.message); } } async function saveLobbySettings() { try { showError(''); showMessage(''); const data = await adminFetch('/api/admin/lobby-settings', { method: 'PUT', body: JSON.stringify(lobbyPayload()), }); lobbySettingsDirty = false; render(data); showMessage('Lobby settings saved.'); } catch (err) { showError(err.message); } } async function createLobby() { try { showError(''); showMessage('Creating lobby and waiting for the Dota GC snapshot...'); els.createLobbyBtn.disabled = true; const data = await adminFetch('/api/admin/lobby', { method: 'POST', body: JSON.stringify(lobbyPayload()), }); lobbySettingsDirty = false; render(data); showMessage('Lobby created. Player monitoring and safe auto-leave are active.'); } catch (err) { showError(err.message); showMessage(''); } finally { if (!lastAdminState?.lobbyAutomation?.status?.armed) els.createLobbyBtn.disabled = false; } } async function stopLobby() { try { showError(''); showMessage('Leaving lobby...'); els.stopLobbyBtn.disabled = true; const data = await adminFetch('/api/admin/lobby', { method: 'DELETE', body: '{}', }); render(data); showMessage('The bot left the lobby.'); } catch (err) { showError(err.message); showMessage(''); } } async function enableNotifications() { try { showError(''); if (!pushSupported()) throw new Error('Push notifications are not supported by this browser.'); const permission = await Notification.requestPermission(); if (permission !== 'granted') throw new Error(`Notification permission is ${permission}.`); const registration = await navigator.serviceWorker.register('/sw.js'); let subscription = await registration.pushManager.getSubscription(); if (!subscription) { const publicKey = lastAdminState?.lobbyAutomation?.push?.publicKey; if (!publicKey) throw new Error('Web Push public key is unavailable.'); subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(publicKey), }); } const data = await adminFetch('/api/admin/push-subscription', { method: 'PUT', body: JSON.stringify({ subscription: subscription.toJSON() }), }); render(data); await updateNotificationStatus(); showMessage('Browser push notifications enabled.'); } catch (err) { showError(err.message); } } async function disableNotifications() { try { showError(''); const subscription = await currentPushSubscription(); if (subscription) { await adminFetch('/api/admin/push-subscription', { method: 'DELETE', body: JSON.stringify({ endpoint: subscription.endpoint }), }); await subscription.unsubscribe(); } await updateNotificationStatus(); showMessage('Browser push notifications disabled on this browser.'); } catch (err) { showError(err.message); } } async function testNotification() { try { showError(''); const data = await adminFetch('/api/admin/test-notification', { method: 'POST', body: '{}' }); const result = data.notificationResult || {}; showMessage(`Test notification sent: ${result.delivered || 0} delivered, ${result.failed || 0} failed.`); } catch (err) { showError(err.message); } } async function saveTelegram() { try { showError(''); showMessage('Checking the Telegram bot and saving recipients...'); const data = await adminFetch('/api/admin/telegram', { method: 'PUT', body: JSON.stringify({ token: els.telegramToken.value, recipientIds: els.telegramRecipientIds.value, }), }); telegramDirty = false; render(data); showMessage('Telegram notifications saved. Use Send test to verify every recipient.'); } catch (err) { showMessage(''); showError(err.message); } } async function disableTelegram() { try { showError(''); const data = await adminFetch('/api/admin/telegram', { method: 'DELETE', body: '{}' }); telegramDirty = false; render(data); showMessage('Telegram notifications disabled and the saved bot token removed.'); } catch (err) { showError(err.message); } } async function testTelegram() { try { showError(''); const data = await adminFetch('/api/admin/test-telegram', { method: 'POST', body: '{}' }); const result = data.telegramResult || {}; render(data); const errors = result.errors?.length ? ` ${result.errors.join('; ')}` : ''; showMessage(`Telegram test: ${result.delivered || 0} delivered, ${result.failed || 0} failed.${errors}`); } catch (err) { showError(err.message); } } async function activateSavedAccount() { try { showError(''); showMessage('Switching Steam account and reconnecting Dota GC...'); const data = await adminFetch('/api/admin/steam-account/activate', { method: 'POST', body: JSON.stringify({ accountName: els.savedSteamAccount.value }), }); credentialsDirty = false; render(data); showMessage('Steam account activated. Reconnection continues in the background.'); } catch (err) { showMessage(''); showError(err.message); } } els.unlockBtn.addEventListener('click', () => { adminPassword = els.adminPassword.value; sessionStorage.setItem('dokaAdminPassword', adminPassword); loadAuth({ preserveEditedInputs: false }); }); els.adminPassword.addEventListener('keydown', (event) => { if (event.key === 'Enter') els.unlockBtn.click(); }); els.reloadBtn.addEventListener('click', () => { credentialsDirty = false; loadAuth({ preserveEditedInputs: false }); }); els.saveBtn.addEventListener('click', saveCredentials); els.submitGuardBtn.addEventListener('click', submitSteamGuard); els.clearTokenBtn.addEventListener('click', clearRefreshToken); els.saveLobbySettingsBtn.addEventListener('click', saveLobbySettings); els.createLobbyBtn.addEventListener('click', createLobby); els.stopLobbyBtn.addEventListener('click', stopLobby); els.enableNotificationsBtn.addEventListener('click', enableNotifications); els.disableNotificationsBtn.addEventListener('click', disableNotifications); els.testNotificationBtn.addEventListener('click', testNotification); els.saveTelegramBtn.addEventListener('click', saveTelegram); els.testTelegramBtn.addEventListener('click', testTelegram); els.disableTelegramBtn.addEventListener('click', disableTelegram); els.activateAccountBtn.addEventListener('click', activateSavedAccount); els.steamAccount.addEventListener('input', () => { credentialsDirty = true; }); els.steamPassword.addEventListener('input', () => { credentialsDirty = true; }); els.savedSteamAccount.addEventListener('change', () => { if (!els.savedSteamAccount.value) return; els.steamAccount.value = els.savedSteamAccount.value; els.steamPassword.value = ''; credentialsDirty = true; }); [els.telegramToken, els.telegramRecipientIds].forEach((element) => { element.addEventListener('input', () => { telegramDirty = true; }); }); [ els.lobbyName, els.lobbyRegion, els.customGameId, els.customGameMode, els.customMapName, els.lobbyPassKey, els.customMinPlayers, els.customMaxPlayers, els.notifyAtPlayers, els.leaveAtPlayers, els.adminNames, els.customGameCrc, els.customGameTimestamp, els.allowSpectating, ].forEach((element) => { element.addEventListener('input', () => { lobbySettingsDirty = true; }); element.addEventListener('change', () => { lobbySettingsDirty = true; }); }); els.steamGuardCode.addEventListener('keydown', (event) => { if (event.key === 'Enter') els.submitGuardBtn.click(); }); if (adminPassword) { els.adminPassword.value = adminPassword; } loadAuth({ preserveEditedInputs: false }); updateNotificationStatus(); setInterval(loadAuth, 5000);