2026-05-29 22:12:02 -05:00
import { createBuildProfileManager } from "./build-profile-manager.js" ;
import {
createBuildHistoryManager ,
formatFileSize ,
} from "./build-history-manager.js" ;
2026-08-01 02:11:52 -05:00
import {
animateElement ,
revealElement ,
staggerChildren ,
} from "./motion.js" ;
2026-08-18 00:12:36 -05:00
import { isLocalServerAddress } from "./build-server-url.js" ;
2026-08-24 09:35:03 -05:00
import { escapeHtml } from "./format.js" ;
2026-05-29 22:12:02 -05:00
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" );
2026-08-19 09:54:52 -05:00
const buildProgressTrack = document . getElementById ( "build-progress-track" );
const buildProgressBar = document . getElementById ( "build-progress-bar" );
2026-03-05 00:04:26 -06:00
const buildOutputDiv = document . getElementById ( "build-output" );
const buildOutputContainer = document . getElementById ( "build-output-container" );
2026-08-01 02:27:35 -05:00
const buildLogIssueCount = document . getElementById ( "build-log-issue-count" );
const buildLogFilterButtons = document . querySelectorAll ( "[data-build-log-filter]" );
const buildLogWrapBtn = document . getElementById ( "build-log-wrap-btn" );
const buildLogFollowBtn = document . getElementById ( "build-log-follow-btn" );
const buildLogCopyBtn = document . getElementById ( "build-log-copy-btn" );
const buildLogClearBtn = document . getElementById ( "build-log-clear-btn" );
2026-03-05 00:04:26 -06:00
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-06-27 19:38:10 -05:00
const serverUrlCurrentBtn = document . getElementById ( "server-url-current-btn" );
2026-08-18 00:12:36 -05:00
const serverUrlLocalWarning = document . getElementById ( "server-url-local-warning" );
2026-04-05 18:18:40 -05:00
const solMemoCheckbox = document . getElementById ( "sol-memo" );
const solSettings = document . getElementById ( "sol-settings" );
2026-04-23 22:54:49 -05:00
const profileSelect = document . getElementById ( "build-profile-select" );
const profileNameInput = document . getElementById ( "build-profile-name" );
const profileSaveBtn = document . getElementById ( "profile-save-btn" );
const profileLoadBtn = document . getElementById ( "profile-load-btn" );
const profileDeleteBtn = document . getElementById ( "profile-delete-btn" );
const profileExportBtn = document . getElementById ( "profile-export-btn" );
const profileImportBtn = document . getElementById ( "profile-import-btn" );
const profileImportFile = document . getElementById ( "profile-import-file" );
2026-05-07 18:39:25 -05:00
const persistenceCheckbox = document . querySelector ( 'input[name="enable-persistence"]' );
const persistenceMethodContainer = document . getElementById ( "persistence-method-container" );
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 persistenceStartupNameContainer = document . getElementById ( "persistence-startup-name-container" );
const startupNameMacosHint = document . getElementById ( "startup-name-macos-hint" );
const startupNameDefaultHint = document . getElementById ( "startup-name-default-hint" );
const startupNameError = document . getElementById ( "startup-name-error" );
const platformInputs = document . querySelectorAll ( 'input[name="platform"]' );
2026-06-22 12:54:12 -05:00
const buildPluginsSection = document . getElementById ( "build-plugins-section" );
const buildPluginsList = document . getElementById ( "build-plugins-list" );
const buildPluginsCount = document . getElementById ( "build-plugins-count" );
2026-08-19 09:54:52 -05:00
const buildProviderSelect = document . getElementById ( "build-provider" );
const buildProviderDescription = document . getElementById ( "build-provider-description" );
2026-07-15 23:26:41 -05:00
const macosSdkUploadSection = document . getElementById ( "macos-sdk-upload-section" );
const macosSdkFileInput = document . getElementById ( "macos-sdk-file" );
const macosSdkRightsInput = document . getElementById ( "macos-sdk-rights" );
const macosSdkFileStatus = document . getElementById ( "macos-sdk-file-status" );
const macosSdkHostWarning = document . getElementById ( "macos-sdk-host-warning" );
const macosSdkHostPlatform = document . getElementById ( "macos-sdk-host-platform" );
2026-07-15 23:29:12 -05:00
const disableCgoInput = document . querySelector ( 'input[name="disable-cgo"]' );
const disableCgoWarning = document . getElementById ( "disable-cgo-warning" );
2026-03-05 00:04:26 -06:00
2026-03-12 12:08:02 -05:00
let currentServerVersion = null ;
2026-04-14 10:23:59 -05:00
let currentUserRole = null ;
2026-06-10 19:54:18 -05:00
let currentUsername = null ;
2026-05-29 22:12:02 -05:00
let showAllBuilds = false ;
2026-06-22 12:54:12 -05:00
let buildPlugins = [];
2026-07-15 23:26:41 -05:00
let macosSdkRequiredForDarwinCgo = false ;
let macosSdkServerHost = null ;
async function loadMacosSdkStatus () {
try {
const res = await fetch ( "/api/build/macos-sdk/status" , { credentials : "include" });
if ( ! res . ok ) return ;
const data = await res . json ();
macosSdkRequiredForDarwinCgo = data . requiredForDarwinCgo === true ;
macosSdkServerHost = typeof data . hostPlatform === "string" ? data . hostPlatform : null ;
} catch {}
updateMacosSdkVisibility ();
}
function needsMacosSdkUpload () {
2026-08-19 09:54:52 -05:00
if ( buildProviderSelect ? . value ) return false ;
2026-07-15 23:26:41 -05:00
const hasDarwin = Array . from ( document . querySelectorAll ( 'input[name="platform"]:checked' ))
. some (( input ) => input . value . startsWith ( "darwin-" ));
return macosSdkRequiredForDarwinCgo && hasDarwin && ! document . querySelector ( 'input[name="disable-cgo"]' ) ? . checked ;
}
function updateMacosSdkVisibility () {
const hasDarwin = Array . from ( document . querySelectorAll ( 'input[name="platform"]:checked' ))
. some (( input ) => input . value . startsWith ( "darwin-" ));
const cgoEnabled = ! document . querySelector ( 'input[name="disable-cgo"]' ) ? . checked ;
macosSdkUploadSection ? . classList . toggle ( "hidden" , ! needsMacosSdkUpload ());
const unsupportedHost = hasDarwin && cgoEnabled && !! macosSdkServerHost && macosSdkServerHost !== "linux" && macosSdkServerHost !== "darwin" ;
macosSdkHostWarning ? . classList . toggle ( "hidden" , ! unsupportedHost );
if ( macosSdkHostPlatform && unsupportedHost ) macosSdkHostPlatform . textContent = macosSdkServerHost ;
}
2026-07-15 23:29:12 -05:00
function updateDisableCgoWarning () {
disableCgoWarning ? . classList . toggle ( "hidden" , ! disableCgoInput ? . checked );
}
2026-07-15 23:26:41 -05:00
async function uploadMacosSdk () {
const file = macosSdkFileInput ? . files ? .[ 0 ];
if ( ! file ) throw new Error ( "Select a macOS SDK archive for this Darwin CGO build" );
if ( ! macosSdkRightsInput ? . checked ) throw new Error ( "Confirm that you have permission to use the uploaded macOS SDK" );
if ( file . size > 1024 * 1024 * 1024 ) throw new Error ( "macOS SDK archive exceeds the 1 GB limit" );
if ( macosSdkFileStatus ) macosSdkFileStatus . textContent = `Uploading ${ file . name } ( ${ formatFileSize ( file . size ) } )...` ;
const res = await fetch ( "/api/build/macos-sdk/upload" , {
method : "POST" ,
credentials : "include" ,
headers : {
"Content-Type" : "application/octet-stream" ,
"X-Overlord-Filename" : encodeURIComponent ( file . name ),
"X-Overlord-Sdk-Rights" : "confirmed" ,
},
body : file ,
});
const data = await res . json (). catch (() => ({}));
if ( ! res . ok ) throw new Error ( data . error || "macOS SDK upload failed" );
if ( macosSdkFileStatus ) macosSdkFileStatus . textContent = ` ${ file . name } uploaded; validating during the build.` ;
return data . uploadId ;
}
2026-03-12 12:08:02 -05:00
2026-07-14 00:03:55 -05:00
async function loadSolRpcEndpoints () {
const field = document . getElementById ( "sol-rpc-endpoints" );
if ( ! field || field . value . trim ()) return ;
try {
const res = await fetch ( "/api/sol/rpc-endpoints" , { credentials : "include" });
if ( ! res . ok ) return ;
const data = await res . json ();
const endpoints = Array . isArray ( data ? . endpoints )
? data . endpoints . map (( item ) => String ( item || "" ). trim ()). filter ( Boolean )
: [];
field . value = endpoints . join ( "\n" );
} catch {}
}
2026-03-12 12:08:02 -05:00
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 ) {
if ( isRawList ) {
2026-06-27 19:38:10 -05:00
return getCurrentRawServerListUrl ();
2026-03-05 00:04:26 -06:00
}
2026-05-17 13:31:47 -05:00
return "" ;
}
2026-06-27 19:38:10 -05:00
function getCurrentServerHost () {
return window . location . host || window . location . hostname || "" ;
}
function getCurrentRawServerListUrl () {
const protocol = window . location . protocol === "https:" ? "https" : "http" ;
const host = getCurrentServerHost ();
return host ? ` ${ protocol } :// ${ host } /list.txt` : "" ;
}
function getCurrentServerUrlForMode () {
return getCurrentServerHost ();
}
2026-05-17 13:31:47 -05:00
function stripServerUrlPrefix ( value ) {
return String ( value || "" ). replace ( /^\s*(wss?|https?):\/\//i , "" ). trimStart ();
}
function updateServerUrlHintMode () {
const isRaw = rawServerListCheckbox ? . checked ?? false ;
const normalHint = document . getElementById ( "server-url-hint" );
const rawHint = document . getElementById ( "server-url-raw-hint" );
if ( normalHint ) normalHint . classList . toggle ( "hidden" , isRaw );
if ( rawHint ) rawHint . classList . toggle ( "hidden" , ! isRaw );
2026-03-05 00:04:26 -06:00
}
function updateServerUrlPlaceholder () {
if ( ! serverUrlInput ) return ;
const isRaw = rawServerListCheckbox ? . checked ?? false ;
2026-05-17 13:31:47 -05:00
serverUrlInput . placeholder = getDefaultServerUrlPlaceholder ( isRaw );
2026-06-27 19:38:10 -05:00
updateServerUrlCurrentButton ();
2026-05-17 13:31:47 -05:00
updateServerUrlHintMode ();
2026-03-05 00:04:26 -06:00
}
2026-06-27 19:38:10 -05:00
function updateServerUrlCurrentButton () {
if ( ! serverUrlCurrentBtn ) return ;
const isRaw = rawServerListCheckbox ? . checked ?? false ;
2026-08-18 00:12:36 -05:00
const isCurrentHost = ! isRaw
&& !! serverUrlInput ? . value . trim ()
&& stripServerUrlPrefix ( serverUrlInput . value ) === getCurrentServerUrlForMode ();
2026-06-27 19:38:10 -05:00
serverUrlCurrentBtn . classList . toggle ( "hidden" , isRaw );
serverUrlCurrentBtn . disabled = isRaw ;
2026-08-18 00:12:36 -05:00
serverUrlCurrentBtn . setAttribute ( "aria-pressed" , String ( isCurrentHost ));
serverUrlCurrentBtn . classList . toggle ( "bg-slate-800/70" , ! isCurrentHost );
serverUrlCurrentBtn . classList . toggle ( "border-slate-700" , ! isCurrentHost );
serverUrlCurrentBtn . classList . toggle ( "text-slate-300" , ! isCurrentHost );
serverUrlCurrentBtn . classList . toggle ( "bg-blue-600" , isCurrentHost );
serverUrlCurrentBtn . classList . toggle ( "border-blue-400" , isCurrentHost );
serverUrlCurrentBtn . classList . toggle ( "text-white" , isCurrentHost );
serverUrlCurrentBtn . classList . toggle ( "shadow-lg" , isCurrentHost );
serverUrlCurrentBtn . classList . toggle ( "shadow-blue-950/50" , isCurrentHost );
}
function updateLocalServerWarning () {
const showWarning = isLocalServerAddress ( serverUrlInput ? . value );
serverUrlLocalWarning ? . classList . toggle ( "hidden" , ! showWarning );
serverUrlInput ? . classList . toggle ( "border-slate-700" , ! showWarning );
serverUrlInput ? . classList . toggle ( "border-red-500" , showWarning );
serverUrlInput ? . setAttribute ( "aria-invalid" , String ( showWarning ));
updateServerUrlCurrentButton ();
2026-06-27 19:38:10 -05:00
}
2026-03-05 00:04:26 -06:00
let isBuilding = false ;
2026-08-01 02:27:35 -05:00
const BUILD_LOG_MAX_VISIBLE_LINES = 5000 ;
const BUILD_LOG_WRAP_KEY = "overlord.buildLog.wrap.v1" ;
let buildLogRawChunks = [];
2026-08-19 09:54:52 -05:00
function updateBuildProgress ( progress ) {
const value = Number ( progress );
const visible = progress !== null && progress !== undefined && Number . isFinite ( value );
buildProgressTrack ? . classList . toggle ( "hidden" , ! visible );
if ( ! visible ) {
if ( buildProgressBar ) buildProgressBar . style . width = "0%" ;
buildProgressTrack ? . removeAttribute ( "aria-valuenow" );
return ;
}
const clamped = Math . max ( 0 , Math . min ( 100 , value ));
if ( buildProgressBar ) buildProgressBar . style . width = ` ${ clamped } %` ;
buildProgressTrack ? . setAttribute ( "aria-valuenow" , String ( Math . round ( clamped )));
}
2026-08-01 02:27:35 -05:00
let buildLogLineCount = 0 ;
let buildLogIssueTotal = 0 ;
let buildLogFollow = true ;
function updateBuildLogSummary () {
if ( buildLogIssueCount ) buildLogIssueCount . textContent = buildLogIssueTotal . toLocaleString ();
}
function setBuildLogFollow ( enabled ) {
buildLogFollow = enabled ;
buildLogFollowBtn ? . setAttribute ( "aria-pressed" , String ( enabled ));
if ( enabled && buildOutputContainer ) {
buildOutputContainer . scrollTop = buildOutputContainer . scrollHeight ;
}
}
function resetBuildLog ( showEmptyState = false ) {
buildLogRawChunks = [];
buildLogLineCount = 0 ;
buildLogIssueTotal = 0 ;
if ( buildOutputDiv ) {
buildOutputDiv . innerHTML = showEmptyState
? '<div class="build-log-empty">Output cleared. New build events will appear here.</div>'
: "" ;
}
updateBuildLogSummary ();
}
function classifyBuildLogLine ( line , requestedLevel ) {
const normalizedLevel = String ( requestedLevel || "info" ). toLowerCase ();
if ( normalizedLevel === "error" || /\b(error|failed|failure|fatal|panic)\b/i . test ( line )) return "error" ;
if ( normalizedLevel === "warn" || normalizedLevel === "warning" || /\b(warn(?:ing)?|retry|skipp?ed|hint)\b/i . test ( line )) return "warning" ;
if ( normalizedLevel === "success" || /\b(success(?:ful(?:ly)?)?|completed|\[ok\])\b/i . test ( line )) return "success" ;
if ( /^(?:\s*[=─-]{2,}|\s*\[[^\]]+\]|\s*(?:starting|build id|building|uploading|pushing|reconnecting)\b)/i . test ( line )) return "stage" ;
return "detail" ;
}
function appendBuildLogLine ( line , requestedLevel ) {
if ( ! buildOutputDiv || ! line . trim ()) return ;
buildOutputDiv . querySelector ( ".build-log-empty" ) ? . remove ();
const level = classifyBuildLogLine ( line , requestedLevel );
const row = document . createElement ( "div" );
row . className = "build-log-line" ;
row . dataset . logLevel = level ;
buildLogLineCount += 1 ;
if ( level === "warning" || level === "error" ) buildLogIssueTotal += 1 ;
const number = document . createElement ( "span" );
number . className = "build-log-number" ;
number . textContent = String ( buildLogLineCount );
const content = document . createElement ( "span" );
content . className = "build-log-content" ;
content . appendChild ( document . createTextNode ( line ));
row . appendChild ( number );
row . appendChild ( content );
buildOutputDiv . appendChild ( row );
while ( buildOutputDiv . children . length > BUILD_LOG_MAX_VISIBLE_LINES ) {
buildOutputDiv . firstElementChild ? . remove ();
}
if ( level !== "detail" ) {
animateElement ( row , [
{ opacity : 0 , transform : "translateX(5px)" },
{ opacity : 1 , transform : "translateX(0)" },
], { duration : 170 });
}
}
function initBuildConsole () {
let wrapEnabled = false ;
try { wrapEnabled = localStorage . getItem ( BUILD_LOG_WRAP_KEY ) === "1" ; } catch {}
buildOutputDiv ? . classList . toggle ( "log-wrap" , wrapEnabled );
buildLogWrapBtn ? . setAttribute ( "aria-pressed" , String ( wrapEnabled ));
buildLogFilterButtons . forEach (( button ) => {
button . addEventListener ( "click" , () => {
const filter = button . dataset . buildLogFilter || "all" ;
buildOutputDiv . dataset . filter = filter ;
buildLogFilterButtons . forEach (( candidate ) => {
candidate . setAttribute ( "aria-pressed" , String ( candidate === button ));
});
});
});
buildLogWrapBtn ? . addEventListener ( "click" , () => {
const enabled = ! buildOutputDiv . classList . contains ( "log-wrap" );
buildOutputDiv . classList . toggle ( "log-wrap" , enabled );
buildLogWrapBtn . setAttribute ( "aria-pressed" , String ( enabled ));
try { localStorage . setItem ( BUILD_LOG_WRAP_KEY , enabled ? "1" : "0" ); } catch {}
});
buildLogFollowBtn ? . addEventListener ( "click" , () => setBuildLogFollow ( ! buildLogFollow ));
buildOutputContainer ? . addEventListener ( "scroll" , () => {
const distanceFromBottom = buildOutputContainer . scrollHeight
- buildOutputContainer . scrollTop
- buildOutputContainer . clientHeight ;
if ( distanceFromBottom > 40 && buildLogFollow ) setBuildLogFollow ( false );
}, { passive : true });
buildLogCopyBtn ? . addEventListener ( "click" , async () => {
const text = buildLogRawChunks . join ( "" );
if ( ! text ) return ;
let copied = false ;
try {
await navigator . clipboard . writeText ( text );
copied = true ;
} catch {
const textarea = document . createElement ( "textarea" );
textarea . value = text ;
textarea . style . position = "fixed" ;
textarea . style . opacity = "0" ;
document . body . appendChild ( textarea );
textarea . select ();
copied = document . execCommand ? .( "copy" ) === true ;
textarea . remove ();
}
if ( copied ) {
buildLogCopyBtn . innerHTML = '<i class="fa-solid fa-check text-emerald-400"></i><span class="sr-only">Copied</span>' ;
setTimeout (() => {
buildLogCopyBtn . innerHTML = '<i class="fa-regular fa-copy"></i><span class="sr-only">Copy output</span>' ;
}, 1400 );
} else {
window . showToast ? .( "Could not copy build output" , "error" );
}
});
buildLogClearBtn ? . addEventListener ( "click" , () => {
resetBuildLog ( true );
setBuildLogFollow ( true );
});
updateBuildLogSummary ();
}
initBuildConsole ();
2026-03-05 00:04:26 -06:00
2026-08-01 02:11:52 -05:00
function revealBuildResults () {
if ( buildResults . classList . contains ( "hidden" )) {
revealElement ( buildResults , { duration : 240 , offset : 8 });
}
requestAnimationFrame (() => {
staggerChildren ( buildFilesDiv , { limit : 10 , interval : 28 , duration : 220 , offset : 6 });
});
}
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" ;
2026-08-01 02:11:52 -05:00
header . setAttribute ( "aria-expanded" , String ( startOpen ));
2026-03-21 02:12:35 -05:00
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 );
2026-08-01 02:11:52 -05:00
section . dataset . open = String ( ! nowCollapsed );
header . setAttribute ( "aria-expanded" , String ( ! nowCollapsed ));
animateElement ( header , [
{ backgroundColor : "rgba(51, 65, 85, 0.62)" },
{ backgroundColor : "rgba(30, 41, 59, 0.5)" },
], { duration : 180 });
2026-03-21 02:12:35 -05:00
});
});
}
2026-07-14 10:21:20 -05:00
function initBuilderTabs () {
const tabs = document . querySelectorAll ( ".builder-tab[data-builder-tab]" );
const panels = document . querySelectorAll ( "[data-builder-panel]" );
function switchTab ( tabName ) {
tabs . forEach (( t ) => {
t . setAttribute ( "aria-selected" , t . dataset . builderTab === tabName ? "true" : "false" );
});
panels . forEach (( p ) => {
if ( p . dataset . builderPanel === tabName ) {
p . removeAttribute ( "hidden" );
2026-08-01 02:11:52 -05:00
animateElement ( p , [
{ opacity : 0 , transform : "translateY(7px)" },
{ opacity : 1 , transform : "translateY(0)" },
], { duration : 200 });
2026-07-14 10:21:20 -05:00
} else {
p . setAttribute ( "hidden" , "" );
}
});
}
tabs . forEach (( tab ) => {
tab . addEventListener ( "click" , () => {
switchTab ( tab . dataset . builderTab );
});
});
const defaultTab = document . querySelector ( '.builder-tab[aria-selected="true"]' );
if ( defaultTab ) {
switchTab ( defaultTab . dataset . builderTab );
}
}
2026-03-21 02:12:35 -05:00
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-06-22 12:54:12 -05:00
function pluginSettingInputId ( pluginId , key ) {
return `build-plugin- ${ pluginId } - ${ key } ` . replace ( /[^A-Za-z0-9_-]/g , "-" );
}
function buildPluginCurrentSettings ( plugin ) {
const settings = {};
for ( const setting of plugin . build ? . settings || []) {
const el = document . querySelector ( `[data-build-plugin-id=" ${ plugin . id } "][data-build-setting-key=" ${ setting . key } "]` );
if ( ! el ) continue ;
if ( setting . type === "boolean" ) settings [ setting . key ] = !! el . checked ;
else if ( setting . type === "number" ) settings [ setting . key ] = Number ( el . value || setting . default || 0 );
else settings [ setting . key ] = el . value ;
}
return settings ;
}
function collectBuildPluginSettings () {
const result = {};
for ( const plugin of buildPlugins ) {
const enabledEl = document . querySelector ( `[data-build-plugin-enable=" ${ plugin . id } "]` );
result [ plugin . id ] = {
enabled : enabledEl ? !! enabledEl . checked : plugin . build ? . enabledByDefault !== false ,
settings : buildPluginCurrentSettings ( plugin ),
};
}
return result ;
}
2026-08-19 09:54:52 -05:00
function selectedBuildProviderPlugin () {
const id = buildProviderSelect ? . value || "" ;
return id ? buildPlugins . find (( plugin ) => plugin . id === id && plugin . build ? . provider ) : null ;
}
function updateBuildProviderUi () {
const provider = selectedBuildProviderPlugin ();
const supported = provider ? new Set ( provider . build . provider . platforms || []) : null ;
for ( const input of document . querySelectorAll ( 'input[name="platform"]' )) {
const allowed = ! supported || supported . has ( input . value );
input . disabled = ! allowed ;
input . closest ( "label" ) ? . classList . toggle ( "opacity-40" , ! allowed );
if ( ! allowed ) input . checked = false ;
}
if ( provider && ! document . querySelector ( 'input[name="platform"]:checked:not(:disabled)' )) {
const first = document . querySelector ( 'input[name="platform"]:not(:disabled)' );
if ( first ) first . checked = true ;
}
if ( provider ) {
if ( buildProviderDescription ) {
buildProviderDescription . textContent = provider . build . provider . description
|| provider . build . description
|| `Uses the ${ provider . name || provider . id } build provider.` ;
}
const enabled = document . querySelector ( `[data-build-plugin-enable=" ${ provider . id } "]` );
if ( enabled ) enabled . checked = true ;
} else if ( buildProviderDescription ) {
buildProviderDescription . textContent = "Uses the built-in full-featured Go client builder." ;
}
updateWindowsSectionVisibility ();
updateMacosSdkVisibility ();
}
function renderBuildProviders () {
if ( ! buildProviderSelect ) return ;
const selected = buildProviderSelect . value ;
buildProviderSelect . innerHTML = '<option value="">Full Go Agent</option>' ;
for ( const plugin of buildPlugins ) {
if ( ! plugin . build ? . provider || plugin . enabled === false ) continue ;
const option = document . createElement ( "option" );
option . value = plugin . id ;
option . textContent = plugin . build . provider . label || plugin . build . label || plugin . name || plugin . id ;
buildProviderSelect . appendChild ( option );
}
if ( Array . from ( buildProviderSelect . options ). some (( option ) => option . value === selected )) {
buildProviderSelect . value = selected ;
}
updateBuildProviderUi ();
}
buildProviderSelect ? . addEventListener ( "change" , () => {
updateBuildProviderUi ();
saveFormSettings ();
});
2026-06-22 12:54:12 -05:00
function setBuildField ( field , value ) {
const fieldMap = {
2026-08-19 09:54:52 -05:00
buildProvider : "#build-provider" ,
2026-06-22 12:54:12 -05:00
useDonut : "#donut-mode" ,
2026-07-14 16:30:00 -05:00
donutSingleThreaded : "#donut-single-threaded" ,
donutExitMode : "#donut-exit-mode" ,
donutEntropy : "#donut-entropy" ,
donutPreserveHeaders : "#donut-preserve-headers" ,
donutResumeOffset : "#donut-resume-offset" ,
2026-06-22 12:54:12 -05:00
useLinuxShellcode : "#linux-shellcode-mode" ,
shellcodeConsole : "#shellcode-console" ,
useSgn : "#sgn-mode" ,
enableUpx : 'input[name="enable-upx"]' ,
upxStripHeaders : 'input[name="upx-strip-headers"]' ,
obfuscate : 'input[name="obfuscate"]' ,
garbleLiterals : 'input[name="garble-literals"]' ,
garbleTiny : 'input[name="garble-tiny"]' ,
disableCgo : 'input[name="disable-cgo"]' ,
2026-07-11 14:55:26 -05:00
enableNvenc : 'input[name="enable-nvenc"]' ,
enableAmf : 'input[name="enable-amf"]' ,
2026-07-11 16:52:24 -05:00
enableQsv : 'input[name="enable-qsv"]' ,
2026-06-22 12:54:12 -05:00
stripDebug : 'input[name="strip-debug"]' ,
noPrinting : 'input[name="no-printing"]' ,
enableWebrtc : 'input[name="enable-webrtc"]' ,
2026-07-19 17:16:35 -05:00
promptWebrtcFirewallOnStart : 'input[name="prompt-webrtc-firewall-on-start"]' ,
2026-06-22 12:54:12 -05:00
enableWinRE : 'input[name="enable-winre"]' ,
fetchPublicIP : 'input[name="fetch-public-ip"]' ,
enablePersistence : 'input[name="enable-persistence"]' ,
hideConsole : 'input[name="hide-console"]' ,
requireAdmin : 'input[name="require-admin"]' ,
criticalProcess : 'input[name="critical-process"]' ,
outputName : "#output-name" ,
outputExtension : "#output-extension" ,
sgnIterations : "#sgn-iterations" ,
sleepSeconds : "#sleep-seconds" ,
};
const selector = fieldMap [ field ] || `# ${ field } ` ;
const el = document . querySelector ( selector );
if ( ! el ) return ;
if ( el . type === "checkbox" ) el . checked = !! value ;
else el . value = String ( value ?? "" );
el . dispatchEvent ( new Event ( "change" , { bubbles : true }));
el . dispatchEvent ( new Event ( "input" , { bubbles : true }));
if ( field === "useDonut" ) applyDonutMode ( !! value );
if ( field === "useLinuxShellcode" ) applyLinuxShellcodeMode ( !! value );
if ( field === "useSgn" ) applySgnMode ( !! value );
}
function getRequirementValue ( req , plugin ) {
if ( req . field ) {
const settings = collectFormSettings ();
return req . field . split ( "." ). reduce (( cur , key ) => cur && cur [ key ], settings );
}
if ( req . pluginSetting ) {
const settings = buildPluginCurrentSettings ( plugin );
return req . pluginSetting . split ( "." ). reduce (( cur , key ) => cur && cur [ key ], settings );
}
return undefined ;
}
function buildRequirementMet ( req , plugin ) {
if ( Array . isArray ( req . platforms )) {
const selected = Array . from ( document . querySelectorAll ( 'input[name="platform"]:checked' )). map (( el ) => el . value );
return req . platforms . some (( platform ) => selected . includes ( platform ));
}
const value = getRequirementValue ( req , plugin );
if ( req . truthy === true && ! value ) return false ;
if ( req . falsy === true && value ) return false ;
if ( Object . prototype . hasOwnProperty . call ( req , "equals" ) && value !== req . equals ) return false ;
if ( Object . prototype . hasOwnProperty . call ( req , "notEquals" ) && value === req . notEquals ) return false ;
if ( Object . prototype . hasOwnProperty . call ( req , "includes" )) {
if ( ! Array . isArray ( value ) || ! value . includes ( req . includes )) return false ;
}
return true ;
}
function validateBuildPluginRequirements () {
const messages = [];
for ( const plugin of buildPlugins ) {
const enabled = document . querySelector ( `[data-build-plugin-enable=" ${ plugin . id } "]` ) ? . checked ;
if ( ! enabled ) continue ;
for ( const req of plugin . build ? . requires || []) {
if ( ! buildRequirementMet ( req , plugin )) {
messages . push ( req . message || ` ${ plugin . name } has unmet build requirements` );
}
}
}
return messages ;
}
function renderBuildPluginSetting ( plugin , setting ) {
const id = pluginSettingInputId ( plugin . id , setting . key );
const wrap = document . createElement ( "label" );
wrap . className = "flex flex-col gap-1 text-sm" ;
const label = document . createElement ( "span" );
label . className = "text-slate-300 font-medium" ;
label . textContent = setting . label || setting . key ;
wrap . appendChild ( label );
let input ;
if ( setting . type === "boolean" ) {
wrap . className = "flex items-center gap-2 text-sm text-slate-300" ;
input = document . createElement ( "input" );
input . type = "checkbox" ;
input . checked = !! setting . default ;
input . className = "w-4 h-4" ;
wrap . innerHTML = "" ;
wrap . appendChild ( input );
wrap . appendChild ( label );
} else if ( setting . type === "select" ) {
input = document . createElement ( "select" );
input . className = "w-full px-3 py-2 bg-slate-800/70 border border-slate-700 rounded-lg text-sm focus:outline-none focus:border-fuchsia-500 transition-colors" ;
for ( const opt of setting . options || []) {
const option = document . createElement ( "option" );
option . value = typeof opt === "string" ? opt : opt . value ;
option . textContent = typeof opt === "string" ? opt : ( opt . label || opt . value );
input . appendChild ( option );
}
if ( setting . default !== undefined ) input . value = String ( setting . default );
wrap . appendChild ( input );
} else if ( setting . type === "textarea" ) {
input = document . createElement ( "textarea" );
input . rows = 3 ;
input . value = setting . default !== undefined ? String ( setting . default ) : "" ;
input . placeholder = setting . placeholder || "" ;
input . className = "w-full px-3 py-2 bg-slate-800/70 border border-slate-700 rounded-lg text-sm focus:outline-none focus:border-fuchsia-500 transition-colors" ;
wrap . appendChild ( input );
} else {
input = document . createElement ( "input" );
input . type = setting . type === "number" ? "number" : "text" ;
if ( setting . min !== undefined ) input . min = String ( setting . min );
if ( setting . max !== undefined ) input . max = String ( setting . max );
input . value = setting . default !== undefined ? String ( setting . default ) : "" ;
input . placeholder = setting . placeholder || "" ;
input . className = "w-full px-3 py-2 bg-slate-800/70 border border-slate-700 rounded-lg text-sm focus:outline-none focus:border-fuchsia-500 transition-colors" ;
wrap . appendChild ( input );
}
input . id = id ;
input . dataset . buildPluginId = plugin . id ;
input . dataset . buildSettingKey = setting . key ;
input . addEventListener ( "input" , saveFormSettings );
input . addEventListener ( "change" , saveFormSettings );
if ( setting . description ) {
const desc = document . createElement ( "span" );
desc . className = "text-xs text-slate-500" ;
desc . textContent = setting . description ;
wrap . appendChild ( desc );
}
return wrap ;
}
function renderBuildPlugins () {
if ( ! buildPluginsSection || ! buildPluginsList ) return ;
buildPluginsList . innerHTML = "" ;
if ( buildPlugins . length === 0 ) {
buildPluginsSection . classList . add ( "hidden" );
return ;
}
buildPluginsSection . classList . remove ( "hidden" );
if ( buildPluginsCount ) buildPluginsCount . textContent = ` ${ buildPlugins . length } available` ;
for ( const plugin of buildPlugins ) {
const card = document . createElement ( "div" );
card . className = "rounded-lg border border-slate-700 bg-slate-800/40 p-3 flex flex-col gap-3" ;
const top = document . createElement ( "div" );
top . className = "flex items-start justify-between gap-3" ;
const title = document . createElement ( "div" );
const nameEl = document . createElement ( "div" );
nameEl . className = "font-medium text-slate-100" ;
nameEl . textContent = plugin . build ? . label || plugin . name ;
title . appendChild ( nameEl );
if ( plugin . build ? . description ) {
const desc = document . createElement ( "div" );
desc . className = "text-xs text-slate-500 mt-1" ;
desc . textContent = plugin . build . description ;
title . appendChild ( desc );
}
const toggle = document . createElement ( "label" );
toggle . className = "flex items-center gap-2 text-xs text-slate-300 cursor-pointer" ;
const cb = document . createElement ( "input" );
cb . type = "checkbox" ;
cb . checked = plugin . build ? . enabledByDefault !== false ;
cb . dataset . buildPluginEnable = plugin . id ;
cb . className = "w-4 h-4" ;
cb . addEventListener ( "change" , saveFormSettings );
toggle . appendChild ( cb );
toggle . appendChild ( document . createTextNode ( "Enabled" ));
top . appendChild ( title );
top . appendChild ( toggle );
card . appendChild ( top );
if ( plugin . build ? . settings ? . length ) {
const grid = document . createElement ( "div" );
grid . className = "grid grid-cols-1 md:grid-cols-2 gap-3" ;
for ( const setting of plugin . build . settings ) grid . appendChild ( renderBuildPluginSetting ( plugin , setting ));
card . appendChild ( grid );
}
if ( plugin . build ? . actions ? . length ) {
const actions = document . createElement ( "div" );
actions . className = "flex flex-wrap gap-2" ;
for ( const action of plugin . build . actions ) {
const btn = document . createElement ( "button" );
btn . type = "button" ;
btn . className = "inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-fuchsia-700 hover:bg-fuchsia-600 text-white text-sm transition-colors" ;
const icon = document . createElement ( "i" );
icon . className = action . icon || "fa-solid fa-wand-magic-sparkles" ;
const text = document . createElement ( "span" );
text . textContent = action . label ;
btn . appendChild ( icon );
btn . appendChild ( text );
if ( action . description ) btn . title = action . description ;
btn . addEventListener ( "click" , () => {
for ( const req of action . requires || []) {
if ( ! buildRequirementMet ( req , plugin )) {
alert ( req . message || "This action requires other build settings first." );
return ;
}
}
for ( const [ field , value ] of Object . entries ( action . setBuild || {})) setBuildField ( field , value );
for ( const [ key , value ] of Object . entries ( action . setSettings || {})) {
const el = document . querySelector ( `[data-build-plugin-id=" ${ plugin . id } "][data-build-setting-key=" ${ key } "]` );
if ( ! el ) continue ;
if ( el . type === "checkbox" ) el . checked = !! value ;
else el . value = String ( value ?? "" );
el . dispatchEvent ( new Event ( "change" , { bubbles : true }));
}
saveFormSettings ();
});
actions . appendChild ( btn );
}
card . appendChild ( actions );
}
buildPluginsList . appendChild ( card );
}
}
function applyBuildPluginSettings ( settings ) {
if ( ! settings || typeof settings !== "object" ) return ;
for ( const plugin of buildPlugins ) {
const saved = settings [ plugin . id ];
if ( ! saved || typeof saved !== "object" ) continue ;
const enabled = document . querySelector ( `[data-build-plugin-enable=" ${ plugin . id } "]` );
if ( enabled && saved . enabled !== undefined ) enabled . checked = !! saved . enabled ;
for ( const [ key , value ] of Object . entries ( saved . settings || {})) {
const el = document . querySelector ( `[data-build-plugin-id=" ${ plugin . id } "][data-build-setting-key=" ${ key } "]` );
if ( ! el ) continue ;
if ( el . type === "checkbox" ) el . checked = !! value ;
else el . value = String ( value ?? "" );
}
}
}
async function loadBuildPlugins () {
try {
const res = await fetch ( "/api/build/plugins" , { credentials : "include" });
if ( ! res . ok ) return ;
const data = await res . json ();
buildPlugins = Array . isArray ( data . plugins ) ? data . plugins : [];
renderBuildPlugins ();
2026-08-19 09:54:52 -05:00
renderBuildProviders ();
2026-06-22 12:54:12 -05:00
try {
const raw = localStorage . getItem ( BUILD_SETTINGS_KEY );
2026-08-19 09:54:52 -05:00
if ( raw ) {
const saved = JSON . parse ( raw );
applyBuildPluginSettings ( saved . buildPlugins );
if ( buildProviderSelect && typeof saved . buildProvider === "string" ) {
buildProviderSelect . value = saved . buildProvider ;
updateBuildProviderUi ();
}
}
2026-06-22 12:54:12 -05:00
} catch {}
} catch ( err ) {
console . warn ( "Failed to load build plugins:" , err );
}
}
2026-03-26 10:26:25 -05:00
const BUILD_SETTINGS_KEY = "overlord_build_settings" ;
2026-04-23 22:54:49 -05:00
function collectFormSettings () {
return {
2026-08-19 09:54:52 -05:00
buildProvider : buildProviderSelect ? . value || "" ,
2026-04-23 22:54:49 -05:00
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 ,
solMemo : document . getElementById ( "sol-memo" ) ? . checked ?? false ,
solAddress : document . getElementById ( "sol-address" ) ? . value ?? "" ,
solRpcEndpoints : document . getElementById ( "sol-rpc-endpoints" ) ? . value ?? "" ,
outputName : document . getElementById ( "output-name" ) ? . value ?? "" ,
2026-07-08 22:13:29 -05:00
initialClientTag : document . getElementById ( "initial-client-tag" ) ? . value ?? "" ,
2026-04-23 22:54:49 -05:00
iosBundleId : document . getElementById ( "ios-bundle-id" ) ? . 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 ,
2026-07-11 14:55:26 -05:00
enableNvenc : document . querySelector ( 'input[name="enable-nvenc"]' ) ? . checked ?? true ,
enableAmf : document . querySelector ( 'input[name="enable-amf"]' ) ? . checked ?? true ,
2026-07-11 16:52:24 -05:00
enableQsv : document . querySelector ( 'input[name="enable-qsv"]' ) ? . checked ?? true ,
2026-04-23 22:54:49 -05:00
noPrinting : document . querySelector ( 'input[name="no-printing"]' ) ? . checked ?? false ,
2026-05-10 17:06:39 -05:00
enableKeylogger : document . querySelector ( 'input[name="enable-keylogger"]' ) ? . checked ?? true ,
2026-05-13 13:44:33 -05:00
enableWebrtc : document . querySelector ( 'input[name="enable-webrtc"]' ) ? . checked ?? false ,
2026-07-19 17:16:35 -05:00
promptWebrtcFirewallOnStart : document . querySelector ( 'input[name="prompt-webrtc-firewall-on-start"]' ) ? . checked ?? false ,
2026-06-06 23:34:44 -05:00
enableWinRE : document . querySelector ( 'input[name="enable-winre"]' ) ? . checked ?? false ,
2026-05-14 19:56:50 -05:00
fetchPublicIP : document . querySelector ( 'input[name="fetch-public-ip"]' ) ? . checked ?? false ,
2026-04-23 22:54:49 -05:00
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 ,
persistenceMethods : Array . from ( document . querySelectorAll ( 'input[name="persistence-method"]:checked' )). map (( el ) => el . value ),
startupName : document . getElementById ( "startup-name" ) ? . value ?? "" ,
hideConsole : document . querySelector ( 'input[name="hide-console"]' ) ? . checked ?? false ,
requireAdmin : document . querySelector ( 'input[name="require-admin"]' ) ? . checked ?? false ,
criticalProcess : document . querySelector ( 'input[name="critical-process"]' ) ? . checked ?? false ,
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" ,
cryptableMode : document . getElementById ( "cryptable-mode" ) ? . checked ?? false ,
2026-05-13 23:55:51 -04:00
useDonut : document . getElementById ( "donut-mode" ) ? . checked ?? false ,
2026-07-14 16:30:00 -05:00
donutSingleThreaded : document . getElementById ( "donut-single-threaded" ) ? . checked ?? false ,
donutExitMode : parseInt ( document . getElementById ( "donut-exit-mode" ) ? . value , 10 ) || undefined ,
donutEntropy : parseInt ( document . getElementById ( "donut-entropy" ) ? . value , 10 ) || undefined ,
donutPreserveHeaders : document . getElementById ( "donut-preserve-headers" ) ? . checked ?? false ,
donutResumeOffset : parseInt ( document . getElementById ( "donut-resume-offset" ) ? . value , 10 ) || undefined ,
2026-05-13 23:55:51 -04:00
useLinuxShellcode : document . getElementById ( "linux-shellcode-mode" ) ? . checked ?? false ,
shellcodeConsole : document . getElementById ( "shellcode-console" ) ? . checked ?? false ,
2026-05-17 13:11:21 -05:00
useSgn : document . getElementById ( "sgn-mode" ) ? . checked ?? false ,
sgnIterations : parseInt ( document . getElementById ( "sgn-iterations" ) ? . value , 10 ) || 1 ,
2026-06-22 12:54:12 -05:00
buildPlugins : collectBuildPluginSettings (),
2026-04-23 22:54:49 -05:00
};
}
function applyFormSettings ( settings ) {
if ( ! settings || typeof settings !== "object" ) return ;
const setVal = ( id , val ) => {
const el = document . getElementById ( id );
if ( el && val !== undefined && val !== null ) el . value = String ( val );
};
const setCb = ( sel , val ) => {
const el = document . querySelector ( sel );
if ( el && val !== undefined ) el . checked = !! val ;
};
2026-08-19 09:54:52 -05:00
if ( settings . buildProvider !== undefined && buildProviderSelect ) {
buildProviderSelect . value = String ( settings . buildProvider || "" );
updateBuildProviderUi ();
}
2026-04-23 22:54:49 -05:00
if ( Array . isArray ( settings . platforms )) {
settings . platforms . forEach (({ value , checked }) => {
const el = document . querySelector ( `input[name="platform"][value=" ${ value } "]` );
if ( el ) el . checked = !! checked ;
});
}
2026-05-17 13:31:47 -05:00
if ( settings . serverUrl !== undefined ) {
const isRaw = !! settings . rawServerList ;
setVal ( "server-url" , isRaw ? settings . serverUrl : stripServerUrlPrefix ( settings . serverUrl ));
}
2026-04-23 22:54:49 -05:00
if ( settings . rawServerList !== undefined ) setCb ( "#raw-server-list" , settings . rawServerList );
if ( settings . solMemo !== undefined ) setCb ( "#sol-memo" , settings . solMemo );
if ( settings . solAddress !== undefined ) setVal ( "sol-address" , settings . solAddress );
if ( settings . solRpcEndpoints !== undefined ) setVal ( "sol-rpc-endpoints" , settings . solRpcEndpoints );
if ( settings . outputName !== undefined ) setVal ( "output-name" , settings . outputName );
2026-07-08 22:13:29 -05:00
if ( settings . initialClientTag !== undefined ) setVal ( "initial-client-tag" , settings . initialClientTag );
2026-04-23 22:54:49 -05:00
if ( settings . iosBundleId !== undefined ) setVal ( "ios-bundle-id" , settings . iosBundleId );
if ( settings . mutex !== undefined ) setVal ( "mutex" , settings . mutex );
if ( settings . disableMutex !== undefined ) setCb ( 'input[name="disable-mutex"]' , settings . disableMutex );
if ( settings . stripDebug !== undefined ) setCb ( 'input[name="strip-debug"]' , settings . stripDebug );
if ( settings . disableCgo !== undefined ) setCb ( 'input[name="disable-cgo"]' , settings . disableCgo );
2026-07-11 14:55:26 -05:00
if ( settings . enableNvenc !== undefined ) setCb ( 'input[name="enable-nvenc"]' , settings . enableNvenc );
if ( settings . enableAmf !== undefined ) setCb ( 'input[name="enable-amf"]' , settings . enableAmf );
2026-07-11 16:52:24 -05:00
if ( settings . enableQsv !== undefined ) setCb ( 'input[name="enable-qsv"]' , settings . enableQsv );
2026-04-23 22:54:49 -05:00
if ( settings . noPrinting !== undefined ) setCb ( 'input[name="no-printing"]' , settings . noPrinting );
2026-05-10 17:06:39 -05:00
if ( settings . enableKeylogger !== undefined ) setCb ( 'input[name="enable-keylogger"]' , settings . enableKeylogger );
2026-05-13 13:44:33 -05:00
if ( settings . enableWebrtc !== undefined ) setCb ( 'input[name="enable-webrtc"]' , settings . enableWebrtc );
2026-07-19 17:16:35 -05:00
if ( settings . promptWebrtcFirewallOnStart !== undefined ) setCb ( 'input[name="prompt-webrtc-firewall-on-start"]' , settings . promptWebrtcFirewallOnStart );
2026-06-06 23:34:44 -05:00
if ( settings . enableWinRE !== undefined ) setCb ( 'input[name="enable-winre"]' , settings . enableWinRE );
2026-05-14 19:56:50 -05:00
if ( settings . fetchPublicIP !== undefined ) setCb ( 'input[name="fetch-public-ip"]' , settings . fetchPublicIP );
2026-04-23 22:54:49 -05:00
if ( settings . obfuscate !== undefined ) setCb ( 'input[name="obfuscate"]' , settings . obfuscate );
if ( settings . garbleLiterals !== undefined ) setCb ( 'input[name="garble-literals"]' , settings . garbleLiterals );
if ( settings . garbleTiny !== undefined ) setCb ( 'input[name="garble-tiny"]' , settings . garbleTiny );
if ( settings . garbleSeed !== undefined ) setVal ( "garble-seed" , settings . garbleSeed );
if ( settings . enableUpx !== undefined ) setCb ( 'input[name="enable-upx"]' , settings . enableUpx );
if ( settings . upxStripHeaders !== undefined ) setCb ( 'input[name="upx-strip-headers"]' , settings . upxStripHeaders );
if ( settings . sleepSeconds !== undefined ) setVal ( "sleep-seconds" , settings . sleepSeconds );
if ( settings . enablePersistence !== undefined ) setCb ( 'input[name="enable-persistence"]' , settings . enablePersistence );
if ( Array . isArray ( settings . persistenceMethods )) {
document . querySelectorAll ( 'input[name="persistence-method"]' ). forEach (( el ) => {
el . checked = settings . persistenceMethods . includes ( el . value );
});
}
if ( settings . startupName !== undefined ) setVal ( "startup-name" , settings . startupName );
if ( settings . hideConsole !== undefined ) setCb ( 'input[name="hide-console"]' , settings . hideConsole );
if ( settings . requireAdmin !== undefined ) setCb ( 'input[name="require-admin"]' , settings . requireAdmin );
if ( settings . criticalProcess !== undefined ) setCb ( 'input[name="critical-process"]' , settings . criticalProcess );
if ( settings . assemblyTitle !== undefined ) setVal ( "assembly-title" , settings . assemblyTitle );
if ( settings . assemblyProduct !== undefined ) setVal ( "assembly-product" , settings . assemblyProduct );
if ( settings . assemblyCompany !== undefined ) setVal ( "assembly-company" , settings . assemblyCompany );
if ( settings . assemblyVersion !== undefined ) setVal ( "assembly-version" , settings . assemblyVersion );
if ( settings . assemblyCopyright !== undefined ) setVal ( "assembly-copyright" , settings . assemblyCopyright );
if ( settings . outputExtension !== undefined ) setVal ( "output-extension" , settings . outputExtension );
if ( settings . cryptableMode !== undefined ) setCb ( "#cryptable-mode" , settings . cryptableMode );
2026-05-13 23:55:51 -04:00
if ( settings . useDonut !== undefined ) {
setCb ( "#donut-mode" , settings . useDonut );
if ( settings . useDonut ) applyDonutMode ( true );
}
2026-07-14 16:30:00 -05:00
if ( settings . donutSingleThreaded !== undefined ) setCb ( "#donut-single-threaded" , settings . donutSingleThreaded );
if ( settings . donutExitMode !== undefined ) setVal ( "donut-exit-mode" , settings . donutExitMode );
if ( settings . donutEntropy !== undefined ) setVal ( "donut-entropy" , settings . donutEntropy );
if ( settings . donutPreserveHeaders !== undefined ) setCb ( "#donut-preserve-headers" , settings . donutPreserveHeaders );
if ( settings . donutResumeOffset !== undefined ) setVal ( "donut-resume-offset" , settings . donutResumeOffset );
2026-05-13 23:55:51 -04:00
if ( settings . useLinuxShellcode !== undefined ) {
setCb ( "#linux-shellcode-mode" , settings . useLinuxShellcode );
if ( settings . useLinuxShellcode ) applyLinuxShellcodeMode ( true );
}
if ( settings . shellcodeConsole !== undefined ) setCb ( "#shellcode-console" , settings . shellcodeConsole );
2026-05-17 13:11:21 -05:00
if ( settings . useSgn !== undefined ) {
setCb ( "#sgn-mode" , settings . useSgn );
if ( settings . useSgn ) applySgnMode ( true );
}
if ( settings . sgnIterations !== undefined ) setVal ( "sgn-iterations" , settings . sgnIterations );
2026-06-22 12:54:12 -05:00
if ( settings . buildPlugins !== undefined ) applyBuildPluginSettings ( settings . buildPlugins );
2026-04-23 22:54:49 -05:00
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 );
}
updateWindowsSectionVisibility ();
updateIosSectionVisibility ();
updatePersistenceSettingsVisibility ();
2026-05-13 23:55:51 -04:00
updateShellcodeCheckboxVisibility ();
2026-07-15 23:29:12 -05:00
updateDisableCgoWarning ();
updateMacosSdkVisibility ();
2026-04-23 22:54:49 -05:00
if ( solMemoCheckbox && solSettings ) {
solSettings . classList . toggle ( "hidden" , ! solMemoCheckbox . checked );
}
if ( serverUrlInput && rawServerListCheckbox ) {
2026-06-27 19:38:10 -05:00
updateServerUrlPlaceholder ();
2026-08-18 00:12:36 -05:00
updateLocalServerWarning ();
2026-04-23 22:54:49 -05:00
}
applyCryptableMode ( document . getElementById ( "cryptable-mode" ) ? . checked || false );
2026-07-19 17:16:35 -05:00
updateWebrtcFirewallPromptOption ();
2026-04-23 22:54:49 -05:00
}
2026-03-26 10:26:25 -05:00
function saveFormSettings () {
try {
2026-04-23 22:54:49 -05:00
const settings = collectFormSettings ();
2026-03-26 10:26:25 -05:00
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 ;
2026-04-23 22:54:49 -05:00
const settings = JSON . parse ( raw );
applyFormSettings ( settings );
2026-03-26 10:26:25 -05:00
} catch ( err ) {
console . error ( "Failed to restore form settings:" , err );
}
}
2026-05-29 22:12:02 -05:00
const buildProfileManager = createBuildProfileManager ({
elements : {
profileSelect ,
profileNameInput ,
profileSaveBtn ,
profileLoadBtn ,
profileDeleteBtn ,
profileExportBtn ,
profileImportBtn ,
profileImportFile ,
},
collectFormSettings ,
applyFormSettings ,
saveFormSettings ,
});
const { loadBuildProfiles } = buildProfileManager ;
2026-04-23 22:54:49 -05:00
2026-05-29 22:12:02 -05:00
const buildHistoryManager = createBuildHistoryManager ({
buildResults ,
buildFilesDiv ,
getCurrentServerVersion : () => currentServerVersion ,
getCurrentUserRole : () => currentUserRole ,
getShowAllBuilds : () => showAllBuilds ,
});
const {
displayBuild ,
loadSavedBuilds ,
saveBuildToStorage ,
} = buildHistoryManager ;
2026-04-23 22:54:49 -05:00
2026-04-14 15:44:16 -05:00
const CRYPTABLE_DISABLE_TARGETS = [
'input[name="enable-persistence"]' ,
'input[name="enable-upx"]' ,
'input[name="upx-strip-headers"]' ,
'input[name="require-admin"]' ,
'input[name="critical-process"]' ,
];
const CRYPTABLE_DISABLE_INPUTS = [
"#assembly-title" ,
"#assembly-product" ,
"#assembly-company" ,
"#assembly-version" ,
"#assembly-copyright" ,
"#output-extension" ,
"#sleep-seconds" ,
];
2026-05-10 17:06:39 -05:00
const CRYPTABLE_HIDE_SECTIONS = [ 5 , 6 , 7 ];
2026-04-14 15:44:16 -05:00
function applyCryptableMode ( enabled ) {
const badge = document . getElementById ( "cryptable-badge" );
if ( badge ) badge . classList . toggle ( "hidden" , ! enabled );
CRYPTABLE_DISABLE_TARGETS . forEach (( sel ) => {
const el = document . querySelector ( sel );
if ( ! el ) return ;
if ( enabled ) {
el . checked = false ;
el . disabled = true ;
el . closest ( "label" ) ? . classList . add ( "opacity-40" , "pointer-events-none" );
} else {
el . disabled = false ;
el . closest ( "label" ) ? . classList . remove ( "opacity-40" , "pointer-events-none" );
}
});
CRYPTABLE_DISABLE_INPUTS . forEach (( sel ) => {
const el = document . querySelector ( sel );
if ( ! el ) return ;
if ( enabled ) {
el . dataset . preCryptableValue = el . value ;
el . value = el . type === "number" ? "0" : "" ;
el . disabled = true ;
el . classList . add ( "opacity-40" );
} else {
if ( el . dataset . preCryptableValue !== undefined ) {
el . value = el . dataset . preCryptableValue ;
delete el . dataset . preCryptableValue ;
}
el . disabled = false ;
el . classList . remove ( "opacity-40" );
}
});
const iconUploadEl = document . getElementById ( "icon-upload" );
const iconLabelEl = document . getElementById ( "icon-label" );
if ( iconUploadEl ) iconUploadEl . disabled = enabled ;
if ( iconLabelEl && enabled ) iconLabelEl . closest ( "label" ) ? . classList . toggle ( "opacity-40" , enabled );
if ( iconLabelEl && ! enabled ) iconLabelEl . closest ( "label" ) ? . classList . remove ( "opacity-40" );
const cloneExeUploadEl = document . getElementById ( "clone-exe-upload" );
if ( cloneExeUploadEl ) cloneExeUploadEl . disabled = enabled ;
if ( cloneExeUploadEl ) cloneExeUploadEl . closest ( "label" ) ? . classList . toggle ( "opacity-40" , enabled );
const bindAddLabelEl = document . getElementById ( "bind-add-label" );
if ( bindAddLabelEl ) {
bindAddLabelEl . classList . toggle ( "opacity-40" , enabled );
bindAddLabelEl . classList . toggle ( "pointer-events-none" , enabled );
}
const sections = document . querySelectorAll ( ".accordion-section" );
CRYPTABLE_HIDE_SECTIONS . forEach (( idx ) => {
const sec = sections [ idx - 1 ];
if ( ! sec ) return ;
if ( enabled ) {
sec . classList . add ( "opacity-30" , "pointer-events-none" );
sec . dataset . cryptableDisabled = "true" ;
} else {
sec . classList . remove ( "opacity-30" , "pointer-events-none" );
delete sec . dataset . cryptableDisabled ;
}
});
if ( enabled ) {
updatePersistenceSettingsVisibility ();
const upxC = document . getElementById ( "upx-settings-container" );
if ( upxC ) upxC . classList . add ( "hidden" );
}
2026-05-17 22:14:11 -05:00
updateShellcodeCheckboxVisibility ();
2026-04-14 15:44:16 -05:00
saveFormSettings ();
}
2026-05-17 22:14:11 -05:00
function applyCryptableShellcodeGate () {
const cryptableOn = !! document . getElementById ( "cryptable-mode" ) ? . checked ;
const setGate = ( checkboxId , hintId ) => {
const cb = document . getElementById ( checkboxId );
const hint = document . getElementById ( hintId );
if ( ! cb ) return ;
const label = cb . closest ( "label" );
if ( cryptableOn ) {
cb . disabled = false ;
label ? . classList . remove ( "opacity-40" , "pointer-events-none" );
if ( hint ) hint . classList . add ( "hidden" );
} else {
if ( cb . checked ) {
cb . checked = false ;
cb . dispatchEvent ( new Event ( "change" , { bubbles : true }));
}
cb . disabled = true ;
label ? . classList . add ( "opacity-40" , "pointer-events-none" );
if ( hint ) hint . classList . remove ( "hidden" );
}
};
setGate ( "donut-mode" , "donut-cryptable-hint" );
setGate ( "linux-shellcode-mode" , "linux-sc-cryptable-hint" );
}
2026-05-13 23:55:51 -04:00
function updateShellcodeCheckboxVisibility () {
const selected = Array . from ( document . querySelectorAll ( 'input[name="platform"]:checked' )). map (( el ) => el . value );
const hasWindows = selected . some (( p ) => p . startsWith ( "windows-" ));
const hasLinuxAmd64 = selected . includes ( "linux-amd64" );
const donutRow = document . getElementById ( "donut-row" );
if ( donutRow ) donutRow . classList . toggle ( "hidden" , ! hasWindows );
const linuxScRow = document . getElementById ( "linux-sc-row" );
if ( linuxScRow ) linuxScRow . classList . toggle ( "hidden" , ! hasLinuxAmd64 );
2026-05-17 13:11:21 -05:00
2026-05-17 22:14:11 -05:00
applyCryptableShellcodeGate ();
2026-05-17 13:11:21 -05:00
// SGN is only meaningful when there's shellcode to encode — i.e. Donut or
// Linux shellcode is enabled on a compatible platform.
const donutOn = document . getElementById ( "donut-mode" ) ? . checked && hasWindows ;
const linuxScOn = document . getElementById ( "linux-shellcode-mode" ) ? . checked && hasLinuxAmd64 ;
const sgnRow = document . getElementById ( "sgn-row" );
if ( sgnRow ) sgnRow . classList . toggle ( "hidden" , ! ( donutOn || linuxScOn ));
if ( ! ( donutOn || linuxScOn )) {
const sgnCheckbox = document . getElementById ( "sgn-mode" );
if ( sgnCheckbox ? . checked ) {
sgnCheckbox . checked = false ;
applySgnMode ( false );
}
}
}
function applySgnMode ( enabled ) {
const badge = document . getElementById ( "sgn-badge" );
if ( badge ) badge . classList . toggle ( "hidden" , ! enabled );
saveFormSettings ();
2026-05-13 23:55:51 -04:00
}
function applyDonutMode ( enabled ) {
const badge = document . getElementById ( "donut-badge" );
if ( badge ) badge . classList . toggle ( "hidden" , ! enabled );
if ( enabled ) {
const selected = Array . from ( document . querySelectorAll ( 'input[name="platform"]:checked' )). map (( el ) => el . value );
const hasNonWindows = selected . some (( p ) => ! p . startsWith ( "windows-" ));
const nonWinWarn = document . getElementById ( "donut-nonwin-warn" );
if ( nonWinWarn ) nonWinWarn . classList . toggle ( "hidden" , ! hasNonWindows );
}
saveFormSettings ();
}
function applyLinuxShellcodeMode ( enabled ) {
const badge = document . getElementById ( "linux-sc-badge" );
if ( badge ) badge . classList . toggle ( "hidden" , ! enabled );
if ( enabled ) {
const selected = Array . from ( document . querySelectorAll ( 'input[name="platform"]:checked' )). map (( el ) => el . value );
const hasNonAmd64Linux = selected . some (( p ) => p . startsWith ( "linux-" ) && p !== "linux-amd64" );
const nonX64Warn = document . getElementById ( "linux-sc-nonx64-warn" );
if ( nonX64Warn ) nonX64Warn . classList . toggle ( "hidden" , ! hasNonAmd64Linux );
}
saveFormSettings ();
}
2026-07-19 17:16:35 -05:00
function updateWebrtcFirewallPromptOption () {
const webrtcEnabled = document . querySelector ( 'input[name="enable-webrtc"]' ) ? . checked ?? false ;
const promptInput = document . querySelector ( 'input[name="prompt-webrtc-firewall-on-start"]' );
if ( ! promptInput ) return ;
promptInput . disabled = ! webrtcEnabled ;
if ( ! webrtcEnabled ) promptInput . checked = false ;
}
2026-03-26 10:26:25 -05:00
restoreFormSettings ();
2026-08-18 00:12:36 -05:00
if ( serverUrlInput && ! serverUrlInput . value . trim ()) {
serverUrlInput . value = getCurrentServerUrlForMode ();
saveFormSettings ();
}
updateLocalServerWarning ();
2026-03-21 02:12:35 -05:00
initAccordions ();
2026-07-14 10:21:20 -05:00
initBuilderTabs ();
2026-06-22 12:54:12 -05:00
loadBuildPlugins ();
2026-07-15 23:26:41 -05:00
loadMacosSdkStatus ();
2026-03-21 02:12:35 -05:00
updateWindowsSectionVisibility ();
2026-05-13 23:55:51 -04:00
updateShellcodeCheckboxVisibility ();
2026-07-15 23:29:12 -05:00
updateDisableCgoWarning ();
2026-03-05 00:04:26 -06:00
init ();
2026-04-05 18:18:40 -05:00
if ( solMemoCheckbox && solSettings ) {
solSettings . classList . toggle ( "hidden" , ! solMemoCheckbox . checked );
}
2026-03-05 00:04:26 -06:00
if ( rawServerListCheckbox && serverUrlInput ) {
rawServerListCheckbox . addEventListener ( "change" , () => {
const isRaw = rawServerListCheckbox . checked ;
const current = serverUrlInput . value . trim ();
2026-04-05 18:18:40 -05:00
if ( isRaw && solMemoCheckbox ) {
solMemoCheckbox . checked = false ;
if ( solSettings ) solSettings . classList . add ( "hidden" );
}
2026-03-05 00:04:26 -06:00
if ( isRaw ) {
2026-05-17 13:31:47 -05:00
// Raw-list mode wants a real https:// URL pointing at a .txt of endpoints.
// Swap any wss:// → https:// / ws:// → http:// the user may have left over.
2026-03-05 00:04:26 -06:00
if ( current . startsWith ( "wss://" )) {
serverUrlInput . value = "https://" + current . slice ( "wss://" . length );
} else if ( current . startsWith ( "ws://" )) {
serverUrlInput . value = "http://" + current . slice ( "ws://" . length );
}
} else {
2026-05-17 13:31:47 -05:00
// Non-raw mode: bare domain only. The agent prepends wss:// itself.
serverUrlInput . value = stripServerUrlPrefix ( current );
}
2026-06-27 19:38:10 -05:00
updateServerUrlPlaceholder ();
2026-08-18 00:12:36 -05:00
updateLocalServerWarning ();
2026-05-17 13:31:47 -05:00
});
2026-06-27 19:38:10 -05:00
if ( serverUrlCurrentBtn ) {
serverUrlCurrentBtn . addEventListener ( "click" , () => {
if ( solMemoCheckbox ? . checked ) {
solMemoCheckbox . checked = false ;
if ( solSettings ) solSettings . classList . add ( "hidden" );
}
serverUrlInput . value = getCurrentServerUrlForMode ();
serverUrlInput . dispatchEvent ( new Event ( "input" , { bubbles : true }));
serverUrlInput . dispatchEvent ( new Event ( "change" , { bubbles : true }));
saveFormSettings ();
2026-08-18 00:12:36 -05:00
updateLocalServerWarning ();
2026-06-27 19:38:10 -05:00
serverUrlInput . focus ();
});
}
2026-05-17 13:31:47 -05:00
// Live-strip protocol prefixes in non-raw mode so the field stays clean and
// the user sees their typo corrected immediately.
serverUrlInput . addEventListener ( "input" , () => {
2026-08-18 00:12:36 -05:00
if ( rawServerListCheckbox . checked ) {
updateLocalServerWarning ();
return ;
}
2026-05-17 13:31:47 -05:00
const before = serverUrlInput . value ;
const after = stripServerUrlPrefix ( before );
if ( after !== before ) {
const caret = Math . max ( 0 , ( serverUrlInput . selectionStart ?? after . length ) - ( before . length - after . length ));
serverUrlInput . value = after ;
try { serverUrlInput . setSelectionRange ( caret , caret ); } catch {}
2026-03-05 00:04:26 -06:00
}
2026-08-18 00:12:36 -05:00
updateLocalServerWarning ();
2026-03-05 00:04:26 -06:00
});
}
2026-05-17 15:01:34 -05:00
const rawServerListHelpBtn = document . getElementById ( "raw-server-list-help" );
const rawServerListModal = document . getElementById ( "raw-server-list-modal" );
const rawServerListModalClose = document . getElementById ( "raw-server-list-modal-close" );
const rawServerListModalOk = document . getElementById ( "raw-server-list-modal-ok" );
function showRawServerListModal () {
if ( ! rawServerListModal ) return ;
rawServerListModal . classList . remove ( "hidden" );
rawServerListModal . classList . add ( "flex" );
}
function hideRawServerListModal () {
if ( ! rawServerListModal ) return ;
rawServerListModal . classList . remove ( "flex" );
rawServerListModal . classList . add ( "hidden" );
}
if ( rawServerListHelpBtn ) rawServerListHelpBtn . addEventListener ( "click" , showRawServerListModal );
if ( rawServerListModalClose ) rawServerListModalClose . addEventListener ( "click" , hideRawServerListModal );
if ( rawServerListModalOk ) rawServerListModalOk . addEventListener ( "click" , hideRawServerListModal );
if ( rawServerListModal ) {
rawServerListModal . addEventListener ( "click" , ( e ) => {
if ( e . target === rawServerListModal ) hideRawServerListModal ();
});
}
document . addEventListener ( "keydown" , ( e ) => {
if ( e . key === "Escape" && rawServerListModal && ! rawServerListModal . classList . contains ( "hidden" )) {
hideRawServerListModal ();
}
});
2026-04-05 18:18:40 -05:00
if ( solMemoCheckbox && solSettings ) {
solMemoCheckbox . addEventListener ( "change" , () => {
const isSol = solMemoCheckbox . checked ;
solSettings . classList . toggle ( "hidden" , ! isSol );
if ( isSol && rawServerListCheckbox ) {
rawServerListCheckbox . checked = false ;
2026-06-27 19:38:10 -05:00
updateServerUrlPlaceholder ();
2026-04-05 18:18:40 -05:00
}
2026-07-14 00:03:55 -05:00
if ( isSol ) loadSolRpcEndpoints ();
2026-04-05 18:18:40 -05:00
});
}
2026-03-18 21:05:58 -05:00
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-04-19 15:07:44 -05:00
ios : selectedPlatforms . some (( platform ) => platform . startsWith ( "ios-" )),
2026-03-18 21:05:58 -05:00
};
2026-03-12 13:52:02 -05:00
}
2026-04-04 20:42:54 -05:00
function validateStartupName () {
if ( ! startupNameError ) return true ;
const families = getSelectedPlatformFamilies ();
const val = document . getElementById ( "startup-name" ) ? . value . trim () || "" ;
2026-04-19 15:07:44 -05:00
if (( families . darwin || families . ios ) && val && ! val . startsWith ( "com." )) {
startupNameError . textContent = "macOS/iOS requires the name to start with \"com.\" (e.g. com.apple.updater)" ;
2026-04-04 20:42:54 -05:00
startupNameError . classList . remove ( "hidden" );
return false ;
}
startupNameError . textContent = "" ;
startupNameError . classList . add ( "hidden" );
return true ;
}
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 );
}
2026-04-04 20:42:54 -05:00
if ( persistenceStartupNameContainer ) {
persistenceStartupNameContainer . classList . toggle ( "hidden" , ! hasSupportedFamily );
}
if ( startupNameMacosHint ) {
startupNameMacosHint . classList . toggle ( "hidden" , ! families . darwin );
}
if ( startupNameDefaultHint ) {
startupNameDefaultHint . classList . toggle ( "hidden" , families . darwin );
}
2026-03-18 21:05:58 -05:00
if ( persistenceEmptyState ) {
persistenceEmptyState . classList . toggle ( "hidden" , hasSupportedFamily );
}
2026-04-04 20:42:54 -05:00
validateStartupName ();
2026-03-18 21:05:58 -05:00
}
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-04-19 15:07:44 -05:00
input . addEventListener ( "change" , updateIosSectionVisibility );
2026-07-15 23:26:41 -05:00
input . addEventListener ( "change" , updateMacosSdkVisibility );
});
2026-07-15 23:29:12 -05:00
disableCgoInput ? . addEventListener ( "change" , updateMacosSdkVisibility );
disableCgoInput ? . addEventListener ( "change" , updateDisableCgoWarning );
2026-07-15 23:26:41 -05:00
macosSdkFileInput ? . addEventListener ( "change" , () => {
const file = macosSdkFileInput . files ? .[ 0 ];
if ( macosSdkFileStatus && file ) macosSdkFileStatus . textContent = ` ${ file . name } selected ( ${ formatFileSize ( file . size ) } ).` ;
2026-03-18 21:05:58 -05:00
});
2026-04-04 20:42:54 -05:00
document . getElementById ( "startup-name" ) ? . addEventListener ( "input" , validateStartupName );
2026-04-19 15:07:44 -05:00
function updateIosSectionVisibility () {
const families = getSelectedPlatformFamilies ();
const iosBundleIdContainer = document . getElementById ( "ios-bundle-id-container" );
if ( iosBundleIdContainer ) {
iosBundleIdContainer . classList . toggle ( "hidden" , ! families . ios );
}
}
updateIosSectionVisibility ();
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-07-19 17:16:35 -05:00
document . querySelector ( 'input[name="enable-webrtc"]' ) ? . addEventListener ( "change" , updateWebrtcFirewallPromptOption );
2026-03-26 10:26:25 -05:00
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-04-14 15:44:16 -05:00
const cryptableCheckbox = document . getElementById ( "cryptable-mode" );
if ( cryptableCheckbox ) {
cryptableCheckbox . addEventListener ( "change" , () => {
applyCryptableMode ( cryptableCheckbox . checked );
});
if ( cryptableCheckbox . checked ) {
applyCryptableMode ( true );
}
}
2026-05-13 23:55:51 -04:00
const donutCheckbox = document . getElementById ( "donut-mode" );
if ( donutCheckbox ) {
donutCheckbox . addEventListener ( "change" , () => { applyDonutMode ( donutCheckbox . checked ); });
if ( donutCheckbox . checked ) applyDonutMode ( true );
}
const linuxScCheckbox = document . getElementById ( "linux-shellcode-mode" );
if ( linuxScCheckbox ) {
2026-05-17 13:11:21 -05:00
linuxScCheckbox . addEventListener ( "change" , () => {
applyLinuxShellcodeMode ( linuxScCheckbox . checked );
updateShellcodeCheckboxVisibility ();
});
2026-05-13 23:55:51 -04:00
if ( linuxScCheckbox . checked ) applyLinuxShellcodeMode ( true );
}
2026-05-17 13:11:21 -05:00
if ( donutCheckbox ) {
donutCheckbox . addEventListener ( "change" , () => updateShellcodeCheckboxVisibility ());
}
const sgnCheckbox = document . getElementById ( "sgn-mode" );
if ( sgnCheckbox ) {
sgnCheckbox . addEventListener ( "change" , () => applySgnMode ( sgnCheckbox . checked ));
if ( sgnCheckbox . checked ) applySgnMode ( true );
}
const sgnIterationsInput = document . getElementById ( "sgn-iterations" );
if ( sgnIterationsInput ) {
sgnIterationsInput . addEventListener ( "change" , () => saveFormSettings ());
}
2026-05-13 23:55:51 -04:00
document . querySelectorAll ( 'input[name="platform"]' ). forEach (( el ) => {
el . addEventListener ( "change" , () => {
updateShellcodeCheckboxVisibility ();
if ( donutCheckbox ? . checked ) applyDonutMode ( true );
if ( linuxScCheckbox ? . checked ) applyLinuxShellcodeMode ( true );
});
});
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-04-04 21:09:51 -05:00
function extractPEMetadata ( buffer ) {
const dv = new DataView ( buffer );
const bytes = new Uint8Array ( buffer );
const u8 = ( o ) => dv . getUint8 ( o );
const u16 = ( o ) => dv . getUint16 ( o , true );
const u32 = ( o ) => dv . getUint32 ( o , true );
const alignUp = ( n ) => ( n + 3 ) & ~ 3 ;
if ( buffer . byteLength < 0x40 ) throw new Error ( "File too small" );
if ( u16 ( 0 ) !== 0x5A4D ) throw new Error ( "Not a PE file (missing MZ header)" );
const peOff = u32 ( 0x3C );
if ( peOff + 24 > buffer . byteLength ) throw new Error ( "Invalid PE offset" );
if ( u32 ( peOff ) !== 0x4550 ) throw new Error ( "Not a PE file (missing PE signature)" );
const numSections = u16 ( peOff + 6 );
const optHdrSize = u16 ( peOff + 20 );
const optMagic = u16 ( peOff + 24 );
const is64 = optMagic === 0x20B ; // PE32+ vs PE32
const dataDirsOff = peOff + 24 + ( is64 ? 112 : 96 );
const rsrcRVA = u32 ( dataDirsOff + 2 * 8 ); // index 2 = IMAGE_DIRECTORY_ENTRY_RESOURCE
if ( rsrcRVA === 0 ) throw new Error ( "No resource section RVA" );
const secTableOff = peOff + 24 + optHdrSize ;
let rsrcRaw = 0 ;
for ( let i = 0 ; i < numSections ; i ++ ) {
const s = secTableOff + i * 40 ;
const va = u32 ( s + 12 );
const vsz = Math . max ( u32 ( s + 8 ), u32 ( s + 16 ));
const raw = u32 ( s + 20 );
if ( rsrcRVA >= va && rsrcRVA < va + vsz ) {
rsrcRaw = raw + ( rsrcRVA - va );
break ;
}
}
if ( ! rsrcRaw ) throw new Error ( "Could not locate resource section raw data" );
const rvaToOff = ( rva ) => rsrcRaw + ( rva - rsrcRVA );
function rsrcDir ( dirOff ) {
if ( dirOff + 16 > buffer . byteLength ) return [];
const numNamed = u16 ( dirOff + 12 );
const numId = u16 ( dirOff + 14 );
const total = numNamed + numId ;
const entries = [];
for ( let i = 0 ; i < total ; i ++ ) {
const e = dirOff + 16 + i * 8 ;
if ( e + 8 > buffer . byteLength ) break ;
const nameOrId = u32 ( e );
const dataOrDir = u32 ( e + 4 );
entries . push ({
id : ( nameOrId & 0x80000000 ) ? null : nameOrId ,
isSubDir : ( dataOrDir & 0x80000000 ) !== 0 ,
off : dataOrDir & 0x7FFFFFFF ,
});
}
return entries ;
}
const WANT = new Set ([ 3 , 14 , 16 ]);
const resources = {};
for ( const te of rsrcDir ( rsrcRaw )) {
if ( te . id === null || ! WANT . has ( te . id ) || ! te . isSubDir ) continue ;
resources [ te . id ] = {};
for ( const ne of rsrcDir ( rsrcRaw + te . off )) {
if ( ! ne . isSubDir ) continue ;
const nameId = ne . id ?? 1 ;
resources [ te . id ][ nameId ] = [];
for ( const le of rsrcDir ( rsrcRaw + ne . off )) {
if ( le . isSubDir ) continue ;
const deOff = rsrcRaw + le . off ;
if ( deOff + 8 > buffer . byteLength ) continue ;
const dataRVA = u32 ( deOff );
const dataSize = u32 ( deOff + 4 );
const dataOff = rvaToOff ( dataRVA );
if ( dataOff + dataSize <= buffer . byteLength )
resources [ te . id ][ nameId ]. push ({ dataOff , dataSize });
}
}
}
function parseVersionStrings ( absOff , size ) {
const strings = {};
const end = absOff + size ;
let pos = absOff ;
if ( pos + 6 > end ) return strings ;
const viLen = u16 ( pos );
const viValLen = u16 ( pos + 2 );
pos += 6 ;
while ( pos + 1 < end && ( u8 ( pos ) | u8 ( pos + 1 ))) pos += 2 ;
pos = alignUp ( pos + 2 );
pos += viValLen ;
pos = alignUp ( pos );
const viEnd = Math . min ( absOff + viLen , end );
while ( pos + 6 < viEnd ) {
const childLen = u16 ( pos );
if ( childLen < 6 ) break ;
const childEnd = Math . min ( pos + childLen , viEnd );
let kp = pos + 6 ;
let key = "" ;
while ( kp + 1 < childEnd && ( u8 ( kp ) | u8 ( kp + 1 ))) {
key += String . fromCharCode ( u8 ( kp ) | ( u8 ( kp + 1 ) << 8 ));
kp += 2 ;
}
kp = alignUp ( kp + 2 );
if ( key === "StringFileInfo" ) {
let sp = kp ;
while ( sp + 6 < childEnd ) {
const stLen = u16 ( sp );
if ( stLen < 6 ) break ;
const stEnd = Math . min ( sp + stLen , childEnd );
let tp = sp + 6 ;
while ( tp + 1 < stEnd && ( u8 ( tp ) | u8 ( tp + 1 ))) tp += 2 ;
tp = alignUp ( tp + 2 );
while ( tp + 6 < stEnd ) {
const sLen = u16 ( tp );
if ( sLen < 6 ) break ;
const sEnd = Math . min ( tp + sLen , stEnd );
const sValLen = u16 ( tp + 2 );
let np = tp + 6 ;
let name = "" ;
while ( np + 1 < sEnd && ( u8 ( np ) | u8 ( np + 1 ))) {
name += String . fromCharCode ( u8 ( np ) | ( u8 ( np + 1 ) << 8 ));
np += 2 ;
}
np = alignUp ( np + 2 );
let val = "" ;
const valEnd = Math . min ( np + sValLen * 2 , sEnd );
while ( np + 1 < valEnd && ( u8 ( np ) | u8 ( np + 1 ))) {
val += String . fromCharCode ( u8 ( np ) | ( u8 ( np + 1 ) << 8 ));
np += 2 ;
}
if ( name ) strings [ name ] = val ;
tp = alignUp ( sEnd );
}
sp = alignUp ( stEnd );
}
}
pos = alignUp ( childEnd );
}
return strings ;
}
function buildIco ( groupOff , iconRes ) {
if ( groupOff + 6 > buffer . byteLength ) return null ;
const count = u16 ( groupOff + 4 );
if ( count === 0 || groupOff + 6 + count * 14 > buffer . byteLength ) return null ;
const grpEntries = [];
for ( let i = 0 ; i < count ; i ++ ) {
const e = groupOff + 6 + i * 14 ;
grpEntries . push ({
w : u8 ( e ), h : u8 ( e + 1 ), cc : u8 ( e + 2 ),
planes : u16 ( e + 4 ), bits : u16 ( e + 6 ),
size : u32 ( e + 8 ), id : u16 ( e + 12 ),
});
}
const iconData = [];
for ( const en of grpEntries ) {
const rd = iconRes [ en . id ];
if ( ! rd || rd . length === 0 ) continue ;
iconData . push ({ en , dataOff : rd [ 0 ]. dataOff , dataSize : rd [ 0 ]. dataSize });
}
if ( iconData . length === 0 ) return null ;
let totalSize = 6 + iconData . length * 16 ;
for ( const id of iconData ) totalSize += id . dataSize ;
const ico = new Uint8Array ( totalSize );
const idv = new DataView ( ico . buffer );
let p = 0 ;
idv . setUint16 ( p , 0 , true ); p += 2 ; // reserved
idv . setUint16 ( p , 1 , true ); p += 2 ; // type = ICO
idv . setUint16 ( p , iconData . length , true ); p += 2 ;
let dataOffset = 6 + iconData . length * 16 ;
const entryStart = p ;
let ep = entryStart ;
p += iconData . length * 16 ;
for ( const { en , dataOff , dataSize } of iconData ) {
ico [ ep ] = en . w ;
ico [ ep + 1 ] = en . h ;
ico [ ep + 2 ] = en . cc ;
ico [ ep + 3 ] = 0 ;
idv . setUint16 ( ep + 4 , en . planes , true );
idv . setUint16 ( ep + 6 , en . bits , true );
idv . setUint32 ( ep + 8 , dataSize , true );
idv . setUint32 ( ep + 12 , dataOffset , true );
ep += 16 ;
ico . set ( bytes . subarray ( dataOff , dataOff + dataSize ), dataOffset );
dataOffset += dataSize ;
}
let bin = "" ;
for ( let i = 0 ; i < ico . length ; i ++ ) bin += String . fromCharCode ( ico [ i ]);
return btoa ( bin );
}
const result = {};
if ( resources [ 16 ]) {
const vd = Object . values ( resources [ 16 ]). flat ()[ 0 ];
if ( vd ) {
try { result . strings = parseVersionStrings ( vd . dataOff , vd . dataSize ); } catch ( _ ) {}
}
}
if ( resources [ 14 ] && resources [ 3 ]) {
const gd = Object . values ( resources [ 14 ]). flat ()[ 0 ];
if ( gd ) {
try { result . iconBase64 = buildIco ( gd . dataOff , resources [ 3 ]); } catch ( _ ) {}
}
}
return result ;
}
const cloneExeUpload = document . getElementById ( "clone-exe-upload" );
const cloneExeLabel = document . getElementById ( "clone-exe-label" );
const cloneExeStatus = document . getElementById ( "clone-exe-status" );
function setCloneStatus ( msg , isError ) {
if ( ! cloneExeStatus ) return ;
cloneExeStatus . textContent = msg ;
cloneExeStatus . className = "text-xs " + ( isError ? "text-red-400" : "text-emerald-400" );
cloneExeStatus . classList . remove ( "hidden" );
}
if ( cloneExeUpload ) {
cloneExeUpload . addEventListener ( "change" , () => {
const file = cloneExeUpload . files [ 0 ];
if ( ! file ) return ;
if ( cloneExeLabel ) cloneExeLabel . textContent = file . name ;
setCloneStatus ( "Parsing..." , false );
const reader = new FileReader ();
reader . onload = () => {
try {
const meta = extractPEMetadata ( reader . result );
const s = meta . strings || {};
const fields = [
[ "assembly-title" , s [ "FileDescription" ] ?? s [ "InternalName" ] ?? "" ],
[ "assembly-product" , s [ "ProductName" ] ?? "" ],
[ "assembly-company" , s [ "CompanyName" ] ?? "" ],
[ "assembly-version" , s [ "FileVersion" ] ? . replace ( /,\s*/g , "." ) ?? s [ "ProductVersion" ] ? . replace ( /,\s*/g , "." ) ?? "" ],
[ "assembly-copyright" , s [ "LegalCopyright" ] ?? s [ "LegalTrademarks" ] ?? "" ],
];
let filled = 0 ;
for ( const [ id , val ] of fields ) {
const el = document . getElementById ( id );
if ( el && val ) { el . value = val ; filled ++ ; }
}
if ( meta . iconBase64 ) {
const decodedBytes = Math . floor ( meta . iconBase64 . length * 3 / 4 );
if ( decodedBytes <= 1024 * 1024 ) {
pendingIconBase64 = meta . iconBase64 ;
if ( iconLabel ) iconLabel . textContent = file . name + " (cloned icon)" ;
if ( iconClear ) iconClear . classList . remove ( "hidden" );
setCloneStatus ( `Cloned ${ filled } metadata field(s) + icon` , false );
} else {
setCloneStatus ( `Cloned ${ filled } metadata field(s) (icon too large, skipped)` , false );
}
} else {
setCloneStatus ( filled > 0 ? `Cloned ${ filled } metadata field(s), no icon found` : "No metadata found in file" , ! filled );
}
} catch ( err ) {
setCloneStatus ( "Error: " + err . message , true );
}
cloneExeUpload . value = "" ;
};
reader . readAsArrayBuffer ( file );
});
}
2026-03-21 02:37:10 -05:00
const MAX_BIND_FILES = 5 ;
2026-05-14 18:33:03 -05:00
const MAX_BIND_FILE_BYTES = 200 * 1024 * 1024 ; // 200 MB
2026-03-21 02:37:10 -05:00
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 ) {
2026-04-08 19:47:38 -05:00
alert ( `Each bound file must be under 50 MB. " ${ file . name } " is too large.` );
2026-03-21 02:37:10 -05:00
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 ) {
2026-04-13 20:26:03 -05:00
window . location . href = "/" ;
2026-03-05 00:04:26 -06:00
return ;
}
const data = await res . json ();
2026-07-21 13:18:30 -05:00
const permissions = Array . isArray ( data . permissions ) ? data . permissions : [];
const hasPermission = ( permission ) => permissions . includes ( permission );
2026-04-14 10:23:59 -05:00
currentUserRole = data . role ;
2026-06-10 19:54:18 -05:00
currentUsername = data . username || null ;
2026-03-05 00:04:26 -06:00
usernameDisplay . textContent = data . username ;
2026-06-10 19:54:18 -05:00
loadSgnTxtUnlockState ();
2026-03-05 00:04:26 -06:00
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" ,
);
}
2026-07-21 13:18:30 -05:00
if ( hasPermission ( "users:manage" )) {
2026-03-05 00:04:26 -06:00
usersLink . classList . remove ( "hidden" );
2026-07-21 13:18:30 -05:00
}
if ( hasPermission ( "plugins:manage" )) {
2026-03-05 00:04:26 -06:00
pluginsLink ? . classList . remove ( "hidden" );
2026-07-21 13:18:30 -05:00
}
if ( hasPermission ( "deploys:manage" )) {
2026-03-05 00:04:26 -06:00
document . getElementById ( "deploy-link" ) ? . classList . remove ( "hidden" );
}
2026-07-21 13:18:30 -05:00
if ( hasPermission ( "clients:build" ) || data . canBuild ) {
2026-03-05 00:04:26 -06:00
buildLink ? . classList . remove ( "hidden" );
}
2026-07-21 13:18:30 -05:00
if ( hasPermission ( "scripts:manage" )) {
2026-03-05 00:04:26 -06:00
scriptsLink ? . classList . remove ( "hidden" );
}
2026-07-21 13:18:30 -05:00
if ( ! hasPermission ( "clients:build" ) && ! 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-04-23 22:54:49 -05:00
if ( profileSaveBtn ) profileSaveBtn . disabled = true ;
if ( profileLoadBtn ) profileLoadBtn . disabled = true ;
if ( profileDeleteBtn ) profileDeleteBtn . disabled = true ;
if ( profileExportBtn ) profileExportBtn . disabled = true ;
if ( profileImportBtn ) profileImportBtn . disabled = true ;
2026-03-05 00:04:26 -06:00
}
2026-03-12 12:08:02 -05:00
await loadServerVersion ();
2026-07-14 00:03:55 -05:00
await loadSolRpcEndpoints ();
2026-03-12 12:08:02 -05:00
await loadSavedBuilds ();
2026-04-23 22:54:49 -05:00
await loadBuildProfiles ();
2026-04-14 10:23:59 -05:00
const toggleAllBuildsBtn = document . getElementById ( "toggle-all-builds-btn" );
const toggleAllBuildsLabel = document . getElementById ( "toggle-all-builds-label" );
if ( toggleAllBuildsBtn && currentUserRole === "admin" ) {
toggleAllBuildsBtn . classList . remove ( "hidden" );
toggleAllBuildsBtn . addEventListener ( "click" , async () => {
showAllBuilds = ! showAllBuilds ;
if ( toggleAllBuildsLabel ) {
toggleAllBuildsLabel . textContent = showAllBuilds ? "My Builds" : "Show All" ;
}
buildFilesDiv . innerHTML = "" ;
await loadSavedBuilds ();
});
}
2026-03-05 00:04:26 -06:00
} catch ( err ) {
console . error ( "Failed to fetch user info:" , err );
2026-04-13 20:26:03 -05:00
window . location . href = "/" ;
2026-03-05 00:04:26 -06:00
}
}
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 = "/" ;
});
}
2026-05-29 22:12:02 -05:00
buildProfileManager . bindProfileControls ();
2026-04-23 22:54:49 -05:00
2026-03-05 00:04:26 -06:00
form ? . addEventListener ( "submit" , async ( e ) => {
e . preventDefault ();
if ( isBuilding ) return ;
const platformCheckboxes = form . querySelectorAll (
2026-08-19 09:54:52 -05:00
'input[name="platform"]:checked:not(:disabled)' ,
2026-03-05 00:04:26 -06:00
);
const platforms = Array . from ( platformCheckboxes ). map (( cb ) => cb . value );
if ( platforms . length === 0 ) {
alert ( "Please select at least one platform to build" );
return ;
}
2026-08-19 09:54:52 -05:00
if ( ! buildProviderSelect ? . value && ! validateStartupName ()) {
2026-04-04 20:42:54 -05:00
document . getElementById ( "startup-name" ) ? . focus ();
return ;
}
2026-08-19 09:54:52 -05:00
if ( ! buildProviderSelect ? . value && disableCgoInput ? . checked && ! confirm (
2026-07-15 23:29:12 -05:00
"CGO is disabled. Native features such as voice/audio, macOS keystroke capture and permission checks, native Linux/macOS plugins, and Windows GPU encoder integrations may not work.\n\nContinue with a limited CGO-disabled build?"
)) {
disableCgoInput . focus ();
return ;
}
2026-07-15 23:26:41 -05:00
if ( needsMacosSdkUpload () && ( ! macosSdkFileInput ? . files ? .[ 0 ] || ! macosSdkRightsInput ? . checked )) {
alert ( ! macosSdkFileInput ? . files ? .[ 0 ]
? "Select a macOS SDK archive for this Darwin CGO build."
: "Confirm that you have permission to use the uploaded macOS SDK." );
macosSdkFileInput ? . focus ();
return ;
}
2026-03-05 00:04:26 -06:00
const rawServerList = form . querySelector ( "#raw-server-list" ) ? . checked || false ;
2026-05-17 13:31:47 -05:00
const serverUrlRaw = form . querySelector ( "#server-url" ). value . trim ();
const serverUrl = rawServerList ? serverUrlRaw : stripServerUrlPrefix ( serverUrlRaw );
2026-08-18 00:12:36 -05:00
if ( isLocalServerAddress ( serverUrlRaw ) && ! confirm (
"WARNING: LOCAL SERVER ADDRESS\n\nThis agent will not be able to connect from outside this machine or local network. Outbound connections from remote networks will fail.\n\nBuild with this local address anyway?"
)) {
serverUrlInput ? . focus ();
return ;
}
2026-03-05 00:04:26 -06:00
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 ;
2026-07-11 14:55:26 -05:00
const enableNvenc = form . querySelector ( 'input[name="enable-nvenc"]' ) ? . checked ?? true ;
const enableAmf = form . querySelector ( 'input[name="enable-amf"]' ) ? . checked ?? true ;
2026-07-11 16:52:24 -05:00
const enableQsv = form . querySelector ( 'input[name="enable-qsv"]' ) ? . checked ?? true ;
2026-03-05 00:04:26 -06:00
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-04-04 20:42:54 -05:00
const hasPersistentUnixTarget = platforms . some (( p ) => p . startsWith ( "linux-" ) || p . startsWith ( "darwin-" ));
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-04-04 20:42:54 -05:00
const startupNameVal = ( hasWindowsTarget || hasPersistentUnixTarget )
2026-03-21 03:06:35 -05:00
? ( 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-05-10 17:06:39 -05:00
const enableKeylogger = form . querySelector ( 'input[name="enable-keylogger"]' ) ? . checked ?? true ;
2026-03-05 00:04:26 -06:00
2026-03-16 14:09:06 +01:00
const outputNameVal = form . querySelector ( "#output-name" ) ? . value . trim () || "" ;
2026-07-08 22:13:29 -05:00
const initialClientTagVal = form . querySelector ( "#initial-client-tag" ) ? . value . trim () || "" ;
2026-03-16 14:09:06 +01:00
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 ,
2026-08-19 09:54:52 -05:00
buildProvider : buildProviderSelect ? . value || undefined ,
2026-03-05 00:04:26 -06:00
serverUrl : serverUrl || undefined ,
rawServerList ,
2026-04-05 18:18:40 -05:00
solMemo : document . getElementById ( "sol-memo" ) ? . checked || false ,
solAddress : document . getElementById ( "sol-address" ) ? . value . trim () || undefined ,
solRpcEndpoints : document . getElementById ( "sol-rpc-endpoints" ) ? . value . trim () || undefined ,
2026-03-05 00:04:26 -06:00
mutex : disableMutex ? "" : mutex || undefined ,
disableMutex ,
stripDebug ,
disableCgo ,
2026-07-11 14:55:26 -05:00
enableNvenc ,
enableAmf ,
2026-07-11 16:52:24 -05:00
enableQsv ,
2026-03-05 00:04:26 -06:00
obfuscate ,
enablePersistence ,
2026-03-26 11:05:04 -05:00
persistenceMethods : enablePersistence && hasWindowsTarget ? ( persistenceMethods && persistenceMethods . length > 0 ? persistenceMethods : [ 'startup' ]) : undefined ,
2026-04-04 20:42:54 -05:00
startupName : enablePersistence && ( hasWindowsTarget || hasPersistentUnixTarget ) && startupNameVal ? startupNameVal : undefined ,
2026-03-05 00:04:26 -06:00
hideConsole ,
noPrinting ,
2026-05-10 17:06:39 -05:00
disableKeylogger : ! enableKeylogger ,
2026-05-13 13:44:33 -05:00
enableWebrtc : form . querySelector ( 'input[name="enable-webrtc"]' ) ? . checked || false ,
2026-07-19 17:16:35 -05:00
promptWebrtcFirewallOnStart : form . querySelector ( 'input[name="enable-webrtc"]' ) ? . checked === true && ( form . querySelector ( 'input[name="prompt-webrtc-firewall-on-start"]' ) ? . checked || false ),
2026-06-06 23:34:44 -05:00
enableWinRE : form . querySelector ( 'input[name="enable-winre"]' ) ? . checked || false ,
2026-05-14 19:56:50 -05:00
fetchPublicIP : form . querySelector ( 'input[name="fetch-public-ip"]' ) ? . checked || false ,
2026-03-16 14:09:06 +01:00
outputName : outputNameVal || undefined ,
2026-07-08 22:13:29 -05:00
initialClientTag : initialClientTagVal || undefined ,
2026-03-16 14:09:06 +01:00
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-04-19 15:07:44 -05:00
iosBundleId : platforms . some ( p => p . startsWith ( 'ios-' )) ? ( form . querySelector ( "#ios-bundle-id" ) ? . value . trim () || undefined ) : undefined ,
2026-05-13 23:55:51 -04:00
useDonut : document . getElementById ( "donut-mode" ) ? . checked || false ,
2026-07-14 16:30:00 -05:00
donutSingleThreaded : document . getElementById ( "donut-single-threaded" ) ? . checked || false ,
donutExitMode : parseInt ( document . getElementById ( "donut-exit-mode" ) ? . value , 10 ) || undefined ,
donutEntropy : parseInt ( document . getElementById ( "donut-entropy" ) ? . value , 10 ) || undefined ,
donutPreserveHeaders : document . getElementById ( "donut-preserve-headers" ) ? . checked || false ,
donutResumeOffset : parseInt ( document . getElementById ( "donut-resume-offset" ) ? . value , 10 ) || undefined ,
2026-05-13 23:55:51 -04:00
useLinuxShellcode : document . getElementById ( "linux-shellcode-mode" ) ? . checked || false ,
shellcodeConsole : document . getElementById ( "shellcode-console" ) ? . checked || false ,
2026-05-17 13:11:21 -05:00
useSgn : document . getElementById ( "sgn-mode" ) ? . checked || false ,
sgnIterations : parseInt ( document . getElementById ( "sgn-iterations" ) ? . value , 10 ) || 1 ,
2026-06-10 19:54:18 -05:00
outputSgnTxt : pendingSgnTxtBuild ,
2026-05-17 19:49:50 -05:00
uploadToFileShare : pendingUpload ,
2026-06-22 12:54:12 -05:00
buildPlugins : collectBuildPluginSettings (),
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-' ),
);
2026-04-19 15:07:44 -05:00
const hasIos = platforms . some ( p => p . startsWith ( 'ios-' ));
2026-03-10 18:27:45 -05:00
2026-04-19 15:07:44 -05:00
if ( hasAndroid || hasBsd || hasIos ) {
let warningText = 'WARNING: Some selected targets are experimental/untested.\n\n' ;
2026-03-10 18:27:45 -05:00
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' ;
}
2026-04-19 15:07:44 -05:00
if ( hasIos ) {
warningText += '- iOS targets are experimental (POC). Most features will be stubbed. Output will be packaged as IPA if possible.\n' ;
}
2026-03-10 18:27:45 -05:00
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-06-22 12:54:12 -05:00
const pluginRequirementErrors = validateBuildPluginRequirements ();
if ( pluginRequirementErrors . length > 0 ) {
alert ( `Build plugin requirements are not met:\n\n ${ pluginRequirementErrors . map (( msg ) => `- ${ msg } ` ). join ( "\n" ) } ` );
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-05-17 19:49:50 -05:00
if ( buildUploadBtn ) {
buildUploadBtn . disabled = true ;
buildUploadBtn . innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> <span>Building...</span>' ;
}
2026-06-10 19:54:18 -05:00
if ( buildSgnTxtBtn ) {
buildSgnTxtBtn . disabled = true ;
setSgnTxtButtonState ( "Building TXT..." , "fa-solid fa-spinner fa-spin" );
}
2026-03-05 00:04:26 -06:00
2026-08-01 02:11:52 -05:00
revealElement ( buildStatus , { duration : 220 , offset : 6 });
2026-03-05 00:04:26 -06:00
buildStatusText . textContent = "Starting build..." ;
2026-08-19 09:54:52 -05:00
updateBuildProgress ( null );
2026-07-19 17:16:35 -05:00
buildStatus . querySelector ( "div" ). className =
"flex items-center gap-2 p-3 rounded-lg bg-blue-900/40 border border-blue-700/60" ;
buildStatus . querySelector ( "i" ). className = "fa-solid fa-spinner fa-spin" ;
2026-03-05 00:04:26 -06:00
buildResults . classList . add ( "hidden" );
buildFilesDiv . innerHTML = "" ;
2026-08-01 02:27:35 -05:00
resetBuildLog ();
setBuildLogFollow ( true );
2026-03-05 00:04:26 -06:00
addBuildOutput ( "Starting build process...\n" , "info" );
try {
2026-07-15 23:26:41 -05:00
if ( needsMacosSdkUpload ()) {
buildStatusText . textContent = "Uploading macOS SDK..." ;
addBuildOutput ( "Uploading user-provided macOS SDK...\n" , "info" );
config . macosSdkUploadId = await uploadMacosSdk ();
}
2026-03-05 00:04:26 -06:00
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-08-19 09:54:52 -05:00
if ( ! config . buildProvider && ! config . disableCgo ) {
2026-03-06 12:43:42 -06:00
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" ;
2026-08-19 09:54:52 -05:00
updateBuildProgress ( null );
2026-03-05 00:04:26 -06:00
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-05-17 19:49:50 -05:00
if ( buildUploadBtn ) {
buildUploadBtn . disabled = false ;
buildUploadBtn . innerHTML = '<i class="fa-solid fa-cloud-arrow-up"></i> <span>Build & Upload</span>' ;
}
2026-06-10 19:54:18 -05:00
if ( buildSgnTxtBtn ) {
buildSgnTxtBtn . disabled = false ;
updateSgnTxtLockState ();
}
2026-05-17 19:49:50 -05:00
pendingUpload = false ;
2026-06-10 19:54:18 -05:00
pendingSgnTxtBuild = false ;
2026-03-05 00:04:26 -06:00
}
}
2026-04-14 10:35:33 -05:00
async function checkBuildInfo ( buildId ) {
2026-03-05 00:04:26 -06:00
try {
2026-04-14 10:35:33 -05:00
const res = await fetch ( `/api/build/ ${ buildId } /info` , { credentials : "include" });
if ( ! res . ok ) return null ;
return await res . json ();
} catch {
return null ;
}
}
2026-03-05 00:04:26 -06:00
2026-04-14 10:35:33 -05:00
async function streamBuildOutput ( buildId , config = {}) {
const MAX_RECONNECT_ATTEMPTS = 10 ;
const RECONNECT_DELAY_MS = 2000 ;
let attempts = 0 ;
let completed = false ;
2026-05-17 19:49:50 -05:00
const uploadedShareFiles = [];
2026-03-05 00:04:26 -06:00
2026-04-14 10:35:33 -05:00
while ( ! completed && attempts <= MAX_RECONNECT_ATTEMPTS ) {
if ( attempts > 0 ) {
addBuildOutput ( `\nReconnecting to build stream (attempt ${ attempts } / ${ MAX_RECONNECT_ATTEMPTS } )...\n` , "warn" );
buildStatusText . textContent = "Reconnecting to build stream..." ;
await new Promise (( r ) => setTimeout ( r , RECONNECT_DELAY_MS ));
2026-03-05 00:04:26 -06:00
2026-04-14 10:35:33 -05:00
const info = await checkBuildInfo ( buildId );
if ( info && ( info . status === "completed" || info . status === "success" )) {
buildStatusText . textContent = "Build completed successfully!" ;
buildStatus . querySelector ( "div" ). className =
"flex items-center gap-2 p-3 rounded-lg bg-green-900/40 border border-green-700/60" ;
buildStatus . querySelector ( "i" ). className = "fa-solid fa-circle-check" ;
addBuildOutput ( "\nBuild completed while reconnecting.\n" , "success" );
2026-03-05 00:04:26 -06:00
2026-04-14 10:35:33 -05:00
if ( info . files && info . files . length > 0 ) {
const buildData = {
id : info . id || buildId ,
status : "success" ,
startTime : info . startTime || Date . now (),
expiresAt : info . expiresAt ,
files : info . files ,
};
saveBuildToStorage ( buildData . id , buildData );
2026-08-01 02:11:52 -05:00
revealBuildResults ();
2026-04-14 10:35:33 -05:00
displayBuild ( buildData );
2026-03-05 00:04:26 -06:00
2026-04-14 10:35:33 -05:00
if ( pendingUpdateAll ) {
pendingUpdateAll = false ;
await pushUpdateToAllClients ( buildData . id , pendingUpdateHideWindow );
2026-03-05 00:04:26 -06:00
}
}
2026-04-14 10:35:33 -05:00
return ;
} else if ( info && info . status === "failed" ) {
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" ;
addBuildOutput ( "\nBuild failed while reconnecting.\n" , "error" );
return ;
2026-03-05 00:04:26 -06:00
}
2026-04-14 10:35:33 -05:00
// status is still "running", reconnect to stream
}
2026-03-05 00:04:26 -06:00
2026-04-14 10:35:33 -05:00
let res ;
try {
res = await fetch ( `/api/build/ ${ buildId } /stream` , { credentials : "include" });
} catch ( err ) {
attempts ++ ;
continue ;
}
if ( ! res . ok ) {
if ( attempts > 0 ) {
const info = await checkBuildInfo ( buildId );
if ( info && ( info . status === "completed" || info . status === "success" )) {
buildStatusText . textContent = "Build completed successfully!" ;
buildStatus . querySelector ( "div" ). className =
"flex items-center gap-2 p-3 rounded-lg bg-green-900/40 border border-green-700/60" ;
buildStatus . querySelector ( "i" ). className = "fa-solid fa-circle-check" ;
if ( info . files && info . files . length > 0 ) {
const buildData = {
id : info . id || buildId ,
status : "success" ,
startTime : info . startTime || Date . now (),
expiresAt : info . expiresAt ,
files : info . files ,
};
saveBuildToStorage ( buildData . id , buildData );
2026-08-01 02:11:52 -05:00
revealBuildResults ();
2026-04-14 10:35:33 -05:00
displayBuild ( buildData );
}
return ;
}
}
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 ;
2026-08-19 09:54:52 -05:00
updateBuildProgress ( data . progress );
2026-05-17 19:49:50 -05:00
} else if ( data . type === "file_share_uploaded" ) {
uploadedShareFiles . push ({
id : data . id ,
filename : data . filename ,
platform : data . platform ,
size : data . size ,
});
2026-04-14 10:35:33 -05:00
} else if ( data . type === "complete" ) {
2026-08-19 09:54:52 -05:00
updateBuildProgress ( data . success ? 100 : null );
2026-04-14 10:35:33 -05:00
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" ;
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" ,
);
}
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 );
2026-08-01 02:11:52 -05:00
revealBuildResults ();
2026-04-14 10:35:33 -05:00
displayBuild ( buildData );
2026-05-17 19:49:50 -05:00
if ( uploadedShareFiles . length > 0 ) {
renderShareLinksPanel ( uploadedShareFiles );
}
2026-04-14 10:35:33 -05:00
// Auto-push update if "Build & Update All" was used
if ( pendingUpdateAll ) {
pendingUpdateAll = false ;
await pushUpdateToAllClients ( data . buildId , pendingUpdateHideWindow );
}
}
reader . cancel ();
completed = true ;
return ;
} else if ( data . type === "error" ) {
addBuildOutput ( `\nERROR: ${ data . error } \n` , "error" );
}
}
}
}
// Stream ended cleanly without a "complete" event — build may still be running
completed = true ;
} catch ( streamErr ) {
// Network error — try to reconnect
try { reader . releaseLock (); } catch {}
attempts ++ ;
continue ;
} finally {
try { reader . releaseLock (); } catch {}
}
}
if ( ! completed && attempts > MAX_RECONNECT_ATTEMPTS ) {
const info = await checkBuildInfo ( buildId );
if ( info && ( info . status === "completed" || info . status === "success" )) {
buildStatusText . textContent = "Build completed successfully!" ;
buildStatus . querySelector ( "div" ). className =
"flex items-center gap-2 p-3 rounded-lg bg-green-900/40 border border-green-700/60" ;
buildStatus . querySelector ( "i" ). className = "fa-solid fa-circle-check" ;
addBuildOutput ( "\nBuild completed (recovered after stream loss).\n" , "success" );
if ( info . files && info . files . length > 0 ) {
const buildData = {
id : info . id || buildId ,
status : "success" ,
startTime : info . startTime || Date . now (),
expiresAt : info . expiresAt ,
files : info . files ,
};
saveBuildToStorage ( buildData . id , buildData );
2026-08-01 02:11:52 -05:00
revealBuildResults ();
2026-04-14 10:35:33 -05:00
displayBuild ( buildData );
}
} else {
addBuildOutput ( "\nLost connection to build stream. The build may still be running on the server.\n" , "warn" );
addBuildOutput ( "Refresh the page to check build results.\n" , "warn" );
2026-03-05 00:04:26 -06:00
}
}
}
function addBuildOutput ( text , level = "info" ) {
2026-08-01 02:27:35 -05:00
const rawText = String ( text ?? "" );
if ( ! rawText ) return ;
buildLogRawChunks . push ( rawText );
const normalized = rawText . replace ( /\r\n?/g , "\n" );
normalized . split ( "\n" ). forEach (( line ) => appendBuildLogLine ( line , level ));
updateBuildLogSummary ();
2026-03-05 00:04:26 -06:00
2026-08-01 02:27:35 -05:00
if ( buildLogFollow && buildOutputContainer ) {
requestAnimationFrame (() => {
buildOutputContainer . scrollTop = buildOutputContainer . scrollHeight ;
});
2026-03-05 00:04:26 -06:00
}
}
2026-05-17 19:49:50 -05:00
function renderShareLinksPanel ( items ) {
const existing = document . getElementById ( "build-share-links" );
if ( existing ) existing . remove ();
const panel = document . createElement ( "div" );
panel . id = "build-share-links" ;
panel . className =
"mt-3 p-3 bg-emerald-950/30 border border-emerald-700/50 rounded-lg space-y-2" ;
const header = document . createElement ( "div" );
header . className = "flex items-center gap-2 text-sm font-medium text-emerald-300" ;
header . innerHTML =
'<i class="fa-solid fa-cloud-arrow-up"></i><span>Uploaded to File Share</span>' +
`<span class="text-xs text-slate-400 font-normal">( ${ items . length } )</span>` ;
panel . appendChild ( header );
for ( const item of items ) {
const url = ` ${ window . location . origin } /api/file-share/ ${ item . id } /download` ;
const row = document . createElement ( "div" );
row . className = "flex items-center gap-2 text-xs" ;
const label = document . createElement ( "span" );
label . className = "text-slate-400 shrink-0" ;
label . textContent = item . platform || item . filename ;
const input = document . createElement ( "input" );
input . type = "text" ;
input . readOnly = true ;
input . value = url ;
input . className =
"flex-1 bg-slate-900 border border-slate-700 rounded px-2 py-1 text-slate-200 font-mono select-all" ;
input . addEventListener ( "focus" , () => input . select ());
const copyBtn = document . createElement ( "button" );
copyBtn . type = "button" ;
copyBtn . className =
"px-2 py-1 rounded bg-emerald-700 hover:bg-emerald-600 text-white text-xs flex items-center gap-1 shrink-0" ;
copyBtn . innerHTML = '<i class="fa-solid fa-copy"></i><span>Copy</span>' ;
copyBtn . addEventListener ( "click" , async () => {
try {
await navigator . clipboard . writeText ( url );
copyBtn . innerHTML = '<i class="fa-solid fa-check"></i><span>Copied</span>' ;
if ( window . showToast ) window . showToast ( "Download link copied to clipboard" , "success" );
setTimeout (() => {
copyBtn . innerHTML = '<i class="fa-solid fa-copy"></i><span>Copy</span>' ;
}, 1500 );
} catch {
input . select ();
document . execCommand ? .( "copy" );
}
});
row . appendChild ( label );
row . appendChild ( input );
row . appendChild ( copyBtn );
panel . appendChild ( row );
}
buildFilesDiv . appendChild ( panel );
}
2026-03-05 00:04:26 -06:00
function showBuildFiles ( files , buildId , expiresAt ) {
2026-08-01 02:11:52 -05:00
revealBuildResults ();
2026-03-05 00:04:26 -06:00
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" ;
2026-07-13 21:54:21 -05:00
timer . dataset . controller = "countdown" ;
timer . dataset . countdownExpiresAtValue = String ( expiresAt );
2026-03-05 00:04:26 -06:00
timer . textContent = "Calculating..." ;
right . appendChild ( clockIcon );
right . appendChild ( expiresLabel );
right . appendChild ( timer );
infoRow . appendChild ( left );
infoRow . appendChild ( right );
buildInfoDiv . appendChild ( infoRow );
buildFilesDiv . appendChild ( buildInfoDiv );
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 ) } ` ;
2026-07-13 19:30:03 -05:00
downloadBtn . download = "" ;
2026-03-05 00:04:26 -06:00
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 );
});
}
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 ;
2026-05-17 19:49:50 -05:00
let pendingUpload = false ;
const buildUploadBtn = document . getElementById ( "build-upload-btn" );
2026-06-10 19:54:18 -05:00
let pendingSgnTxtBuild = false ;
const buildSgnTxtBtn = document . getElementById ( "build-sgn-txt-btn" );
const buildSgnTxtLockbox = document . getElementById ( "build-sgn-txt-lockbox" );
const SGN_TXT_UNLOCK_CLICKS = 10 ;
let sgnTxtUnlockClicks = 0 ;
let sgnTxtUnlocked = false ;
function getSgnTxtUnlockStorageKey () {
const userPart = String ( currentUsername || "unknown" ). trim (). toLowerCase () || "unknown" ;
return `overlord_sgn_txt_unlocked: ${ userPart } ` ;
}
function loadSgnTxtUnlockState () {
try {
if ( localStorage . getItem ( getSgnTxtUnlockStorageKey ()) === "true" ) {
sgnTxtUnlockClicks = SGN_TXT_UNLOCK_CLICKS ;
sgnTxtUnlocked = true ;
}
} catch {}
updateSgnTxtLockState ();
}
function saveSgnTxtUnlockState () {
try {
localStorage . setItem ( getSgnTxtUnlockStorageKey (), "true" );
} catch {}
}
function setSgnTxtButtonState ( label , iconClass ) {
if ( ! buildSgnTxtBtn ) return ;
buildSgnTxtBtn . innerHTML = "" ;
const icon = document . createElement ( "i" );
icon . className = iconClass ;
const text = document . createElement ( "span" );
text . textContent = label ;
buildSgnTxtBtn . appendChild ( icon );
buildSgnTxtBtn . appendChild ( text );
}
function pulseSgnTxtLockbox () {
if ( ! buildSgnTxtLockbox ) return ;
buildSgnTxtLockbox . classList . remove ( "pulse" );
void buildSgnTxtLockbox . offsetWidth ;
buildSgnTxtLockbox . classList . add ( "pulse" );
}
function updateSgnTxtLockState () {
if ( ! buildSgnTxtBtn || ! buildSgnTxtLockbox ) return ;
const progress = Math . min ( 1 , Math . max ( 0 , sgnTxtUnlockClicks / SGN_TXT_UNLOCK_CLICKS ));
const stageProgress = ( start , end ) => Math . min ( 1 , Math . max ( 0 , ( progress - start ) / ( end - start )));
const crackProgress = Math . min ( 1 , Math . sqrt ( progress ) * 1.18 );
const midCrackProgress = Math . sqrt ( stageProgress ( 0.18 , 0.72 ));
const lateCrackProgress = Math . sqrt ( stageProgress ( 0.48 , 0.95 ));
buildSgnTxtLockbox . style . setProperty ( "--sgn-chain-brightness" , String ( Math . max ( 0.72 , 1 - progress * 0.28 )));
buildSgnTxtLockbox . style . setProperty ( "--sgn-chain-opacity" , String ( Math . max ( 0.48 , 0.9 - progress * 0.42 )));
buildSgnTxtLockbox . style . setProperty ( "--sgn-crack-opacity" , String ( Math . max ( 0 , Math . min ( 1 , crackProgress ))));
buildSgnTxtLockbox . style . setProperty ( "--sgn-crack-dash" , String ( Math . max ( 0 , 1 - crackProgress )));
buildSgnTxtLockbox . style . setProperty ( "--sgn-crack-mid-opacity" , String ( midCrackProgress * 0.9 ));
buildSgnTxtLockbox . style . setProperty ( "--sgn-crack-mid-dash" , String ( Math . max ( 0 , 1 - midCrackProgress )));
buildSgnTxtLockbox . style . setProperty ( "--sgn-crack-late-opacity" , String ( lateCrackProgress * 0.95 ));
buildSgnTxtLockbox . style . setProperty ( "--sgn-crack-late-dash" , String ( Math . max ( 0 , 1 - lateCrackProgress )));
buildSgnTxtLockbox . style . setProperty ( "--sgn-lock-tilt" , ` ${ Math . round ( progress * 10 ) } deg` );
buildSgnTxtLockbox . classList . toggle ( "locked" , ! sgnTxtUnlocked );
buildSgnTxtLockbox . classList . toggle ( "unlocked" , sgnTxtUnlocked );
if ( sgnTxtUnlocked ) {
setSgnTxtButtonState ( "Build SGN TXT" , "fa-solid fa-file-lines" );
return ;
}
setSgnTxtButtonState ( "Build SGN TXT" , "fa-solid fa-file-lines" );
}
updateSgnTxtLockState ();
2026-03-29 13:39:51 -05:00
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 (() => ({}));
2026-08-24 09:35:03 -05:00
updateAllModalBody . innerHTML = `<p class="text-red-400"><i class="fa-solid fa-circle-xmark mr-2"></i> ${ escapeHtml ( data . error || "Failed to check eligible clients" ) } </p>` ;
2026-03-29 13:39:51 -05:00
return ;
}
const data = await res . json ();
2026-08-24 09:35:03 -05:00
const eligible = Math . max ( 0 , Math . trunc ( Number ( data . eligible ) || 0 ));
const totalOnline = Math . max ( 0 , Math . trunc ( Number ( data . totalOnline ) || 0 ));
const skippedInMemory = Math . max ( 0 , Math . trunc ( Number ( data . skippedInMemory ) || 0 ));
const skippedNoMatch = Math . max ( 0 , Math . trunc ( Number ( data . skippedNoMatch ) || 0 ));
2026-03-29 13:39:51 -05:00
let html = "" ;
2026-08-24 09:35:03 -05:00
html += `<p class="text-white"><i class="fa-solid fa-users mr-2 text-amber-400"></i><strong> ${ eligible } </strong> client(s) will receive the update after build.</p>` ;
2026-03-29 13:39:51 -05:00
html += '<div class="mt-2 text-xs text-slate-400 space-y-1">' ;
2026-08-24 09:35:03 -05:00
html += `<p><i class="fa-solid fa-globe mr-1 text-blue-400"></i> ${ totalOnline } total online client(s)</p>` ;
if ( skippedInMemory > 0 ) {
html += `<p><i class="fa-solid fa-memory mr-1 text-red-400"></i> ${ skippedInMemory } client(s) will be skipped (running in-memory)</p>` ;
2026-03-29 13:39:51 -05:00
}
2026-08-24 09:35:03 -05:00
if ( skippedNoMatch > 0 ) {
html += `<p><i class="fa-solid fa-ban mr-1 text-slate-500"></i> ${ skippedNoMatch } client(s) will be skipped (no matching platform)</p>` ;
2026-03-29 13:39:51 -05:00
}
html += "</div>" ;
2026-08-24 09:35:03 -05:00
if ( eligible === 0 && totalOnline === 0 ) {
2026-03-29 13:39:51 -05:00
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 ) {
2026-08-24 09:35:03 -05:00
const message = err instanceof Error ? err . message : String ( err || "Unknown error" );
updateAllModalBody . innerHTML = `<p class="text-red-400"><i class="fa-solid fa-circle-xmark mr-2"></i>Error: ${ escapeHtml ( message ) } </p>` ;
2026-03-29 13:39:51 -05:00
}
});
}
if ( updateAllConfirm ) {
updateAllConfirm . addEventListener ( "click" , () => {
hideUpdateAllModal ();
pendingUpdateAll = true ;
pendingUpdateHideWindow = !! form . querySelector ( 'input[name="hide-console"]' ) ? . checked ;
form . requestSubmit ();
});
}
2026-05-17 19:49:50 -05:00
if ( buildUploadBtn ) {
buildUploadBtn . addEventListener ( "click" , () => {
if ( isBuilding ) return ;
const platformCheckboxes = form . querySelectorAll ( 'input[name="platform"]:checked' );
if ( platformCheckboxes . length === 0 ) {
alert ( "Please select at least one platform to build" );
return ;
}
pendingUpload = true ;
form . requestSubmit ();
});
}
2026-06-10 19:54:18 -05:00
if ( buildSgnTxtBtn ) {
buildSgnTxtBtn . addEventListener ( "click" , () => {
if ( isBuilding ) return ;
const sgnEnabled = document . getElementById ( "sgn-mode" ) ? . checked || false ;
if ( ! sgnTxtUnlocked ) {
sgnTxtUnlockClicks += 1 ;
if ( sgnTxtUnlockClicks >= SGN_TXT_UNLOCK_CLICKS ) {
sgnTxtUnlocked = true ;
saveSgnTxtUnlockState ();
updateSgnTxtLockState ();
if ( window . showToast ) window . showToast ( "SGN TXT build unlocked" , "success" );
} else {
updateSgnTxtLockState ();
}
pulseSgnTxtLockbox ();
return ;
}
if ( ! sgnEnabled ) {
alert ( "Enable the SGN Polymorphic Encoding option before building an SGN TXT file." );
return ;
}
const platformCheckboxes = form . querySelectorAll ( 'input[name="platform"]:checked' );
if ( platformCheckboxes . length === 0 ) {
alert ( "Please select at least one platform to build" );
return ;
}
pendingSgnTxtBuild = true ;
form . requestSubmit ();
});
}
2026-03-29 13:39:51 -05:00
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" );
}
}