diff --git a/client/src/pages/AdminPage.jsx b/client/src/pages/AdminPage.jsx index 36b3a44..ffecfba 100644 --- a/client/src/pages/AdminPage.jsx +++ b/client/src/pages/AdminPage.jsx @@ -217,6 +217,8 @@ function MatchAdminCard({ m, run, busy, token, reload, setNotice }) { Ansehen {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('Pausieren', { path: '/server/pause', confirm: 'Match pausieren?' })} + {(m.server?.status === 'configured') && btn('Fortsetzen', { path: '/server/resume' })} {(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 && btn('Server neu starten / wiederholen', { path: '/server/retry' })} diff --git a/server/src/adminRoutes.js b/server/src/adminRoutes.js index 6de7bd5..f1afd33 100644 --- a/server/src/adminRoutes.js +++ b/server/src/adminRoutes.js @@ -92,6 +92,8 @@ export function createAdminRouter({ service, manager, config }) { action('server/stop', (v) => manager.stopByOrganizer(v)); action('server/retry', (v) => manager.retry(v)); action('server/warmup-end', (v) => manager.endWarmup(v)); + action('server/pause', (v) => manager.setPaused(v, true)); + action('server/resume', (v) => manager.setPaused(v, false)); action('series/next', (v) => manager.loadNextMap(v)); action('veto/start', (v) => service.startVeto(v)); action('veto/action', (v, b) => service.vetoAction( diff --git a/server/src/servers/serverManager.js b/server/src/servers/serverManager.js index 7f496b6..6954747 100644 --- a/server/src/servers/serverManager.js +++ b/server/src/servers/serverManager.js @@ -391,6 +391,21 @@ export class ServerManager { this.service.changed(viewer.matchId); } + /** Admin: pausiert oder setzt das laufende Match auf dem Server fort (mp_pause_match / mp_unpause_match). */ + async setPaused(viewer, paused) { + this.service.require(viewer, 'organizer'); + const row = this.row(viewer.matchId); + if (row?.status !== 'configured') throw new HttpError(409, 'Der Server ist nicht bereit', 'wrong_state'); + if (!this.adapter.sendCommands) throw new HttpError(501, 'Dieser Adapter unterstützt keine Befehle', 'unsupported'); + try { + await this.adapter.sendCommands(JSON.parse(row.handle), [paused ? 'mp_pause_match' : 'mp_unpause_match']); + } catch (err) { + throw new HttpError(502, `Befehl konnte nicht gesendet werden: ${err.message}`, 'server_error'); + } + 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. */ startDemoSimulation(matchId) { const match = this.service.getMatch(matchId); diff --git a/server/test/api.test.js b/server/test/api.test.js index 4a7f9e8..3e2d4d0 100644 --- a/server/test/api.test.js +++ b/server/test/api.test.js @@ -401,6 +401,7 @@ test('Admin-Modus: Anmeldung, Verwaltung aller Matches, Sperre ohne Anmeldung', // Ein Match abbrechen wie ein Admin assert.equal((await call('POST', `/admin/matches/${id}/cancel`, { ...admin, body: {} })).status, 200); + assert.equal((await call('POST', `/admin/matches/${id}/server/pause`, { ...admin, body: {} })).status, 409, 'ohne Server keine Pause'); assert.equal(app.service.getMatch(id).status, 'cancelled'); // Endgültig löschen