From fe3818775422a17db0d5301cf4e3112a2a8413c1 Mon Sep 17 00:00:00 2001 From: KingKDot Date: Sat, 1 Aug 2026 02:27:35 -0500 Subject: [PATCH] better build menu --- Overlord-Server/public/assets/build.js | 173 +++++++++++++++++++++++-- Overlord-Server/public/build.html | 104 ++++++++++++++- 2 files changed, 259 insertions(+), 18 deletions(-) diff --git a/Overlord-Server/public/assets/build.js b/Overlord-Server/public/assets/build.js index 6f02b8a..951d95e 100644 --- a/Overlord-Server/public/assets/build.js +++ b/Overlord-Server/public/assets/build.js @@ -15,6 +15,12 @@ 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 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"); const buildResults = document.getElementById("build-results"); const buildFilesDiv = document.getElementById("build-files"); const logoutBtn = document.getElementById("logout-btn"); @@ -199,6 +205,147 @@ function updateServerUrlCurrentButton() { } let isBuilding = false; +const BUILD_LOG_MAX_VISIBLE_LINES = 5000; +const BUILD_LOG_WRAP_KEY = "overlord.buildLog.wrap.v1"; +let buildLogRawChunks = []; +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 + ? '
Output cleared. New build events will appear here.
' + : ""; + } + 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 = 'Copied'; + setTimeout(() => { + buildLogCopyBtn.innerHTML = 'Copy output'; + }, 1400); + } else { + window.showToast?.("Could not copy build output", "error"); + } + }); + + buildLogClearBtn?.addEventListener("click", () => { + resetBuildLog(true); + setBuildLogFollow(true); + }); + updateBuildLogSummary(); +} + +initBuildConsole(); function revealBuildResults() { if (buildResults.classList.contains("hidden")) { @@ -2053,7 +2200,8 @@ async function startBuild(config) { buildResults.classList.add("hidden"); buildFilesDiv.innerHTML = ""; - buildOutputDiv.innerHTML = ""; + resetBuildLog(); + setBuildLogFollow(true); addBuildOutput("Starting build process...\n", "info"); try { @@ -2298,7 +2446,6 @@ async function streamBuildOutput(buildId, config = {}) { } } - buildOutputContainer.scrollTop = buildOutputContainer.scrollHeight; } // Stream ended cleanly without a "complete" event — build may still be running completed = true; @@ -2340,20 +2487,18 @@ async function streamBuildOutput(buildId, config = {}) { } function addBuildOutput(text, level = "info") { - const span = document.createElement("span"); - span.textContent = text; + 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(); - 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"; + if (buildLogFollow && buildOutputContainer) { + requestAnimationFrame(() => { + buildOutputContainer.scrollTop = buildOutputContainer.scrollHeight; + }); } - - buildOutputDiv.appendChild(span); } function renderShareLinksPanel(items) { diff --git a/Overlord-Server/public/build.html b/Overlord-Server/public/build.html index 2eecc05..1c63225 100644 --- a/Overlord-Server/public/build.html +++ b/Overlord-Server/public/build.html @@ -79,6 +79,78 @@ background: linear-gradient(135deg, rgba(37, 99, 235, 0.95), rgba(79, 70, 229, 0.9)); box-shadow: 0 8px 22px rgba(37, 99, 235, 0.22); } + .build-console { + overflow: hidden; + border: 1px solid rgb(51 65 85); + border-radius: 0.75rem; + background: rgb(2 6 23 / 0.92); + box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.025); + } + .build-console-toolbar { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 0.65rem; + padding: 0.6rem 0.75rem; + border-bottom: 1px solid rgb(30 41 59); + background: linear-gradient(180deg, rgb(15 23 42 / 0.96), rgb(8 15 30 / 0.96)); + } + .build-console-group { display: flex; align-items: center; gap: 0.35rem; } + .build-console-control { + display: inline-flex; + align-items: center; + gap: 0.35rem; + min-height: 1.9rem; + padding: 0.3rem 0.55rem; + border: 1px solid transparent; + border-radius: 0.45rem; + color: rgb(148 163 184); + font-size: 0.7rem; + font-weight: 600; + } + .build-console-control:hover { color: rgb(226 232 240); background: rgb(51 65 85 / 0.5); } + .build-console-control[aria-pressed="true"] { + color: rgb(241 245 249); + border-color: rgb(100 116 139 / 0.45); + background: rgb(51 65 85 / 0.72); + } + #build-output-container { + min-height: 28rem; + max-height: calc(100vh - 23rem); + overflow: auto; + overscroll-behavior: contain; + scrollbar-gutter: stable; + } + #build-output { min-width: max-content; padding: 0.55rem 0; } + #build-output.log-wrap { min-width: 0; } + .build-log-line { + display: grid; + grid-template-columns: 3rem minmax(0, 1fr); + min-height: 1.65rem; + border-left: 2px solid transparent; + color: rgb(148 163 184); + font-family: inherit; + font-size: 0.78rem; + line-height: 1.55; + } + .build-log-line:hover { background: rgb(30 41 59 / 0.34); } + .build-log-number { + padding: 0.15rem 0.65rem 0.15rem 0.35rem; + color: rgb(71 85 105); + text-align: right; + user-select: none; + font-family: ui-monospace, SFMono-Regular, Consolas, monospace; + } + .build-log-content { padding: 0.15rem 0.8rem 0.15rem 0; white-space: pre; } + #build-output.log-wrap .build-log-content { white-space: pre-wrap; overflow-wrap: anywhere; } + .build-log-line[data-log-level="stage"] { color: rgb(186 230 253); border-left-color: rgb(56 189 248); background: rgb(14 165 233 / 0.055); } + .build-log-line[data-log-level="success"] { color: rgb(134 239 172); border-left-color: rgb(34 197 94); background: rgb(22 163 74 / 0.06); } + .build-log-line[data-log-level="warning"] { color: rgb(253 224 71); border-left-color: rgb(234 179 8); background: rgb(202 138 4 / 0.06); } + .build-log-line[data-log-level="error"] { color: rgb(253 164 175); border-left-color: rgb(244 63 94); background: rgb(225 29 72 / 0.08); } + #build-output[data-filter="important"] .build-log-line[data-log-level="detail"], + #build-output[data-filter="issues"] .build-log-line:not([data-log-level="warning"]):not([data-log-level="error"]) { display: none; } + .build-log-empty { padding: 1.2rem; color: rgb(100 116 139); font-size: 0.78rem; } [data-builder-panel][hidden] { display: none !important; } .builder-profile-bar { display: grid; @@ -1244,10 +1316,34 @@ -
-
- Ready to build. Select platforms and click "Start Build". +
+
+
+ + + +
+
+ + + + +
+
+
+
+
Ready to build. Select platforms and click “Start Build”.
+