public site and containers for that.
Build and Publish Container / Publish Linux container (push) Failing after 1m52s
Build and Publish Container / Publish Linux container (push) Failing after 1m52s
This commit is contained in:
@@ -0,0 +1,360 @@
|
||||
const $ = (selector) => document.querySelector(selector);
|
||||
const $$ = (selector) => [...document.querySelectorAll(selector)];
|
||||
|
||||
const INTRO_MINIMUM_MS = 900;
|
||||
const REQUIRED_EXPANSIONS = {
|
||||
royalty: { name: "Royalty", art: "/art/NotOwned_Royalty.png" },
|
||||
ideology: { name: "Ideology", art: "/art/NotOwned_Ideology.png" },
|
||||
biotech: { name: "Biotech", art: "/art/NotOwned_Biotech.png" },
|
||||
anomaly: { name: "Anomaly", art: "/art/NotOwned_Anomaly.png" },
|
||||
odyssey: { name: "Odyssey", art: null }
|
||||
};
|
||||
const INSTALL_PATHS = {
|
||||
windows: {
|
||||
label: "Windows",
|
||||
path: "%USERPROFILE%\\AppData\\LocalLow\\Ludeon Studios\\RimWorld by Ludeon Studios\\ModLists"
|
||||
},
|
||||
macos: {
|
||||
label: "macOS",
|
||||
path: "~/Library/Application Support/RimWorld/ModLists"
|
||||
},
|
||||
linux: {
|
||||
label: "Linux",
|
||||
path: "~/.config/unity3d/Ludeon Studios/RimWorld by Ludeon Studios/ModLists"
|
||||
}
|
||||
};
|
||||
const state = {
|
||||
currentStep: 0,
|
||||
listType: "standard",
|
||||
sortAcknowledged: false,
|
||||
polling: null,
|
||||
rmlReady: false
|
||||
};
|
||||
|
||||
function detectedOperatingSystem() {
|
||||
const platform = String(navigator.userAgentData?.platform || navigator.platform || navigator.userAgent || "").toLowerCase();
|
||||
if (platform.includes("mac")) return "macos";
|
||||
if (platform.includes("linux") || platform.includes("x11")) return "linux";
|
||||
return "windows";
|
||||
}
|
||||
|
||||
function renderInstallLocation() {
|
||||
const install = INSTALL_PATHS[detectedOperatingSystem()];
|
||||
$("#detected-os-name").textContent = install.label;
|
||||
$("#detected-os-path").textContent = install.path;
|
||||
}
|
||||
|
||||
function resetListType() {
|
||||
const standard = $('input[name="list-type"][value="standard"]');
|
||||
const acknowledgement = $("#acknowledge-autosort");
|
||||
standard.checked = true;
|
||||
state.listType = "standard";
|
||||
state.sortAcknowledged = false;
|
||||
$("#homebrew-settings").hidden = true;
|
||||
$("#autosort-confirmation").classList.remove("acknowledged");
|
||||
acknowledgement.setAttribute("aria-pressed", "false");
|
||||
acknowledgement.textContent = "Yes, I understand";
|
||||
setMessage("", "", "#wizard-message");
|
||||
updateDownloadAvailability();
|
||||
}
|
||||
|
||||
function formatDate(value, fallback = "Not available") {
|
||||
if (!value) return fallback;
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit"
|
||||
}).format(new Date(value));
|
||||
}
|
||||
|
||||
function formatRelativeDate(value) {
|
||||
if (!value) return "Waiting for the first refresh";
|
||||
const elapsedSeconds = Math.max(0, Math.round((Date.now() - new Date(value).getTime()) / 1000));
|
||||
if (elapsedSeconds < 60) return "Updated just now";
|
||||
const minutes = Math.floor(elapsedSeconds / 60);
|
||||
if (minutes < 60) return `Updated ${minutes} minute${minutes === 1 ? "" : "s"} ago`;
|
||||
const hours = Math.floor(minutes / 60);
|
||||
if (hours < 24) return `Updated ${hours} hour${hours === 1 ? "" : "s"} ago`;
|
||||
const days = Math.floor(hours / 24);
|
||||
return `Updated ${days} day${days === 1 ? "" : "s"} ago`;
|
||||
}
|
||||
|
||||
function setMessage(text, kind = "", selector = "#action-message") {
|
||||
const element = $(selector);
|
||||
element.textContent = text;
|
||||
element.className = `${element.dataset.baseClass || element.className.split(" ")[0]} ${kind}`.trim();
|
||||
}
|
||||
|
||||
async function api(path, options = {}) {
|
||||
const response = await fetch(path, options);
|
||||
const contentType = response.headers.get("content-type") || "";
|
||||
if (!response.ok) {
|
||||
let detail = `${response.status} ${response.statusText}`;
|
||||
if (contentType.includes("json")) {
|
||||
const body = await response.json();
|
||||
detail = typeof body.detail === "string" ? body.detail : body.detail?.message || detail;
|
||||
}
|
||||
throw new Error(detail);
|
||||
}
|
||||
return contentType.includes("json") ? response.json() : response;
|
||||
}
|
||||
|
||||
function renderServiceState(data) {
|
||||
const element = $("#service-state");
|
||||
const label = element.querySelector("span:last-child");
|
||||
const total = Number(data.totals?.total_mods || 0);
|
||||
const ready = Number(data.totals?.ready_mods || 0);
|
||||
element.className = "service-state";
|
||||
|
||||
if (data.scheduler?.running) {
|
||||
element.classList.add("working");
|
||||
label.textContent = "Updating now";
|
||||
} else if (data.latest_run?.status === "failed") {
|
||||
element.classList.add("failed");
|
||||
label.textContent = "Update needs attention";
|
||||
} else if (!data.steamcmd_available) {
|
||||
element.classList.add("failed");
|
||||
label.textContent = "Downloads unavailable";
|
||||
} else if (ready < total) {
|
||||
element.classList.add("working");
|
||||
label.textContent = "Preparing RML";
|
||||
} else {
|
||||
element.classList.add("ready");
|
||||
label.textContent = "Ready";
|
||||
}
|
||||
}
|
||||
|
||||
function updateDownloadAvailability() {
|
||||
const downloadButton = $("#download-list");
|
||||
const availability = $("#rml-availability");
|
||||
const needsFile = state.listType === "homebrew" && !$("#mods-config").files[0];
|
||||
|
||||
downloadButton.disabled = !state.rmlReady || needsFile || !state.sortAcknowledged;
|
||||
if (!state.rmlReady) {
|
||||
availability.textContent = "The current RML is still being prepared. You can review every step now and download when it is ready.";
|
||||
availability.className = "availability waiting";
|
||||
} else if (needsFile) {
|
||||
availability.textContent = "Choose ModsConfig.xml to finish your homebrew list.";
|
||||
availability.className = "availability waiting";
|
||||
} else if (!state.sortAcknowledged) {
|
||||
availability.textContent = "Confirm the auto-sort step above to unlock your download.";
|
||||
availability.className = "availability waiting";
|
||||
} else {
|
||||
availability.textContent = state.listType === "homebrew"
|
||||
? "Your homebrew list is ready to build."
|
||||
: "The current Progression list is ready to download.";
|
||||
availability.className = "availability ready";
|
||||
}
|
||||
}
|
||||
|
||||
function renderDashboard(data) {
|
||||
renderServiceState(data);
|
||||
const totals = data.totals || {};
|
||||
const total = Number(totals.total_mods || 0);
|
||||
const ready = Number(totals.ready_mods || 0);
|
||||
const counts = Object.fromEntries(
|
||||
(data.collections || []).map(collection => [String(collection.name).toLowerCase(), Number(collection.item_count || 0)])
|
||||
);
|
||||
|
||||
$("#count-total").textContent = total ? total.toLocaleString() : "—";
|
||||
for (const category of ["core", "content", "cosmetics"]) {
|
||||
const formatted = counts[category]?.toLocaleString() || "—";
|
||||
$(`#count-${category}`).textContent = formatted;
|
||||
$(`[data-collection-count="${category}"]`).textContent = formatted;
|
||||
}
|
||||
|
||||
const updatedAt = data.rml_updated_at;
|
||||
$("#last-updated").textContent = data.scheduler?.running ? "Updating now" : formatRelativeDate(updatedAt);
|
||||
$("#last-updated").title = formatDate(updatedAt);
|
||||
|
||||
state.rmlReady = total > 0 && ready === total && !data.scheduler?.running;
|
||||
const nextRefresh = data.scheduler?.next_refresh_at;
|
||||
if (data.scheduler?.running) {
|
||||
$("#update-detail").textContent = "The hourly Workshop refresh is currently running.";
|
||||
} else if (!state.rmlReady) {
|
||||
$("#update-detail").textContent = "The current RML is being prepared from the latest Workshop data.";
|
||||
} else {
|
||||
$("#update-detail").textContent = nextRefresh
|
||||
? `Next automatic check ${formatDate(nextRefresh)}.`
|
||||
: "Workshop data refreshes every hour.";
|
||||
}
|
||||
updateDownloadAvailability();
|
||||
}
|
||||
|
||||
async function loadDashboard() {
|
||||
try {
|
||||
renderDashboard(await api("/api/status"));
|
||||
} catch (error) {
|
||||
const element = $("#service-state");
|
||||
element.className = "service-state failed";
|
||||
element.querySelector("span:last-child").textContent = "Backend unavailable";
|
||||
$("#last-updated").textContent = "Unavailable";
|
||||
$("#update-detail").textContent = "The service could not be reached.";
|
||||
state.rmlReady = false;
|
||||
updateDownloadAvailability();
|
||||
setMessage(error.message, "error");
|
||||
}
|
||||
}
|
||||
|
||||
function showStep(step) {
|
||||
const panels = $$(".wizard-step");
|
||||
const progressItems = $$(".wizard-progress li");
|
||||
state.currentStep = Math.max(0, Math.min(step, panels.length - 1));
|
||||
|
||||
panels.forEach((panel, index) => {
|
||||
const active = index === state.currentStep;
|
||||
panel.hidden = !active;
|
||||
panel.classList.toggle("active", active);
|
||||
});
|
||||
|
||||
progressItems.forEach((item, index) => {
|
||||
item.classList.toggle("active", index === state.currentStep);
|
||||
item.classList.toggle("complete", index < state.currentStep);
|
||||
if (index === state.currentStep) item.setAttribute("aria-current", "step");
|
||||
else item.removeAttribute("aria-current");
|
||||
});
|
||||
|
||||
if (state.currentStep === 2) updateExpansionRequirements();
|
||||
}
|
||||
|
||||
function updateExpansionRequirements() {
|
||||
const inputs = $$("#dlc-options input");
|
||||
const missing = inputs
|
||||
.filter(input => !input.checked)
|
||||
.map(input => REQUIRED_EXPANSIONS[input.value]);
|
||||
const warning = $("#expansion-warning");
|
||||
const iconList = $("#missing-expansion-icons");
|
||||
|
||||
inputs.forEach(input => {
|
||||
const isMissing = !input.checked;
|
||||
input.closest("label").classList.toggle("missing", isMissing);
|
||||
input.setAttribute("aria-invalid", String(isMissing));
|
||||
});
|
||||
|
||||
warning.hidden = missing.length === 0;
|
||||
$("#expansions-next").disabled = missing.length > 0;
|
||||
if (!missing.length) {
|
||||
iconList.replaceChildren();
|
||||
$("#expansion-warning-text").textContent = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const names = missing.map(expansion => expansion.name);
|
||||
$("#expansion-warning-text").textContent = `${names.join(", ")} ${missing.length === 1 ? "is" : "are"} required before you can continue.`;
|
||||
iconList.innerHTML = missing.map(expansion => expansion.art
|
||||
? `<img src="${expansion.art}" alt="">`
|
||||
: `<span class="missing-expansion-fallback"><b>!</b><small>ODYSSEY</small><em>REQUIRED</em></span>`
|
||||
).join("");
|
||||
}
|
||||
|
||||
function selectedDlcQuery() {
|
||||
return $$("#dlc-options input:checked")
|
||||
.map(input => `dlc=${encodeURIComponent(input.value)}`)
|
||||
.join("&");
|
||||
}
|
||||
|
||||
async function downloadResponse(responsePromise, filename) {
|
||||
const response = await responsePromise;
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
$("#start-wizard").addEventListener("click", () => {
|
||||
resetListType();
|
||||
showStep(0);
|
||||
$("#download-wizard").showModal();
|
||||
});
|
||||
|
||||
$("#close-wizard").addEventListener("click", () => $("#download-wizard").close());
|
||||
|
||||
$("#download-wizard").addEventListener("click", event => {
|
||||
if (event.target === event.currentTarget) event.currentTarget.close();
|
||||
});
|
||||
|
||||
$$('[data-next]').forEach(button => button.addEventListener("click", () => showStep(state.currentStep + 1)));
|
||||
$$('[data-back]').forEach(button => button.addEventListener("click", () => showStep(state.currentStep - 1)));
|
||||
|
||||
$("#setup-confirm").addEventListener("change", event => {
|
||||
$("#setup-next").disabled = !event.target.checked;
|
||||
});
|
||||
|
||||
$$("#dlc-options input").forEach(input => input.addEventListener("change", updateExpansionRequirements));
|
||||
|
||||
$("#show-all-paths").addEventListener("click", event => {
|
||||
const paths = $("#all-install-paths");
|
||||
const show = paths.hidden;
|
||||
paths.hidden = !show;
|
||||
event.currentTarget.setAttribute("aria-expanded", String(show));
|
||||
event.currentTarget.textContent = show ? "Hide other OS" : "Show other OS";
|
||||
});
|
||||
|
||||
$("#acknowledge-autosort").addEventListener("click", event => {
|
||||
state.sortAcknowledged = true;
|
||||
$("#autosort-confirmation").classList.add("acknowledged");
|
||||
event.currentTarget.setAttribute("aria-pressed", "true");
|
||||
event.currentTarget.textContent = "Understood ✓";
|
||||
updateDownloadAvailability();
|
||||
});
|
||||
|
||||
$$('input[name="list-type"]').forEach(input => input.addEventListener("change", event => {
|
||||
state.listType = event.target.value;
|
||||
$("#homebrew-settings").hidden = state.listType !== "homebrew";
|
||||
setMessage("", "", "#wizard-message");
|
||||
updateDownloadAvailability();
|
||||
}));
|
||||
|
||||
$("#mods-config").addEventListener("change", event => {
|
||||
const file = event.target.files[0];
|
||||
$("#file-label").textContent = file?.name || "Choose ModsConfig.xml";
|
||||
$(".file-picker").classList.toggle("has-file", Boolean(file));
|
||||
updateDownloadAvailability();
|
||||
});
|
||||
|
||||
$("#download-list").addEventListener("click", async () => {
|
||||
const button = $("#download-list");
|
||||
button.disabled = true;
|
||||
setMessage("Building your mod list…", "", "#wizard-message");
|
||||
|
||||
try {
|
||||
if (state.listType === "homebrew") {
|
||||
const file = $("#mods-config").files[0];
|
||||
if (!file) throw new Error("Choose ModsConfig.xml before building a homebrew list.");
|
||||
const form = new FormData();
|
||||
form.append("mods_config", file);
|
||||
await downloadResponse(api("/api/modlists/homebrew", { method: "POST", body: form }), "ProgressionHomebrew.rml");
|
||||
setMessage("ProgressionHomebrew.rml is ready. Auto-sort it after loading.", "success", "#wizard-message");
|
||||
} else {
|
||||
const query = selectedDlcQuery();
|
||||
await downloadResponse(api(`/api/modlists/vanilla${query ? `?${query}` : ""}`), "ProgressionVanilla.rml");
|
||||
setMessage("ProgressionVanilla.rml is ready.", "success", "#wizard-message");
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage(error.message, "error", "#wizard-message");
|
||||
} finally {
|
||||
updateDownloadAvailability();
|
||||
}
|
||||
});
|
||||
|
||||
async function start() {
|
||||
$("#action-message").dataset.baseClass = "action-message";
|
||||
$("#wizard-message").dataset.baseClass = "wizard-message";
|
||||
renderInstallLocation();
|
||||
const minimumIntro = new Promise(resolve => window.setTimeout(resolve, INTRO_MINIMUM_MS));
|
||||
await Promise.all([loadDashboard(), minimumIntro]);
|
||||
document.body.classList.add("is-ready");
|
||||
window.setTimeout(() => {
|
||||
$("#intro").hidden = true;
|
||||
}, 850);
|
||||
}
|
||||
|
||||
start();
|
||||
state.polling = window.setInterval(loadDashboard, 60_000);
|
||||
Reference in New Issue
Block a user