Compare commits

..
Author SHA1 Message Date
mregosar b3fd2f1351 Merge feature/steam-lan-browser in main 2026-10-29 14:37:06 +01:00
mregosar af58156209 Merge feature/admin-server-zeit in main 2026-10-29 14:37:06 +01:00
mregosar b5fa27fbd3 Server im LAN-Reiter des Steam-Serverbrowsers: LAN-Ports, Host-Netz, Teamnamen, CSTV +100 2026-10-29 14:37:06 +01:00
mregosar 89f579a014 Merge fix/alte-server-handles in main 2026-10-29 14:37:06 +01:00
mregosar 3cc682f8d5 Admin: Datum und Uhrzeit der Server-Anforderung anzeigen 2026-10-29 14:37:06 +01:00
mregosar 685509cf4c Docker: alte Server ohne passenden Host-Namen über die Adresse finden 2026-10-29 14:37:06 +01:00
mregosar ab3c94bffd Merge feature/admin-server-einstellungen in main 2026-10-29 14:37:05 +01:00
mregosar 69d620bb19 Merge feature/admin-server-adresse in main 2026-10-29 14:37:05 +01:00
mregosar c1c9e7a9f9 Admin: Gameserver-Einstellungen, Docker-Hosts und Image-Update im Admin-Bereich 2026-10-29 14:37:05 +01:00
mregosar 657934b8c4 Merge feature/host-disk in main 2026-10-29 14:37:05 +01:00
mregosar fcee62c146 Admin: Adresszeile kompakt gestalten, bei gestopptem Server ausblenden 2026-10-29 14:37:05 +01:00
mregosar 0f3a66fed5 Merge feature/host-usage in main 2026-10-29 14:37:05 +01:00
mregosar dfe6a7d218 Admin: freier Speicher pro Docker-Host anzeigen
Die Karte Docker-Hosts zeigt den freien Speicher unter /var/lib/docker, per SSH
mit df gemessen und eine Minute zwischengespeichert. Unter 20 GB wird die
Zeile rot.
2026-10-29 14:37:05 +01:00
mregosar 39fe8da5f5 Speichermessung: Bind-Ordner eines Volumes auch ohne laufenden Container finden 2026-10-29 14:37:05 +01:00
mregosar 50dcb2537d Merge feature/multi-docker-host in main 2026-10-29 14:37:05 +01:00
mregosar 69fb27f44b Admin: Auslastung der Docker-Hosts mit den laufenden Matches
Die Admin-Übersicht zeigt pro Docker-Host belegte und freie Plätze und die
Matches, die dort laufen. Ohne konfigurierte Hosts bleibt die Karte aus.
2026-10-29 14:37:05 +01:00
mregosar a6c7ba1a23 Admin: freien Speicher dort messen, wo das Gameserver-Volume liegt 2026-10-29 14:37:05 +01:00
mregosar 436f605356 Mehrere Docker-Hosts: Matches verteilt auf die Hosts starten
CS2_DOCKER_HOSTS nimmt eine Liste von Docker-Hosts. Jedes Match läuft auf
dem Host mit den meisten freien Ports und merkt sich seinen Host, damit
Status, Konfiguration und Stopp dort landen. Ohne die Variable bleibt alles
beim bisherigen einzelnen Host. deploy/docker-host-einrichten.sh richtet
einen Host per SSH ein.
2026-10-29 14:37:05 +01:00
mregosar f40ca1c55f Admin: IP und Ports des Servers in der Matchkarte anzeigen 2026-10-29 14:37:05 +01:00
mregosar 1a0ab7ef00 Merge feature/public-live-view in main 2026-10-29 14:37:04 +01:00
mregosar c4b7b4c1ec Merge feature/overview-live-cards in main 2026-10-29 14:37:04 +01:00
mregosar 54bb4aca12 Gäste sehen laufende Matches: Veto, Serienstand und Map-Reihenfolge
Ohne Zugang sind Veto und Serienstand sichtbar. Spielerlisten, Codes,
Verbindungsdaten und das Ereignisprotokoll bleiben Teilnehmern vorbehalten.
2026-10-29 14:37:04 +01:00
mregosar ca7cbffeac Merge docs/admin-hilfe in main 2026-10-29 14:37:04 +01:00
mregosar e15677a3f5 Übersicht: laufende Matches als Karten mit Map, Stand und Serie 2026-10-29 14:37:04 +01:00
mregosar 7211284d7e Merge feature/admin-status in main 2026-10-29 14:37:04 +01:00
mregosar a728f47519 Docs: Hilfe für Turnier-Admins (PDF mit Screenshots) 2026-10-29 14:37:04 +01:00
mregosar 9f34777d10 Admin: Pausenanzeige, Live-Stand und passender Umschalter
Pausierte Matches zeigen ein Badge und den Knopf Fortsetzen. Laufende Matches
zeigen Map, Stand und Runde. Der Pausenzustand wird gespeichert und bei
Mapwechsel oder Serverstopp zurückgesetzt.
2026-10-29 14:37:04 +01:00
mregosar c4debd3014 Merge feature/remove-real-logs in main 2026-10-29 07:38:38 +01:00
mregosar 5886461a9e Echte Logdatei durch anonymisierte Fassung ersetzen
Die Testdatei enthielt Spielernamen und SteamID eines echten Accounts.
Sie ist jetzt durch eine Kopie mit Platzhaltern ersetzt, der Parser-Test
prüft weiterhin echte CS2-Logzeilen.
2026-10-29 07:38:38 +01:00
mregosar 09e73a9527 Merge docs/reverse-proxy in main 2026-10-29 07:32:41 +01:00
mregosar 3e4a85f76c README: Betrieb hinter einem Reverse-Proxy
Abschnitt mit nginx-Beispiel, Hinweisen zu Subdomain, WebSockets,
PUBLIC_URL und TRUST_PROXY sowie zur Absicherung. TRUST_PROXY steht
jetzt auch in der Variablentabelle.
2026-10-29 07:32:41 +01:00
mregosar 790109405a Merge feature/veto-timer-bar in main 2026-10-26 14:02:46 +01:00
mregosar f24b2bedbc Veto-Timer als Leiste in Teamfarbe
Die verbleibende Zeit des Veto-Schritts erscheint als Leiste mit
Sekundenanzeige, in den letzten 10 Sekunden rot. Der Server schickt
die Schrittdauer mit, damit die Leiste zu VETO_STEP_SECONDS passt.
2026-10-26 14:02:46 +01:00
mregosar 656bfbaadf Merge feature/veto-timer in main 2026-10-26 11:26:34 +01:00
mregosar fcc9371a9e Merge feature/admin-settings in main 2026-10-26 11:26:25 +01:00
mregosar be474ee143 Admin: Reiter Matches und Einstellungen, Discord-Embeds, CSTV-Zugang
Der Admin-Bereich bekommt Reiter. Die Discord-Einstellungen liegen
unter „Einstellungen“. Meldungen sind als Embeds formatiert, und die
Match-Startmeldung enthält den CSTV-Zugang (Passwort und Adresse).
2026-10-26 11:03:54 +01:00
mregosar a7539091ff Veto-Timer: 30 Sekunden pro Schritt, danach automatische Auswahl
Läuft die Frist eines Veto-Schritts ab (VETO_STEP_SECONDS, Standard 30,
0 = aus), wählt das Backend für das Team am Zug eine zufällige Map bzw.
Seite. Die Veto-Anzeige zeigt die verbleibende Zeit.
2026-10-26 11:02:33 +01:00
mregosar 301da179ed Merge feature/discord-webhook in main 2026-10-26 01:12:15 +01:00
mregosar 7bceceb31d Discord-Webhook für Streamer-Meldungen mit wählbaren Ereignissen
Meldungen zu Veto, Match-Start, Map- und Serienende, Matchende und
Serverfehlern gehen an DISCORD_WEBHOOK_URL. Im Admin-Bereich wählt man,
welche Ereignisse gemeldet werden, und kann eine Testmeldung senden.
2026-10-26 01:12:15 +01:00
mregosar eee4ceb315 Merge docs/readme-update in main 2026-10-26 00:53:46 +01:00
mregosar c33185db3e README: Match-Übersicht, Pausen und Turnierregeln dokumentiert 2026-10-26 00:53:46 +01:00
mregosar 1da08b256e Merge Rules in main 2026-10-26 00:50:27 +01:00
mregosar 1d627c212a Merge feature/admin-pause in main 2026-10-26 00:50:27 +01:00
mregosar e7f595176a Turnierregeln für die CS2-Server-Konfiguration
Setzt Freezetime 20, Buytime 20, Startgeld 800, Rundenzeit 1:55 und
Halbzeit 1 fest und erlaubt Pausen mit sv_pausable 1. Die Werte
stammen aus FACEIT- und ESL-Richtwerten, nicht aus einer aktuellen
Regel des konkreten Turniers.
2026-10-26 00:48:26 +01:00
mregosar bfe315d70c Admin: Pausieren und Fortsetzen des laufenden Matches
Neue Admin-Aktionen server/pause und server/resume senden
mp_pause_match bzw. mp_unpause_match an den CS2-Server. Nur bei
konfiguriertem Server verfügbar.
2026-10-26 00:48:25 +01:00
mregosar d2a811f177 Merge feature/organizer-stream-link in main 2026-10-26 00:24:41 +01:00
mregosar c69d5548e5 Organisator: Link zur Übersicht des eigenen Matches
Der Organisator bekommt einen Link auf /stream/match/<id>, die
Stream-Übersicht nur für dieses Match. Der Zuschauercode bleibt
außen vor. Ist STREAM_KEY gesetzt, wird der Schlüssel angehängt.
2026-10-26 00:24:41 +01:00
mregosar af4f036515 Merge feature/stream-match-link in main 2026-10-26 00:15:37 +01:00
mregosar 91213d354f Stream-Übersicht: Matches anklickbar, Einzelansicht pro Match
Karten und Listeneinträge der Stream-Übersicht führen zur Übersicht
des jeweiligen Matches (/stream/match/:matchId). Der Zuschauercode
bleibt geheim, daher wird nur die Match-ID verlinkt.
2026-10-26 00:15:37 +01:00
mregosar 4fa4db3640 Merge feature/round-rollback in main 2026-10-25 23:44:59 +01:00
mregosar a98a34d769 Rollback: Rundensicherung per Startparameter aktivieren
CS2 legt Rundensicherungen nur mit -allow_round_backup_files an (sonst
Antwort 'Round backup files are disabled'). Der Docker-Adapter setzt den
Parameter über CS2_ADDITIONAL_ARGS. Am echten Server geprüft: Sicherungen
für Runde 0 bis 2 vorhanden, Zurückspielen auf Runde 1 funktioniert.
2026-10-25 23:44:40 +01:00
mregosar 3bfe087b7d 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.
2026-10-25 23:24:49 +01:00
mregosar 43ecb8a1ad Merge feature/admin-mode in main 2026-10-25 23:16:45 +01:00
mregosar 46d1954a6d Admin-Modus: Verwaltung aller Matches unter /admin
Anmeldung mit ADMIN_PASSWORD (12 h, Sitzung nur im Speicher, Sperre nach
5 Fehlversuchen pro Minute). Alle Matches mit Codes, Servern, Spielern,
Veto und Verlauf. Aktionen: Veto starten bzw. Schritte für beide Teams,
Warmup beenden, nächste Map, Server stoppen/neu starten, abschließen,
abbrechen, Spieler entfernen, Codes erneuern, Match löschen.
Ohne ADMIN_PASSWORD ist der Admin-Modus komplett aus.
2026-10-25 23:16:44 +01:00
mregosar 30ec5359b1 AMP und feste Host-Namen aus Doku und Tests entfernen
Der Docker-Host ist beliebig; Beispiele nutzen jetzt DOCKER-HOST-IP bzw.
den SSH-Alias docker-host statt eines Rechnernamens. Testfixture heißt
echte-logs-wingman.log.
2026-10-25 22:54:38 +01:00
mregosar 5527c2ce58 Stack unabhängig vom Host: deploy/compose.yml mit .env
CS2 Mapveto startet jetzt auf jedem Docker-Host (Linux) mit
'docker compose --env-file deploy/.env -f deploy/compose.yml up -d'.
Die Adresse kommt aus HOST_IP, nichts ist mehr fest eingetragen.
Anleitung für einen neuen Server: Image laden oder bauen, Spieldateien
kopieren oder neu laden, konfigurieren, starten.

