Files
Cs2-Mapveto/client/src/components/ServerPanel.jsx
T
mregosar 263c7e7b7c Live-Ergebnisse: Stream-Seite, Anzeige, automatischer Mapwechsel
- 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
2026-10-25 19:20:10 +01:00

139 lines
6.4 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 && ['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>
);
}