Compare commits

...
4 Commits
12 changed files with 154 additions and 33 deletions

No files matched your search

+1 -1
View File
@@ -65,7 +65,7 @@ KNIFE_MAP=de_mirage
KNIFE_WARMUP_SECONDS=90
KNIFE_MAX_SERVERS=4
# Anfang des Servernamens im Serverbrowser, danach folgen die Teamnamen
CS2_SERVERNAME_PREFIX=MaxLAN
CS2_SERVERNAME_PREFIX=Maxlan
DOCKER_BIN=docker
# --- Static-Pool-Adapter (SERVER_ADAPTER=static) ---
+9 -8
View File
@@ -194,7 +194,7 @@ Die Wahl wird als `mp_teamname_1` (Team mit CT-Start) und `mp_teamname_2` (T-Sta
- **Map-Bilder** liegen lokal unter `client/public/maps/<map>.webp` (960 px breit, je ca. 50–90 KB). `"image"` in `map-pools.json` verweist darauf. Neue oder fehlende Bilder lädt `npm run images` von [ghostcap-gaming/cs2-map-images](https://github.com/ghostcap-gaming/cs2-map-images), verkleinert sie und trägt den Pfad ein (`npm run images -- --force` lädt alle neu). Die Bilder stammen aus dem Spiel; die Rechte liegen bei Valve. Ohne Bild zeigt die Kachel ein Muster in der Map-Farbe.
- **Seiten-Embleme** für CT und T liegen unter `client/public/sides/ct.png` und `client/public/sides/t.png` (quadratisch, z. B. 256×256). Fehlt eine Datei, erscheint ein schlichtes Ersatzsymbol.
- **Logo und Farben:** Das MaxLAN-e.V.-Logo liegt freigestellt unter `client/public/brand/maxlan-logo.webp`. Die Farben stehen als Variablen am Anfang von `client/src/styles.css`: Logo-Blau `#344A99` für Team A und Hauptbuttons, Logo-Rot `#A72A1C` für Team B und den Abschluss der Kopfzeile, jeweils aufgehellt für die Lesbarkeit.
- **Logo und Farben:** Das Maxlan-e.V.-Logo liegt freigestellt unter `client/public/brand/maxlan-logo.webp`. Die Farben stehen als Variablen am Anfang von `client/src/styles.css`: Logo-Blau `#344A99` für Team A und Hauptbuttons, Logo-Rot `#A72A1C` für Team B und den Abschluss der Kopfzeile, jeweils aufgehellt für die Lesbarkeit.
- Bestehende Matches behalten die Map-Liste, mit der sie erstellt wurden. Neue Bilder erscheinen in neu erstellten Matches.
> Die mitgelieferten Map-Pools entsprechen dem Stand der Entwicklung. Valve ändert den aktiven Map-Pool regelmäßig. Map-IDs bitte gegen die eigene Serverinstallation prüfen. Eine Map, die nicht existiert, führt zu „Konfiguration fehlgeschlagen“ mit Meldung.
@@ -271,10 +271,10 @@ Jeder Host braucht eigene Spieldateien (rund 60 bis 90 GB). Die Ports müssen au
### Messer-Server für die Seitenwahl
Im Admin-Bereich (Reiter „Matches“) startet „Messer-Server starten“ einen Gameserver, der keinem Match gehört und dauerhaft läuft. Teams verbinden sich, spielen eine Messerrunde und gehen wieder.
Im Admin-Bereich (Einstellungen → Gameserver → Messer-Server) startet „Messer-Server starten“ einen Gameserver, der keinem Match gehört und dauerhaft läuft. Teams verbinden sich, spielen eine Messerrunde und gehen wieder.
- Modus wählbar: **Competitive 5v5** (12 Plätze, Maps aus dem Competitive-Pool) oder **Wingman 2v2** (6 Plätze, Maps aus dem Wingman-Pool).
- Offen, ohne Passwort, sichtbar im LAN-Reiter von Steam (`MaxLAN | Messer 1 5v5 (Seitenwahl)`) und auf der Startseite mit „Verbinden“.
- Offen, ohne Passwort, sichtbar im LAN-Reiter von Steam (`Maxlan | Messer 1 5v5 (Seitenwahl)`) und auf der Startseite mit „Verbinden“.
- Nur Messer, kein Geld, eine Runde. Vorher läuft eine Aufwärmphase zum Verbinden (`KNIFE_WARMUP_SECONDS`, Standard 90). Nach der Runde startet das Spiel von selbst neu (`mp_match_end_restart`). Wird der Server leer, setzt die App ihn zusätzlich zurück.
- Fällt ein Messer-Server aus, startet die App ihn neu, höchstens fünfmal in Folge. Danach steht er auf „ausgefallen“ und lässt sich im Admin-Bereich neu starten.
- Knöpfe pro Server: „Runde starten“ (Aufwärmphase sofort beenden), „Zurücksetzen“, „Neu starten“ (neuer Container), „Entfernen“.
@@ -288,7 +288,7 @@ Der LAN-Reiter des Steam-Serverbrowsers (Steam → Ansicht → Spielserver → L
- Pro Host lassen sich die LAN-Ports in `CS2_DOCKER_HOSTS` mit `"lanPorts":"27016-27019"` einschränken, wenn dort schon etwas anderes auf einem dieser Ports läuft. `"lanPorts":""` schaltet es für einen Host ab. Im Admin-Bereich steht das in der Spalte „LAN-Ports (Steam)“.
- Ist ein Port trotzdem belegt, versucht der Start automatisch die nächsten freien Ports.
- Server auf LAN-Ports laufen im Netz des Hosts (`--network host`), weil die Port-Weiterleitung von Docker die Suchanfragen (Broadcasts) des Serverbrowsers nicht weitergibt. Die Firewall des Hosts muss die Ports deshalb selbst erlauben.
- Der Servername im Browser ist `MaxLAN | Team A vs. Team B`. Den Anfang ändert `CS2_SERVERNAME_PREFIX`.
- Der Servername im Browser ist `Maxlan | Team A vs. Team B`. Den Anfang ändert `CS2_SERVERNAME_PREFIX`.
- Die Server sind mit Passwort geschützt und zeigen ein Schloss. Ohne `CS2_GSLT` laufen sie mit `sv_lan 1` und erscheinen nicht im Internet-Reiter.
### Option B: Vorhandene Server (`SERVER_ADAPTER=static`)
@@ -459,8 +459,6 @@ Unter `/admin` mit dem Passwort aus `ADMIN_PASSWORD` (leer = Admin-Bereich aus).
### Reiter „Matches“
- **Docker-Hosts:** pro Host belegte und freie Plätze, freier Speicher dort, wo das Gameserver-Volume liegt, und die Matches bzw. Messer-Server darauf.
- **Messer-Server (Seitenwahl):** starten (Modus 5v5 oder 2v2 und Map wählen), Runde starten, zurücksetzen, neu starten, entfernen. Siehe „Messer-Server für die Seitenwahl“.
- **Suche und Filter:** Suche nach Team, Spieler oder Match-ID. Filter Alle, Laufend, Fehler (Zahl rot, wenn es Fehler gibt), Abgeschlossen und Archiv. Der Filter bleibt im Browser gespeichert.
- **Matchkarte:** Teams, Modus, Format, Status, Live-Stand, Server- und CSTV-Adresse (`Server 192.168.178.45:27015 · CSTV …:27115`), Zeitpunkt der Server-Anforderung, Pausen-Abzeichen. Bei einem Serverfehler steht die Ursache rot in der Karte.
- **Aktionen:** Ansehen, Veto starten, Aufwärmphase beenden, Pausieren/Fortsetzen, Nächste Map, Server neu starten / wiederholen, Server stoppen, Match abschließen, Match abbrechen, Archivieren / Aus dem Archiv holen, Löschen. Destruktive Aktionen fragen nach.
@@ -471,9 +469,12 @@ Startet man den Server eines laufenden Matches neu, lädt die Anwendung danach d
### Reiter „Einstellungen“
Die Einstellungen sind in eine Seitenleiste gegliedert: **Veto** (Map-Pools), **Gameserver** (Match-Regeln, CSTV, Zeiten, Messer-Server), **Server-Betrieb** (Docker-Hosts und Image-Update) und **Benachrichtigungen** (Discord). Am Handy stehen die Menüpunkte als Knöpfe über dem Inhalt. Der zuletzt gewählte Punkt bleibt im Browser gespeichert.
- **Messer-Server:** starten (Modus 5v5 oder 2v2 und Map wählen), Runde starten, zurücksetzen, neu starten, entfernen. Siehe „Messer-Server für die Seitenwahl“.
- **Map-Pools:** pro Modus wählen, welche Maps im Veto stehen. Der Veto-Ablauf passt sich der Anzahl an, siehe „Maps im Admin-Bereich wählen“.
- **Gameserver:** Runden Competitive und Wingman, Verlängerung, zusätzliche Konsolenbefehle, CSTV an/aus und Verzögerung, Zeit pro Veto-Schritt, Leerlauf-Frist, maximale Laufzeit, Startzeit (inklusive Download) und Archiv-Frist. Die Werte aus der `.env` sind die Vorgabe. Was hier geändert wird, steht in der Datenbank und gilt auch nach einem Neustart. Mit „live“ markierte Werte gehen beim Speichern sofort per RCON an alle laufenden Server. Befehle, die Passwörter ändern oder den Server beenden, sind gesperrt.
- **Docker-Hosts:** Hosts hinzufügen, entfernen und bearbeiten (Name, Docker-Ziel, Adresse, Portbereich, LAN-Ports, Volume). Die Liste ersetzt `CS2_DOCKER_HOSTS`. „Liste aus .env verwenden“ schaltet zurück. Hosts mit laufenden Servern lassen sich nicht entfernen und ihre Adresse nicht ändern.
- **Docker-Hosts:** oben die Auslastung pro Host (belegte und freie Plätze, freier Speicher dort, wo das Gameserver-Volume liegt, Matches und Messer-Server darauf), darunter Hosts hinzufügen, entfernen und bearbeiten (Name, Docker-Ziel, Adresse, Portbereich, LAN-Ports, Volume). Die Liste ersetzt `CS2_DOCKER_HOSTS`. „Liste aus .env verwenden“ schaltet zurück. Hosts mit laufenden Servern lassen sich nicht entfernen und ihre Adresse nicht ändern.
- **Image aktualisieren:** lädt das CS2-Image auf allen Hosts gleichzeitig neu (per SSH direkt auf dem Host). Laufende Server laufen weiter.
- **Discord-Meldungen:** welche Ereignisse gemeldet werden, plus Testmeldung.
@@ -499,7 +500,7 @@ Alle Variablen mit Standardwerten stehen in [`.env.example`](.env.example). Die
| `CS2_PORT_RANGE_START`, `CS2_PORT_RANGE_END` | `27015`, `27115` | Bereich der Spielports; CSTV liegt jeweils `CS2_TV_PORT_OFFSET` darüber |
| `CS2_LAN_PORTS` | `27015-27020` | Spielports für den LAN-Reiter von Steam, werden zuerst vergeben; leer = aus |
| `CS2_TV_PORT_OFFSET` | `100` | Abstand CSTV-Port zum Spielport (27015 → 27115) |
| `CS2_SERVERNAME_PREFIX` | `MaxLAN` | Anfang des Servernamens im Serverbrowser |
| `CS2_SERVERNAME_PREFIX` | `Maxlan` | Anfang des Servernamens im Serverbrowser |
| `CS2_START_TIMEOUT_MINUTES` | `20` | Startzeit eines Servers inklusive Download der Spieldateien |
| `KNIFE_MAP` | `de_mirage` | Standard-Map für Messer-Server |
| `KNIFE_WARMUP_SECONDS` | `90` | Zeit zum Verbinden, bevor die Messerrunde beginnt |
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#000000" />
<title>CS2 Mapveto – MaxLAN e.V.</title>
<title>CS2 Mapveto – Maxlan e.V.</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='6' fill='%23000'/><path d='M8 6l16 20M24 6L8 26' stroke='%23a72a1c' stroke-width='5' stroke-linecap='round'/></svg>" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
+2 -2
View File
@@ -32,8 +32,8 @@ function MainLayout() {
return (
<div className="app">
<header className="topbar">
<Link to="/" className="brand" aria-label="MaxLAN e.V. CS2 Mapveto – zur Übersicht">
<img src="/brand/maxlan-logo.webp" alt="MaxLAN e.V." className="brand-logo" width="743" height="174" />
<Link to="/" className="brand" aria-label="Maxlan e.V. CS2 Mapveto – zur Übersicht">
<img src="/brand/maxlan-logo.webp" alt="Maxlan e.V." className="brand-logo" width="743" height="174" />
<span className="brand-app">CS2 Mapveto</span>
</Link>
{config?.server.simulated && (
+112 -13
View File
@@ -59,7 +59,6 @@ const storeToken = (t) => {
export default function AdminPage() {
const [token, setToken] = useState(getToken);
const [matches, setMatches] = useState(null);
const [hosts, setHosts] = useState([]);
const [error, setError] = useState(null);
const [notice, setNotice] = useState(null);
const [busy, setBusy] = useState(null);
@@ -90,7 +89,6 @@ export default function AdminPage() {
const res = await api(`/admin/matches${filter === 'archive' ? '?archived=1' : ''}`, { token });
setMatches(res.matches);
setArchivedCount(res.archivedCount ?? 0);
setHosts(res.hosts ?? []);
setError(null);
} catch (err) {
if (err.status === 401) {
@@ -147,14 +145,9 @@ export default function AdminPage() {
<button className={`admin-tab ${tab === 'settings' ? 'is-active' : ''}`} onClick={() => setTab('settings')}>Einstellungen</button>
</nav>
{tab === 'settings' && token && <MapPoolsPanel token={token} setNotice={setNotice} />}
{tab === 'settings' && token && <ServerSettingsPanel token={token} setNotice={setNotice} />}
{tab === 'settings' && token && <DockerHostsPanel token={token} setNotice={setNotice} />}
{tab === 'settings' && token && <NotificationSettings token={token} setNotice={setNotice} />}
{tab === 'settings' && token && <SettingsLayout token={token} setNotice={setNotice} />}
{notice && <div className={`toast toast-${notice.tone}`} role="status">{notice.text}</div>}
{error && <div className="alert alert-error">{error}</div>}
{tab === 'matches' && <HostUsage hosts={hosts} />}
{tab === 'matches' && <KnifePanel token={token} setNotice={setNotice} />}
{tab === 'matches' && matches === null && !error && <p className="muted">Lade Matches …</p>}
{tab === 'matches' && (
<MatchToolbar
@@ -479,6 +472,22 @@ function MatchAdminCard({ m, run, busy, token, reload, setNotice }) {
</section>
);
}
/** Auslastung der Docker-Hosts, regelmäßig neu geladen (Einstellungen → Docker-Hosts). */
function HostUsageLive({ token }) {
const [hosts, setHosts] = useState([]);
useEffect(() => {
let alive = true;
const load = () => api('/admin/hosts', { token }).then((res) => alive && setHosts(res.hosts)).catch(() => {});
load();
const t = setInterval(load, REFRESH_MS);
return () => {
alive = false;
clearInterval(t);
};
}, [token]);
return <HostUsage hosts={hosts} />;
}
/** Auslastung der Docker-Hosts: wie viele Gameserver läuft auf welchem Host, mit Liste der Matches. */
function HostUsage({ hosts }) {
if (hosts.length === 0) return null;
@@ -534,7 +543,7 @@ const shownDefault = (field) => {
};
/** Gameserver-Einstellungen: Vorgaben aus der .env, im Admin-Bereich änderbar. */
function ServerSettingsPanel({ token, setNotice }) {
function ServerSettingsPanel({ token, setNotice, groups = SETTING_GROUPS.map((g) => g.key), title = 'Gameserver' }) {
const [fields, setFields] = useState(null);
const [values, setValues] = useState({});
const [busy, setBusy] = useState(false);
@@ -598,13 +607,13 @@ function ServerSettingsPanel({ token, setNotice }) {
return (
<section className="admin-settings">
<h2>Gameserver</h2>
<h2>{title}</h2>
<p className="small muted">
Markierte Werte (live) gehen beim Speichern sofort an alle laufenden Server. Die übrigen gelten für neue Server und Vetos.
</p>
{SETTING_GROUPS.map((g) => (
{SETTING_GROUPS.filter((g) => groups.includes(g.key)).map((g) => (
<div key={g.key} className="setting-group">
<h3>{g.title}</h3>
{groups.length > 1 && <h3>{g.title}</h3>}
<div className="setting-grid">
{fields.filter((f) => f.group === g.key).map((f) => (
<div key={f.key} className="setting-item">
@@ -667,7 +676,14 @@ function DockerHostsPanel({ token, setNotice }) {
}, [pulling, load]);
if (!data) return null;
if (!data.editable) return null;
if (!data.editable) {
return (
<section className="admin-settings">
<h2>Docker-Hosts</h2>
<p className="muted">Docker-Hosts und Image-Update gibt es nur mit dem Docker-Adapter (<code>SERVER_ADAPTER=docker</code>).</p>
</section>
);
}
const original = JSON.stringify(data.hosts.map((h) => ({ ...EMPTY_HOST, ...h, dockerHost: h.dockerHost ?? '' })));
const dirty = JSON.stringify(rows) !== original;
@@ -1000,3 +1016,86 @@ function MapPoolsPanel({ token, setNotice }) {
</section>
);
}
const SETTINGS_KEY = 'cs2mv:admin:settings';
/** Menüpunkte der Einstellungen, in Gruppen für die Seitenleiste. */
const SETTINGS_MENU = [
{
title: 'Veto',
items: [{ key: 'maps', label: 'Map-Pools', hint: 'Maps im Veto' }],
},
{
title: 'Gameserver',
items: [
{ key: 'match', label: 'Match-Regeln', hint: 'Runden, Verlängerung, Befehle' },
{ key: 'tv', label: 'CSTV', hint: 'an/aus, Verzögerung' },
{ key: 'times', label: 'Zeiten', hint: 'Veto, Leerlauf, Start, Archiv' },
{ key: 'knife', label: 'Messer-Server', hint: 'Seitenwahl, 5v5 und 2v2' },
],
},
{
title: 'Server-Betrieb',
items: [{ key: 'hosts', label: 'Docker-Hosts', hint: 'Hosts und Image-Update' }],
},
{
title: 'Benachrichtigungen',
items: [{ key: 'discord', label: 'Discord', hint: 'Meldungen und Test' }],
},
];
const SETTINGS_KEYS = SETTINGS_MENU.flatMap((g) => g.items.map((i) => i.key));
const readSettingsSection = () => {
try {
const v = localStorage.getItem(SETTINGS_KEY);
return SETTINGS_KEYS.includes(v) ? v : SETTINGS_KEYS[0];
} catch {
return SETTINGS_KEYS[0];
}
};
/** Einstellungen mit Seitenleiste: links die Menüpunkte, rechts der gewählte Bereich. */
function SettingsLayout({ token, setNotice }) {
const [section, setSectionState] = useState(readSettingsSection);
const setSection = (key) => {
setSectionState(key);
try {
localStorage.setItem(SETTINGS_KEY, key);
} catch {
/* ignorieren */
}
};
const current = SETTINGS_MENU.flatMap((g) => g.items).find((i) => i.key === section);
return (
<div className="settings-layout">
<nav className="settings-nav" aria-label="Einstellungen">
{SETTINGS_MENU.map((g) => (
<div key={g.title} className="settings-nav-group">
<div className="settings-nav-title">{g.title}</div>
{g.items.map((i) => (
<button
key={i.key}
className={`settings-nav-item ${section === i.key ? 'is-active' : ''}`}
aria-current={section === i.key ? 'page' : undefined}
onClick={() => setSection(i.key)}
>
<span>{i.label}</span>
<small>{i.hint}</small>
</button>
))}
</div>
))}
</nav>
<div className="settings-content">
{section === 'maps' && <MapPoolsPanel token={token} setNotice={setNotice} />}
{['match', 'tv', 'times'].includes(section) && (
<ServerSettingsPanel key={section} token={token} setNotice={setNotice} groups={[section]} title={current.label} />
)}
{section === 'knife' && <KnifePanel token={token} setNotice={setNotice} />}
{section === 'hosts' && <HostUsageLive token={token} />}
{section === 'hosts' && <DockerHostsPanel token={token} setNotice={setNotice} />}
{section === 'discord' && <NotificationSettings token={token} setNotice={setNotice} />}
</div>
</div>
);
}
+2 -2
View File
@@ -46,7 +46,7 @@ export default function StreamOverviewPage() {
return (
<ScaledStage>
<div className="st-message">
<img src="/brand/maxlan-logo.webp" alt="MaxLAN e.V." />
<img src="/brand/maxlan-logo.webp" alt="Maxlan e.V." />
<h1>Stream-Übersicht nicht verfügbar</h1>
<p>{error}</p>
</div>
@@ -64,7 +64,7 @@ export default function StreamOverviewPage() {
<ScaledStage>
<div className="so-root">
<header className="st-top">
<img className="st-logo" src="/brand/maxlan-logo.webp" alt="MaxLAN e.V." />
<img className="st-logo" src="/brand/maxlan-logo.webp" alt="Maxlan e.V." />
<div className="so-title">
<strong>Counter-Strike 2</strong>
{liveCount > 0 && <span className="so-count so-count-live">{liveCount} live</span>}
+2 -2
View File
@@ -67,7 +67,7 @@ export function ScaledStage({ children }) {
function StreamMessage({ title, text }) {
return (
<div className="st-message">
<img src="/brand/maxlan-logo.webp" alt="MaxLAN e.V." />
<img src="/brand/maxlan-logo.webp" alt="Maxlan e.V." />
<h1>{title}</h1>
{text && <p>{text}</p>}
</div>
@@ -99,7 +99,7 @@ function StreamContent({ matchId, code }) {
return (
<div className={`st-root phase-${phase} ${phase !== 'lobby' && state.series.length > 0 ? 'has-maps' : 'no-maps'}`}>
<header className="st-top">
<img className="st-logo" src="/brand/maxlan-logo.webp" alt="MaxLAN e.V." />
<img className="st-logo" src="/brand/maxlan-logo.webp" alt="Maxlan e.V." />
<div className="st-meta">
<span>Counter-Strike 2</span>
<span>{state.modeLabel}</span>
+20 -1
View File
@@ -1,6 +1,6 @@
/* ---------- Tokens ---------- */
:root {
/* MaxLAN-Farben aus dem Logo: Blau #344A99, Rot #A72A1C auf Schwarz */
/* Maxlan-Farben aus dem Logo: Blau #344A99, Rot #A72A1C auf Schwarz */
--brand-blue: #344a99;
--brand-red: #a72a1c;
@@ -626,3 +626,22 @@ input:focus, select:focus { border-color: var(--link); outline: none; }
.flow-steps .flow-ban { color: var(--error); border-color: color-mix(in srgb, var(--error) 50%, transparent); }
.flow-steps .flow-pick { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 50%, transparent); }
.flow-steps .flow-decider { color: var(--text); border-color: var(--primary); }
/* Admin-Einstellungen mit Seitenleiste */
.settings-layout { display: grid; grid-template-columns: 230px minmax(0, 1fr); gap: 20px; align-items: start; }
.settings-nav { position: sticky; top: 16px; display: flex; flex-direction: column; gap: 14px; }
.settings-nav-title { font-size: .72rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); margin: 0 0 4px 10px; }
.settings-nav-group { display: flex; flex-direction: column; gap: 2px; }
.settings-nav-item { display: flex; flex-direction: column; align-items: flex-start; gap: 1px; width: 100%; text-align: left; font: inherit; color: var(--text); background: transparent; border: 1px solid transparent; border-left: 3px solid transparent; border-radius: 4px; padding: 7px 10px; cursor: pointer; }
.settings-nav-item small { font-size: .75rem; color: var(--muted); }
.settings-nav-item:hover { background: var(--surface); }
.settings-nav-item.is-active { background: var(--surface); border-color: var(--line); border-left-color: var(--primary); }
.settings-content > .admin-settings { margin-bottom: 0; }
@media (max-width: 760px) {
.settings-layout { grid-template-columns: minmax(0, 1fr); gap: 12px; }
.settings-nav { position: static; flex-direction: row; flex-wrap: wrap; gap: 6px; }
.settings-nav-group { flex-direction: row; flex-wrap: wrap; gap: 6px; }
.settings-nav-title { display: none; }
.settings-nav-item { width: auto; white-space: nowrap; border: 1px solid var(--line); border-radius: 999px; padding: 5px 12px; }
.settings-nav-item small { display: none; }
.settings-nav-item.is-active { border-color: var(--primary); }
}
Binary file not shown.
+3 -1
View File
@@ -164,6 +164,9 @@ export function createAdminRouter({ service, manager, config, settings, knife, p
}));
// Alle Matches mit allen Details (Codes, Verbindungsdaten, Spieler, Veto, Serie, Ereignisse)
// Auslastung und Speicher der Docker-Hosts mit den Servern darauf
router.get('/hosts', requireAdmin, wrap(async () => ({ hosts: await manager.hostUsage() })));
// ?archived=1 liefert das Archiv, sonst alle nicht archivierten Matches
router.get('/matches', requireAdmin, wrap(async (req) => {
const archived = req.query.archived === '1';
@@ -174,7 +177,6 @@ export function createAdminRouter({ service, manager, config, settings, knife, p
return {
matches: ids.map(({ id }) => service.snapshot(id, adminViewer(id))),
archivedCount,
hosts: await manager.hostUsage(),
};
}));
+1 -1
View File
@@ -155,7 +155,7 @@ export function loadConfig(overrides = {}) {
// Abstand des CSTV-Ports zum Spielport, z. B. Spiel 27015 -> CSTV 27115
tvPortOffset: int('CS2_TV_PORT_OFFSET', 100),
// Anfang des Servernamens im Serverbrowser, danach folgen die Teamnamen
serverNamePrefix: str('CS2_SERVERNAME_PREFIX', 'MaxLAN'),
serverNamePrefix: str('CS2_SERVERNAME_PREFIX', 'Maxlan'),
// Optional: mehrere Docker-Hosts als JSON-Liste, z. B.
// [{"name":"host1","dockerHost":"ssh://cs2@192.168.178.111","publicHost":"192.168.178.111","portStart":27100,"portEnd":27139}]
hosts: parseDockerHosts(str('CS2_DOCKER_HOSTS')),
+1 -1
View File
@@ -90,7 +90,7 @@ export class DockerServerAdapter {
return plan;
}
/** Servername im Steam-Serverbrowser, z. B. "MaxLAN | Team A vs. Team B". */
/** Servername im Steam-Serverbrowser, z. B. "Maxlan | Team A vs. Team B". */
serverName(matchId, teamNames) {
const prefix = this.options.serverNamePrefix ? sanitizeConsoleText(this.options.serverNamePrefix, 24) : '';
if (!teamNames) return `${prefix} ${matchId}`.trim();