2026-07-13 15:32:53 +03:00
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' ),
2026-08-06 20:33:27 +01:00
notificationPanel : document . querySelector ( '#notificationPanel' ),
telegramPanel : document . querySelector ( '#telegramPanel' ),
lobbyPanel : document . querySelector ( '#lobbyPanel' ),
2026-07-13 15:32:53 +03:00
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' ),
2026-08-06 20:33:27 +01:00
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' ),
2026-07-13 15:32:53 +03:00
};
let adminPassword = sessionStorage . getItem ( 'dokaAdminPassword' ) || '' ;
let credentialsDirty = false ;
2026-08-06 20:33:27 +01:00
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 ;
2026-07-13 15:32:53 +03:00
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 );
}
2026-08-06 20:33:27 +01:00
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 ;
}
}
2026-07-13 15:32:53 +03:00
function render ( data , options = {}) {
2026-08-06 20:33:27 +01:00
lastAdminState = data ;
2026-07-13 15:32:53 +03:00
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 ();
2026-08-06 20:33:27 +01:00
els . notificationPanel . hidden = ! unlocked ();
els . telegramPanel . hidden = ! unlocked ();
els . lobbyPanel . hidden = ! unlocked ();
2026-07-13 15:32:53 +03:00
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 || '-' ;
2026-08-06 20:33:27 +01:00
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 );
2026-07-13 15:32:53 +03:00
const editingCredentials = document . activeElement === els . steamAccount
|| document . activeElement === els . steamPassword
|| credentialsDirty ;
if ( ! preserveEditedInputs || ! editingCredentials ) {
els . steamAccount . value = credentials . accountName || '' ;
2026-08-06 20:33:27 +01:00
els . steamPassword . value = '' ;
els . steamPassword . placeholder = credentials . passwordSaved
? 'saved; leave blank to keep the saved password'
: 'password required for a new account' ;
2026-07-13 15:32:53 +03:00
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 );
}
}
2026-08-06 20:33:27 +01:00
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 );
}
}
2026-07-13 15:32:53 +03:00
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 );
2026-08-06 20:33:27 +01:00
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 );
2026-07-13 15:32:53 +03:00
els . steamAccount . addEventListener ( 'input' , () => {
credentialsDirty = true ;
});
els . steamPassword . addEventListener ( 'input' , () => {
credentialsDirty = true ;
});
2026-08-06 20:33:27 +01:00
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 ;
});
});
2026-07-13 15:32:53 +03:00
els . steamGuardCode . addEventListener ( 'keydown' , ( event ) => {
if ( event . key === 'Enter' ) els . submitGuardBtn . click ();
});
if ( adminPassword ) {
els . adminPassword . value = adminPassword ;
}
loadAuth ({ preserveEditedInputs : false });
2026-08-06 20:33:27 +01:00
updateNotificationStatus ();
2026-07-13 15:32:53 +03:00
setInterval ( loadAuth , 5000 );