- Stream-Übersicht unter /stream/<Zuschauercode> (1920x1080, Lobby, Veto, Spielstand) - Live-Stand, Rundenverlauf und Serienstand auf Match-Seite und Übersicht - Parser mit echten AMP-Logs geprüft: Game Over mit leerer Map-Gruppe, Match_Start vor der Aufwärmphase, veraltete Seitenzuordnung nach der Halbzeit - Nach Map-Ende automatisch nächste Map bzw. Abschluss der entschiedenen Serie - LOG_DEBUG zur Fehlersuche, README und .env.example ergänzt
139 lines
6.4 KiB
React
139 lines
6.4 KiB
React
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 && ['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.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>
|
||
);
|
||
}
|