Der Socket.IO-Server lief als zweiter Server auf Port 3001 und brauchte
deswegen cors: { origin: "*" }; die Clients bauten ihre Verbindungs-URL aus
window.location.hostname und dem festen Port zusammen.
Er wird jetzt ohne eigenen Port erzeugt und in bin/www per io.attach() an den
bestehenden HTTP-Server gehaengt. Erzeugen und Anhaengen sind getrennt, weil das
Modul beim Laden der Routen ausgewertet wird, also bevor der HTTP-Server
existiert. Die Clients rufen nur noch io() ohne Argument auf und verbinden sich
damit zur Herkunft der Seite zurueck.
Damit entfallen der zweite Port, die CORS-Ausnahme und eine moegliche zweite
Firewall-Regel auf dem Pi. Verifiziert: Port 3001 lauscht nicht mehr, alle
Events (score, timerDurationLeft, timerEnded, scoreSideswitch, refresh) kommen
ueber Port 3000 an, Reconnect funktioniert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|---|---|---|
| scoreboard | ||
| .gitignore | ||
| README.md | ||
Scoreboard-JS
Echtzeit-Anzeigetafel für Sportveranstaltungen — zeigt Timer und Spielstand auf einem separaten Monitor an und wird über ein Admin-Panel gesteuert.
Übersicht
Das System besteht aus zwei Teilen:
| Teil | URL | Beschreibung |
|---|---|---|
| Monitor | http://<host>:3000/ |
Vollbildanzeige für den Beamer/Monitor |
| Admin-Panel | http://<host>:3000/admin |
Steuerung von Timer, Scoreboard und Anzeige |
Alle Änderungen vom Admin-Panel werden über WebSockets (Socket.IO) in Echtzeit auf dem Monitor angezeigt — ohne Seiten-Reload.
Der Monitor rendert je nach Serverzustand eine von drei Ansichten:
| Ansicht | Template | Wann |
|---|---|---|
| Splashscreen | splashscreen.hbs |
Solange der Splashscreen aktiv ist (Standard beim Start) |
| Timer + Score | indexScore.hbs |
Splashscreen aus, Scoreboard aktiv (Standard) |
| Nur Timer | index.hbs |
Splashscreen aus, Scoreboard deaktiviert |
Voraussetzungen
- Node.js (v18 oder neuer)
- npm
Installation & Start
cd scoreboard
npm install
npm start
Der Server startet mit nodemon und ist anschließend unter Port 3000 erreichbar.
Die WebSocket-Verbindung läuft über denselben Port — es ist kein zweiter Port nötig.
Port ändern
Der HTTP-Port lässt sich über die Umgebungsvariable PORT setzen:
PORT=80 npm start
Auf dem Anzeige-Rechner (Raspberry Pi) wird Port 80 verwendet, damit der
Kiosk-Browser die Seite ohne Portangabe unter http://localhost öffnen kann.
Port 80 erfordert erhöhte Rechte.
Projektstruktur
scoreboard/
├── app.js # Express-App, Middleware, Routen
├── bin/www # HTTP-Server, startet die App
├── db.json # Persistente Liste bekannter Teamnamen
├── controllers/
│ ├── socketio.js # Socket.IO-Server (teilt sich den HTTP-Port)
│ ├── timer.js # Timer-Logik (Start, Pause, Reset, IncDec)
│ ├── score.js # Score-Logik (Punkte, Teams, Seitenwechsel)
│ ├── db.js # Lesen/Schreiben der Teamnamen in db.json
│ ├── etc.js # Splashscreen-Status
│ └── cli.js # Start/Stop des Kiosk-Browsers auf dem Anzeigerechner
├── routes/
│ ├── admin.js # REST-Endpunkte für das Admin-Panel
│ └── index.js # Monitor-Ansicht (waehlt das passende Template)
├── views/
│ ├── admin.hbs # Admin-Panel (Handlebars-Template)
│ ├── splashscreen.hbs # Monitor: Vereinslogo
│ ├── index.hbs # Monitor: nur Timer
│ └── indexScore.hbs # Monitor: Timer + Score + Teamnamen
└── public/
├── img/ # Logos für den Splashscreen
├── javascripts/
│ ├── admin.js # Frontend-Logik Admin-Panel
│ ├── index.js # Frontend-Logik Monitor (ohne Score)
│ ├── indexScore.js # Frontend-Logik Monitor (mit Score)
│ └── splashscreen.js # Frontend-Logik Splashscreen (nur Refresh-Handler)
└── stylesheets/
├── admin.css
├── index.css
├── indexScore.css
├── splashscreen.css
└── seven-segment.css # Seven-Segment-Display-Schrift für den Timer
Funktionen
Timer
- Countdown-Timer mit konfigurierbarer Startzeit (Standard: 7:00 Minuten)
- Start / Pause
- Zurücksetzen auf eine neue Zeit (0:00 bis 10:00, in 15-Sekunden-Schritten)
- Feineinstellung im laufenden Betrieb: ±1s, ±5s, ±10s
- Nach Ablauf wird
timerEndedan alle Clients gesendet und "ENDE" auf dem Monitor angezeigt
Scoreboard
- Punktestand für Team A und Team B, direkt im Admin-Panel über +1/-1 steuerbar
- Score direkt auf einen bestimmten Wert setzen
- Score zurücksetzen (beide Teams auf 0)
- Scoreboard ein-/ausblenden: Der Monitor wechselt zwischen Timer-only-Ansicht und Timer+Score-Ansicht
Teams konfigurieren
Jedes Team hat folgende Felder:
| Feld | Beschreibung |
|---|---|
name |
Hauptname des Teams |
name2 |
Zweiter Name (nur bei Spielgemeinschaft) |
isSpielgemeinschaft |
Wenn aktiv, werden beide Namen untereinander angezeigt |
Die Namen können frei eingetippt oder per Dropdown aus der Team-Datenbank gewählt werden.
Team-Datenbank
Häufig verwendete Teamnamen werden in db.json gespeichert und stehen im
Konfigurationsdialog als Auswahl zur Verfügung. Teams lassen sich im
Debugfenster hinzufügen und löschen.
Seitenwechsel (Halbzeit)
Über "Seitenwechsel" tauschen Team A und Team B auf dem Monitor die Seiten — der Spielstand bleibt unverändert, nur die Anzeigepositionen (links/rechts) wechseln.
Spiegelung des Admin-Panels
Sitzt die Bedienung hinter dem Monitor, stimmt die Links/Rechts-Zuordnung im Admin-Panel nicht mehr mit der Sicht auf die Anzeigetafel überein. Der Schalter "Teamanzeige im Adminpanel spiegeln" im Debugfenster dreht die Darstellung im Admin-Panel um — inklusive der +1/-1-Buttons, die dann dem jeweils richtigen Team zugeordnet werden. Die Monitoranzeige bleibt davon unberührt.
Splashscreen
Standardmäßig zeigt der Monitor beim Start das Vereinslogo. Über den Schalter im Debugfenster wird zwischen Splashscreen und Spielanzeige umgeschaltet.
Debugfenster
Sammelt die selten benötigten Schalter: Spiegelung, Scoreboard ein/aus, Splashscreen ein/aus, Monitor neu laden, Kiosk-Browser starten/stoppen sowie die Verwaltung der Team-Datenbank.
Kiosk-Browser steuern
Auf dem Anzeige-Rechner (Raspberry Pi) kann der Chromium-Browser aus dem
Admin-Panel heraus im Kiosk-Modus gestartet und wieder beendet werden. Die
Funktion ist Linux-spezifisch und setzt einen Benutzer pi mit Zugriff auf
Display :0 voraus.
Monitor neu laden
Der Button "Monitor neu laden" sendet ein refresh-Event an alle verbundenen Clients und löst einen Seiten-Reload des Monitors aus. Nötig, wenn zwischen zwei Ansichten gewechselt wurde.
Architektur / Datenfluss
Admin-Panel (Browser)
│
│ HTTP REST (fetch)
▼
Express-Server :3000
routes/admin.js
│
├── controllers/timer.js ──┐
├── controllers/score.js ──┤
├── controllers/etc.js ──┤
├── controllers/db.js ──┐ │
└── controllers/cli.js │ │ io.sockets.emit(event, data)
│ ▼
db.json Socket.IO (am selben Port)
│
WebSocket-Verbindung
│
┌──────────────────────┼──────────────────────┐
▼ ▼ ▼
Monitor (splashscreen) Monitor (index) Monitor (indexScore)
splashscreen.js index.js indexScore.js
Der gesamte Spielzustand (Timer, Score, Teams, Schalter) liegt im
Arbeitsspeicher der Controller. Ein Serverneustart setzt ihn zurück; einzig die
Teamnamen in db.json bleiben erhalten.
Bricht die WebSocket-Verbindung ab — auf dem Pi, der das WLAN selbst aufspannt,
kommt das vor — verbindet sich der Client automatisch neu und holt sich beim
connect-Event den aktuellen Stand über die REST-Endpunkte. Ein Wechsel der
Ansicht während der Trennung (Splashscreen, Scoreboard ein/aus) wird dabei
nicht bemerkt; dafür ist "Monitor neu laden" gedacht.
WebSocket-Events
| Event | Richtung | Beschreibung |
|---|---|---|
timerDurationLeft |
Server → Client | Aktuelle Restzeit als String MM:SS |
timerEnded |
Server → Client | Timer abgelaufen |
score |
Server → Client | Aktueller Spielstand als String X:Y |
scoreSideswitch |
Server → Client | Seitenwechsel ausgelöst — Frontend neu laden |
refresh |
Server → Client | Kompletten Seiten-Reload auslösen |
REST-API (Admin-Endpunkte)
Alle Endpunkte sind unter /admin erreichbar.
Allgemein
| Methode | Endpunkt | Beschreibung |
|---|---|---|
| GET | /admin/refreshMonitor |
Sendet refresh an alle Monitors |
| GET | /admin/openBrowser |
Startet den Kiosk-Browser auf dem Anzeigerechner |
| GET | /admin/killBrowser |
Beendet den Kiosk-Browser |
Anzeige (etc)
| Methode | Endpunkt | Beschreibung |
|---|---|---|
| GET | /admin/etcGetValues |
Aktuellen Splashscreen-Status abrufen |
| GET | /admin/etcToggleSplashscreen |
Splashscreen ein-/ausblenden |
Antwortformat:
{ "splashscreenEnabled": true }
Timer
| Methode | Endpunkt | Body | Beschreibung |
|---|---|---|---|
| GET | /admin/timerStart |
— | Timer starten |
| GET | /admin/timerPause |
— | Timer pausieren |
| POST | /admin/timerReset |
{ duration: <Sekunden> } |
Timer zurücksetzen |
| POST | /admin/timerIncDec |
{ value: <Sekunden> } |
Timer erhöhen (positiv) / verringern (negativ) |
| GET | /admin/timerGetValues |
— | Aktuelle Timer-Werte abrufen |
timerStart und timerPause antworten mit 406, wenn die Aktion nicht möglich
ist (Timer läuft bereits, ist bereits pausiert oder abgelaufen).
Antwortformat timerGetValues:
{
"isPaused": true,
"duration": "PT7M",
"durationLeft": "PT7M",
"print": "07:00"
}
Scoreboard
| Methode | Endpunkt | Body | Beschreibung |
|---|---|---|---|
| GET | /admin/scoreToggle |
— | Scoreboard ein-/ausblenden |
| GET | /admin/scoreGetValues |
— | Aktuelle Score-Werte abrufen |
| GET | /admin/scoreClearScore |
— | Spielstand beider Teams auf 0 |
| GET | /admin/scoreToggleSideswitch |
— | Seiten der Teams tauschen |
| GET | /admin/scoreToggleReferenceMirrored |
— | Admin-Panel-Darstellung spiegeln |
| POST | /admin/scoreSetScore |
{ team, score } |
Score direkt setzen |
| POST | /admin/scoreAlterScore |
{ team, dir } |
Score um 1 ändern (dir: "inc" / "dec") |
| POST | /admin/scoreConfigTeams |
{ teamA: { name, name2, isSpielgemeinschaft }, teamB: { ... } } |
Teams konfigurieren |
team ist jeweils "teamA" oder "teamB".
Antwortformat scoreGetValues:
{
"enabled": true,
"teamA": { "name": "Team A", "name2": "", "score": 0, "isSpielgemeinschaft": 0 },
"teamB": { "name": "Team B", "name2": "", "score": 0, "isSpielgemeinschaft": 0 },
"sideswitch": false,
"referenceMirrored": false,
"print": "0:0",
"teams": ["Arheilgen", "Prechtal"]
}
Team-Datenbank
| Methode | Endpunkt | Body | Beschreibung |
|---|---|---|---|
| GET | /admin/dbGetValues |
— | Alle gespeicherten Teamnamen abrufen |
| POST | /admin/dbAddTeam |
{ teamName } |
Teamnamen hinzufügen (Duplikate und Leerstrings werden ignoriert) |
| POST | /admin/dbDeleteTeam |
{ teamName } |
Teamnamen löschen |
Antwortformat:
{ "teams": ["Arheilgen", "Prechtal"] }
Abhängigkeiten
| Paket | Verwendung |
|---|---|
express |
HTTP-Server und Routing |
socket.io |
WebSocket-Kommunikation |
moment |
Timer-Berechnung und Zeitformatierung |
hbs |
Handlebars als View-Engine |
morgan |
HTTP-Request-Logging |
nodemon |
Automatischer Server-Neustart bei Dateiänderungen |