better build menu
This commit is contained in:
@@ -15,6 +15,12 @@ const buildStatus = document.getElementById("build-status");
|
|||||||
const buildStatusText = document.getElementById("build-status-text");
|
const buildStatusText = document.getElementById("build-status-text");
|
||||||
const buildOutputDiv = document.getElementById("build-output");
|
const buildOutputDiv = document.getElementById("build-output");
|
||||||
const buildOutputContainer = document.getElementById("build-output-container");
|
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 buildResults = document.getElementById("build-results");
|
||||||
const buildFilesDiv = document.getElementById("build-files");
|
const buildFilesDiv = document.getElementById("build-files");
|
||||||
const logoutBtn = document.getElementById("logout-btn");
|
const logoutBtn = document.getElementById("logout-btn");
|
||||||
@@ -199,6 +205,147 @@ function updateServerUrlCurrentButton() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let isBuilding = false;
|
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
|
||||||
|
? '<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();
|
||||||
|
|
||||||
function revealBuildResults() {
|
function revealBuildResults() {
|
||||||
if (buildResults.classList.contains("hidden")) {
|
if (buildResults.classList.contains("hidden")) {
|
||||||
@@ -2053,7 +2200,8 @@ async function startBuild(config) {
|
|||||||
buildResults.classList.add("hidden");
|
buildResults.classList.add("hidden");
|
||||||
buildFilesDiv.innerHTML = "";
|
buildFilesDiv.innerHTML = "";
|
||||||
|
|
||||||
buildOutputDiv.innerHTML = "";
|
resetBuildLog();
|
||||||
|
setBuildLogFollow(true);
|
||||||
addBuildOutput("Starting build process...\n", "info");
|
addBuildOutput("Starting build process...\n", "info");
|
||||||
|
|
||||||
try {
|
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
|
// Stream ended cleanly without a "complete" event — build may still be running
|
||||||
completed = true;
|
completed = true;
|
||||||
@@ -2340,20 +2487,18 @@ async function streamBuildOutput(buildId, config = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addBuildOutput(text, level = "info") {
|
function addBuildOutput(text, level = "info") {
|
||||||
const span = document.createElement("span");
|
const rawText = String(text ?? "");
|
||||||
span.textContent = 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") {
|
if (buildLogFollow && buildOutputContainer) {
|
||||||
span.className = "text-red-400";
|
requestAnimationFrame(() => {
|
||||||
} else if (level === "success") {
|
buildOutputContainer.scrollTop = buildOutputContainer.scrollHeight;
|
||||||
span.className = "text-green-400";
|
});
|
||||||
} else if (level === "warn") {
|
|
||||||
span.className = "text-yellow-400";
|
|
||||||
} else {
|
|
||||||
span.className = "text-slate-300";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildOutputDiv.appendChild(span);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderShareLinksPanel(items) {
|
function renderShareLinksPanel(items) {
|
||||||
|
|||||||
@@ -79,6 +79,78 @@
|
|||||||
background: linear-gradient(135deg, rgba(37, 99, 235, 0.95), rgba(79, 70, 229, 0.9));
|
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);
|
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; }
|
[data-builder-panel][hidden] { display: none !important; }
|
||||||
.builder-profile-bar {
|
.builder-profile-bar {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -1244,10 +1316,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="build-output-container"
|
<div class="build-console">
|
||||||
class="bg-slate-950 border border-slate-700 rounded-lg p-4 min-h-[28rem] max-h-[calc(100vh-20rem)] overflow-y-auto font-mono text-sm">
|
<div class="build-console-toolbar" aria-label="Build output controls">
|
||||||
<div id="build-output" class="text-slate-400">
|
<div class="build-console-group" role="group" aria-label="Filter build output">
|
||||||
Ready to build. Select platforms and click "Start Build".
|
<button type="button" class="build-console-control" data-build-log-filter="all" aria-pressed="true">All</button>
|
||||||
|
<button type="button" class="build-console-control" data-build-log-filter="important" aria-pressed="false">Important</button>
|
||||||
|
<button type="button" class="build-console-control" data-build-log-filter="issues" aria-pressed="false">
|
||||||
|
Issues <span id="build-log-issue-count" class="text-amber-300">0</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="build-console-group">
|
||||||
|
<button id="build-log-wrap-btn" type="button" class="build-console-control" aria-pressed="false" title="Wrap long lines">
|
||||||
|
<i class="fa-solid fa-align-left"></i><span>Wrap</span>
|
||||||
|
</button>
|
||||||
|
<button id="build-log-follow-btn" type="button" class="build-console-control" aria-pressed="true" title="Follow new output">
|
||||||
|
<i class="fa-solid fa-arrow-down"></i><span>Follow</span>
|
||||||
|
</button>
|
||||||
|
<button id="build-log-copy-btn" type="button" class="build-console-control" title="Copy full output">
|
||||||
|
<i class="fa-regular fa-copy"></i><span class="sr-only">Copy output</span>
|
||||||
|
</button>
|
||||||
|
<button id="build-log-clear-btn" type="button" class="build-console-control" title="Clear output">
|
||||||
|
<i class="fa-solid fa-eraser"></i><span class="sr-only">Clear output</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="build-output-container">
|
||||||
|
<div id="build-output" class="text-slate-400" data-filter="all" role="log" aria-live="polite" aria-relevant="additions">
|
||||||
|
<div class="build-log-empty">Ready to build. Select platforms and click “Start Build”.</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user