Geprüft auf amp-cluster mit Demo-Adapter: Stack startet, Webseite
antwortet, Stack wieder entfernt, Spieldateien unverändert.
2026-10-25 22:46:51 +01:00
mregosar 1b7c13b592 CS2 Mapveto als ein Container (Dockerfile, compose.yml, Anleitung)
Ein Image mit Webseite, Backend, SQLite und Docker-Client; startet
Match-Container über /var/run/docker.sock auf demselben Host. Tests
laufen beim Bauen mit. Auf amp-cluster gebaut (301 MB) und geprüft.

Dazu: Testbefehl für Node 22 angepasst, beim Beenden werden ausstehende
Live-Updates verworfen und laufende Server-Aktionen abgewartet, damit nach
dem Schließen der Datenbank nichts mehr darauf zugreift.
2026-10-25 22:40:52 +01:00
mregosar e3a1ed4a63 Docker-Warteschlange: Update-Check per RCON statt nur per Logzeile erkennen
Die Zeile 'Starting CS2 Dedicated Server' rutscht nach dem Serverstart
schnell aus den letzten Logzeilen. Der nächste Match-Container wartete
dadurch bis zur Zeitgrenze (20 min). Jetzt gilt ein Container als fertig,
sobald er per RCON antwortet; die Logzeile bleibt als zweite Erkennung.
2026-10-25 22:16:57 +01:00
mregosar 84dad0993c Docker-Adapter mit echtem Host getestet, zwei Fehler behoben
- CSTV belegt einen Spielerplatz: Container starten jetzt mit 6 bzw. 12
  Plätzen (Spieler + CSTV + Reserve), sonst kam der letzte Spieler nicht drauf
