Admin: Einstellungen mit Seitenleiste, Docker-Hosts und Messer-Server in die Einstellungen
This commit is contained in:
1 parent
ea06237846
commit
75b1701d8a
4 files changed
+139
-18
No files matched your search
+112
-13
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user