From e25c8606d922622c2931453bbe4b3d9a4f73ab31 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Fri, 10 Apr 2026 22:16:51 +0200 Subject: [PATCH 1/6] 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) }) From 92eb59d2efd363a2bf4eebe88136902487589ced Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Wed, 19 Aug 2026 20:39:12 +0200 Subject: [PATCH 2/6] Teamdatenbank ohne lowdb, Pfad unabhaengig vom Startverzeichnis lowdb v7 ist ESM-only, das Projekt ist CommonJS. Der bisherige Umweg ueber eine async IIFE mit await import() hatte zur Folge, dass db bei einem sehr fruehen Request noch undefined sein konnte. Fuer eine Liste von Teamnamen genuegt fs: read/write lesen und schreiben db.json direkt und synchron, die Race-Condition entfaellt und lowdb kann als Abhaengigkeit raus. Die exportierte API bleibt unveraendert, score.js und routes/admin.js brauchen keine Anpassung. Der Pfad zu db.json wird jetzt ueber __dirname aufgeloest statt relativ zum Arbeitsverzeichnis, damit die Datei unabhaengig vom Startverzeichnis immer an derselben Stelle liegt. Fehlt oder bricht die Datei, wird auf eine leere Teamliste zurueckgefallen. Co-Authored-By: Claude Opus 5 --- scoreboard/controllers/db.js | 61 ++++++++++++++++++++++-------------- scoreboard/package-lock.json | 26 --------------- scoreboard/package.json | 1 - 3 files changed, 38 insertions(+), 50 deletions(-) diff --git a/scoreboard/controllers/db.js b/scoreboard/controllers/db.js index a135404..73e29d3 100644 --- a/scoreboard/controllers/db.js +++ b/scoreboard/controllers/db.js @@ -1,46 +1,61 @@ "use strict"; -let db; +// Datenbank-Controller +// Haelt die Liste der bekannten Teamnamen persistent in db.json vor, +// damit sie im Adminpanel als Auswahl-Dropdown angeboten werden koennen. -// Import ESM into CJS file using immediately invoked function expression because of missing top-level await in CJS -(async () => { - const { LowSync } = await import('lowdb'); - const { JSONFileSync } = await import('lowdb/node'); - db = new LowSync(new JSONFileSync('db.json'), { teams: []}) -})(); +const fs = require('node:fs'); +const path = require('node:path'); +// Pfad relativ zum Modul, nicht zum Arbeitsverzeichnis, damit die Datei +// unabhaengig vom Startverzeichnis immer an derselben Stelle liegt +const dbFile = path.join(__dirname, '..', 'db.json'); + +// Liest db.json ein, faellt bei fehlender oder defekter Datei auf eine leere Liste zurueck +function read() { + try { + const data = JSON.parse(fs.readFileSync(dbFile, 'utf-8')); + if(!Array.isArray(data.teams)) data.teams = []; // Fehlender oder falscher teams-Key + return data; + } catch (err) { + console.error("db.json konnte nicht gelesen werden, verwende leere Teamliste: ", err.message); + return { teams: [] }; + } +} + +// Schreibt den uebergebenen Datensatz zurueck nach db.json +function write(data) { + fs.writeFileSync(dbFile, JSON.stringify(data, null, 2)); +} + +// Returns all teams from db function getTeams() { - db.read(); - return db.data.teams + return read().teams; } // Adds passed teamName to db if not already existing function addTeam(teamName) { - console.log(teamName) - db.read(); - if(!db.data.teams.includes(teamName) && teamName != "") { // Teamname not in db - db.data.teams.push(teamName); - db.write(); - } else { // Teamname already in db + const data = read(); + if(!data.teams.includes(teamName) && teamName != "") { // Teamname not in db + data.teams.push(teamName); + write(data); } } // Deletes passed teamName from db if existing function deleteTeam(teamName) { - db.read(); - if(db.data.teams.includes(teamName) && teamName != "") { // Teamname in db - db.data.teams.splice(db.data.teams.indexOf(teamName), 1) // Delete corresponding array entry - db.write(); + const data = read(); + if(data.teams.includes(teamName) && teamName != "") { // Teamname in db + data.teams.splice(data.teams.indexOf(teamName), 1); // Delete corresponding array entry + write(data); } } // Returns important values from db function getValues() { - db.read(); - console.log(db.data) - return db.data + return read(); } module.exports = { getTeams, addTeam, deleteTeam, getValues -} \ No newline at end of file +} diff --git a/scoreboard/package-lock.json b/scoreboard/package-lock.json index 4eea9ad..5e9c263 100644 --- a/scoreboard/package-lock.json +++ b/scoreboard/package-lock.json @@ -14,7 +14,6 @@ "express-ws": "^5.0.2", "hbs": "~4.0.4", "http-errors": "~1.6.3", - "lowdb": "^7.0.1", "moment": "^2.30.1", "morgan": "~1.9.1", "socket.io": "^4.7.5", @@ -470,20 +469,6 @@ "node": ">= 0.10" } }, - "node_modules/lowdb": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-7.0.1.tgz", - "integrity": "sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw==", - "dependencies": { - "steno": "^4.0.2" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/typicode" - } - }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -858,17 +843,6 @@ "node": ">= 0.6" } }, - "node_modules/steno": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/steno/-/steno-4.0.2.tgz", - "integrity": "sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/typicode" - } - }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", diff --git a/scoreboard/package.json b/scoreboard/package.json index 63f7a92..3fad970 100644 --- a/scoreboard/package.json +++ b/scoreboard/package.json @@ -12,7 +12,6 @@ "express-ws": "^5.0.2", "hbs": "~4.0.4", "http-errors": "~1.6.3", - "lowdb": "^7.0.1", "moment": "^2.30.1", "morgan": "~1.9.1", "socket.io": "^4.7.5", From ac3e8b183c7b2c386fbd9c8925c5bbd3a162228b Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Wed, 19 Aug 2026 20:39:27 +0200 Subject: [PATCH 3/6] Doppelte Team-IDs im Adminpanel entfernt Mit dem Verschieben der +1/-1-Buttons in das Score-Panel blieb der alte Block darunter als leere Karte stehen. Dadurch existierten teamAcolName und teamBcolName jeweils zweimal im Dokument; getElementById traf nur die erste Instanz, die zweite blieb dauerhaft leer. Karte entfernt und die verbleibende Zeile zentriert. Co-Authored-By: Claude Opus 5 --- scoreboard/views/admin.hbs | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/scoreboard/views/admin.hbs b/scoreboard/views/admin.hbs index b14274a..735a685 100644 --- a/scoreboard/views/admin.hbs +++ b/scoreboard/views/admin.hbs @@ -86,24 +86,7 @@ -
-
-
-
-
-
-
- -
-
-
- -
-
-
-
-
- +
From 6aa5a7b02500a6dacf1451fd13b6470a54cbfd57 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Wed, 19 Aug 2026 20:39:27 +0200 Subject: [PATCH 4/6] Standardport wieder 3000, Port 80 per Umgebungsvariable Port 80 stammt aus dem Kiosk-Setup auf dem Pi, wo Chromium die Seite ohne Portangabe unter http://localhost oeffnet. Als Default erzwingt er lokal erhoehte Rechte. Auf dem Pi stattdessen PORT=80 setzen. Co-Authored-By: Claude Opus 5 --- scoreboard/bin/www | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scoreboard/bin/www b/scoreboard/bin/www index eec0010..95be052 100644 --- a/scoreboard/bin/www +++ b/scoreboard/bin/www @@ -12,7 +12,7 @@ var http = require('http'); * Get port from environment and store in Express. */ -var port = normalizePort(process.env.PORT || '80'); +var port = normalizePort(process.env.PORT || '3000'); app.set('port', port); /** From a3519f11d474be3d0cac0ed95c72abcb0a831a3b Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Wed, 19 Aug 2026 20:39:27 +0200 Subject: [PATCH 5/6] Splashscreen-Stylesheet gefuellt Die Datei war leer, wodurch der Splashscreen den weissen Browser-Hintergrund zeigte statt des Blaus der uebrigen Monitoransichten. Scrollbalken auf dem Beamer unterdrueckt. Co-Authored-By: Claude Opus 5 --- scoreboard/public/stylesheets/splashscreen.css | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/scoreboard/public/stylesheets/splashscreen.css b/scoreboard/public/stylesheets/splashscreen.css index e69de29..ed6984a 100644 --- a/scoreboard/public/stylesheets/splashscreen.css +++ b/scoreboard/public/stylesheets/splashscreen.css @@ -0,0 +1,9 @@ +html, body { + height: 100%; + overflow: hidden; /* Kein Scrollbalken auf dem Beamer */ +} + +body { + font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; + background-color: #47BAEA; /* Gleicher Hintergrund wie die Timer- und Score-Ansicht */ +} From a1cea9c37d8f914dbcdf1b80101bb200469ac194 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Wed, 19 Aug 2026 20:39:27 +0200 Subject: [PATCH 6/6] README auf den gemergten Stand gebracht Ergaenzt: Splashscreen als dritte Monitoransicht und Standard beim Start, Team-Datenbank, Debugfenster, Spiegelung des Adminpanels, Steuerung des Kiosk-Browsers, die zugehoerigen etc-, db- und score-Endpunkte sowie der Hinweis zur PORT-Variable. Korrigiert: Portangabe, Projektstruktur, Antwortformate und die Abhaengigkeitsliste. Co-Authored-By: Claude Opus 5 --- README.md | 149 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 124 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 3fa35a4..d9193b5 100644 --- a/README.md +++ b/README.md @@ -10,16 +10,24 @@ 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 | +| **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 empfohlen) +- Node.js (v18 oder neuer) - npm --- @@ -32,9 +40,21 @@ npm install npm start ``` -Der Server startet mit `nodemon` und ist anschließend unter Port **3000** erreichbar. +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 @@ -43,27 +63,34 @@ Der Socket.IO-Server läuft separat auf Port **3001**. 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) -│ └── cli.js # CLI-Controller (aktuell Testcode) +│ ├── 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) -│ ├── index.hbs # Monitor ohne Scoreboard (nur Timer) -│ └── indexScore.hbs # Monitor mit Scoreboard (Timer + Score + Teamnamen) +│ ├── 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) + │ ├── 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 ``` @@ -81,11 +108,10 @@ scoreboard/ ### Scoreboard -- Punktestand für **Team A** und **Team B** -- Punkte einzeln erhöhen (+1) oder verringern (-1) +- 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 (`index.hbs`) und Timer+Score-Ansicht (`indexScore.hbs`) +- **Scoreboard ein-/ausblenden:** Der Monitor wechselt zwischen Timer-only-Ansicht und Timer+Score-Ansicht ### Teams konfigurieren @@ -97,13 +123,47 @@ Jedes Team hat folgende Felder: | `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" werden Team A und Team B auf dem Monitor die Seiten tauschen — der Spielstand bleibt unverändert, nur die Anzeigepositionen (links/rechts) wechseln. +Ü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" 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. +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. --- @@ -118,19 +178,25 @@ Admin-Panel (Browser) routes/admin.js │ ├── controllers/timer.js ──┐ - └── controllers/score.js ──┤ - │ io.sockets.emit(event, data) - ▼ - Socket.IO-Server :3001 + ├── 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 (index.hbs) Monitor (indexScore.hbs) - index.js indexScore.js + ┌──────────────────────┼──────────────────────┐ + ▼ ▼ ▼ + 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 | @@ -152,6 +218,20 @@ Alle Endpunkte sind unter `/admin` erreichbar. | 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 @@ -163,12 +243,15 @@ Alle Endpunkte sind unter `/admin` erreichbar. | 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": "", - "durationLeft": "", + "duration": "PT7M", + "durationLeft": "PT7M", "print": "07:00" } ``` @@ -181,6 +264,7 @@ Antwortformat `timerGetValues`: | 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 | @@ -190,14 +274,29 @@ Antwortformat `timerGetValues`: Antwortformat `scoreGetValues`: ```json { - "enabled": false, + "enabled": true, "teamA": { "name": "Team A", "name2": "", "score": 0, "isSpielgemeinschaft": 0 }, "teamB": { "name": "Team B", "name2": "", "score": 0, "isSpielgemeinschaft": 0 }, "sideswitch": false, - "print": "0:0" + "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