- 'Live-Daten empfangen' wird auch in der Aufwärmphase angezeigt, wenn
  der Server Logs liefert, die den Spielstand noch nicht ändern
- Anleitung: Serverliste, Plätze, Bots zum Testen

Getestet auf amp-cluster (Docker per SSH, LAN-Modus): Container-Start,
Veto, Konfiguration, Live-Runden, automatischer Mapwechsel, Entfernen.
2026-10-25 22:09:43 +01:00
mregosar 3a109883b3 Docker-Adapter: LAN-Modus ohne GSLT, Container starten nacheinander
Ohne CS2_GSLT starten Match-Container mit CS2_LAN=1. Neue Container
warten, bis der vorige seinen SteamCMD-Update-Check abgeschlossen hat,
damit nie zwei gleichzeitig das gemeinsame Volume aktualisieren.
Anleitung: Docker per SSH (DOCKER_HOST), eindeutige Passwort-Beispiele
mit Warnung vor Sonderzeichen (Startskript nutzt eval).
2026-10-25 21:54:30 +01:00
mregosar 85041b2bff Stream-Übersicht aller laufenden Vetos und Matches
/stream zeigt alle aktiven Matches als Karten (Veto mit Map-Board, Match
mit Live-Stand und Serie), dazu offene Lobbys und Ergebnisse. Raster passt
sich der Anzahl an, ab 7 Matches wird geblättert. Live per Socket, optional
mit STREAM_KEY geschützt, ohne Codes oder Verbindungsdaten.
2026-10-25 19:32:53 +01:00
mregosar ea2207899f Aufwärmphase vor mp_warmup_end als beendet markieren
CS2 sendet Match_Start sofort nach mp_warmup_end, oft bevor die
RCON-Antwort da ist. Bei fehlgeschlagenem Befehl wird die Markierung
zurückgenommen.
2026-10-25 19:23:53 +01:00
mregosar 263c7e7b7c Live-Ergebnisse: Stream-Seite, Anzeige, automatischer Mapwechsel
- Stream-Übersicht unter /stream/<Zuschauercode> (1920x1080, Lobby, Veto, Spielstand)
- Live-Stand, Rundenverlauf und Serienstand auf Match-Seite und Übersicht
- Parser mit echten AMP-Logs geprüft: Game Over mit leerer Map-Gruppe,
  Match_Start vor der Aufwärmphase, veraltete Seitenzuordnung nach der Halbzeit
- Nach Map-Ende automatisch nächste Map bzw. Abschluss der entschiedenen Serie
- LOG_DEBUG zur Fehlersuche, README und .env.example ergänzt
2026-10-25 19:20:10 +01:00
mregosar 306b3aa7e5 Live-Ergebnisse aus CS2-Serverlogs (Backend)
Server bekommt beim Konfigurieren logaddress_add_http mit geheimem
Match-Schlüssel. Ein Parser wertet Rundenende, Seitenzuordnung, Matchstart
und Spielende aus; Stand, Rundenverlauf und Sieger werden pro Map gespeichert
und live verteilt. Der Demo-Adapter simuliert nach der Aufwärmphase einen
Matchverlauf über dieselben Logzeilen.
2026-10-25 18:54:23 +01:00
mregosar 34e5fc7d0e Anleitung: CS2-Server mit Portainer und dauerhaftem Volume
docs/portainer-cs2.md beschreibt Stack, einmaligen Download, Updates ohne
Neu-Download, Anbindung per Static- oder Docker-Adapter, mehrere Server
und Fehlersuche. Standard-Image auf joedwards32/cs2 umgestellt, da
cm2network/cs2 nicht mehr gepflegt wird.
2026-10-25 18:07:17 +01:00
mregosar 8be8b3d879 Organisator kann im selben Browser selbst mitspielen
Ein Aufrufer darf Organisator- und Spieler-Zugang kombinieren
(Authorization + X-Player-Token). Berechtigungen prüfen jetzt einzelne
Merkmale statt einer Rangstufe. In der Lobby gibt es „Selbst mitspielen?“,
Teamlinks funktionieren auch im Organisator-Browser. Online-Status wird
direkt nach dem Beitritt aktualisiert.
2026-10-25 17:53:39 +01:00
mregosar 3a74e16bb8 Größere Map-Kacheln und Ban/Pick-Leiste
Ab dem Veto bekommt das Map-Board die volle Breite (4 Spalten), die
Teamkarten rücken darunter. Die Schrittleiste zeigt pro Schritt das Team.
2026-10-25 17:45:41 +01:00
mregosar f1fc1642ab MaxLAN-Branding: Logo in der Kopfzeile, Farben aus dem Logo
Team A in Logo-Blau, Team B in Logo-Rot, schwarze Kopfzeile mit rotem
Abschluss; CT/T-Farben aus den Emblemen, Warnungen in Orange.
2026-10-25 17:42:38 +01:00
mregosar db4a6bedf7 CT/T-Embleme ergänzt, kein Verbindungshinweis beim ersten Laden 2026-10-25 17:34:03 +01:00
mregosar 7be0d85ab5 Map-Bilder und Seitenwahl mit Emblemen
Map-Bilder aus ghostcap-gaming/cs2-map-images lokal als WebP gespeichert
(npm run images lädt und verkleinert sie). Bilder in Veto-Kacheln,
Pool-Vorschau, Serie und Seitenwahl; CT/T-Embleme aus client/public/sides
mit Ersatzsymbol, falls die Dateien fehlen.
2026-10-25 17:23:55 +01:00
mregosar e304556346 Ein bereiter Spieler pro Team reicht standardmäßig für den Veto-Start
REQUIRE_FULL_TEAMS ist jetzt standardmäßig false; vollständige Teams
lassen sich weiterhin per Umgebungsvariable erzwingen.
2026-10-25 17:11:46 +01:00
mregosar bf438511d6 Lobby zeigt konkret, was einem Team zum Veto-Start fehlt 2026-10-25 17:09:31 +01:00
mregosar f7520a6ca7 Frontend, README und Konfigurationsvorlage
React-Oberfläche mit Übersicht, Match-Erstellung, Beitritt über Team- und
Zuschauercodes, Teamkarten, visuellem Map-Veto, Serie und Serverpanel.
README beschreibt Einrichtung, Umgebungsvariablen, Rollen, Veto-Abläufe,
Serveradapter und CSTV-Zuschauerzugang.
2026-10-25 17:02:18 +01:00
mregosar f08297dd0d Backend: Match-Lobbys, Map-Veto, Echtzeit und CS2-Serveradapter
Express + Socket.IO + SQLite. Veto-Engine mit konfigurierbaren Map-Pools
und validierten Abläufen, rollenbasierte Berechtigungen, Server-Lebenszyklus
mit Demo-, Docker- und Static-Pool-Adapter (RCON), automatisches Herunterfahren.
2026-10-25 16:50:22 +01:00
15 changed files with 278 additions and 33 deletions

