Compare commits

..

7 commits

Author SHA1 Message Date
a1cea9c37d 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 <noreply@anthropic.com>
2026-08-19 20:39:27 +02:00
a3519f11d4 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 <noreply@anthropic.com>
2026-08-19 20:39:27 +02:00
6aa5a7b025 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 <noreply@anthropic.com>
2026-08-19 20:39:27 +02:00
ac3e8b183c 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 <noreply@anthropic.com>
2026-08-19 20:39:27 +02:00
92eb59d2ef 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 <noreply@anthropic.com>
2026-08-19 20:39:12 +02:00
c4325ae8ac Merge branch 'master' of https://git.jappel.io/jappel/scoreboard-js
Konflikt in scoreboard/controllers/cli.js zugunsten der Remote-Version
aufgeloest: openBrowser()/killBrowser() bleiben erhalten, da routes/admin.js
sie verwendet. Der lokale Testcode wurde verworfen (er rief zudem das
child_process-Modul faelschlich als Funktion auf).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 20:32:29 +02:00
e25c8606d9 Added comments and readme 2026-04-10 22:16:51 +02:00
14 changed files with 396 additions and 85 deletions

310
README.md
View file

@ -1,3 +1,311 @@
# Scoreboard-JS
Implements a score- and timeboard for various sports
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
```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: <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`:
```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 |

View file

@ -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();

View file

@ -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);
/**

View file

@ -1,11 +1,16 @@
// CLI-Controller
// Startet und beendet den Anzeige-Browser auf dem Monitor-Rechner (Raspberry Pi).
// Chromium wird im Kiosk-Modus auf Display :0 geöffnet und zeigt die Monitor-Ansicht an.
const { spawn } = require('node:child_process')
let command;
let browserOpen = false;
let command; // Referenz auf den laufenden Chromium-Prozess
let browserOpen = false; // Status des Browsers, verhindert Mehrfachstart
// Startet Chromium im Kiosk-Modus, sofern noch kein Browser laeuft
async function openBrowser() {
if(!browserOpen) {
// const command = spawn("chromium-browser --display=:0 --noerrors --disable-session-crashed-bubble --disable-infobars --start-fullscreen http://localhost:3000 &")
// Als Benutzer "pi" starten, damit der Prozess Zugriff auf das X-Display hat
command = spawn('sudo', ['-upi', 'chromium-browser', '--display=:0', '--incognito', '--noerrors', '--hide-crash-restore-bubble', '--disable-infobars', '--kiosk', 'http://localhost']);
command.stdout.on('data', data => {
@ -19,6 +24,7 @@ async function openBrowser() {
}
}
// Beendet den laufenden Browserprozess, sofern einer gestartet wurde
async function killBrowser() {
if(browserOpen) {
command.kill();

View file

@ -1,44 +1,59 @@
"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 = {

View file

@ -17,7 +17,7 @@ let teamB = {
let sideswitch = false; // Attribute to switch sides after halftime
let referenceMirrored = false; // If sitting behind the scoreboard, switch sides for admin board
// Function prototypes
// Funktionsübersicht (Prototypen ohne Implementierung — nur zur Übersicht)
function setEnabled(status) {};
function configTeam(team, name, name2, isSpielgemeinschaft) {};
function setScore(team, score) {};
@ -32,6 +32,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;
@ -53,7 +54,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") {

View file

@ -1,5 +1,10 @@
// 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: "*" }
});

View file

@ -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",

View file

@ -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",

View file

@ -45,6 +45,7 @@ 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 + '<br>' + values.teamA.name2;
} else {
@ -57,6 +58,7 @@ function updateScoreFrontend(values) {
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 + '<br>' + values.teamA.name2;
} else {
@ -72,6 +74,8 @@ function updateScoreFrontend(values) {
}
// 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

View file

@ -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 */
}

View file

@ -83,8 +83,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());
});
@ -151,6 +152,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);

View file

@ -18,10 +18,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)
})

View file

@ -86,24 +86,7 @@
</div>
</div>
<div class="row mb-4">
<div class="col-sm-12 col-md-12 col-lg-4">
<div class="h-100 card text-center">
<div class="card-body">
<div class="row">
<div class="col-sm-6 col-md-6 col-lg-6 mb-4">
<h5 id="teamAcolName"></h5>
</div>
<div class="col-sm-6 col-md-6 col-lg-6">
<h5 id="teamBcolName"></h5>
</div>
</div>
</div>
</div>
</div>
<div class="row mb-4 justify-content-center">
<div class="col-sm-12 col-md-12 col-lg-4">
<div class="h-100 card text-center">
<div class="card-body">