From e25c8606d922622c2931453bbe4b3d9a4f73ab31 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Fri, 10 Apr 2026 22:16:51 +0200 Subject: [PATCH] Added comments and readme --- README.md | 211 +++++++++++++++++++- scoreboard/app.js | 7 +- scoreboard/controllers/cli.js | 20 +- scoreboard/controllers/score.js | 5 +- scoreboard/controllers/socketio.js | 7 +- scoreboard/public/javascripts/indexScore.js | 10 +- scoreboard/routes/admin.js | 4 +- scoreboard/routes/index.js | 4 +- 8 files changed, 252 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index ef66a02..3fa35a4 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,212 @@ # Scoreboard-JS -Implements a score- and timeboard for various sports \ No newline at end of file +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 (Timer + Score) | +| **Admin-Panel** | `http://:3000/admin` | Steuerung von Timer und Scoreboard | + +Alle Änderungen vom Admin-Panel werden über **WebSockets (Socket.IO)** in Echtzeit auf dem Monitor angezeigt — ohne Seiten-Reload. + +--- + +## Voraussetzungen + +- Node.js (v18 oder neuer empfohlen) +- 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**. + +--- + +## Projektstruktur + +``` +scoreboard/ +├── app.js # Express-App, Middleware, Routen +├── bin/www # HTTP-Server, startet die App +├── controllers/ +│ ├── socketio.js # Socket.IO-Server (Port 3001) +│ ├── timer.js # Timer-Logik (Start, Pause, Reset, IncDec) +│ ├── score.js # Score-Logik (Punkte, Teams, Seitenwechsel) +│ └── cli.js # CLI-Controller (aktuell Testcode) +├── routes/ +│ ├── admin.js # REST-Endpunkte für das Admin-Panel +│ └── index.js # Monitor-Ansicht + WebSocket-Verbindungshandler +├── views/ +│ ├── admin.hbs # Admin-Panel (Handlebars-Template) +│ ├── index.hbs # Monitor ohne Scoreboard (nur Timer) +│ └── indexScore.hbs # Monitor mit Scoreboard (Timer + Score + Teamnamen) +└── public/ + ├── javascripts/ + │ ├── admin.js # Frontend-Logik Admin-Panel + │ ├── index.js # Frontend-Logik Monitor (ohne Score) + │ └── indexScore.js # Frontend-Logik Monitor (mit Score) + └── stylesheets/ + ├── admin.css + ├── index.css + ├── indexScore.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** +- Punkte einzeln erhöhen (+1) oder verringern (-1) +- Score direkt auf einen bestimmten Wert setzen +- Score zurücksetzen (beide Teams auf 0) +- **Scoreboard ein-/ausblenden:** Der Monitor wechselt zwischen Timer-only-Ansicht (`index.hbs`) und Timer+Score-Ansicht (`indexScore.hbs`) + +### 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 | + +### Seitenwechsel (Halbzeit) + +Über "Seitenwechsel" werden Team A und Team B auf dem Monitor die Seiten tauschen — der Spielstand bleibt unverändert, nur die Anzeigepositionen (links/rechts) wechseln. + +### Monitor neu laden + +Der Button "Monitor neu laden" im Admin-Panel sendet ein `refresh`-Event an alle verbundenen Clients und löst einen Seiten-Reload des Monitors aus. Nützlich z.B. nach dem Ein-/Ausblenden des Scoreboards. + +--- + +## Architektur / Datenfluss + +``` +Admin-Panel (Browser) + │ + │ HTTP REST (fetch) + ▼ + Express-Server :3000 + routes/admin.js + │ + ├── controllers/timer.js ──┐ + └── controllers/score.js ──┤ + │ io.sockets.emit(event, data) + ▼ + Socket.IO-Server :3001 + │ + WebSocket-Verbindung + │ + ┌───────────────┴───────────────┐ + ▼ ▼ + Monitor (index.hbs) Monitor (indexScore.hbs) + index.js indexScore.js +``` + +### 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 | + +### 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 | + +Antwortformat `timerGetValues`: +```json +{ + "isPaused": true, + "duration": "", + "durationLeft": "", + "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 | +| 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": false, + "teamA": { "name": "Team A", "name2": "", "score": 0, "isSpielgemeinschaft": 0 }, + "teamB": { "name": "Team B", "name2": "", "score": 0, "isSpielgemeinschaft": 0 }, + "sideswitch": false, + "print": "0:0" +} +``` + +--- + +## 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 | diff --git a/scoreboard/app.js b/scoreboard/app.js index 2d97c1d..089c7ce 100644 --- a/scoreboard/app.js +++ b/scoreboard/app.js @@ -1,11 +1,14 @@ +// Einstiegspunkt der Express-Applikation. +// Registriert Middleware, View-Engine und Routen, und exportiert die App für bin/www. + var createError = require('http-errors'); var express = require('express'); var path = require('path'); var cookieParser = require('cookie-parser'); var logger = require('morgan'); -var indexRouter = require('./routes/index'); -var adminRouter = require('./routes/admin'); +var indexRouter = require('./routes/index'); // Öffentliche Monitor-Ansicht +var adminRouter = require('./routes/admin'); // Admin-Panel (Timer & Scoreboard steuern) var app = express(); diff --git a/scoreboard/controllers/cli.js b/scoreboard/controllers/cli.js index 8b5f6bd..0e3788b 100644 --- a/scoreboard/controllers/cli.js +++ b/scoreboard/controllers/cli.js @@ -1,13 +1,23 @@ +// CLI-Controller (Testdatei) +// Zuständig für die Verarbeitung von Kommandozeilen-Befehlen des Scoreboards. +// Aktuell nur ein Funktionstest mit einem einfachen `ls`-Befehl. + const exec = require('node:child_process'); -// run the `ls` command using exec +// Testaufruf: Listet den Inhalt des aktuellen Verzeichnisses auf, +// um zu prüfen, ob child_process.exec korrekt funktioniert. exec('ls ./', (err, output) => { - // once the command has completed, the callback function is called if (err) { - // log and return if we encounter an error console.error("could not execute command: ", err) return } - // log the output received from the command console.log("Output: \n", output) -}) \ No newline at end of file +}) + + + + + + + + diff --git a/scoreboard/controllers/score.js b/scoreboard/controllers/score.js index 46258f0..6e0ba7c 100644 --- a/scoreboard/controllers/score.js +++ b/scoreboard/controllers/score.js @@ -15,7 +15,7 @@ let teamB = { }; let sideswitch = false; -// Function prototypes +// Funktionsübersicht (Prototypen ohne Implementierung — nur zur Übersicht) function setEnabled(status) {}; function configTeam(team, name, name2, isSpielgemeinschaft) {}; function setScore(team, score) {}; @@ -30,6 +30,7 @@ function setEnabled(status) { enabled = status; } +// Setzt Namen und Spielgemeinschafts-Status eines Teams function configTeam(team, name, name2, isSpielgemeinschaft) { if(team == "teamA") { teamA.name = name; @@ -51,7 +52,7 @@ function setScore(team, score) { } } -// Increment score by one +// Ändert den Score eines Teams um 1 — Richtung "inc" (erhöhen) oder "dec" (verringern) function alterScore(team, dir) { if(team == "teamA") { if(dir == "inc") { diff --git a/scoreboard/controllers/socketio.js b/scoreboard/controllers/socketio.js index 5778fc4..ae048c3 100644 --- a/scoreboard/controllers/socketio.js +++ b/scoreboard/controllers/socketio.js @@ -1,7 +1,12 @@ +// Erstellt den zentralen Socket.IO-Server auf Port 3001. +// Wird von timer.js, score.js und den Routen importiert, +// um Events (Timer, Score, Refresh) an alle verbundenen Clients zu senden. + const { Server } = require('socket.io'); +// CORS auf "*" gesetzt, da Admin und Monitor auf unterschiedlichen Ports laufen können const io = new Server(3001, { cors: { origin: "*" } - }); +}); module.exports = io \ No newline at end of file diff --git a/scoreboard/public/javascripts/indexScore.js b/scoreboard/public/javascripts/indexScore.js index 2b54de2..476cec9 100644 --- a/scoreboard/public/javascripts/indexScore.js +++ b/scoreboard/public/javascripts/indexScore.js @@ -45,33 +45,37 @@ function updateScoreFrontend(values) { document.getElementById("score").innerHTML = values.print; // Set score on admin interface if(!values.sideswitch) { + // Normale Seite: teamA links, teamB rechts if(values.teamA.isSpielgemeinschaft) { document.getElementById("teamA").innerHTML = values.teamA.name + '
' + values.teamA.name2; } else { document.getElementById("teamA").innerHTML = values.teamA.name } - + if(values.teamB.isSpielgemeinschaft) { document.getElementById("teamB").innerHTML = values.teamB.name + '
' + values.teamB.name2; } else { document.getElementById("teamB").innerHTML = values.teamB.name } } else { + // Seitenwechsel nach Halbzeit: teamA und teamB tauschen ihre Anzeigeseite if(values.teamA.isSpielgemeinschaft) { document.getElementById("teamB").innerHTML = values.teamA.name + '
' + values.teamA.name2; } else { document.getElementById("teamB").innerHTML = values.teamA.name } - + if(values.teamB.isSpielgemeinschaft) { document.getElementById("teamA").innerHTML = values.teamB.name + '
' + values.teamB.name2; } else { document.getElementById("teamA").innerHTML = values.teamB.name - } + } } } +// Wird beim Laden der Seite aufgerufen, um Timer und Score sofort mit dem aktuellen +// Serverstand zu synchronisieren (verhindert leere Anzeige nach Seiten-Reload) async function initialUpdate() { timerGetValues(); // Request new values for timer scoreGetValues(); // Request new values for score diff --git a/scoreboard/routes/admin.js b/scoreboard/routes/admin.js index 595bb4e..cf611f4 100644 --- a/scoreboard/routes/admin.js +++ b/scoreboard/routes/admin.js @@ -55,8 +55,9 @@ router.post('/timerReset', function(req, res, next) { res.json(timer.getValues()); // Respond with all important values to update the frontend }); +// Express router endpoint to increase or decrease the timer by a value in seconds router.post('/timerIncDec', function(req, res, next) { - timer.incDec(req.body.value); + timer.incDec(req.body.value); // Positive Werte = erhöhen, negative Werte = verringern res.json(timer.getValues()); }); @@ -116,6 +117,7 @@ router.get('/scoreClearScore', function(req, res, next) { res.json(score.getValues()); // Respond with important values for frontend }); +// Express router endpoint to configure both teams (name, name2, Spielgemeinschaft-flag) router.post('/scoreConfigTeams', function(req, res, next) { score.configTeam("teamA", req.body.teamA.name, req.body.teamA.name2, req.body.teamA.isSpielgemeinschaft); score.configTeam("teamB", req.body.teamB.name, req.body.teamB.name2, req.body.teamB.isSpielgemeinschaft); diff --git a/scoreboard/routes/index.js b/scoreboard/routes/index.js index 7a8422c..4aa1a41 100644 --- a/scoreboard/routes/index.js +++ b/scoreboard/routes/index.js @@ -13,10 +13,12 @@ router.get('/', function(req, res, next) { } }); +// Websocket-Verbindungshandler: wird ausgelöst, sobald ein Client (Monitor) sich verbindet io.on('connection', (socket) => { console.log("A user connected"); - socket.emit("Hello user from server"); + socket.emit("Hello user from server"); // Begrüßungsnachricht an den neuen Client + // Eingehende Nachrichten vom Client loggen (aktuell nur für Debugging) socket.on('message', (message) => { console.log(message) })