Messer-Server für die Seitenwahl: 5v5/2v2, im Admin startbar, setzen sich nach der Runde selbst zurück
This commit is contained in:
1 parent
0bfac952b6
commit
c6c39b914f
15 files changed
+717
-14
No files matched your search
@@ -152,6 +152,7 @@ export default function AdminPage() {
|
||||
{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
|
||||
@@ -798,3 +799,109 @@ function MatchToolbar({ matches, archivedCount, filter, setFilter, query, setQue
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const KNIFE_STATUS = {
|
||||
starting: { text: 'startet', tone: 'pending' },
|
||||
configuring: { text: 'wird eingerichtet', tone: 'pending' },
|
||||
running: { text: 'bereit', tone: 'ok' },
|
||||
config_failed: { text: 'Einrichtung fehlgeschlagen', tone: 'error' },
|
||||
failed: { text: 'ausgefallen', tone: 'error' },
|
||||
};
|
||||
|
||||
/** Messer-Server für die Seitenwahl: laufen dauerhaft, setzen sich nach jeder Messerrunde selbst zurück. */
|
||||
function KnifePanel({ token, setNotice }) {
|
||||
const [data, setData] = useState(null);
|
||||
const [mode, setMode] = useState('');
|
||||
const [map, setMap] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
const res = await api('/admin/knife', { token });
|
||||
setData(res);
|
||||
setMode((m) => m || res.modes[0]?.id || '');
|
||||
setMap((m) => m || res.modes[0]?.defaultMap || '');
|
||||
} catch {
|
||||
/* Fehler zeigt die Matchliste schon an */
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
const t = setInterval(load, REFRESH_MS);
|
||||
return () => clearInterval(t);
|
||||
}, [load]);
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
const run = async (fn, success, question) => {
|
||||
if (question && !window.confirm(question)) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
await fn();
|
||||
if (success) setNotice({ tone: 'ok', text: success });
|
||||
load();
|
||||
} catch (err) {
|
||||
setNotice({ tone: 'error', text: err.message });
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
const post = (path, body) => api(`/admin/knife${path}`, { method: 'POST', body, token });
|
||||
const full = data.servers.length >= data.maxServers;
|
||||
const modeInfo = data.modes.find((x) => x.id === mode) ?? data.modes[0];
|
||||
const chooseMode = (id) => {
|
||||
setMode(id);
|
||||
setMap(data.modes.find((x) => x.id === id)?.defaultMap ?? '');
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="admin-settings knife-panel">
|
||||
<h2>Messer-Server (Seitenwahl)</h2>
|
||||
<p className="small muted">
|
||||
Laufen dauerhaft und ohne Passwort, sichtbar im LAN-Reiter von Steam. Teams verbinden sich, spielen eine Messerrunde
|
||||
und gehen wieder. Nach dem Spiel oder wenn der Server leer ist, setzt er sich selbst zurück.
|
||||
</p>
|
||||
{data.servers.length > 0 && (
|
||||
<div className="knife-grid">
|
||||
{data.servers.map((k) => {
|
||||
const st = KNIFE_STATUS[k.status] ?? { text: k.status, tone: 'pending' };
|
||||
const ready = k.status === 'running';
|
||||
return (
|
||||
<div key={k.id} className="knife-card">
|
||||
<div className="knife-head">
|
||||
<strong>{k.name}</strong>
|
||||
{k.modeShort && <span className="admin-badge knife-mode">{k.modeShort}</span>}
|
||||
<span className="muted small">{k.mapName}</span>
|
||||
<span className={`tone-${st.tone} small knife-state`}>{st.text}</span>
|
||||
</div>
|
||||
{k.connect && <code className="knife-addr">{k.host}:{k.port}</code>}
|
||||
<div className="small muted">
|
||||
{k.playerCount != null ? `${k.playerCount} Spieler` : 'Spieler unbekannt'}
|
||||
{k.message ? ` · ${k.message}` : ''}
|
||||
</div>
|
||||
<div className="admin-actions">
|
||||
<button className="btn btn-sm" disabled={busy || !ready} onClick={() => run(() => post(`/${k.id}/round`), 'Messerrunde gestartet')}>Runde starten</button>
|
||||
<button className="btn btn-sm" disabled={busy || !ready} onClick={() => run(() => post(`/${k.id}/reset`), 'Server zurückgesetzt')}>Zurücksetzen</button>
|
||||
<button className="btn btn-sm" disabled={busy} onClick={() => run(() => post(`/${k.id}/restart`), 'Server wird neu gestartet', `${k.name} komplett neu starten?`)}>Neu starten</button>
|
||||
<button className="btn btn-sm btn-ghost danger" disabled={busy} onClick={() => run(() => api(`/admin/knife/${k.id}`, { method: 'DELETE', token }), `${k.name} entfernt`, `${k.name} stoppen und entfernen?`)}>Entfernen</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<div className="admin-actions knife-new">
|
||||
<select value={modeInfo?.id ?? ''} onChange={(e) => chooseMode(e.target.value)} aria-label="Modus für den neuen Messer-Server" disabled={busy || full}>
|
||||
{data.modes.map((m) => <option key={m.id} value={m.id}>{m.label}</option>)}
|
||||
</select>
|
||||
<select value={map} onChange={(e) => setMap(e.target.value)} aria-label="Map für den neuen Messer-Server" disabled={busy || full}>
|
||||
{modeInfo?.maps.map((m) => <option key={m.id} value={m.id}>{m.name}</option>)}
|
||||
</select>
|
||||
<button className="btn btn-sm btn-primary" disabled={busy || full} onClick={() => run(() => api('/admin/knife', { method: 'POST', body: { mode: modeInfo?.id, map }, token }), 'Messer-Server wird gestartet')}>
|
||||
Messer-Server starten
|
||||
</button>
|
||||
<span className="small muted">{data.servers.length} von {data.maxServers}</span>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user