Files
brass-and-sigil/server/wwwroot/modules/settings.js
T
Matt Sijbers a1331212cb Initial commit: Brass & Sigil monorepo
Self-hosted Minecraft modpack distribution + administration system.

- launcher/  Avalonia 12 desktop client; single-file win-x64 publish.
             Microsoft auth via XboxAuthNet, manifest+SHA-1 mod sync,
             portable install path, sidecar settings.
- server/    brass-sigil-server daemon (.NET 8, linux-x64). Wraps the
             MC subprocess, embedded Kestrel admin panel with cookie
             auth + rate limiting, RCON bridge, scheduled backups,
             BlueMap CLI integration with player markers + skin proxy,
             friend-side whitelist request flow, world wipe with seed
             selection (keep current / random / custom).
- pack/      pack.lock.json (Modrinth + manual CurseForge entries),
             data-only tweak source under tweaks/, build outputs in
             overrides/ (gitignored).
- scripts/   Build-Pack / Build-Tweaks / Update-Pack / Check-Updates
             plus Deploy-Brass.ps1 unified one-shot deploy with
             version-bump pre-flight and daemon-state detection.
2026-05-05 00:19:05 +01:00

136 lines
5.4 KiB
JavaScript

// Server settings: read/write a curated subset of server.properties.
// Changes require an MC restart -- Save writes only, Save & restart bounces MC.
"use strict";
import { api } from "./api.js";
const els = {};
// Map of input element ID -> server.properties key. Keeps the form ↔ file
// translation in one place; new fields can be added by adding a row here +
// matching elements in index.html.
const FIELDS = [
{ id: "ssfMotd", key: "motd", type: "string" },
{ id: "ssfGamemode", key: "gamemode", type: "string" },
{ id: "ssfDifficulty", key: "difficulty", type: "string" },
{ id: "ssfViewDistance", key: "view-distance", type: "int" },
{ id: "ssfSimulationDistance", key: "simulation-distance", type: "int" },
{ id: "ssfMaxPlayers", key: "max-players", type: "int" },
{ id: "ssfSpawnProtection", key: "spawn-protection", type: "int" },
{ id: "ssfPvp", key: "pvp", type: "bool" },
{ id: "ssfHardcore", key: "hardcore", type: "bool" },
{ id: "ssfAllowFlight", key: "allow-flight", type: "bool" },
{ id: "ssfWhiteList", key: "white-list", type: "bool" },
{ id: "ssfEnforceWhitelist", key: "enforce-whitelist", type: "bool" },
{ id: "ssfEnableCommandBlock", key: "enable-command-block", type: "bool" },
];
function readForm() {
const out = {};
for (const f of FIELDS) {
const el = document.getElementById(f.id);
if (!el) continue;
if (f.type === "bool") out[f.key] = el.checked ? "true" : "false";
else if (f.type === "int") {
const v = parseInt(el.value, 10);
if (Number.isFinite(v)) out[f.key] = String(v);
} else {
out[f.key] = el.value;
}
}
return out;
}
function writeForm(values) {
for (const f of FIELDS) {
const el = document.getElementById(f.id);
if (!el) continue;
const v = values[f.key];
if (v === undefined) continue;
if (f.type === "bool") el.checked = (v === "true");
else el.value = v;
}
}
function renderSummary(values) {
document.getElementById("ssMotd").textContent = values["motd"] ?? "--";
document.getElementById("ssDifficulty").textContent = values["difficulty"] ?? "--";
document.getElementById("ssDistances").textContent =
`${values["view-distance"] ?? "--"} / ${values["simulation-distance"] ?? "--"}`;
document.getElementById("ssMaxPlayers").textContent = values["max-players"] ?? "--";
const wl = values["white-list"] === "true";
const enf = values["enforce-whitelist"] === "true";
document.getElementById("ssWhitelist").textContent =
wl ? (enf ? "enforced" : "enabled") : "off";
}
async function refresh() {
try {
const data = await api("/api/server/settings");
renderSummary(data.values || {});
writeForm(data.values || {});
} catch { /* ignore -- panel just shows last-known */ }
}
async function postSettings() {
const payload = readForm();
const res = await fetch("/api/server/settings", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
return { ok: res.ok, body: await res.json().catch(() => ({})) };
}
function showMsg(text, ok = false) {
els.msg.className = ok ? "acct-msg ok" : "acct-msg";
els.msg.textContent = text;
}
export function setupSettings() {
els.msg = document.getElementById("ssMsg");
els.save = document.getElementById("ssSave");
els.restart = document.getElementById("ssRestart");
if (!els.save) return;
els.save.addEventListener("click", async () => {
showMsg("Saving...");
els.save.disabled = true;
try {
const r = await postSettings();
if (!r.ok || r.body.ok === false) {
showMsg(r.body.error || `Error ${r.body.status ?? ""}`);
return;
}
showMsg(r.body.restartRequired
? "Saved. Restart for changes to take effect."
: "Saved.", true);
refresh();
} catch (e) { showMsg(e.message); }
finally { els.save.disabled = false; }
});
els.restart.addEventListener("click", async () => {
if (!confirm("Save changes and restart the server now? Players will be disconnected briefly.")) return;
showMsg("Saving + restarting...");
els.save.disabled = true; els.restart.disabled = true;
try {
const r = await postSettings();
if (!r.ok || r.body.ok === false) {
showMsg(r.body.error || `Save failed: ${r.body.status ?? ""}`);
return;
}
const rr = await fetch("/api/server/restart", { method: "POST" });
const rb = await rr.json().catch(() => ({}));
if (!rr.ok || rb.ok === false) showMsg("Saved, but restart failed: " + (rb.error || rr.status));
else showMsg("Saved + restarting. New settings live in ~30s.", true);
refresh();
} catch (e) { showMsg(e.message); }
finally { els.save.disabled = false; els.restart.disabled = false; }
});
refresh();
// Light poll: pick up out-of-band edits to server.properties.
setInterval(refresh, 30000);
}