From 929134f2bc5dbb6ace8d99d5c3a2945f48a67f07 Mon Sep 17 00:00:00 2001 From: MrEgosar Date: Thu, 29 Oct 2026 13:32:05 +0100 Subject: [PATCH] Admin: Gameserver-Einstellungen, Docker-Hosts und Image-Update im Admin-Bereich --- client/src/pages/AdminPage.jsx | 248 ++++++++++++++++++++++++++++ client/src/styles.css | 19 +++ server/src/adminRoutes.js | 48 +++++- server/src/app.js | 6 +- server/src/config.js | 2 +- server/src/http.js | 4 +- server/src/serverSettings.js | 187 +++++++++++++++++++++ server/src/servers/dockerAdapter.js | 75 ++++++++- server/src/servers/serverManager.js | 72 ++++++++ server/test/serverSettings.test.js | 61 +++++++ 10 files changed, 710 insertions(+), 12 deletions(-) create mode 100644 server/src/serverSettings.js create mode 100644 server/test/serverSettings.test.js diff --git a/client/src/pages/AdminPage.jsx b/client/src/pages/AdminPage.jsx index 0d23b93..1b28105 100644 --- a/client/src/pages/AdminPage.jsx +++ b/client/src/pages/AdminPage.jsx @@ -100,6 +100,8 @@ export default function AdminPage() { + {tab === 'settings' && token && } + {tab === 'settings' && token && } {tab === 'settings' && token && } {notice &&
{notice.text}
} {error &&
{error}
} @@ -438,3 +440,249 @@ function HostUsage({ hosts }) { function gb(bytes) { return `${Math.round(bytes / 1e9)} GB`; } +const SETTING_GROUPS = [ + { key: 'match', title: 'Match-Regeln' }, + { key: 'tv', title: 'CSTV' }, + { key: 'times', title: 'Zeiten' }, +]; + +const toInput = (field, value) => (field.type === 'commands' ? value.join('; ') : value); +const shownDefault = (field) => { + if (field.type === 'bool') return field.default ? 'an' : 'aus'; + if (field.type === 'commands') return field.default.length ? field.default.join('; ') : 'keine'; + return field.default; +}; + +/** Gameserver-Einstellungen: Vorgaben aus der .env, im Admin-Bereich änderbar. */ +function ServerSettingsPanel({ token, setNotice }) { + const [fields, setFields] = useState(null); + const [values, setValues] = useState({}); + const [busy, setBusy] = useState(false); + + const apply = (list) => { + setFields(list); + setValues(Object.fromEntries(list.map((f) => [f.key, toInput(f, f.value)]))); + }; + + useEffect(() => { + api('/admin/server-settings', { token }) + .then((res) => apply(res.fields)) + .catch((err) => setNotice({ tone: 'error', text: err.message })); + }, [token, setNotice]); + + if (!fields) return null; + const changedKeys = fields.filter((f) => String(values[f.key]) !== String(toInput(f, f.value))).map((f) => f.key); + + const save = async () => { + setBusy(true); + try { + const body = Object.fromEntries(changedKeys.map((key) => { + const field = fields.find((f) => f.key === key); + return [key, field.type === 'int' ? Number(values[key]) : values[key]]; + })); + const res = await api('/admin/server-settings', { method: 'PUT', body: { values: body }, token }); + apply(res.fields); + const parts = ['Einstellungen gespeichert']; + if (res.live.updated) parts.push(`${res.live.updated} laufende${res.live.updated === 1 ? 'r' : ''} Server aktualisiert`); + if (res.live.failed.length) parts.push(`${res.live.failed.length} Server nicht erreichbar`); + setNotice({ tone: res.live.failed.length ? 'error' : 'ok', text: parts.join(' · ') }); + } catch (err) { + setNotice({ tone: 'error', text: err.message }); + } finally { + setBusy(false); + } + }; + + const input = (f) => { + if (f.type === 'bool') { + return ( + + ); + } + return ( +