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.
This commit is contained in:
mregosar committed 2026-10-25 17:02:18 +01:00
1 parent f08297dd0d
commit f7520a6ca7
24 files changed
+1938 -1

No files matched your search

+327
View File
@@ -0,0 +1,327 @@
# 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 <http://localhost:5173> ö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/<code>`
- Team B: `/join/<code>`
- Zuschauer: `/watch/<code>` 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, wenn es vollständig ist (abschaltbar mit `REQUIRE_FULL_TEAMS=false`), einen Captain hat und alle bereit sind.
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 |
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.
> 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 <map>`.
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 <https://steamcommunity.com/dev/managegameservers> 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-<matchId>` 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 <serverpasswort>; connect <host>:<port>
```
Zusätzlich bietet die Seite einen `steam://connect/<host>:<port>/<passwort>`-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 <tvpasswort>; connect <host>:<tvPort>`. 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` | `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 <token>`. 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`.