Admin: Live-Konsole der Container und Live-Log der Docker-Hosts (nur lesen, Passwörter ausgeblendet)
This commit is contained in:
1 parent
fb96fb5d08
commit
ee9c488a7e
9 files changed
+328
-3
No files matched your search
@@ -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>)}
|
||||
|
||||
Reference in new issue
Block a user