Rollback: Runden aus CS2-Sicherungen zurückspielen (nur Admin)

Beim Konfigurieren aktiviert CS2 Mapveto die Rundensicherung des Servers
(mp_backup_round_auto, Präfix mit Match-ID). Im Admin-Modus lassen sich
die Sicherungen der laufenden Map auflisten und zurückspielen. Der
Live-Stand wird auf die Runden bis zur Sicherung gekürzt. Organisatoren
haben keinen Zugang zu den Rollback-Routen.

Sicherheit: Dateinamen werden gegen Match-ID und Muster geprüft, damit
keine fremden Dateien oder Befehle in die RCON-Zeile gelangen.
This commit is contained in:
mregosar committed 2026-10-25 23:24:49 +01:00
1 parent 43ecb8a1ad
commit 3bfe087b7d
13 files changed
+327 -5

No files matched your search

+55 -2
View File
@@ -99,7 +99,7 @@ export default function AdminPage() {
<div className="admin-list">
{matches?.map((m) => (
<MatchAdminCard key={m.id} m={m} run={run} busy={busy} />
<MatchAdminCard key={m.id} m={m} run={run} busy={busy} token={token} reload={load} setNotice={setNotice} />
))}
</div>
</div>
@@ -137,7 +137,56 @@ function AdminLogin({ onLogin }) {
);
}
function MatchAdminCard({ m, run, busy }) {
/** Rollback: nur auf der Admin-Seite. Lädt die Sicherungen der aktuellen Map und spielt eine davon zurück. */
function RollbackPanel({ matchId, token, reload, setNotice }) {
const [points, setPoints] = useState(null);
const [file, setFile] = useState('');
const [loading, setLoading] = useState(false);
const load = async () => {
setLoading(true);
try {
const res = await api(`/admin/matches/${matchId}/rollback-points`, { token });
setPoints(res.points);
setFile(res.points[0]?.file ?? '');
} catch (err) {
setNotice({ tone: 'error', text: err.message });
} finally {
setLoading(false);
}
};
const rollback = async () => {
const p = points.find((x) => x.file === file);
if (!p || !window.confirm(`Match auf den Stand nach Runde ${p.round} zurückspielen? Spätere Runden dieser Map gehen verloren.`)) return;
try {
await api(`/admin/matches/${matchId}/rollback`, { method: 'POST', body: { file }, token });
setNotice({ tone: 'ok', text: `Zurückgespielt auf Runde ${p.round}` });
setPoints(null);
reload();
} catch (err) {
setNotice({ tone: 'error', text: err.message });
}
};
return (
<div className="rollback">
<h3>Runde zurückspielen <span className="admin-badge">nur Admin</span></h3>
{!points && <button className="btn btn-sm" onClick={load} disabled={loading}>{loading ? 'Lade …' : 'Sicherungen laden'}</button>}
{points && points.length === 0 && <p className="small muted">Keine Sicherungen für diese Map gefunden.</p>}
{points && points.length > 0 && (
<div className="row">
<select value={file} onChange={(e) => setFile(e.target.value)}>
{points.map((p) => <option key={p.file} value={p.file}>Nach Runde {p.round}</option>)}
</select>
<button className="btn btn-sm btn-danger" onClick={rollback}>Zurückspielen</button>
</div>
)}
</div>
);
}
function MatchAdminCard({ m, run, busy, token, reload, setNotice }) {
const [open, setOpen] = useState(false);
const server = serverLabel(m.server);
const closed = ['finished', 'cancelled'].includes(m.status);
@@ -231,6 +280,10 @@ function MatchAdminCard({ m, run, busy }) {
))}
</div>
{m.status === 'live' && m.server?.status === 'configured' && (
<RollbackPanel matchId={id} token={token} reload={reload} setNotice={setNotice} />
)}
{m.veto && !m.veto.done && m.veto.current && (
<p className="small">Veto: {m.teams[m.veto.current.team].name} ist am Zug ({m.veto.current.type}).</p>
)}
+6
View File
@@ -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; }