# CS2 Mapveto Webanwendung zur Organisation von Counter-Strike-2-Matches: Match-Lobbys mit Team-Einladungen, abwechselndes Map-Veto der Captains, Zuschauerzugang und automatische Verwaltung eines CS2-Servers pro Match. - **Frontend:** React 18 + Vite, deutschsprachig, responsives dunkles Design - **Backend:** Node.js (≥ 18.18), Express, Socket.IO - **Datenbank:** SQLite (better-sqlite3), Datei unter `data/` - **Echtzeit:** Socket.IO; jede Änderung schickt allen Teilnehmern ihren rollenabhängigen Match-Zustand - **Serververwaltung:** austauschbare Adapter hinter einer Schnittstelle (`start`, `status`, `configure`, `stop`) > **Standardmäßig läuft der Demo-Adapter.** Er startet **keine** echten CS2-Server. Oberfläche, Statusanzeigen und Verbindungsdaten sind dann überall als „Demo“ bzw. „simuliert“ gekennzeichnet. Wie die echte Anbindung eingerichtet wird, steht im Abschnitt [Echte Server anbinden](#echte-server-anbinden). --- ## Schnellstart ```bash npm install cp .env.example .env # optional, Standardwerte funktionieren für die Demo npm run dev # Backend auf :3000, Frontend auf :5173 ``` Dann öffnen. **Produktion (ein Prozess, Backend liefert das gebaute Frontend aus):** ```bash npm install npm run build PUBLIC_URL=https://veto.example.de npm start # lauscht auf PORT (Standard 3000) ``` **Tests:** ```bash npm test ``` Die Tests decken Veto-Engine und Ablaufvalidierung, RCON-Protokoll (gegen einen lokalen Test-RCON-Server), Befehls-Bereinigung und den kompletten API-Ablauf ab. Dazu gehören Berechtigungen, doppelte Aktionen, Server-Konfiguration mit Wiederholung, nächste Map, automatisches Herunterfahren und dass keine RCON-Daten an Clients gelangen. --- ## Ablauf: von der Match-Erstellung bis zum Serverbeitritt 1. **Match erstellen** (`/neu`): Der Organisator trägt beide Teamnamen ein und wählt Spielmodus (Wingman 2 gegen 2 oder Competitive 5 gegen 5), Serie (Best of 1/3/5) und wer das Veto beginnt (Münzwurf oder festes Team). Formate, für die der Map-Pool keinen gültigen Veto-Ablauf hat, sind ausgegraut und werden auch vom Backend abgelehnt. Der Browser des Organisators erhält einen geheimen Organisator-Token. Er wird lokal gespeichert und lässt sich über „Organisator-Zugang auf einem anderen Gerät öffnen“ übertragen. 2. **Server vorbereiten:** Direkt beim Anlegen der Lobby fordert das Backend im Hintergrund einen Server an. Er fährt also schon während Lobby und Veto hoch. Der Status ist auf der Match-Seite live sichtbar. 3. **Einladungen:** Das Match hat drei Zugangscodes (Format `ABCD-EFGH`) mit Links: - Team A: `/join/` - Team B: `/join/` - Zuschauer: `/watch/` für Stream und Kommentatoren Diese Codes gelten nur für die Webseite. Der CS2-Serverzugang wird getrennt bereitgestellt (Schritt 6). Der Organisator kann jeden Code jederzeit neu erzeugen; der alte Link wird damit ungültig. 4. **Spieler treten bei:** Über den Teamlink gibt ein Spieler seinen Namen ein (optional die SteamID64) und landet in genau diesem Team. Pro Team sind maximal 2 bzw. 5 Spieler erlaubt. - Der erste Spieler eines Teams wird Captain. Organisator oder Captain können den Captain wechseln. - Jeder Spieler setzt sich auf „bereit“. Ein Team ist bereit, sobald mindestens ein Spieler beigetreten ist, es einen Captain hat und alle beigetretenen Spieler bereit sind. Mit `REQUIRE_FULL_TEAMS=true` muss das Team zusätzlich vollständig sein (2 bzw. 5 Spieler). 5. **Map-Veto:** Sind beide Teams bereit, startet der Organisator das Veto. Die Captains bannen und wählen abwechselnd nach dem festen Ablauf (siehe unten). Die Seite zeigt jederzeit: - welches Team am Zug ist - welche Aktion ansteht - den Zustand jeder Map (verfügbar, gebannt, gewählt, Entscheidungsmap) - den gesamten Verlauf mit Uhrzeit Die letzte verbleibende Map wird automatisch als Entscheidungsmap übernommen. Zuschauer und Organisator sehen alles live, können aber nicht wählen. 6. **Server konfigurieren:** Nach dem Veto wird der bereits laufende Server automatisch eingerichtet: Spielmodus, erste Map, Teamnamen und Startseiten, Match-Einstellungen, Server- und CSTV-Passwort. Die restliche Map-Reihenfolge ist gespeichert. - Spieler sehen danach den kopierbaren Konsolenbefehl `password …; connect host:port` und einen `steam://connect`-Link. - Zuschauer sehen den CSTV-Zugang (eigener Port, eigenes Passwort, Verzögerung). 7. **Während der Serie:** Der Organisator kann die Aufwärmphase beenden (`mp_warmup_end`), die nächste Map laden, den Server stoppen und das Match abschließen oder abbrechen. 8. **Fehlerfälle:** Startet der Server nicht, schlägt die Konfiguration fehl oder fällt der Server aus, zeigt die Seite den Grund. Der Organisator bekommt einen Knopf für den erneuten Versuch: „Server neu starten“ bzw. „Konfiguration wiederholen“. ### Rollen und Berechtigungen | Rolle | Zugang über | Darf | |---|---|---| | Organisator | Token beim Erstellen | Veto starten, Spieler entfernen, Captain setzen, Codes erneuern, Server neu starten/stoppen/konfigurieren, nächste Map, Match beenden; sieht Einladungen, Spieler- und CSTV-Verbindung | | Captain | Teamcode (erster Spieler oder ernannt) | alles wie Spieler, dazu Veto-Aktionen seines Teams und Captain-Übergabe | | Spieler | Teamcode | bereit melden, Team verlassen; sieht die Spieler-Verbindungsdaten nach dem Veto | | Zuschauer | Zuschauercode | alles live verfolgen; sieht nur den CSTV-Zugang, nicht das Spielerpasswort | | Gast | ohne Code | nur Teamnamen und Status | **Organisator spielt selbst mit:** In der Lobby kann der Organisator unter „Selbst mitspielen?“ direkt einem Team beitreten, oder er öffnet einen Teamlink im selben Browser. Er bleibt Organisator und ist zusätzlich Spieler bzw. Captain. Ein zweiter Browser ist nicht nötig. Technisch speichert der Browser dafür beide Zugänge und schickt den Spieler-Token im Header `X-Player-Token` mit. Pro Aufrufer zählt höchstens ein Spieler. Alle Prüfungen passieren im Backend. Die Oberfläche blendet nur zusätzlich aus, was die Rolle nicht darf. ### Schutzmaßnahmen - **Fremde Teambeitritte:** Jeder Code gehört zu genau einem Team eines Matches. Zuschauercodes erlauben keinen Beitritt. Volle Teams, doppelte Namen und doppelte Steam-IDs werden abgelehnt. Codebasierte Endpunkte sind pro IP begrenzt (20 Versuche pro Minute). - **Doppelte Aktionen:** Jede Veto-Aktion trägt die Nummer des Schritts. Sie wird in einer SQLite-Transaktion geprüft und gespeichert; `(match_id, idx)` ist Primärschlüssel. Ein doppelter Klick oder ein veralteter Stand liefert „Diese Aktion ist veraltet“. Serveraktionen sind pro Match gesperrt, solange eine läuft. - **Unberechtigte Befehle:** Veto nur durch den Captain des Teams am Zug, Server- und Match-Befehle nur durch den Organisator. - **Tokens:** Organisator- und Spieler-Tokens sind 256 Bit zufällig und werden nur als SHA-256-Hash gespeichert. - **Server-Zugangsdaten:** RCON-Passwort, Container-Handle und GSLT liegen ausschließlich in der Datenbank bzw. den Umgebungsvariablen des Backends. Die Snapshot-Funktion gibt sie nie aus (per Test abgesichert). Server- und CSTV-Passwort sind als Beitrittsdaten nötig und werden nur an die jeweils berechtigte Rolle geliefert. - **Befehls-Injection:** Teamnamen werden vor der Übergabe an die Serverkonsole von Anführungszeichen, `;` und Steuerzeichen bereinigt. Passwörter sind rein alphanumerisch. ### Wiederherstellung nach Neuladen oder Verbindungsabbruch Der gesamte Zustand liegt in SQLite. Der Browser speichert pro Match nur seinen Zugangstoken (`localStorage`). - **Neuladen der Seite:** Die Seite lädt den Stand per REST und abonniert danach den Socket. - **Abgebrochene Socket-Verbindung:** Nach dem Reconnect meldet sich der Client automatisch neu an und erhält den vollständigen aktuellen Stand. Socket-Nachrichten enthalten immer den kompletten Zustand und keine Deltas, deshalb geht nichts verloren. - **Backend-Neustart:** Unterbrochene Serverstarts werden als fehlgeschlagen markiert (mit „Server neu starten“). Unterbrochene Konfigurationen werden automatisch wiederholt. ### Automatisches Herunterfahren Alle `SERVER_REAPER_INTERVAL_SECONDS` prüft das Backend alle aktiven Server: - Ein Match ist **abgeschlossen oder abgebrochen**: Server wird gestoppt. - **Lobby, Veto oder Server ohne Spielerzahl:** Ist niemand mit der Match-Seite verbunden und gab es länger als `SERVER_IDLE_TIMEOUT_MINUTES` keine Aktivität, wird der Server gestoppt. - **Konfigurierter echter Server:** Gestoppt wird, wenn länger als die Frist keine Spieler auf dem Server waren. Die Spielerzahl kommt per RCON aus `status`. - **Unabhängig davon:** Nach `SERVER_MAX_LIFETIME_HOURS` wird jeder Server gestoppt. Ein gestoppter Server lässt sich vom Organisator neu starten. --- ## Map-Pools und Veto-Abläufe Konfiguration in [`server/config/map-pools.json`](server/config/map-pools.json) (Pfad änderbar über `MAP_POOL_FILE`). Jeder Pool hat: - eine eigene Map-Liste mit ID, Anzeigename und Kartenfarbe - Teamgröße und `game_type`/`game_mode` - je Format einen Ablauf Schritte im Ablauf: | Schritt | Bedeutung | |---|---| | `ban:X` / `ban:Y` | Team X bzw. Y bannt eine Map | | `pick:X` / `pick:Y` | Team X bzw. Y wählt eine Map für die Serie | | `side:X` / `side:Y` | Team wählt die Startseite (CT/T) auf der zuletzt gewählten Map bzw. der Entscheidungsmap | | `decider` | die letzte verbleibende Map wird automatisch übernommen | **X** ist das Team, das das Veto beginnt (Münzwurf oder vom Organisator festgelegt), **Y** das andere. Beim Start wird jeder Ablauf geprüft: - Anzahl Picks = Maps der Serie − 1 - Bans + Picks + 1 = Pool-Größe - genau eine Entscheidungsmap - Seitenwahl nur direkt nach einem Pick oder der Entscheidungsmap Ungültige oder fehlende Abläufe machen nur dieses Format unauswählbar (mit Begründung in der Oberfläche). ### Mitgelieferte Abläufe **Competitive (7 Maps):** Ancient, Anubis, Dust II, Inferno, Mirage, Nuke, Train | Format | Ablauf | |---|---| | BO1 | X bannt, Y bannt, X bannt, Y bannt, X bannt, Y bannt → Entscheidungsmap, X wählt Seite | | BO3 | X bannt, Y bannt, X pickt (Y wählt Seite), Y pickt (X wählt Seite), X bannt, Y bannt → Entscheidungsmap, X wählt Seite | | BO5 | X bannt, Y bannt, X pickt (Y Seite), Y pickt (X Seite), X pickt (Y Seite), Y pickt (X Seite) → Entscheidungsmap, Y wählt Seite | **Wingman (4 Maps):** Inferno, Nuke, Overpass, Vertigo | Format | Ablauf | |---|---| | BO1 | X bannt, Y bannt, X bannt → Entscheidungsmap, Y wählt Seite | | BO3 | X bannt, Y pickt (X Seite), X pickt (Y Seite) → Entscheidungsmap, X wählt Seite | | BO5 | **nicht unterstützt:** Mit 4 Maps sind keine 5 Karten möglich. Mit einem Pool von mindestens 5 Maps und einem `bo5`-Ablauf wird es automatisch auswählbar. | ### Regel für Startseiten - Auf einer **gewählten Map** wählt immer das **Gegnerteam des wählenden Teams** die Startseite. - Auf der **Entscheidungsmap** wählt das Team, das im Ablauf dafür vorgesehen ist. In BO1 ist das in Competitive Team X, weil Y den letzten Ban hatte, und in Wingman Team Y. Ohne `side`-Schritt werden die Seiten im Spiel festgelegt. Die Wahl wird als `mp_teamname_1` (Team mit CT-Start) und `mp_teamname_2` (T-Start) an den Server übergeben. ### Bilder für Maps und Seitenwahl - **Map-Bilder** liegen lokal unter `client/public/maps/.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. - 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. --- ## Echte Server anbinden Die Serververwaltung ist hinter [`server/src/servers/adapter.js`](server/src/servers/adapter.js) gekapselt. Auswahl über `SERVER_ADAPTER`. ### Ablauf, den jeder Adapter bedient 1. **`start({ matchId, mode, secrets })`**: Server starten bzw. reservieren. - Rückgabe: `{ handle, host, port, tvPort }`. - `secrets` (RCON-, Server- und CSTV-Passwort) erzeugt das Backend pro Match. 2. **`status(handle)`**: liefert `starting | running | stopped | failed`, optional mit Spielerzahl. - Wird alle `SERVER_STATUS_POLL_SECONDS` abgefragt, solange der Server startet, danach alle 30 s. 3. **`configure(handle, plan)`**: in zwei Phasen. 1. `game_type`, `game_mode`, `sv_password`, `tv_enable`, `tv_delay`, `tv_password`, dann `changelevel `. 2. Warten, bis `status` die Map meldet (max. 2 Minuten), dann die Match-Einstellungen (`mp_teamname_1/2`, `mp_maxrounds`, `mp_overtime_enable`, `mp_match_can_clinch`, `mp_warmup_pausetimer 1`, `CS2_EXTRA_COMMANDS`). Die zweite Phase kommt erst nach dem Mapwechsel, weil die `gamemode_*.cfg` des Spiels diese Werte beim Laden der Map sonst überschreibt. 4. **`stop(handle)`**: Server beenden bzw. freigeben. ### Option A: Docker (`SERVER_ADAPTER=docker`) Startet pro Match einen Container mit dem Community-Image [`cm2network/cs2`](https://github.com/CM2Walki/CS2) (SteamCMD, CS2-Dedicated-Server App 730). Voraussetzungen: 1. Docker auf dem Backend-Host. Das Backend ruft die Docker-CLI auf und braucht dafür Zugriff auf den Docker-Daemon. 2. **Game Server Login Token** für App-ID 730 unter erstellen und als `CS2_GSLT` eintragen. Ohne GSLT sind Server nicht öffentlich erreichbar. Pro gleichzeitig laufendem Server ist ein eigenes Token empfehlenswert. 3. `CS2_PUBLIC_HOST`: IP oder DNS-Name, unter dem Spieler den Host erreichen. 4. Ports `CS2_PORT_RANGE_START`–`CS2_PORT_RANGE_END` in Firewall bzw. Router freigeben: - pro Match zwei aufeinanderfolgende Ports - Spielport per UDP und TCP (TCP wird für RCON genutzt, nur für das Backend freigeben) - CSTV-Port per UDP 5. Etwa 60 GB Speicher für die Spieldateien im Volume `CS2_DOCKER_VOLUME`. Das Volume wird von allen Match-Containern geteilt. Der erste Start lädt das Spiel herunter und kann deutlich länger als 10 Minuten dauern (`CS2_START_TIMEOUT_MINUTES`). Spieldateien am besten vorab mit einem einzelnen Container laden, bevor mehrere Matches gleichzeitig starten. Container heißen `cs2mv-` und tragen das Label `cs2-mapveto=1`. Stoppen bedeutet `docker rm -f`. ### Option B: Vorhandene Server (`SERVER_ADAPTER=static`) Für selbst betriebene, dauerhaft laufende CS2-Server: ```env SERVER_ADAPTER=static CS2_STATIC_SERVERS=203.0.113.10:27015:rconPasswort1:27020,203.0.113.10:27025:rconPasswort2:27030 ``` Auf jedem Server müssen gesetzt sein: - `rcon_password` - CSTV-Port `tv_port`, falls abweichend von Spielport + 5 (dieser Wert wird angenommen, wenn kein vierter Teil angegeben ist) - ein GSLT (`sv_setsteamaccount`) Bei diesem Adapter bedeutet „Start“: einen freien Server reservieren und per RCON prüfen. „Stopp“ gibt ihn frei und setzt ein neues zufälliges `sv_password`. Der Serverprozess selbst wird nicht beendet. ### Eigener Adapter (z. B. Hosting-API) 1. Klasse mit `name`, `simulated = false` und den vier Methoden anlegen. Vorlage: `dockerAdapter.js`. Für die Konfiguration per RCON gibt es `configureViaRcon` aus `rconConfigure.js`. 2. In `createServerAdapter` in `adapter.js` registrieren. 3. Zugangsdaten der Hosting-API ausschließlich über Umgebungsvariablen in `config.js` einlesen. ### Verbindung für Spieler ``` password ; connect : ``` Zusätzlich bietet die Seite einen `steam://connect/:/`-Link an. ### Zuschauer: CSTV (früher GOTV) CS2 bringt **CSTV** mit (in der Konsole weiterhin `tv_*`-Befehle; früher GOTV). Die Anwendung setzt: - `tv_enable 1` (greift erst nach dem Mapwechsel, deshalb vor `changelevel`) - `tv_delay` (Standard 90 s) als Verzögerung gegen Ghosting - `tv_password` als eigenes Passwort, getrennt vom Spielerpasswort Zuschauer verbinden sich mit `password ; connect :`. Sie belegen keinen Spielerplatz und können die Spieler nicht beeinflussen. Für größere Zuschauerzahlen unterstützt CS2 CSTV-Relays (`tv_relay`) und HTTP-Broadcast (`tv_broadcast_url`). Beides ist nicht eingebaut und kann über `CS2_EXTRA_COMMANDS` bzw. die Serverkonfiguration ergänzt werden. ### Prüfstand der CS2-Angaben Die offizielle Seite [Counter-Strike 2/Dedicated Servers](https://developer.valvesoftware.com/wiki/Counter-Strike_2/Dedicated_Servers) im Valve Developer Wiki war während der Entwicklung wegen eines Bot-Schutzes nicht automatisiert abrufbar. Grundlage waren: - das [Source-RCON-Protokoll](https://developer.valvesoftware.com/wiki/Source_RCON_Protocol) - die Dokumentation des Images [cm2network/cs2](https://github.com/CM2Walki/CS2): Umgebungsvariablen, Ports, `TV_*` - Hoster-Anleitungen zu CSTV, z. B. [ZAP-Hosting](https://zap-hosting.com/guides/de/docs/cs2-cstv) - [FACEIT zu CSTV-Zuschauern](https://support.faceit.com/hc/en-us/articles/17050697184668) Bekannte Werte: `game_type 0` + `game_mode 1` = Competitive, `game_mode 2` = Wingman; CSTV-Standardport 27020. **Nicht gegen einen echten CS2-Server getestet:** Docker- und Static-Adapter sowie die Auswertung der `status`-Ausgabe (Map-Name, Spielerzahl). Das RCON-Protokoll ist gegen einen Test-Server geprüft. Vor dem ersten echten Einsatz bitte ein Testmatch durchspielen und bei Bedarf `parseStatus` in `server/src/servers/rcon.js` an die tatsächliche Ausgabe anpassen. ### Bekannte Grenzen von Vanilla-CS2 - Ohne Server-Plugin kann CS2 Spieler **nicht automatisch einem Team zuordnen**. Spieler wählen ihr Team im Spiel selbst. Teamnamen und Startseiten setzt die Anwendung. Für feste Teamzuordnung, Pausen und automatischen Mapwechsel eignen sich Plugins wie MatchZy oder Get5-Nachfolger. Sie lassen sich über einen eigenen Adapter anbinden; die SteamID64 der Spieler wird dafür bereits erfasst. - Ergebnisse werden nicht ausgelesen. Der Organisator lädt die nächste Map manuell. --- ## Umgebungsvariablen Alle Variablen mit Standardwerten stehen in [`.env.example`](.env.example). Die wichtigsten: | Variable | Standard | Zweck | |---|---|---| | `PORT` | `3000` | Port des Backends | | `PUBLIC_URL` | `http://localhost:5173` | Basis für Einladungslinks | | `DATABASE_FILE` | `data/cs2-mapveto.db` | SQLite-Datei | | `CREATE_MATCH_PASSWORD` | leer | Passwort zum Erstellen von Matches | | `REQUIRE_FULL_TEAMS` | `false` | `true`: Veto nur mit vollständigen Teams | | `SERVER_ADAPTER` | `demo` | `demo`, `docker` oder `static` | | `SERVER_IDLE_TIMEOUT_MINUTES` | `30` | Frist bis zum automatischen Herunterfahren | | `SERVER_MAX_LIFETIME_HOURS` | `8` | harte Obergrenze pro Server | | `CS2_TV_DELAY` | `90` | CSTV-Verzögerung in Sekunden | | `CS2_GSLT` | – | **Pflicht bei Docker:** Game Server Login Token | | `CS2_PUBLIC_HOST` | – | **Pflicht bei Docker:** öffentliche Adresse | | `CS2_STATIC_SERVERS` | – | **Pflicht bei Static:** Serverliste | | `DEMO_FAIL_FIRST_CONFIGURE` | `false` | Demo: Konfigurationsfehler simulieren | Benötigte Zugangsdaten für den Echtbetrieb: - **Docker:** GSLT, Zugriff auf den Docker-Daemon, öffentliche IP bzw. Portfreigaben - **Static:** RCON-Passwörter der Server RCON-Passwörter für Docker-Container erzeugt das Backend selbst. --- ## Projektstruktur ``` client/ React-Frontend (Vite) src/pages/ Übersicht, Match erstellen, Match-Seite, Beitritt src/components/ Teamkarten, Veto-Board, Serverpanel, Einladungen, Verlauf src/lib/ API, Socket-Hook, lokale Zugangsdaten server/ config/map-pools.json Map-Pools und Veto-Abläufe src/veto.js reine Veto-Logik und Ablaufvalidierung src/matchService.js Lobby, Rollen, Veto, rollenabhängige Snapshots src/servers/ Adapter-Schnittstelle, Demo/Docker/Static, RCON, ServerManager src/http.js REST-API src/realtime.js Socket.IO test/ node:test-Tests ``` ### REST-API (Auszug) Alle Aktionen laufen per `POST` mit `Authorization: Bearer `. Der Token ist der Organisator- oder Spieler-Token bzw. der Zuschauercode. | Endpunkt | Wer | |---|---| | `GET /api/config`, `GET /api/matches` | alle | | `POST /api/matches` | alle (ggf. mit `CREATE_MATCH_PASSWORD`) | | `POST /api/access` `{code}` | prüft einen Code | | `GET /api/matches/:id` | rollenabhängiger Zustand | | `POST /api/matches/:id/join` `{code, nickname, steamId?}` | mit Teamcode | | `…/ready`, `…/leave` | Spieler | | `…/veto/action` `{stepIndex, map?, side?}` | Captain am Zug | | `…/captain` `{playerId}` | Organisator, Captain des Teams | | `…/veto/start`, `…/kick`, `…/codes/regenerate`, `…/server/retry`, `…/server/stop`, `…/server/warmup-end`, `…/series/next`, `…/finish`, `…/cancel` | Organisator | Socket.IO: `match:subscribe {matchId, token}` liefert `match:state` bei jeder Änderung; `overview:subscribe` liefert `overview`.