Admin: Live-Konsole der Container und Live-Log der Docker-Hosts (nur lesen, Passwörter ausgeblendet)

This commit is contained in:
mregosar committed 2026-11-01 17:16:58 +01:00
1 parent fb96fb5d08
commit ee9c488a7e
9 files changed
+328 -3

No files matched your search

+22 -2
View File
@@ -4,6 +4,7 @@ import { api } from '../lib/api.js';
import { FORMAT_LABEL, serverLabel } from '../lib/labels.js';
import CopyField from '../components/CopyField.jsx';
import MapImage from '../components/MapImage.jsx';
import ConsoleDialog from '../components/ConsoleDialog.jsx';
const KEY = 'cs2mv:admin';
const FILTER_KEY = 'cs2mv:admin:filter';
@@ -327,6 +328,7 @@ function RollbackPanel({ matchId, token, reload, setNotice }) {
function MatchAdminCard({ m, run, busy, token, reload, setNotice }) {
const [open, setOpen] = useState(false);
const [showConsole, setShowConsole] = useState(false);
const server = serverLabel(m.server);
const closed = ['finished', 'cancelled'].includes(m.status);
const id = m.id;
@@ -395,6 +397,9 @@ function MatchAdminCard({ m, run, busy, token, reload, setNotice }) {
{m.server && ['start_failed', 'failed', 'stopped', 'config_failed', 'ready'].includes(m.server.status) && !closed &&
btn('Server neu starten / wiederholen', { path: '/server/retry' })}
{m.server && !['stopped'].includes(m.server.status) && btn('Server stoppen', { path: '/server/stop', confirm: 'Server stoppen?' })}
{m.server && m.server.status !== 'stopped' && (
<button className="btn btn-sm" onClick={() => setShowConsole(true)}>Konsole</button>
)}
{!closed && btn('Match abschließen', { path: '/finish', confirm: 'Match als abgeschlossen markieren?' })}
{!closed && btn('Match abbrechen', { path: '/cancel', confirm: 'Match abbrechen?' }, 'btn-ghost danger')}
{closed && !m.archivedAt && btn('Archivieren', { path: '/archive', success: 'Match archiviert' })}
@@ -402,6 +407,10 @@ function MatchAdminCard({ m, run, busy, token, reload, setNotice }) {
{btn('Löschen', { method: 'DELETE', path: '', confirm: `Match ${m.teams.A.name} vs. ${m.teams.B.name} endgültig löschen?`, success: 'Match gelöscht' }, 'btn-ghost danger')}
</div>
{showConsole && (
<ConsoleDialog title={`${m.teams.A.name} vs. ${m.teams.B.name}`} path={`/admin/logs/match/${id}`} token={token} onClose={() => setShowConsole(false)} />
)}
{open && (
<div className="admin-details">
<div className="admin-grid">
@@ -488,14 +497,19 @@ function HostUsageLive({ token }) {
clearInterval(t);
};
}, [token]);
return <HostUsage hosts={hosts} />;
return <HostUsage hosts={hosts} token={token} />;
}
/** Auslastung der Docker-Hosts: wie viele Gameserver läuft auf welchem Host, mit Liste der Matches. */
function HostUsage({ hosts }) {
function HostUsage({ hosts, token }) {
const [logFor, setLogFor] = useState(null);
if (hosts.length === 0) return null;
return (
<section className="host-usage">
{logFor && (
<ConsoleDialog title={`Docker-Host ${logFor}`} path={`/admin/host-log/${encodeURIComponent(logFor)}`} token={token}
note="Nur lesen: Ereignisse des Hosts der letzten 2 Stunden und alles Neue." onClose={() => setLogFor(null)} />
)}
<h2>Docker-Hosts</h2>
<div className="host-grid">
{hosts.map((h) => {
@@ -506,6 +520,7 @@ function HostUsage({ hosts }) {
<strong>{h.name}</strong>
<span className="muted small">{h.publicHost}</span>
<span className="host-count">{h.used} / {h.total} belegt</span>
{token && <button className="btn btn-sm host-log-btn" onClick={() => setLogFor(h.name)}>Live-Log</button>}
</div>
<div className="host-bar" role="progressbar" aria-valuenow={percent} aria-valuemin={0} aria-valuemax={100}>
<span style={{ width: `${percent}%` }} />
@@ -831,6 +846,7 @@ const KNIFE_STATUS = {
/** Messer-Server für die Seitenwahl: laufen dauerhaft, setzen sich nach jeder Messerrunde selbst zurück. */
function KnifePanel({ token, setNotice }) {
const [data, setData] = useState(null);
const [consoleFor, setConsoleFor] = useState(null);
const [mode, setMode] = useState('');
const [map, setMap] = useState('');
const [busy, setBusy] = useState(false);
@@ -904,6 +920,7 @@ function KnifePanel({ token, setNotice }) {
<button className="btn btn-sm" disabled={busy || !ready} onClick={() => run(() => post(`/${k.id}/round`), 'Messerrunde gestartet')}>Runde starten</button>
<button className="btn btn-sm" disabled={busy || !ready} onClick={() => run(() => post(`/${k.id}/reset`), 'Server zurückgesetzt')}>Zurücksetzen</button>
<button className="btn btn-sm" disabled={busy} onClick={() => run(() => post(`/${k.id}/restart`), 'Server wird neu gestartet', `${k.name} komplett neu starten?`)}>Neu starten</button>
<button className="btn btn-sm" onClick={() => setConsoleFor(k)}>Konsole</button>
<button className="btn btn-sm btn-ghost danger" disabled={busy} onClick={() => run(() => api(`/admin/knife/${k.id}`, { method: 'DELETE', token }), `${k.name} entfernt`, `${k.name} stoppen und entfernen?`)}>Entfernen</button>
</div>
</div>
@@ -911,6 +928,9 @@ function KnifePanel({ token, setNotice }) {
})}
</div>
)}
{consoleFor && (
<ConsoleDialog title={consoleFor.name} path={`/admin/logs/knife/${consoleFor.id}`} token={token} onClose={() => setConsoleFor(null)} />
)}
<div className="admin-actions knife-new">
<select value={modeInfo?.id ?? ''} onChange={(e) => chooseMode(e.target.value)} aria-label="Modus für den neuen Messer-Server" disabled={busy || full}>
{data.modes.map((m) => <option key={m.id} value={m.id}>{m.label}</option>)}