Frontend, README und Konfigurationsvorlage

React-Oberfläche mit Übersicht, Match-Erstellung, Beitritt über Team- und
Zuschauercodes, Teamkarten, visuellem Map-Veto, Serie und Serverpanel.
README beschreibt Einrichtung, Umgebungsvariablen, Rollen, Veto-Abläufe,
Serveradapter und CSTV-Zuschauerzugang.
This commit is contained in:
mregosar committed 2026-10-25 17:02:18 +01:00
1 parent f08297dd0d
commit f7520a6ca7
24 files changed
+1938 -1

No files matched your search

+110
View File
@@ -0,0 +1,110 @@
import { serverLabel } from '../lib/labels.js';
import CopyField from './CopyField.jsx';
const RETRY = {
start_failed: 'Server neu starten',
failed: 'Server neu starten',
stopped: 'Server neu starten',
config_failed: 'Konfiguration wiederholen',
ready: 'Konfiguration starten',
};
const ACTIVE = ['starting', 'ready', 'configuring', 'configured', 'config_failed', 'failed'];
export default function ServerPanel({ state, isOrg, act, busy }) {
const { server } = state;
const label = serverLabel(server);
const closed = ['finished', 'cancelled'].includes(state.status);
const sim = server?.simulated;
const canRetry = server && !closed && RETRY[server.status] && (server.status !== 'ready' || state.status === 'server_setup');
const hasNext = state.series.some((s) => s.number === (state.currentMapNumber ?? 0) + 1);
return (
<section className={`panel server-panel tone-border-${label.tone}`}>
<h2>Spielserver</h2>
<div className="server-status">
<span className={`status-dot tone-${label.tone} ${['starting', 'configuring'].includes(server?.status) ? 'pulse' : ''}`} />
<strong className={`tone-${label.tone}`}>{label.text}</strong>
</div>
{server?.message && <p className="small">{server.message}</p>}
{sim && (
<div className="alert alert-demo">
Demo-Adapter aktiv: Es läuft kein echter CS2-Server. Start, Konfiguration und Verbindungsdaten sind simuliert.
Wie die echte Anbindung eingerichtet wird, steht in der README.
</div>
)}
{!server && <p className="muted small">Für dieses Match wurde noch kein Server angefordert.</p>}
{server && ['starting', 'ready'].includes(server.status) && state.status !== 'server_setup' && (
<p className="muted small">
{sim ? 'Der Start wird während des Vetos simuliert.' : 'Der Server fährt während des Vetos hoch.'} Die Verbindungsdaten erscheinen nach dem Veto.
</p>
)}
{server?.connect && (
<div className="connect">
<h3>{sim ? 'Spieler-Verbindung (Demo-Beispiel, nicht verbindbar)' : 'Verbindung für Spieler'}</h3>
<CopyField label="In die CS2-Konsole einfügen" value={server.connect.command} secret />
{!sim && <a className="btn btn-primary" href={server.connect.steamUrl}>In CS2 verbinden</a>}
<p className="small muted">Server {server.connect.host}:{server.connect.port}. Das Passwort ist nur für Spieler dieses Matches sichtbar.</p>
</div>
)}
{server?.gotv && (
<div className="connect gotv">
<h3>{sim ? 'CSTV/GOTV (Demo-Beispiel, nicht verbindbar)' : 'Zuschauen über CSTV (GOTV)'}</h3>
<CopyField label="Für Stream und Kommentatoren" value={server.gotv.command} secret />
<p className="small muted">
Übertragung mit {server.gotv.delaySeconds} Sekunden Verzögerung gegen Ghosting. Zuschauer belegen keinen Spielerplatz.
</p>
</div>
)}
{state.viewer.role === 'spectator' && server?.status === 'configured' && !server.gotv && (
<p className="small muted">CSTV ist auf diesem Server deaktiviert.</p>
)}
{isOrg && server && !closed && (
<div className="org-controls">
{canRetry && (
<button className="btn btn-primary" disabled={!!busy} onClick={() => act('server/retry', null, 'Erneuter Versuch gestartet')}>
{RETRY[server.status]}
</button>
)}
{server.status === 'configured' && (
<button className="btn" disabled={!!busy} onClick={() => act('server/warmup-end', null, 'Aufwärmphase beendet')}>
Aufwärmphase beenden
</button>
)}
{server.status === 'configured' && state.status === 'live' && hasNext && (
<button className="btn" disabled={!!busy} onClick={() => act('series/next', null, 'Nächste Map wird geladen')}>
Nächste Map laden
</button>
)}
{ACTIVE.includes(server.status) && (
<button className="btn btn-ghost" disabled={!!busy}
onClick={() => window.confirm('Server wirklich stoppen?') && act('server/stop', null, 'Server gestoppt')}>
Server stoppen
</button>
)}
</div>
)}
{isOrg && !closed && (
<div className="org-controls">
{['live', 'server_setup'].includes(state.status) && (
<button className="btn" disabled={!!busy}
onClick={() => window.confirm('Match abschließen und Server herunterfahren?') && act('finish', null, 'Match abgeschlossen')}>
Match abschließen
</button>
)}
<button className="btn btn-ghost danger" disabled={!!busy}
onClick={() => window.confirm('Match abbrechen? Das kann nicht rückgängig gemacht werden.') && act('cancel', null, 'Match abgebrochen')}>
Match abbrechen
</button>
</div>
)}
{server && ACTIVE.includes(server.status) && (
<p className="small muted">Ungenutzte Server werden nach {server.idleTimeoutMinutes} Minuten automatisch heruntergefahren.</p>
)}
</section>
);
}