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:
@@ -263,8 +263,15 @@
|
||||
<button class="modal-close" aria-label="Close">×</button>
|
||||
</div>
|
||||
<p style="font-size: 12px; color: var(--text-muted); margin: 0 0 14px;">
|
||||
These map to <code>server.properties</code>. MC reads them at startup, so changes need a server restart to take effect.
|
||||
These map to <code>server.properties</code> and the daemon config. MC reads them at startup, so changes need a server restart to take effect.
|
||||
</p>
|
||||
|
||||
<div class="settings-section-label">Resources</div>
|
||||
<div class="settings-grid">
|
||||
<label>Memory (MB)<input id="ssfMemoryMB" type="number" min="1024" max="65536" step="256" /></label>
|
||||
</div>
|
||||
|
||||
<div class="settings-section-label" style="margin-top: 14px;">MC server.properties</div>
|
||||
<div class="settings-grid">
|
||||
<label>MOTD<input id="ssfMotd" type="text" /></label>
|
||||
<label>Gamemode<select id="ssfGamemode">
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -382,6 +382,14 @@ button.ghost-btn {
|
||||
.card.collapsible:not(.expanded) h2 { margin: 0; }
|
||||
|
||||
/* Server settings modal */
|
||||
.settings-section-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.settings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
Reference in New Issue
Block a user