feat(panel): edit memoryMB from the Settings modal

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.
This commit is contained in:
Matt Sijbers
2026-05-20 21:25:47 +01:00
parent 9bf3cc69b0
commit 683c2c19ff
4 changed files with 116 additions and 5 deletions
+57 -4
View File
@@ -25,6 +25,13 @@ const FIELDS = [
{ 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) {
@@ -64,22 +71,68 @@ function renderSummary(values) {
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() {
const payload = readForm();
const res = await fetch("/api/server/settings", {
// 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(payload),
body: JSON.stringify(propsPayload),
});
return { ok: res.ok, body: await res.json().catch(() => ({})) };
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) {