public site and containers for that.
Build and Publish Container / Publish Linux container (push) Failing after 1m52s

This commit is contained in:
2026-10-02 12:57:22 -04:00
parent 86a187b08a
commit 7ac541def5
25 changed files with 3265 additions and 4 deletions
+360
View File
@@ -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);