328 lines
11 KiB
JavaScript
328 lines
11 KiB
JavaScript
export 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 formatStubVersion(version) {
|
||
|
|
if (typeof version === "string" && version.trim()) {
|
||
|
|
return version.trim();
|
||
|
|
}
|
||
|
|
return "unknown (legacy build)";
|
||
|
|
}
|
||
|
|
|
||
|
|
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 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);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export 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";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export function createBuildHistoryManager({
|
||
|
|
buildResults,
|
||
|
|
buildFilesDiv,
|
||
|
|
getCurrentServerVersion,
|
||
|
|
getCurrentUserRole,
|
||
|
|
getShowAllBuilds,
|
||
|
|
}) {
|
||
|
|
function isVersionMismatch(versionValue) {
|
||
|
|
const currentServerVersion = getCurrentServerVersion();
|
||
|
|
if (!currentServerVersion) return false;
|
||
|
|
return versionValue !== currentServerVersion;
|
||
|
|
}
|
||
|
|
|
||
|
|
async function deleteBuild(buildId) {
|
||
|
|
if (!confirm("Are you sure you want to delete this build?")) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
const res = await fetch(`/api/build/${encodeURIComponent(buildId)}/delete`, {
|
||
|
|
method: "DELETE",
|
||
|
|
credentials: "include",
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!res.ok) {
|
||
|
|
const data = await res.json().catch(() => ({}));
|
||
|
|
throw new Error(data.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.");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function loadSavedBuilds() {
|
||
|
|
try {
|
||
|
|
const queryParam =
|
||
|
|
getShowAllBuilds() && getCurrentUserRole() === "admin" ? "?all=true" : "";
|
||
|
|
const res = await fetch(`/api/build/list${queryParam}`, {
|
||
|
|
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";
|
||
|
|
const versionValue = formatStubVersion(file.version);
|
||
|
|
const platformText = document.createElement("span");
|
||
|
|
platformText.textContent = `${file.platform} | `;
|
||
|
|
const versionText = document.createElement("span");
|
||
|
|
versionText.className = isVersionMismatch(versionValue)
|
||
|
|
? "server-version-number-mismatch"
|
||
|
|
: "server-version-number";
|
||
|
|
versionText.textContent =
|
||
|
|
versionValue === "unknown (legacy build)" ? versionValue : `v${versionValue}`;
|
||
|
|
filePlatform.appendChild(platformText);
|
||
|
|
filePlatform.appendChild(versionText);
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
displayBuild,
|
||
|
|
loadSavedBuilds,
|
||
|
|
saveBuildToStorage,
|
||
|
|
};
|
||
|
|
}
|