diff --git a/client/src/pages/AdminPage.jsx b/client/src/pages/AdminPage.jsx index d3c9237..36b3a44 100644 --- a/client/src/pages/AdminPage.jsx +++ b/client/src/pages/AdminPage.jsx @@ -99,7 +99,7 @@ export default function AdminPage() {
Keine Sicherungen für diese Map gefunden.
} + {points && points.length > 0 && ( +Veto: {m.teams[m.veto.current.team].name} ist am Zug ({m.veto.current.type}).
)} diff --git a/client/src/styles.css b/client/src/styles.css index d5a0258..8d192c9 100644 --- a/client/src/styles.css +++ b/client/src/styles.css @@ -506,3 +506,9 @@ input:focus, select:focus { border-color: var(--link); outline: none; } .admin-login { margin-top: 40px; } .danger { color: var(--error); } @media (max-width: 640px) { .admin-players { grid-template-columns: 1fr; } } + +/* ---------- Rollback (nur Admin) ---------- */ +.rollback { border: 1px dashed var(--brand-red); border-radius: var(--radius); padding: 12px 14px; } +.rollback h3 { margin: 0 0 8px; display: flex; align-items: center; gap: 10px; } +.rollback select { min-width: 200px; } +.admin-badge { font: 600 .75rem var(--body); color: #fff; background: var(--brand-red); padding: 2px 8px; border-radius: 3px; } diff --git a/server/src/adminRoutes.js b/server/src/adminRoutes.js index 2196b6f..6de7bd5 100644 --- a/server/src/adminRoutes.js +++ b/server/src/adminRoutes.js @@ -101,6 +101,13 @@ export function createAdminRouter({ service, manager, config }) { action('kick', (v, b) => service.kick(v, b.playerId)); action('codes/regenerate', (v, b) => service.regenerateCode(v, b.target)); + // Rollback: nur für Admins. Diese Routen gibt es für Organisatoren nicht. + router.get('/matches/:id/rollback-points', requireAdmin, wrap(async (req) => { + service.getMatch(req.params.id); + return { points: await manager.rollbackPoints(adminViewer(req.params.id)) }; + })); + action('rollback', (v, b) => manager.rollback(v, b.file)); + // Match endgültig löschen (Server wird vorher freigegeben) router.delete('/matches/:id', requireAdmin, wrap(async (req) => { await manager.deleteMatch(req.params.id); diff --git a/server/src/matchService.js b/server/src/matchService.js index 5d65f8c..33adac3 100644 --- a/server/src/matchService.js +++ b/server/src/matchService.js @@ -2,6 +2,7 @@ import { EventEmitter } from 'node:events'; import { BEST_OF, computeVetoState, resolveAction, VetoError } from './veto.js'; import { formatCode, hashToken, normalizeCode, randomCode, randomId, randomToken, safeEqual } from './security.js'; import { applyLogLines, initialLiveState, seriesScore, splitLogBody } from './liveScore.js'; +import { trimLiveState } from './rounds.js'; import { sanitizeConsoleText } from './servers/cs2Config.js'; export class HttpError extends Error { @@ -448,6 +449,20 @@ export class MatchService extends EventEmitter { .run(matchId, number); } + /** Stand auf den Zeitpunkt nach einer Runde zurückstellen (nach Rollback). */ + rewindLiveScore(matchId, roundNumber, file) { + const match = this.getMatch(matchId); + const row = this.seriesMaps(matchId).find((m) => m.number === match.current_map_number); + if (!row) return; + const live = row.live ? trimLiveState(JSON.parse(row.live), roundNumber) : null; + const score = live ? live.score : { A: 0, B: 0 }; + this.db + .prepare("UPDATE series_maps SET score_a = ?, score_b = ?, winner = NULL, status = 'live', live = ?, live_updated_at = ? WHERE match_id = ? AND number = ?") + .run(score.A, score.B, live ? JSON.stringify(live) : null, this.now(), matchId, row.number); + this.log(matchId, 'rollback', `Map ${row.number} zurückgespielt auf Runde ${roundNumber} (${file})`); + this.changed(matchId); + } + /** Organisator hat die Aufwärmphase beendet: ab dem nächsten Match_Start wird gezählt. */ markWarmupEnded(matchId, ended = true) { const match = this.getMatch(matchId); diff --git a/server/src/rounds.js b/server/src/rounds.js new file mode 100644 index 0000000..6f0b1ad --- /dev/null +++ b/server/src/rounds.js @@ -0,0 +1,62 @@ +// Rundensicherung und Zurückspielen (Rollback). +// +// CS2 speichert nach jeder Runde eine Sicherungsdatei des Spielzustands auf dem Server (mp_backup_round_auto). +// Diese Datei lässt sich später wieder laden (mp_backup_restore_load_file). Dieses Modul kennt die Namen, +// wertet die Liste aus und kürzt den gespeicherten Live-Stand passend zur Rückkehr. + +// Präfix und Muster wie bei TMT2: der Präfix enthält die Match-ID, das Muster Map, Runde und Stand. +export const backupPrefix = (matchId) => `round_backup_${matchId}`; +export const BACKUP_PATTERN = '%prefix%_%date%_%time%_%map%_round%round%_score_%score1%_%score2%.txt'; + +// Endung: _