Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f90351dd8d | ||
|
|
d2625d23ab | ||
|
|
8b6abdb8b2 | ||
|
|
eb80c4845c | ||
|
|
90996d1b0a | ||
|
|
da986d44c8 |
No files matched your search
@@ -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' })}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { api } from '../lib/api.js';
|
|||||||
import { FORMAT_LABEL, serverLabel, timeAgo } from '../lib/labels.js';
|
import { FORMAT_LABEL, serverLabel, timeAgo } from '../lib/labels.js';
|
||||||
import { getAccess } from '../lib/session.js';
|
import { getAccess } from '../lib/session.js';
|
||||||
import { useOverview } from '../lib/useMatch.js';
|
import { useOverview } from '../lib/useMatch.js';
|
||||||
|
import MapImage from '../components/MapImage.jsx';
|
||||||
|
|
||||||
const GROUPS = [
|
const GROUPS = [
|
||||||
{ key: 'open', title: 'Offen', empty: 'Keine offenen Lobbys. Erstelle ein Match, um zu starten.' },
|
{ key: 'open', title: 'Offen', empty: 'Keine offenen Lobbys. Erstelle ein Match, um zu starten.' },
|
||||||
@@ -32,7 +33,11 @@ export default function Overview() {
|
|||||||
return (
|
return (
|
||||||
<section key={g.key} className={`group group-${g.key}`}>
|
<section key={g.key} className={`group group-${g.key}`}>
|
||||||
<h2>{g.title} <span className="count">{list.length}</span></h2>
|
<h2>{g.title} <span className="count">{list.length}</span></h2>
|
||||||
{list.length === 0 ? <p className="muted small">{g.empty}</p> : (
|
{list.length === 0 ? <p className="muted small">{g.empty}</p> : g.key === 'running' ? (
|
||||||
|
<div className="run-grid">
|
||||||
|
{list.map((m) => <RunningCard key={m.id} match={m} />)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<ul className="match-list">
|
<ul className="match-list">
|
||||||
{list.map((m) => <MatchRow key={m.id} match={m} />)}
|
{list.map((m) => <MatchRow key={m.id} match={m} />)}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -79,6 +84,34 @@ function MatchRow({ match: m }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Karte für ein laufendes Match: Map als Hintergrund, Stand groß, Serie und Status darunter. */
|
||||||
|
function RunningCard({ match: m }) {
|
||||||
|
const server = serverLabel(m.server);
|
||||||
|
const map = m.currentMap;
|
||||||
|
const live = map && !map.finished;
|
||||||
|
const bo1 = m.format === 'bo1';
|
||||||
|
return (
|
||||||
|
<Link to={`/m/${m.id}`} className={`run-card ${live ? 'is-live' : ''}`} style={{ '--map': map?.color ?? '#3a4150' }}>
|
||||||
|
{map?.image && <MapImage map={{ image: map.image, color: map.color, name: map.name }} className="run-art" />}
|
||||||
|
<div className="run-top">
|
||||||
|
<span className="run-mode">{m.modeLabel}, {FORMAT_LABEL[m.format]}</span>
|
||||||
|
{live ? <span className="live-badge run-live">Live</span> : <span className={`run-status tone-${server.tone}`}>{m.statusLabel}</span>}
|
||||||
|
</div>
|
||||||
|
<div className="run-teams">
|
||||||
|
<span className="run-team a">{m.teamA}</span>
|
||||||
|
<span className="run-score">
|
||||||
|
{map ? <>{map.score.A}<i>:</i>{map.score.B}</> : <i className="run-vs">vs.</i>}
|
||||||
|
</span>
|
||||||
|
<span className="run-team b">{m.teamB}</span>
|
||||||
|
</div>
|
||||||
|
<div className="run-foot">
|
||||||
|
<span>{map ? `Map ${map.number}: ${map.name}` : 'Veto läuft'}</span>
|
||||||
|
{!bo1 && m.seriesScore && <span className="run-series">Serie {m.seriesScore.A}:{m.seriesScore.B}</span>}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function CodeEntry() {
|
function CodeEntry() {
|
||||||
const [code, setCode] = useState('');
|
const [code, setCode] = useState('');
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
|||||||
@@ -529,3 +529,28 @@ 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); }
|
||||||
|
/* Laufende Matches in der Übersicht */
|
||||||
|
.run-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 16px; }
|
||||||
|
.run-card { position: relative; display: flex; flex-direction: column; justify-content: space-between; gap: 10px; min-height: 190px; padding: 16px 18px; border-radius: var(--radius); border: 1px solid var(--line); background: var(--surface); color: var(--text); text-decoration: none; overflow: hidden; isolation: isolate; transition: transform .15s ease, border-color .15s ease; }
|
||||||
|
.run-card:hover { transform: translateY(-2px); border-color: var(--map); }
|
||||||
|
.run-card.is-live { box-shadow: inset 3px 0 0 var(--brand-red); }
|
||||||
|
.run-art { position: absolute; inset: 0; z-index: -1; opacity: .28; }
|
||||||
|
.run-art::after { background: linear-gradient(180deg, rgba(0,0,0,.2), rgba(0,0,0,.92)); }
|
||||||
|
.run-top { display: flex; justify-content: space-between; align-items: center; gap: 8px; font-size: .8rem; color: var(--muted); }
|
||||||
|
.run-live { animation: run-pulse 1.6s ease-in-out infinite; }
|
||||||
|
@keyframes run-pulse { 50% { opacity: .55; } }
|
||||||
|
.run-status { font-weight: 600; }
|
||||||
|
.run-teams { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 8px; text-align: center; }
|
||||||
|
.run-team { font: 700 1.25rem var(--display); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.run-team.a { color: var(--team-a); text-align: right; }
|
||||||
|
.run-team.b { color: var(--team-b); text-align: left; }
|
||||||
|
.run-score { font: 700 2.25rem var(--display); font-variant-numeric: tabular-nums; }
|
||||||
|
.run-score i { font-style: normal; color: var(--muted); padding: 0 4px; }
|
||||||
|
.run-vs { font-size: 1rem; color: var(--muted); font-style: normal; }
|
||||||
|
.run-foot { display: flex; justify-content: space-between; gap: 8px; font-size: .875rem; color: var(--muted); }
|
||||||
|
.run-series { font-variant-numeric: tabular-nums; }
|
||||||
Binary file not shown.
@@ -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');
|
||||||
}
|
}
|
||||||
@@ -791,7 +791,14 @@ export class MatchService extends EventEmitter {
|
|||||||
return {
|
return {
|
||||||
seriesScore: maps.length ? seriesScore(maps) : null,
|
seriesScore: maps.length ? seriesScore(maps) : null,
|
||||||
currentMap: current && (current.status === 'live' || current.status === 'done')
|
currentMap: current && (current.status === 'live' || current.status === 'done')
|
||||||
? { number: current.number, name: this.mapName(m, current.map), score: { A: current.score_a, B: current.score_b }, finished: current.status === 'done' }
|
? {
|
||||||
|
number: current.number,
|
||||||
|
name: this.mapName(m, current.map),
|
||||||
|
image: JSON.parse(m.maps).find((x) => x.id === current.map)?.image ?? null,
|
||||||
|
color: JSON.parse(m.maps).find((x) => x.id === current.map)?.color ?? null,
|
||||||
|
score: { A: current.score_a, B: current.score_b },
|
||||||
|
finished: current.status === 'done',
|
||||||
|
}
|
||||||
: null,
|
: null,
|
||||||
id: m.id,
|
id: m.id,
|
||||||
teamA: m.team_a_name,
|
teamA: m.team_a_name,
|
||||||
@@ -944,6 +951,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,
|
||||||
|
|||||||
@@ -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}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user