# 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://:3000/` | Vollbildanzeige für den Beamer/Monitor | | **Admin-Panel** | `http://: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 ```bash cd scoreboard npm install npm start ``` Der Server startet mit `nodemon` und ist anschließend unter Port **3000** erreichbar. Der Socket.IO-Server läuft separat auf Port **3001**. ### Port ändern Der HTTP-Port lässt sich über die Umgebungsvariable `PORT` setzen: ```bash 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 (Port 3001) │ ├── 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 + WebSocket-Verbindungshandler ├── 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 `timerEnded` an 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-Server :3001 │ 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. ### 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: ```json { "splashscreenEnabled": true } ``` ### Timer | Methode | Endpunkt | Body | Beschreibung | |---|---|---|---| | GET | `/admin/timerStart` | — | Timer starten | | GET | `/admin/timerPause` | — | Timer pausieren | | POST | `/admin/timerReset` | `{ duration: }` | Timer zurücksetzen | | POST | `/admin/timerIncDec` | `{ value: }` | 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`: ```json { "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`: ```json { "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: ```json { "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 |