Server 2.10.3: browse the MSCO mod list in the web panel

Mods -> MSCO mod list: search the approved mods from msco.mbm.wtf/mods and add or
update one with a click. The server lists and downloads only from the official
keymaster (never from a URL in the answer), checks the SHA-256 and stores it in
data/mods like an uploaded mod. Admins only. 6 new tests (154 pass).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 18:08:37 +02:00
co-authored by Claude Opus 5.5
parent 266a462fc3
commit d946c95198
7 changed files with 228 additions and 2 deletions
+44 -1
View File
@@ -761,6 +761,10 @@ function renderWorld(page) {
function renderMods(page) {
const rows = h("tbody");
const catalogRows = h("tbody", {}, h("tr", {}, h("td", { colspan: 4, class: "empty" }, "Loading …")));
const search = h("input", { type: "search", placeholder: "Search mods …", maxlength: 60 });
let catalog = null;
search.addEventListener("input", () => drawCatalog());
const fileInput = h("input", { type: "file", accept: ".dll" });
const kb = (n) => (n / 1024).toFixed(n < 102400 ? 1 : 0) + " KB";
const upload = async () => {
@@ -798,7 +802,45 @@ function renderMods(page) {
h("div", { class: "section" },
h("div", { class: "section-head" }, h("h2", {}, "Server mods"), h("span", { class: "muted" }, "players get these when they join")),
h("div", { class: "table-wrap" }, h("table", {},
h("thead", {}, h("tr", {}, h("th", {}, "Mod"), h("th", {}, "Version"), h("th", {}, "Size"), h("th", {}, "SHA-256"), h("th", {}, ""))), rows))));
h("thead", {}, h("tr", {}, h("th", {}, "Mod"), h("th", {}, "Version"), h("th", {}, "Size"), h("th", {}, "SHA-256"), h("th", {}, ""))), rows))),
h("div", { class: "section" },
h("div", { class: "section-head" }, h("h2", {}, "MSCO mod list"), h("span", { class: "muted" }, "mods by players, checked by the MSCO admins")),
h("div", { class: "section-body" }, h("div", { class: "row" }, search,
h("button", { class: "btn ghost small", type: "button", onclick: openList }, "msco.mbm.wtf/mods"))),
h("div", { class: "table-wrap" }, h("table", {},
h("thead", {}, h("tr", {}, h("th", {}, "Mod"), h("th", {}, "Version"), h("th", {}, "Downloads"), h("th", {}, ""))), catalogRows))));
function openList() { window.open("https://msco.mbm.wtf/mods", "_blank", "noopener"); }
async function loadCatalog() {
try { catalog = await api("GET", "mods/catalog"); }
catch (e) { catalogRows.replaceChildren(h("tr", {}, h("td", { colspan: 4, class: "empty" }, e.message))); return; }
drawCatalog();
}
function drawCatalog() {
if (!catalog) return;
const q = search.value.trim().toLowerCase();
const list = catalog.filter((m) => !q || (m.name + " " + m.description + " " + m.author).toLowerCase().includes(q));
if (!catalog.length) { catalogRows.replaceChildren(h("tr", {}, h("td", { colspan: 4, class: "empty" }, "The MSCO mod list is empty."))); return; }
if (!list.length) { catalogRows.replaceChildren(h("tr", {}, h("td", { colspan: 4, class: "empty" }, "No mod matches the search."))); return; }
catalogRows.replaceChildren(...list.map((m) => h("tr", {},
h("td", {}, h("b", {}, m.name), h("span", { class: "muted" }, " · " + m.author),
m.description ? h("div", { class: "muted" }, m.description) : null),
h("td", {}, m.version, h("div", { class: "muted" }, kb(m.size))),
h("td", {}, String(m.downloads)),
h("td", {}, h("div", { class: "actions" },
m.upToDate ? h("span", { class: "muted" }, "Added")
: h("button", { class: "btn small primary", type: "button", onclick: (e) => install(m, e.target) }, m.installed ? "Update" : "Add to server"))))));
}
async function install(m, button) {
const ok = await modal({ title: (m.installed ? "Update " : "Add ") + m.name + "?", confirm: m.installed ? "Update" : "Add",
text: "Every player gets this mod offered when they join and decides whether to install it. It was checked by the MSCO admins - still only add mods you want on your server." });
if (!ok) return;
button.disabled = true;
await act(api("POST", "mods/catalog/" + encodeURIComponent(m.id) + "/install"));
load();
loadCatalog();
}
async function load() {
try {
@@ -812,6 +854,7 @@ function renderMods(page) {
} catch (e) { toast(e.message, true); }
}
load();
loadCatalog();
}
// ------------------------------------------------------------------ Benutzer