Compare commits
7 commits
799dd9bd8c
...
a1cea9c37d
| Author | SHA1 | Date | |
|---|---|---|---|
| a1cea9c37d | |||
| a3519f11d4 | |||
| 6aa5a7b025 | |||
| ac3e8b183c | |||
| 92eb59d2ef | |||
| c4325ae8ac | |||
| e25c8606d9 |
14 changed files with 396 additions and 85 deletions
310
README.md
310
README.md
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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") {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
26
scoreboard/package-lock.json
generated
26
scoreboard/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue