Compare commits

..
Author SHA1 Message Date
mregosar 90996d1b0a Admin: Pausenanzeige, Live-Stand und passender Umschalter
Pausierte Matches zeigen ein Badge und den Knopf Fortsetzen. Laufende Matches
zeigen Map, Stand und Runde. Der Pausenzustand wird gespeichert und bei
Mapwechsel oder Serverstopp zurückgesetzt.
2026-10-29 09:21:32 +01:00
SPP UserandClaude Opus 5.5 da986d44c8 Merge feature/remove-real-logs in main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 07:38:38 +01:00
5 changed files with 25 additions and 5 deletions

No files matched your search

+13 -2
View File
@@ -271,6 +271,9 @@ function MatchAdminCard({ m, run, busy, token, reload, setNotice }) {
</button> </button>
); );
const hasNext = m.series.some((s) => s.number === m.currentMapNumber + 1); const hasNext = m.series.some((s) => s.number === m.currentMapNumber + 1);
const current = m.series.find((s) => s.number === m.currentMapNumber);
const configured = m.server?.status === 'configured';
const paused = !!m.server?.paused;
return ( return (
<section className={`panel admin-card status-${m.status}`}> <section className={`panel admin-card status-${m.status}`}>
@@ -283,6 +286,14 @@ function MatchAdminCard({ m, run, busy, token, reload, setNotice }) {
{m.modeLabel}, {FORMAT_LABEL[m.format]} · {m.statusLabel} · <span className={`tone-${server.tone}`}>{server.text}</span> {m.modeLabel}, {FORMAT_LABEL[m.format]} · {m.statusLabel} · <span className={`tone-${server.tone}`}>{server.text}</span>
{m.format !== 'bo1' && <> · Serie {m.seriesScore.A}:{m.seriesScore.B}</>} {m.format !== 'bo1' && <> · Serie {m.seriesScore.A}:{m.seriesScore.B}</>}
</div> </div>
{m.status === 'live' && current && (
<div className="admin-live">
<span>Map {current.number}: {current.name}</span>
<strong>{current.score.A} : {current.score.B}</strong>
{current.live?.roundsPlayed != null && <span className="muted">Runde {current.live.roundsPlayed + 1}</span>}
</div>
)}
{paused && <span className="admin-badge is-paused">Pausiert</span>}
</div> </div>
<button className="link-btn" onClick={() => setOpen(!open)}>{open ? 'Weniger' : 'Details'}</button> <button className="link-btn" onClick={() => setOpen(!open)}>{open ? 'Weniger' : 'Details'}</button>
</div> </div>
@@ -291,8 +302,8 @@ function MatchAdminCard({ m, run, busy, token, reload, setNotice }) {
<Link className="btn btn-sm" to={`/m/${id}`}>Ansehen</Link> <Link className="btn btn-sm" to={`/m/${id}`}>Ansehen</Link>
{m.status === 'lobby' && btn('Veto starten', { path: '/veto/start' }, 'btn-primary')} {m.status === 'lobby' && btn('Veto starten', { path: '/veto/start' }, 'btn-primary')}
{(m.server?.status === 'configured') && btn('Aufwärmphase beenden', { path: '/server/warmup-end' })} {(m.server?.status === 'configured') && btn('Aufwärmphase beenden', { path: '/server/warmup-end' })}
{(m.server?.status === 'configured') && btn('Pausieren', { path: '/server/pause', confirm: 'Match pausieren?' })} {configured && !paused && btn('Pausieren', { path: '/server/pause', confirm: 'Match pausieren?' }, 'btn-warn')}
{(m.server?.status === 'configured') && btn('Fortsetzen', { path: '/server/resume' })} {configured && paused && btn('Fortsetzen', { path: '/server/resume' }, 'btn-primary')}
{(m.server?.status === 'configured') && hasNext && btn('Nächste Map', { path: '/series/next' })} {(m.server?.status === 'configured') && hasNext && btn('Nächste Map', { path: '/series/next' })}
{m.server && ['start_failed', 'failed', 'stopped', 'config_failed', 'ready'].includes(m.server.status) && !closed && {m.server && ['start_failed', 'failed', 'stopped', 'config_failed', 'ready'].includes(m.server.status) && !closed &&
btn('Server neu starten / wiederholen', { path: '/server/retry' })} btn('Server neu starten / wiederholen', { path: '/server/retry' })}
+5
View File
@@ -529,3 +529,8 @@ input:focus, select:focus { border-color: var(--link); outline: none; }
.turn-seconds { min-width: 3ch; text-align: right; color: var(--muted); font-size: .875rem; font-variant-numeric: tabular-nums; } .turn-seconds { min-width: 3ch; text-align: right; color: var(--muted); font-size: .875rem; font-variant-numeric: tabular-nums; }
.turn-timer.is-low .turn-bar span { background: var(--error); } .turn-timer.is-low .turn-bar span { background: var(--error); }
.turn-timer.is-low .turn-seconds { color: var(--error); font-weight: 600; } .turn-timer.is-low .turn-seconds { color: var(--error); font-weight: 600; }
.admin-live { display: flex; gap: 10px; align-items: baseline; margin-top: 4px; font-size: .9rem; }
.admin-live strong { font-variant-numeric: tabular-nums; }
.admin-badge { display: inline-block; margin-top: 6px; padding: 2px 10px; border-radius: 999px; font-size: .8rem; font-weight: 600; }
.admin-badge.is-paused { background: color-mix(in srgb, var(--pending) 22%, transparent); color: var(--pending); border: 1px solid var(--pending); }
.btn-warn { border-color: var(--pending); color: var(--pending); }
+2
View File
@@ -128,4 +128,6 @@ function migrate(db) {
for (const [name, type] of add) { for (const [name, type] of add) {
if (!seriesColumns.has(name)) db.exec(`ALTER TABLE series_maps ADD COLUMN ${name} ${type}`); if (!seriesColumns.has(name)) db.exec(`ALTER TABLE series_maps ADD COLUMN ${name} ${type}`);
} }
const serverColumns = columns('servers');
if (!serverColumns.has('paused')) db.exec('ALTER TABLE servers ADD COLUMN paused INTEGER NOT NULL DEFAULT 0');
} }
+1
View File
@@ -944,6 +944,7 @@ export class MatchService extends EventEmitter {
status: row.status, status: row.status,
message: row.message, message: row.message,
playerCount: row.player_count, playerCount: row.player_count,
paused: !!row.paused,
attempts: row.attempts, attempts: row.attempts,
updatedAt: row.updated_at, updatedAt: row.updated_at,
idleTimeoutMinutes: this.config.idleTimeoutMinutes, idleTimeoutMinutes: this.config.idleTimeoutMinutes,
+4 -3
View File
@@ -308,7 +308,8 @@ export class ServerManager {
if (!seriesMap) throw new HttpError(409, 'Das Veto ist noch nicht abgeschlossen', 'veto_pending'); if (!seriesMap) throw new HttpError(409, 'Das Veto ist noch nicht abgeschlossen', 'veto_pending');
const mapName = pool.maps.find((m) => m.id === seriesMap.map)?.name ?? seriesMap.map; const mapName = pool.maps.find((m) => m.id === seriesMap.map)?.name ?? seriesMap.map;
this.update(matchId, { status: 'configuring', message: `Lade Map ${seriesMap.number}: ${mapName}` }); // Ein Mapwechsel beendet die Pause, CS2 lädt die Map neu
this.update(matchId, { status: 'configuring', paused: 0, message: `Lade Map ${seriesMap.number}: ${mapName}` });
this.service.log(matchId, 'server', `Konfiguration für Map ${seriesMap.number} (${mapName}) gestartet`); this.service.log(matchId, 'server', `Konfiguration für Map ${seriesMap.number} (${mapName}) gestartet`);
const plan = buildMapCommands({ const plan = buildMapCommands({
@@ -418,8 +419,8 @@ export class ServerManager {
} catch (err) { } catch (err) {
throw new HttpError(502, `Befehl konnte nicht gesendet werden: ${err.message}`, 'server_error'); throw new HttpError(502, `Befehl konnte nicht gesendet werden: ${err.message}`, 'server_error');
} }
this.update(viewer.matchId, { paused: paused ? 1 : 0 });
this.service.log(viewer.matchId, 'server', paused ? 'Match pausiert (Admin)' : 'Match fortgesetzt (Admin)'); this.service.log(viewer.matchId, 'server', paused ? 'Match pausiert (Admin)' : 'Match fortgesetzt (Admin)');
this.service.changed(viewer.matchId);
} }
/** Demo-Modus: simuliert einen Matchverlauf, indem echte CS2-Logzeilen erzeugt und ausgewertet werden. */ /** Demo-Modus: simuliert einen Matchverlauf, indem echte CS2-Logzeilen erzeugt und ausgewertet werden. */
@@ -463,7 +464,7 @@ export class ServerManager {
const row = this.row(matchId); const row = this.row(matchId);
if (!row || row.status === 'stopped') return; if (!row || row.status === 'stopped') return;
if (row.handle) await this.safeStop(row); if (row.handle) await this.safeStop(row);
this.update(matchId, { status: 'stopped', stopped_at: Date.now(), message: reason, player_count: null }); this.update(matchId, { status: 'stopped', stopped_at: Date.now(), message: reason, player_count: null, paused: 0 });
this.service.log(matchId, 'server', `Server gestoppt: ${reason}`); this.service.log(matchId, 'server', `Server gestoppt: ${reason}`);
} }