Files
Cs2-Mapveto/client/src/components/ServerPanel.jsx
T
mregosar 13320313ec Messerrunde auf dem eigenen Match-Server, danach derselbe Server fürs Match
Bei Seitenwahl per Messerrunde richtet die App den Match-Server in der Lobby als
Messer-Server ein (Passwort des Matches, Aufwärmphase ohne Ende). Spieler sehen die
Verbindungsdaten in der Lobby, der Organisator startet bzw. wiederholt die Runde.
Nach dem Veto wird derselbe Server fürs Match eingerichtet, die Messer-Einstellungen
werden zurückgenommen.
2026-11-01 18:25:07 +01:00

140 lines
6.7 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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'];
/** Zeigt dem Organisator, ob Live-Daten vom Server ankommen und ob die Map vorbei ist. */
function LiveDataStatus({ state, hasNext }) {
const current = state.series.find((s) => s.number === state.currentMapNumber);
if (!current) return null;
const updatedAt = current.live?.updatedAt;
if (current.status === 'done') {
const auto = state.autoAdvanceSeconds >= 0;
return (
<div className="alert alert-ok">
Map {current.number} ist beendet.{' '}
{auto
? (hasNext ? `Die nächste Map wird nach ${state.autoAdvanceSeconds} s automatisch geladen, sofern die Serie nicht entschieden ist.` : 'Das Match wird automatisch abgeschlossen.')
: (hasNext ? 'Lade jetzt die nächste Map.' : 'Das war die letzte Map – schließ das Match ab.')}
</div>
);
}
return (
<p className="small live-data">
<span className={`status-dot ${updatedAt ? 'tone-ok' : 'tone-muted'}`} />
{updatedAt
? `Live-Daten vom Server empfangen, zuletzt um ${new Date(updatedAt).toLocaleTimeString('de-DE')}.`
: 'Noch keine Live-Daten. Sie kommen, sobald die Aufwärmphase endet und der Server diese Webseite erreicht (LOG_RECEIVER_URL).'}
</p>
);
}
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?.knife && <p className="muted small">Vor dem Veto läuft hier die Messerrunde, die Verbindungsdaten stehen in der Lobby. Nach dem Veto wird derselbe Server für das Match eingerichtet.</p>}
{server && !server.knife && ['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 && !server.knife && (
<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.isSpectator && !state.viewer.isOrganizer && server?.status === 'configured' && !server.gotv && (
<p className="small muted">CSTV ist auf diesem Server deaktiviert.</p>
)}
{isOrg && server?.status === 'configured' && <LiveDataStatus state={state} hasNext={hasNext} />}
{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>
);
}