No files matched your search

+6
View File
@@ -56,6 +56,12 @@ CS2_DOCKER_EXTRA_ARGS=
# Einrichtung pro Host: bash deploy/docker-host-einrichten.sh <ssh-ziel>
# CS2_DOCKER_HOSTS=[{"name":"host1","dockerHost":"ssh://cs2@192.168.178.111","publicHost":"192.168.178.111","portStart":27100,"portEnd":27139}]
CS2_START_TIMEOUT_MINUTES=20
# Spielports für den LAN-Reiter des Steam-Serverbrowsers (Steam sucht nur 27015–27020). Leer = aus.
CS2_LAN_PORTS=27015-27020
# CSTV-Port = Spielport + dieser Abstand (z. B. 27015 -> 27115)
CS2_TV_PORT_OFFSET=100
# Anfang des Servernamens im Serverbrowser, danach folgen die Teamnamen
CS2_SERVERNAME_PREFIX=MaxLAN
DOCKER_BIN=docker
# --- Static-Pool-Adapter (SERVER_ADAPTER=static) ---
+10
View File
@@ -254,6 +254,16 @@ Mit `CS2_DOCKER_HOSTS` verteilt die Anwendung Matches auf mehrere Docker-Hosts.
Jeder Host braucht eigene Spieldateien (rund 60 bis 90 GB). Die Ports müssen auf jedem Host frei sein.
### Server im Steam-Serverbrowser (LAN-Reiter)
Der LAN-Reiter des Steam-Serverbrowsers (Steam → Ansicht → Spielserver → LAN) fragt nur die Ports 27015 bis 27020 ab. Deshalb vergibt die App den Spielport zuerst aus `CS2_LAN_PORTS` (Standard `27015-27020`), danach aus dem normalen Portbereich. Der CSTV-Port liegt immer 100 über dem Spielport (Spiel 27015, CSTV 27115; Spiel 27100, CSTV 27200), der Abstand lässt sich mit `CS2_TV_PORT_OFFSET` ändern. Auf jedem Host müssen also auch die Ports 100 über dem Portbereich frei sein. So passen bis zu sechs Server pro Host in den LAN-Reiter. Weitere Server laufen im normalen Portbereich. Sie sind dann nicht im LAN-Reiter sichtbar, Spieler erreichen sie aber über den Verbindungsbefehl.
- 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`.
- 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`)
Für selbst betriebene, dauerhaft laufende CS2-Server:
+15 -2
View File
@@ -298,12 +298,19 @@ function MatchAdminCard({ m, run, busy, token, reload, setNotice }) {
{current.live?.roundsPlayed != null && <span className="muted">Runde {current.live.roundsPlayed + 1}</span>}
</div>
)}
{m.server?.address && m.server.status !== 'stopped' && (
{m.server && (m.server.requestedAt || (m.server.address && m.server.status !== 'stopped')) && (
<div className="admin-address">
{m.server.address && m.server.status !== 'stopped' && (
<>
<span><span className="muted">Server</span> <code>{m.server.address.host}:{m.server.address.port}</code></span>
{m.server.address.tvPort && (
<span><span className="muted">CSTV</span> <code>{m.server.address.host}:{m.server.address.tvPort}</code></span>
)}
</>
)}
{m.server.requestedAt && (
<span><span className="muted">Angefordert</span> {formatDateTime(m.server.requestedAt)}</span>
)}
</div>
)}
{paused && <span className="admin-badge is-paused">Pausiert</span>}
@@ -548,13 +555,14 @@ function ServerSettingsPanel({ token, setNotice }) {
);
}
const EMPTY_HOST = { name: '', dockerHost: 'ssh://', publicHost: '', portStart: 27100, portEnd: 27139, volume: 'cs2-mapveto-gamefiles' };
const EMPTY_HOST = { name: '', dockerHost: 'ssh://', publicHost: '', portStart: 27100, portEnd: 27139, lanPorts: '27015-27020', volume: 'cs2-mapveto-gamefiles' };
const HOST_COLUMNS = [
{ key: 'name', label: 'Name', placeholder: 'host1' },
{ key: 'dockerHost', label: 'Docker-Ziel', placeholder: 'ssh://server@192.168.178.45' },
{ key: 'publicHost', label: 'Adresse für Spieler', placeholder: '192.168.178.45' },
{ key: 'portStart', label: 'Port von', type: 'number' },
{ key: 'portEnd', label: 'Port bis', type: 'number' },
{ key: 'lanPorts', label: 'LAN-Ports (Steam)', placeholder: '27015-27020' },
{ key: 'volume', label: 'Volume', placeholder: 'cs2-mapveto-gamefiles' },
];
@@ -616,6 +624,7 @@ function DockerHostsPanel({ token, setNotice }) {
...(r.rconHost && r.rconHost !== r.publicHost ? { rconHost: r.rconHost } : {}),
portStart: Number(r.portStart),
portEnd: Number(r.portEnd),
lanPorts: String(r.lanPorts ?? '').trim(),
volume: r.volume.trim() || undefined,
})),
},
@@ -686,3 +695,7 @@ function DockerHostsPanel({ token, setNotice }) {
</section>
);
}
/** Datum und Uhrzeit auf Deutsch, z. B. 29.10.2026, 12:54 Uhr */
function formatDateTime(ms) {
return `${new Date(ms).toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' })} Uhr`;
}
+1 -1
View File
@@ -578,7 +578,7 @@ input:focus, select:focus { border-color: var(--link); outline: none; }
.setting-field textarea { resize: vertical; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: .85rem; }
.setting-live { display: inline-block; margin-right: 6px; padding: 0 6px; border-radius: 999px; font-size: .7rem; font-weight: 600; color: var(--primary); border: 1px solid var(--primary); }
.host-edit { display: flex; flex-direction: column; gap: 10px; margin-bottom: 12px; }
.host-edit-row { display: grid; grid-template-columns: 1fr 2fr 1.4fr .8fr .8fr 1.4fr auto; gap: 8px; align-items: end; padding-bottom: 10px; border-bottom: 1px solid var(--line); }
.host-edit-row { display: grid; grid-template-columns: 1fr 2fr 1.4fr .8fr .8fr 1.1fr 1.4fr auto; gap: 8px; align-items: end; padding-bottom: 10px; border-bottom: 1px solid var(--line); }
@media (max-width: 900px) {
.host-edit-row { grid-template-columns: 1fr 1fr; }
.host-col-dockerHost { grid-column: 1 / -1; }
+2 -1
View File
@@ -39,6 +39,7 @@ Fertig. Diesen Eintrag in CS2_DOCKER_HOSTS (JSON-Liste in der .env) aufnehmen:
Hinweise:
- name muss pro Host eindeutig sein.
- Ports 27100–27139 dürfen auf dem Host frei sein. Pro Host sind bis zu 20 Matches möglich.
- Frei sein müssen: 27015–27020 (LAN-Reiter von Steam), 27100–27139 und jeweils 100 darüber für CSTV
(27115–27120, 27200–27239). Pro Host sind bis zu 40 Matches möglich, 6 davon im LAN-Reiter.
- Der erste Server-Start lädt die CS2-Spieldateien (rund 60 bis 90 GB). Das dauert lange.
EOF
+2
View File
@@ -1,4 +1,5 @@
import express from 'express';
import { formatPortList } from './config.js';
import { HttpError } from './matchService.js';
import { createRateLimiter, randomToken, safeEqual } from './security.js';
@@ -96,6 +97,7 @@ export function createAdminRouter({ service, manager, config, settings }) {
portStart: h.portRangeStart,
portEnd: h.portRangeEnd,
volume: h.volume,
lanPorts: formatPortList(h.lanPorts),
})),
overridden: settings.hostsOverridden(),
editable: !!manager.adapter.setHosts,
+36
View File
@@ -22,6 +22,34 @@ function bool(name, fallback) {
return ['1', 'true', 'yes', 'ja', 'on'].includes(raw.toLowerCase());
}
/** Portliste aus Text wie "27015-27020" oder "27016,27018-27019". Leer = keine Ports. */
export function parsePortList(value, label = 'LAN-Ports') {
if (Array.isArray(value)) value = value.join(',');
const ports = [];
for (const part of String(value ?? '').split(',').map((p) => p.trim()).filter(Boolean)) {
const m = /^(\d+)(?:\s*-\s*(\d+))?$/.exec(part);
if (!m) throw new Error(`${label}: "${part}" ist kein Port oder Bereich`);
const from = Number(m[1]);
const to = Number(m[2] ?? m[1]);
if (from < 1024 || to > 65535 || to < from || to - from > 100) throw new Error(`${label}: Bereich ${part} ist ungültig`);
for (let p = from; p <= to; p += 1) if (!ports.includes(p)) ports.push(p);
}
return ports;
}
/** Portliste kompakt als Text, z. B. [27015, 27016, 27017] -> "27015-27017". */
export function formatPortList(ports = []) {
const sorted = [...ports].sort((a, b) => a - b);
const parts = [];
for (let i = 0; i < sorted.length; i += 1) {
let j = i;
while (j + 1 < sorted.length && sorted[j + 1] === sorted[j] + 1) j += 1;
parts.push(i === j ? String(sorted[i]) : `${sorted[i]}-${sorted[j]}`);
i = j;
}
return parts.join(',');
}
export function parseDockerHosts(raw) {
if (!raw) return null;
let list;
@@ -44,6 +72,7 @@ export function parseDockerHosts(raw) {
portRangeStart: h.portStart == null ? undefined : Number(h.portStart),
portRangeEnd: h.portEnd == null ? undefined : Number(h.portEnd),
volume: h.volume ?? undefined,
lanPorts: h.lanPorts === undefined ? undefined : parsePortList(h.lanPorts, `LAN-Ports von ${h.name}`),
};
});
}
@@ -111,6 +140,13 @@ export function loadConfig(overrides = {}) {
volume: str('CS2_DOCKER_VOLUME', 'cs2-mapveto-gamefiles'),
extraArgs: str('CS2_DOCKER_EXTRA_ARGS'),
startTimeoutMinutes: int('CS2_START_TIMEOUT_MINUTES', 20),
// Spielports, die der Steam-Serverbrowser im LAN-Reiter absucht (Steam fragt nur 27015–27020 ab).
// Diese Ports werden zuerst vergeben, CSTV kommt dann aus dem normalen Portbereich. Leer = aus.
lanPorts: parsePortList(str('CS2_LAN_PORTS', '27015-27020'), 'CS2_LAN_PORTS'),
// 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'),
// 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')),
+4 -2
View File
@@ -956,8 +956,10 @@ export class MatchService extends EventEmitter {
updatedAt: row.updated_at,
idleTimeoutMinutes: this.config.idleTimeoutMinutes,
};
if (viewer.isOrganizer && row.host) {
view.address = { host: row.host, port: row.port, tvPort: row.tv_port };
if (viewer.isOrganizer) {
// Zeitpunkt der letzten Anforderung (neuer Start oder "Server neu starten / wiederholen")
view.requestedAt = row.started_at ?? null;
if (row.host) view.address = { host: row.host, port: row.port, tvPort: row.tv_port };
}
const configured = row.status === 'configured';
if (!configured || viewer.role === 'public') return view;
+6
View File
@@ -160,6 +160,12 @@ export class ServerSettings {
throw new HttpError(400, `Ungültige Adresse für ${h.name}`, 'invalid');
}
if (h.volume && !/^[\w.-]+$/.test(h.volume)) throw new HttpError(400, `Ungültiger Volume-Name für ${h.name}`, 'invalid');
const lan = h.lanPorts ?? this.config.docker.lanPorts ?? [];
const rangeStart = h.portRangeStart ?? this.config.docker.portRangeStart;
const rangeEnd = h.portRangeEnd ?? this.config.docker.portRangeEnd;
if (lan.some((p) => p >= rangeStart && p <= rangeEnd)) {
throw new HttpError(400, `LAN-Ports von ${h.name} dürfen nicht im normalen Portbereich liegen`, 'invalid');
}
const start = h.portRangeStart ?? this.config.docker.portRangeStart;
const end = h.portRangeEnd ?? this.config.docker.portRangeEnd;
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 1024 || end > 65535 || end - start < 1) {
+88 -19
View File
@@ -2,8 +2,39 @@ import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { configureViaRcon, listRoundBackups, loadRoundBackup, rconStatus } from './rconConfigure.js';
import { runRcon } from './rcon.js';
import { sanitizeConsoleText } from './cs2Config.js';
const exec = promisify(execFile);
/**
* Plant Spiel- und CSTV-Port für einen neuen Server. Der CSTV-Port liegt immer `tvPortOffset` (Standard 100)
* über dem Spielport. Spielports kommen zuerst aus den LAN-Ports (damit der Server im LAN-Reiter des
* Steam-Serverbrowsers auftaucht), danach aus dem normalen Bereich. null = kein Platz.
*/
export function planPorts(host, used) {
const offset = host.tvPortOffset ?? 100;
const free = (p) => !used.has(p) && p <= 65535;
const candidates = [...(host.lanPorts ?? [])];
for (let p = host.portRangeStart; p <= host.portRangeEnd; p += 1) candidates.push(p);
const port = candidates.find((p) => free(p) && free(p + offset));
return port === undefined ? null : { port, tvPort: port + offset };
}
/** Wie viele Server passen noch auf einen Host, wenn bereits `used` belegt ist. */
export function countSlots(host, used) {
const taken = new Set(used);
let count = 0;
for (;;) {
const plan = planPorts(host, taken);
if (!plan) return count;
taken.add(plan.port);
taken.add(plan.tvPort);
count += 1;
}
}
/** Fehler von "docker run", wenn ein Port auf dem Host schon von etwas anderem belegt ist. */
const PORT_BUSY = /port is already allocated|address already in use/i;
const DISK_CACHE_MS = 60_000;
/** Liest Gesamt- und freien Speicher aus einer Zeile von `df -B1` (Spalten: Filesystem, 1B-blocks, Used, Available …). */
@@ -52,12 +83,19 @@ export class DockerServerAdapter {
return stdout.trim();
}
allocatePort() {
const used = this.getUsedPorts();
for (let p = this.options.portRangeStart; p + 1 <= this.options.portRangeEnd; p += 2) {
if (!used.has(p) && !used.has(p + 1)) return p;
allocatePorts(blocked = new Set()) {
const used = new Set([...this.getUsedPorts(), ...blocked]);
const plan = planPorts(this.options, used);
if (!plan) throw new Error('Kein freier Port im Bereich CS2_PORT_RANGE_START–CS2_PORT_RANGE_END');
return plan;
}
throw new Error('Kein freier Port im Bereich CS2_PORT_RANGE_START–CS2_PORT_RANGE_END');
/** 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();
const title = `${sanitizeConsoleText(teamNames.A)} vs. ${sanitizeConsoleText(teamNames.B)}`;
return (prefix ? `${prefix} | ${title}` : title).slice(0, 63);
}
async start(args) {
@@ -108,13 +146,34 @@ export class DockerServerAdapter {
}
}
async launch({ matchId, mode, secrets }) {
const port = this.allocatePort();
const tvPort = port + 1;
/**
* Im Host-Netz meldet Docker keinen Portkonflikt, CS2 startet dann einfach nicht. Deshalb vorher per SSH
* nachsehen, ob ein fremder Dienst den Port belegt. Ohne SSH-Ziel (oder in Tests) gilt der Port als frei.
*/
async hostPortsFree(ports) {
if (this.runDocker || !this.options.dockerHost?.startsWith('ssh://')) return true;
try {
const filter = ports.map((p) => `sport = :${Number(p)}`).join(' or ');
const { stdout } = await exec('ssh', [
'-o', 'BatchMode=yes', '-o', 'ConnectTimeout=5', this.options.dockerHost.slice('ssh://'.length),
`ss -Hlnut '( ${filter} )'`,
], { timeout: 10_000 });
return stdout.trim() === '';
} catch {
return true;
}
}
async launch({ matchId, mode, secrets, teamNames }, blocked = new Set()) {
const { port, tvPort } = this.allocatePorts(blocked);
const lan = (this.options.lanPorts ?? []).includes(port);
if (lan && blocked.size < 10 && !(await this.hostPortsFree([port, tvPort]))) {
return this.launch({ matchId, mode, secrets, teamNames }, new Set([...blocked, port, tvPort]));
}
const container = `cs2mv-${matchId.replace(/[^a-zA-Z0-9_-]/g, '')}`.toLowerCase();
const env = {
...(this.lanMode ? { CS2_LAN: '1' } : { SRCDS_TOKEN: this.options.gslt, CS2_LAN: '0' }),
CS2_SERVERNAME: `CS2 Mapveto ${matchId}`,
CS2_SERVERNAME: this.serverName(matchId, teamNames),
CS2_PORT: String(port),
CS2_RCONPW: secrets.rconPassword,
CS2_PW: secrets.serverPassword,
@@ -133,19 +192,32 @@ export class DockerServerAdapter {
};
const args = ['run', '-d', '--name', container, '--label', 'cs2-mapveto=1'];
for (const [k, v] of Object.entries(env)) args.push('-e', `${k}=${v}`);
if (lan) {
// Der LAN-Reiter von Steam sucht per Broadcast. Die Port-Weiterleitung von Docker reicht Broadcasts
// nicht an den Container weiter, deshalb laufen Server auf LAN-Ports direkt im Netz des Hosts.
args.push('--network', 'host');
} else {
args.push(
'-p', `${port}:${port}/tcp`,
'-p', `${port}:${port}/udp`,
'-p', `${tvPort}:${tvPort}/udp`,
'-v', `${this.options.volume}:/home/steam/cs2-dedicated`,
);
}
args.push('-v', `${this.options.volume}:/home/steam/cs2-dedicated`);
if (this.options.extraArgs) args.push(...this.options.extraArgs.split(' ').filter(Boolean));
args.push(this.options.image);
// Ein evtl. übrig gebliebener Container gleichen Namens (z. B. nach Retry) wird ersetzt.
await this.docker(['rm', '-f', container]).catch(() => {});
// Das erste "docker run" kann das Image laden und dauert dann länger.
try {
await this.docker(args, 15 * 60_000);
} catch (err) {
// Port von einem fremden Dienst belegt (die App kennt nur ihre eigenen Server): mit anderen Ports erneut versuchen
if (!PORT_BUSY.test(`${err.message} ${err.stderr ?? ''}`) || blocked.size >= 10) throw err;
await this.docker(['rm', '-f', container]).catch(() => {});
return this.launch({ matchId, mode, secrets, teamNames }, new Set([...blocked, port, tvPort]));
}
return {
handle: {
@@ -250,6 +322,8 @@ export class DockerHostPool {
portRangeStart: h.portRangeStart ?? options.portRangeStart,
portRangeEnd: h.portRangeEnd ?? options.portRangeEnd,
volume: h.volume ?? options.volume,
lanPorts: h.lanPorts ?? options.lanPorts ?? [],
tvPortOffset: options.tvPortOffset ?? 100,
};
const merged = { ...options, ...host, host: host.publicHost };
// Die Startzeit kann der Admin zur Laufzeit ändern, deshalb immer aus den gemeinsamen Einstellungen lesen
@@ -340,20 +414,15 @@ export class DockerHostPool {
return this.diskInflight.get(name);
}
/** Freie Port-Paare auf einem Host. */
/** Wie viele Server noch auf einen Host passen. */
freeSlots(host) {
const used = this.getUsedPorts(host.publicHost);
let free = 0;
for (let p = host.portRangeStart; p + 1 <= host.portRangeEnd; p += 2) {
if (!used.has(p) && !used.has(p + 1)) free += 1;
}
return free;
return countSlots(host, this.getUsedPorts(host.publicHost));
}
/** Auslastung pro Host: Anzahl Port-Paare insgesamt und belegt. */
/** Auslastung pro Host: Anzahl Serverplätze insgesamt und belegt. */
hostStats() {
return this.hosts.map((h) => {
const total = Math.floor((h.portRangeEnd - h.portRangeStart + 1) / 2);
const total = countSlots(h, new Set());
return { name: h.name, publicHost: h.publicHost, total, used: total - this.freeSlots(h) };
});
}
+6 -1
View File
@@ -314,7 +314,12 @@ export class ServerManager {
const match = this.service.getMatch(matchId);
try {
const result = await this.adapter.start({ matchId, mode: match.mode, secrets });
const result = await this.adapter.start({
matchId,
mode: match.mode,
secrets,
teamNames: { A: match.team_a_name, B: match.team_b_name },
});
this.update(matchId, {
handle: JSON.stringify(result.handle),
host: result.host,
+2 -2
View File
@@ -39,7 +39,7 @@ test('Ohne GSLT startet der Container im LAN-Modus, mit GSLT mit Token', async (
assert.equal(env.CS2_MAXPLAYERS, '6', '4 Spieler + CSTV + Reserve');
assert.equal(env.CS2_ADDITIONAL_ARGS, '-allow_round_backup_files', 'Rundensicherung für Rollback');
assert.equal(env.CS2_PORT, '27100');
assert.equal(env.TV_PORT, '27101');
assert.equal(env.TV_PORT, '27200');
assert.ok(runArgs.includes('cs2-mapveto-gamefiles:/home/steam/cs2-dedicated'));
assert.equal(r.host, '192.168.178.111');
@@ -74,7 +74,7 @@ test('Zweiter Container startet erst, wenn der erste seinen Update-Check abgesch
d.finishUpdate(first.handle.container);
const r2 = await second;
assert.equal(d.calls.filter((c) => c[0] === 'run').length, 2);
assert.equal(r2.port, 27102, 'zweiter Container bekommt die nächsten Ports');
assert.equal(r2.port, 27101, 'zweiter Container bekommt den nächsten Port');
d.finishUpdate(r2.handle.container);
});
+3 -3
View File
@@ -81,11 +81,11 @@ test('Alte Handles ohne Host-Namen laufen auf dem ersten Host', () => {
assert.equal(p.adapterFor({ container: 'x' }), p.adapters.get('a'));
});
test('Auslastung pro Host: Port-Paare insgesamt und belegt', () => {
test('Auslastung pro Host: Serverplätze insgesamt und belegt', () => {
const { pool: p } = pool([hostA, hostB], { '10.0.0.1': [27100] });
assert.deepEqual(p.hostStats(), [
{ name: 'a', publicHost: '10.0.0.1', total: 2, used: 1 },
{ name: 'b', publicHost: '10.0.0.2', total: 20, used: 0 },
{ name: 'a', publicHost: '10.0.0.1', total: 4, used: 1 },
{ name: 'b', publicHost: '10.0.0.2', total: 40, used: 0 },
]);
});
test('df-Ausgabe wird in Gesamt- und freien Speicher zerlegt', async () => {
+95
View File
@@ -0,0 +1,95 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { formatPortList, parsePortList } from '../src/config.js';
import { countSlots, DockerHostPool, planPorts } from '../src/servers/dockerAdapter.js';
const host = { portRangeStart: 27100, portRangeEnd: 27105, lanPorts: [27015, 27016] };
const secrets = { rconPassword: 'Rcon123', serverPassword: 'Pw123', tvPassword: 'Tv123' };
test('Portlisten werden gelesen und kompakt geschrieben', () => {
assert.deepEqual(parsePortList('27015-27017, 27020'), [27015, 27016, 27017, 27020]);
assert.deepEqual(parsePortList(''), []);
assert.throws(() => parsePortList('abc'), /kein Port/);
assert.throws(() => parsePortList('80-90'), /ungültig/);
assert.equal(formatPortList([27020, 27015, 27016, 27017]), '27015-27017,27020');
});
test('Spielport kommt zuerst aus den LAN-Ports, CSTV liegt immer 100 darüber', () => {
assert.deepEqual(planPorts(host, new Set()), { port: 27015, tvPort: 27115 });
assert.deepEqual(planPorts(host, new Set([27015, 27115])), { port: 27016, tvPort: 27116 });
// LAN-Ports voll: weiter im normalen Bereich, CSTV ebenfalls +100
assert.deepEqual(planPorts(host, new Set([27015, 27016, 27115, 27116])), { port: 27100, tvPort: 27200 });
// Spielport frei, aber sein CSTV-Port belegt: nächster Spielport
assert.deepEqual(planPorts({ ...host, lanPorts: [] }, new Set([27200])), { port: 27101, tvPort: 27201 });
assert.equal(planPorts({ ...host, lanPorts: [] }, new Set([27100, 27101, 27102, 27103, 27104, 27105])), null);
});
test('Abstand zwischen Spiel- und CSTV-Port ist einstellbar', () => {
assert.deepEqual(planPorts({ ...host, tvPortOffset: 5 }, new Set()), { port: 27015, tvPort: 27020 });
});
test('Plätze pro Host: 2 LAN-Ports plus 6 Spielports im normalen Bereich', () => {
assert.equal(countSlots(host, new Set()), 8);
assert.equal(countSlots({ ...host, lanPorts: [] }, new Set()), 6);
});
function fakePool({ busy = [] } = {}) {
const calls = [];
const runDocker = async (args) => {
calls.push(args);
if (args[0] === 'run') {
const ports = args.filter((a, i) => args[i - 1] === '-p').map((a) => Number(a.split(':')[0]));
const portEnv = args.find((a) => a.startsWith('CS2_PORT='));
if (args.includes('--network') && portEnv) ports.push(Number(portEnv.split('=')[1]));
if (ports.some((p) => busy.includes(p))) {
const err = new Error('Command failed');
err.stderr = 'Bind for 0.0.0.0:27015 failed: port is already allocated';
throw err;
}
}
if (args[0] === 'inspect') return 'running';
return '';
};
const p = new DockerHostPool({
bin: 'docker', image: 'joedwards32/cs2:latest', host: '10.0.0.9', rconHost: '10.0.0.9',
portRangeStart: 27100, portRangeEnd: 27139, gslt: '', volume: 'v', extraArgs: '',
startTimeoutMinutes: 1, lanPorts: [27015, 27016], serverNamePrefix: 'MaxLAN',
hosts: [{ name: 'a', publicHost: '10.0.0.1' }],
}, { runDocker });
for (const a of p.adapters.values()) a.waitForUpdateCheck = async () => {};
return { pool: p, calls };
}
test('Servername zeigt die Teams, Spielport ist ein LAN-Port', async () => {
const { pool: p, calls } = fakePool();
const r = await p.start({ matchId: 'm1', mode: 'wingman', secrets, teamNames: { A: 'Rote "Füchse"', B: 'Blau' } });
assert.equal(r.port, 27015);
assert.equal(r.tvPort, 27115);
const run = calls.find((c) => c[0] === 'run');
assert.ok(run.includes('CS2_SERVERNAME=MaxLAN | Rote Füchse vs. Blau'));
// Auf LAN-Ports im Host-Netz, damit Broadcasts des Serverbrowsers ankommen; keine Port-Weiterleitung
assert.equal(run[run.indexOf('--network') + 1], 'host');
assert.ok(!run.includes('-p'));
});
test('Server außerhalb der LAN-Ports laufen weiter mit Port-Weiterleitung', async () => {
const { pool: p, calls } = fakePool();
await p.start({ matchId: 'x1', mode: 'wingman', secrets, teamNames: { A: 'A', B: 'B' } });
await p.start({ matchId: 'x2', mode: 'wingman', secrets, teamNames: { A: 'A', B: 'B' } });
calls.length = 0;
// Die App kennt ihre Ports aus der Datenbank; hier sind beide LAN-Ports belegt
const adapter = p.adapters.get('a');
adapter.getUsedPorts = () => new Set([27015, 27016, 27115, 27116]);
await adapter.launch({ matchId: 'x3', mode: 'wingman', secrets });
const run = calls.find((c) => c[0] === 'run');
assert.ok(!run.includes('--network'));
assert.ok(run.includes('27100:27100/udp'));
assert.ok(run.includes('27200:27200/udp'));
});
test('Ist ein LAN-Port von einem fremden Dienst belegt, nimmt der Start den nächsten', async () => {
const { pool: p, calls } = fakePool({ busy: [27015] });
const r = await p.start({ matchId: 'm2', mode: 'competitive', secrets, teamNames: { A: 'A', B: 'B' } });
assert.equal(r.port, 27016);
assert.equal(calls.filter((c) => c[0] === 'run').length, 2);
});
+2 -2
View File
@@ -52,9 +52,9 @@ test('Docker-Hosts: Prüfung der Liste und Übernahme in den Pool, Startzeit wir
{ name: 'a', publicHost: '10.0.0.1', dockerHost: 'ssh://server@10.0.0.1', portStart: 27100, portEnd: 27139 },
{ name: 'b', publicHost: '10.0.0.2', dockerHost: 'ssh://b', portStart: 27100, portEnd: 27119 },
]);
const pool = new DockerHostPool({ ...config.docker, startTimeoutMinutes: 20, hosts: null });
const pool = new DockerHostPool({ ...config.docker, startTimeoutMinutes: 20, lanPorts: [], hosts: null });
pool.setHosts(hosts);
assert.deepEqual(pool.hostStats().map((h) => [h.name, h.total]), [['a', 20], ['b', 10]]);
assert.deepEqual(pool.hostStats().map((h) => [h.name, h.total]), [['a', 40], ['b', 20]]);
pool.options.startTimeoutMinutes = 45;
assert.equal(pool.adapters.get('b').options.startTimeoutMinutes, 45);