// 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" }, ]; // Fields that live in server-config.json (the daemon's own config), not // server.properties. Endpoint is /api/daemon/config, payload is JSON-typed // (not stringified like server.properties). const DAEMON_FIELDS = [ { id: "ssfMemoryMB", key: "memoryMB", type: "int" }, ]; 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"; } function readDaemonForm() { const out = {}; for (const f of DAEMON_FIELDS) { const el = document.getElementById(f.id); if (!el) continue; if (f.type === "int") { const v = parseInt(el.value, 10); if (Number.isFinite(v)) out[f.key] = v; } else { out[f.key] = el.value; } } return out; } function writeDaemonForm(values) { for (const f of DAEMON_FIELDS) { const el = document.getElementById(f.id); if (!el) continue; const v = values[f.key]; if (v === undefined) continue; el.value = String(v); } } async function refresh() { try { const data = await api("/api/server/settings"); renderSummary(data.values || {}); writeForm(data.values || {}); } catch { /* ignore -- panel just shows last-known */ } try { const daemon = await api("/api/daemon/config"); writeDaemonForm(daemon || {}); } catch { /* ignore */ } } async function postSettings() { // Two endpoints, two payloads. We always POST both -- backend short-circuits // when nothing changed, and combining them in one round-trip would mean // either endpoint failing leaves the other in an inconsistent state. const propsPayload = readForm(); const daemonPayload = readDaemonForm(); const propsRes = await fetch("/api/server/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(propsPayload), }); const propsBody = await propsRes.json().catch(() => ({})); const daemonRes = await fetch("/api/daemon/config", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(daemonPayload), }); const daemonBody = await daemonRes.json().catch(() => ({})); const ok = propsRes.ok && daemonRes.ok && propsBody.ok !== false && daemonBody.ok !== false; const error = propsBody.error || daemonBody.error || null; const restartRequired = (propsBody.restartRequired || daemonBody.restartRequired) ?? false; return { ok, body: { ok, error, restartRequired } }; } 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); }