Admin: Einstellungen mit Seitenleiste, Docker-Hosts und Messer-Server in die Einstellungen

This commit is contained in:
mregosar committed 2026-10-29 22:04:24 +01:00
1 parent ea06237846
commit 75b1701d8a
4 files changed
+139 -18

No files matched your search

+112 -13
View File
@@ -59,7 +59,6 @@ const storeToken = (t) => {
export default function AdminPage() {
const [token, setToken] = useState(getToken);
const [matches, setMatches] = useState(null);
const [hosts, setHosts] = useState([]);
const [error, setError] = useState(null);
const [notice, setNotice] = useState(null);
const [busy, setBusy] = useState(null);
@@ -90,7 +89,6 @@ export default function AdminPage() {
const res = await api(`/admin/matches${filter === 'archive' ? '?archived=1' : ''}`, { token });
setMatches(res.matches);
setArchivedCount(res.archivedCount ?? 0);
setHosts(res.hosts ?? []);
setError(null);
} catch (err) {
if (err.status === 401) {
@@ -147,14 +145,9 @@ export default function AdminPage() {
<button className={`admin-tab ${tab === 'settings' ? 'is-active' : ''}`} onClick={() => setTab('settings')}>Einstellungen</button>
</nav>
{tab === 'settings' && token && <MapPoolsPanel token={token} setNotice={setNotice} />}
{tab === 'settings' && token && <ServerSettingsPanel token={token} setNotice={setNotice} />}
{tab === 'settings' && token && <DockerHostsPanel token={token} setNotice={setNotice} />}
{tab === 'settings' && token && <NotificationSettings token={token} setNotice={setNotice} />}
{tab === 'settings' && token && <SettingsLayout token={token} setNotice={setNotice} />}
{notice && <div className={`toast toast-${notice.tone}`} role="status">{notice.text}</div>}
{error && <div className="alert alert-error">{error}</div>}
{tab === 'matches' && <HostUsage hosts={hosts} />}
{tab === 'matches' && <KnifePanel token={token} setNotice={setNotice} />}
{tab === 'matches' && matches === null && !error && <p className="muted">Lade Matches …</p>}
{tab === 'matches' && (
<MatchToolbar
@@ -479,6 +472,22 @@ function MatchAdminCard({ m, run, busy, token, reload, setNotice }) {
</section>
);
}
/** Auslastung der Docker-Hosts, regelmäßig neu geladen (Einstellungen → Docker-Hosts). */
function HostUsageLive({ token }) {
const [hosts, setHosts] = useState([]);
useEffect(() => {
let alive = true;
const load = () => api('/admin/hosts', { token }).then((res) => alive && setHosts(res.hosts)).catch(() => {});
load();
const t = setInterval(load, REFRESH_MS);
return () => {
alive = false;
clearInterval(t);
};
}, [token]);
return <HostUsage hosts={hosts} />;
}
/** Auslastung der Docker-Hosts: wie viele Gameserver läuft auf welchem Host, mit Liste der Matches. */
function HostUsage({ hosts }) {
if (hosts.length === 0) return null;
@@ -534,7 +543,7 @@ const shownDefault = (field) => {
};
/** Gameserver-Einstellungen: Vorgaben aus der .env, im Admin-Bereich änderbar. */
function ServerSettingsPanel({ token, setNotice }) {
function ServerSettingsPanel({ token, setNotice, groups = SETTING_GROUPS.map((g) => g.key), title = 'Gameserver' }) {
const [fields, setFields] = useState(null);
const [values, setValues] = useState({});
const [busy, setBusy] = useState(false);
@@ -598,13 +607,13 @@ function ServerSettingsPanel({ token, setNotice }) {
return (
<section className="admin-settings">
<h2>Gameserver</h2>
<h2>{title}</h2>
<p className="small muted">
Markierte Werte (live) gehen beim Speichern sofort an alle laufenden Server. Die übrigen gelten für neue Server und Vetos.
</p>
{SETTING_GROUPS.map((g) => (
{SETTING_GROUPS.filter((g) => groups.includes(g.key)).map((g) => (
<div key={g.key} className="setting-group">
<h3>{g.title}</h3>
{groups.length > 1 && <h3>{g.title}</h3>}
<div className="setting-grid">
{fields.filter((f) => f.group === g.key).map((f) => (
<div key={f.key} className="setting-item">
@@ -667,7 +676,14 @@ function DockerHostsPanel({ token, setNotice }) {
}, [pulling, load]);
if (!data) return null;
if (!data.editable) return null;
if (!data.editable) {
return (
<section className="admin-settings">
<h2>Docker-Hosts</h2>
<p className="muted">Docker-Hosts und Image-Update gibt es nur mit dem Docker-Adapter (<code>SERVER_ADAPTER=docker</code>).</p>
</section>
);
}
const original = JSON.stringify(data.hosts.map((h) => ({ ...EMPTY_HOST, ...h, dockerHost: h.dockerHost ?? '' })));
const dirty = JSON.stringify(rows) !== original;
@@ -1000,3 +1016,86 @@ function MapPoolsPanel({ token, setNotice }) {
</section>
);
}
const SETTINGS_KEY = 'cs2mv:admin:settings';
/** Menüpunkte der Einstellungen, in Gruppen für die Seitenleiste. */
const SETTINGS_MENU = [
{
title: 'Veto',
items: [{ key: 'maps', label: 'Map-Pools', hint: 'Maps im Veto' }],
},
{
title: 'Gameserver',
items: [
{ key: 'match', label: 'Match-Regeln', hint: 'Runden, Verlängerung, Befehle' },
{ key: 'tv', label: 'CSTV', hint: 'an/aus, Verzögerung' },
{ key: 'times', label: 'Zeiten', hint: 'Veto, Leerlauf, Start, Archiv' },
{ key: 'knife', label: 'Messer-Server', hint: 'Seitenwahl, 5v5 und 2v2' },
],
},
{
title: 'Server-Betrieb',
items: [{ key: 'hosts', label: 'Docker-Hosts', hint: 'Hosts und Image-Update' }],
},
{
title: 'Benachrichtigungen',
items: [{ key: 'discord', label: 'Discord', hint: 'Meldungen und Test' }],
},
];
const SETTINGS_KEYS = SETTINGS_MENU.flatMap((g) => g.items.map((i) => i.key));
const readSettingsSection = () => {
try {
const v = localStorage.getItem(SETTINGS_KEY);
return SETTINGS_KEYS.includes(v) ? v : SETTINGS_KEYS[0];
} catch {
return SETTINGS_KEYS[0];
}
};
/** Einstellungen mit Seitenleiste: links die Menüpunkte, rechts der gewählte Bereich. */
function SettingsLayout({ token, setNotice }) {
const [section, setSectionState] = useState(readSettingsSection);
const setSection = (key) => {
setSectionState(key);
try {
localStorage.setItem(SETTINGS_KEY, key);
} catch {
/* ignorieren */
}
};
const current = SETTINGS_MENU.flatMap((g) => g.items).find((i) => i.key === section);
return (
<div className="settings-layout">
<nav className="settings-nav" aria-label="Einstellungen">
{SETTINGS_MENU.map((g) => (
<div key={g.title} className="settings-nav-group">
<div className="settings-nav-title">{g.title}</div>
{g.items.map((i) => (
<button
key={i.key}
className={`settings-nav-item ${section === i.key ? 'is-active' : ''}`}
aria-current={section === i.key ? 'page' : undefined}
onClick={() => setSection(i.key)}
>
<span>{i.label}</span>
<small>{i.hint}</small>
</button>
))}
</div>
))}
</nav>
<div className="settings-content">
{section === 'maps' && <MapPoolsPanel token={token} setNotice={setNotice} />}
{['match', 'tv', 'times'].includes(section) && (
<ServerSettingsPanel key={section} token={token} setNotice={setNotice} groups={[section]} title={current.label} />
)}
{section === 'knife' && <KnifePanel token={token} setNotice={setNotice} />}
{section === 'hosts' && <HostUsageLive token={token} />}
{section === 'hosts' && <DockerHostsPanel token={token} setNotice={setNotice} />}
{section === 'discord' && <NotificationSettings token={token} setNotice={setNotice} />}
</div>
</div>
);
}