683c2c19ff
Adds GET/POST /api/daemon/config (safe subset of server-config.json -- just memoryMB for now) and a "Resources" section in the existing Settings modal. The Save and Save+restart buttons now POST both /api/server/settings and /api/daemon/config; restartRequired is OR'd across the two endpoints. Future fields like backupKeep / backupSchedule / bluemapDir can be added by extending DAEMON_FIELDS in settings.js + the validation block in RunCommand.cs. Sensitive fields (passwords, manifestUrl, ports) are intentionally kept off this endpoint.
189 lines
7.3 KiB
JavaScript
189 lines
7.3 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" },
|
|
];
|
|
|
|
// 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);
|
|
}
|