Compare commits

..

3 commits

Author SHA1 Message Date
2a1e0904e8 Socket.IO teilt sich den HTTP-Port statt eines eigenen Servers auf 3001
Der Socket.IO-Server lief als zweiter Server auf Port 3001 und brauchte
deswegen cors: { origin: "*" }; die Clients bauten ihre Verbindungs-URL aus
window.location.hostname und dem festen Port zusammen.

Er wird jetzt ohne eigenen Port erzeugt und in bin/www per io.attach() an den
bestehenden HTTP-Server gehaengt. Erzeugen und Anhaengen sind getrennt, weil das
Modul beim Laden der Routen ausgewertet wird, also bevor der HTTP-Server
existiert. Die Clients rufen nur noch io() ohne Argument auf und verbinden sich
damit zur Herkunft der Seite zurueck.

Damit entfallen der zweite Port, die CORS-Ausnahme und eine moegliche zweite
Firewall-Regel auf dem Pi. Verifiziert: Port 3001 lauscht nicht mehr, alle
Events (score, timerDurationLeft, timerEnded, scoreSideswitch, refresh) kommen
ueber Port 3000 an, Reconnect funktioniert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 21:08:34 +02:00
cd4877bdd2 Frontend: Reconnect-Sync, Dropdowns ohne String-Interpolation, Fehlerpfad
Alle vier Frontends horchten auf ein Event 'connected', das nie gesendet wurde;
der Server verschickte stattdessen ein Event mit dem Namen "Hello user from
server" und leerem Payload. Beides ist entfernt. An seine Stelle tritt das
Socket.IO-eigene 'connect', das auch nach einem Reconnect feuert: Adminpanel und
Monitor holen sich dort den aktuellen Stand. Da der Pi das WLAN selbst
aufspannt, sind kurze Abbrueche im Betrieb normal — bisher blieb die Anzeige
danach auf einem veralteten Stand stehen, bis das naechste Ereignis eintraf.

Die Teamnamen-Dropdowns bauten ihre Eintraege als HTML-String zusammen und
interpolierten den Namen in einen onclick-Aufruf. Ein Name mit Apostroph, etwa
"SG D'Horn", zerlegte den Handler und machte den Eintrag unbrauchbar. Die
Eintraege entstehen jetzt ueber createTeamDropdownItem() mit textContent und
addEventListener, der Name kommt per Closure statt als String. Die
Schleifenvariable ist zudem korrekt deklariert, vorher war sie global.

fetchJson() kapselt Anfragen und liefert null, wenn der Server nicht erreichbar
ist oder mit einem Fehler antwortet, statt still in der Konsole zu scheitern.
timerStart und timerPause behalten ihre eigene Auswertung des 406-Status.

Das Verbindungs-Handling ist von routes/index.js nach controllers/socketio.js
gewandert, wo der Socket.IO-Server erzeugt wird, und protokolliert jetzt auch
Verbindungsabbrueche.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 21:06:45 +02:00
6f4c63aa6e Bugfixes: Timer-Zuschlag, Debugfenster-Schalter, Score-Untergrenze
timerIncDec beendete den Timer auch beim Hinzufuegen von Zeit: die Pruefung
verglich den Betrag des Werts mit der Restzeit, ohne die Richtung zu beachten.
Bei 5s Restzeit fuehrte "+10s" damit zum Spielende statt zu 15s. Die Pruefung
gilt jetzt nur noch fuer negative Werte. Beim Verringern unter 0 wird die
Restzeit zusaetzlich sauber auf 00:00 gesetzt, sonst blieb sie stehen und der
Timer war trotz "ENDE" auf dem Monitor wieder startbar.

Im Debugfenster zeigten alle drei Label auf scoreSwitchEnable. Ein Klick auf
den Text "Teamanzeige spiegeln" oder "Splashscreen anzeigen" blendete damit das
Scoreboard aus. Jede Beschriftung zeigt jetzt auf ihren eigenen Schalter.

scoreSetScore sendet den neuen Stand jetzt wie alterScore und clearScore an die
Monitore; der Endpunkt wird vom Panel nicht verwendet, ist aber dokumentiert.

alterScore laesst den Spielstand nicht mehr unter 0 laufen.

Die toten Funktionsprototypen in timer.js und score.js sind entfernt: sie wurden
durch Hoisting ohnehin ueberschrieben und listeten toggleReferenceMirrored und
print schon nicht mehr auf. Ebenso die Wert-Snapshots duration, durationLeft und
isPaused aus module.exports, die dauerhaft die Startwerte lieferten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 21:06:30 +02:00
11 changed files with 182 additions and 135 deletions

View file

@ -41,7 +41,7 @@ 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**. Die WebSocket-Verbindung läuft über denselben Port — es ist kein zweiter Port nötig.
### Port ändern ### Port ändern
@ -65,7 +65,7 @@ scoreboard/
├── bin/www # HTTP-Server, startet die App ├── bin/www # HTTP-Server, startet die App
├── db.json # Persistente Liste bekannter Teamnamen ├── db.json # Persistente Liste bekannter Teamnamen
├── controllers/ ├── controllers/
│ ├── socketio.js # Socket.IO-Server (Port 3001) │ ├── socketio.js # Socket.IO-Server (teilt sich den HTTP-Port)
│ ├── timer.js # Timer-Logik (Start, Pause, Reset, IncDec) │ ├── timer.js # Timer-Logik (Start, Pause, Reset, IncDec)
│ ├── score.js # Score-Logik (Punkte, Teams, Seitenwechsel) │ ├── score.js # Score-Logik (Punkte, Teams, Seitenwechsel)
│ ├── db.js # Lesen/Schreiben der Teamnamen in db.json │ ├── db.js # Lesen/Schreiben der Teamnamen in db.json
@ -73,7 +73,7 @@ scoreboard/
│ └── cli.js # Start/Stop des Kiosk-Browsers auf dem Anzeigerechner │ └── cli.js # Start/Stop des Kiosk-Browsers auf dem Anzeigerechner
├── routes/ ├── routes/
│ ├── admin.js # REST-Endpunkte für das Admin-Panel │ ├── admin.js # REST-Endpunkte für das Admin-Panel
│ └── index.js # Monitor-Ansicht + WebSocket-Verbindungshandler │ └── index.js # Monitor-Ansicht (waehlt das passende Template)
├── views/ ├── views/
│ ├── admin.hbs # Admin-Panel (Handlebars-Template) │ ├── admin.hbs # Admin-Panel (Handlebars-Template)
│ ├── splashscreen.hbs # Monitor: Vereinslogo │ ├── splashscreen.hbs # Monitor: Vereinslogo
@ -183,7 +183,7 @@ Admin-Panel (Browser)
├── controllers/db.js ──┐ │ ├── controllers/db.js ──┐ │
└── controllers/cli.js │ │ io.sockets.emit(event, data) └── controllers/cli.js │ │ io.sockets.emit(event, data)
│ ▼ │ ▼
db.json Socket.IO-Server :3001 db.json Socket.IO (am selben Port)
WebSocket-Verbindung WebSocket-Verbindung
@ -197,6 +197,12 @@ Der gesamte Spielzustand (Timer, Score, Teams, Schalter) liegt **im
Arbeitsspeicher** der Controller. Ein Serverneustart setzt ihn zurück; einzig die Arbeitsspeicher** der Controller. Ein Serverneustart setzt ihn zurück; einzig die
Teamnamen in `db.json` bleiben erhalten. Teamnamen in `db.json` bleiben erhalten.
Bricht die WebSocket-Verbindung ab — auf dem Pi, der das WLAN selbst aufspannt,
kommt das vor — verbindet sich der Client automatisch neu und holt sich beim
`connect`-Event den aktuellen Stand über die REST-Endpunkte. Ein **Wechsel der
Ansicht** während der Trennung (Splashscreen, Scoreboard ein/aus) wird dabei
nicht bemerkt; dafür ist "Monitor neu laden" gedacht.
### WebSocket-Events ### WebSocket-Events
| Event | Richtung | Beschreibung | | Event | Richtung | Beschreibung |

View file

@ -7,6 +7,7 @@
var app = require('../app'); var app = require('../app');
var debug = require('debug')('scoreboard:server'); var debug = require('debug')('scoreboard:server');
var http = require('http'); var http = require('http');
var io = require('../controllers/socketio');
/** /**
* Get port from environment and store in Express. * Get port from environment and store in Express.
@ -21,6 +22,12 @@ app.set('port', port);
var server = http.createServer(app); var server = http.createServer(app);
/**
* Attach Socket.IO to the same server, so it shares the HTTP port.
*/
io.attach(server);
/** /**
* Listen on provided port, on all network interfaces. * Listen on provided port, on all network interfaces.
*/ */

View file

@ -17,16 +17,6 @@ let teamB = {
let sideswitch = false; // Attribute to switch sides after halftime let sideswitch = false; // Attribute to switch sides after halftime
let referenceMirrored = false; // If sitting behind the scoreboard, switch sides for admin board let referenceMirrored = false; // If sitting behind the scoreboard, switch sides for admin board
// Funktionsübersicht (Prototypen ohne Implementierung — nur zur Übersicht)
function setEnabled(status) {};
function configTeam(team, name, name2, isSpielgemeinschaft) {};
function setScore(team, score) {};
function alterScore(team, dir) {};
function clearScore() {};
function toggleSideswitch() {};
function getEnabled() {};
function getValues() {};
// Enable or disable the scoreboard // Enable or disable the scoreboard
function setEnabled(status) { function setEnabled(status) {
enabled = status; enabled = status;
@ -52,6 +42,7 @@ function setScore(team, score) {
} else if(team == "teamB") { } else if(team == "teamB") {
teamB.score = score; teamB.score = score;
} }
io.sockets.emit('score', print()); // Wie alterScore und clearScore den Monitor mitziehen
} }
// Ändert den Score eines Teams um 1 — Richtung "inc" (erhöhen) oder "dec" (verringern) // Ändert den Score eines Teams um 1 — Richtung "inc" (erhöhen) oder "dec" (verringern)
@ -61,7 +52,7 @@ function alterScore(team, dir) {
teamA.score++; teamA.score++;
console.log("teamA inc"); console.log("teamA inc");
} }
else if(dir == "dec") { else if(dir == "dec" && teamA.score > 0) { // Kein negativer Spielstand
teamA.score--; teamA.score--;
console.log("teamA dec"); console.log("teamA dec");
} }
@ -70,7 +61,7 @@ function alterScore(team, dir) {
teamB.score++; teamB.score++;
console.log("teamB inc"); console.log("teamB inc");
} }
else if(dir == "dec") { else if(dir == "dec" && teamB.score > 0) { // Kein negativer Spielstand
teamB.score--; teamB.score--;
console.log("teamB dec"); console.log("teamB dec");
} }

View file

@ -1,12 +1,25 @@
// Erstellt den zentralen Socket.IO-Server auf Port 3001. // Erstellt den zentralen Socket.IO-Server.
// Wird von timer.js, score.js und den Routen importiert, // Wird von timer.js, score.js und den Routen importiert,
// um Events (Timer, Score, Refresh) an alle verbundenen Clients zu senden. // um Events (Timer, Score, Refresh) an alle verbundenen Clients zu senden.
const { Server } = require('socket.io'); const { Server } = require('socket.io');
// CORS auf "*" gesetzt, da Admin und Monitor auf unterschiedlichen Ports laufen können // Der Server wird ohne eigenen Port erzeugt und in bin/www per io.attach() an den
const io = new Server(3001, { // HTTP-Server gehaengt. Dieses Modul wird beim Laden der Routen ausgewertet, also
cors: { origin: "*" } // bevor der HTTP-Server existiert — daher die Trennung von Erzeugen und Anhaengen.
// Weil Socket.IO damit unter derselben Herkunft laeuft wie die Seite, entfaellt die
// CORS-Ausnahme.
const io = new Server();
// Verbindungs-Handling: der Client meldet sich beim Verbinden selbst mit dem
// Socket.IO-eigenen 'connect'-Event und holt sich den aktuellen Stand ueber die
// REST-Endpunkte. Serverseitig wird die Verbindung nur protokolliert.
io.on('connection', (socket) => {
console.log("A user connected");
socket.on('disconnect', (reason) => {
console.log("A user disconnected: ", reason);
});
}); });
module.exports = io module.exports = io

View file

@ -7,15 +7,6 @@ let durationLeft = moment.duration(7, 'minutes'); // durationLeft after the t
let isPaused = true; // Status of the timer let isPaused = true; // Status of the timer
// Function prototypes
function start() {};
function pause() {};
function reset() {};
function end() {};
function print() {};
function incDec() {};
function getValues() {};
// Start timer not paused and not already finished // Start timer not paused and not already finished
function start() { function start() {
if(isPaused == true && durationLeft.asSeconds() != 0) { // Only allow start if timer ist currently paused and durationLeft is not 0 seconds if(isPaused == true && durationLeft.asSeconds() != 0) { // Only allow start if timer ist currently paused and durationLeft is not 0 seconds
@ -45,7 +36,9 @@ function pause() {
return false return false
} }
// Reset timer to passed value, and send durationLeft to all clients // Reset timer to passed value, and send durationLeft to all clients.
// Ein laufender Timer wird bewusst nicht angehalten, sondern zaehlt von der
// neuen Zeit weiter herunter.
function reset(newDuration) { function reset(newDuration) {
console.log("Timer Zurückgesetzt"); console.log("Timer Zurückgesetzt");
duration = moment.duration(newDuration, 'seconds').clone(); // Set initial duration to received duration in seconds duration = moment.duration(newDuration, 'seconds').clone(); // Set initial duration to received duration in seconds
@ -69,7 +62,8 @@ function print() {
// Increase or decrease the timer depending on the passed value in seconds // Increase or decrease the timer depending on the passed value in seconds
function incDec(value) { function incDec(value) {
if(Math.abs(value) >= durationLeft.asSeconds()) { // If abs from passed value is greater than seconds left on timer, end timer if(value < 0 && Math.abs(value) >= durationLeft.asSeconds()) { // Nur beim Verringern: faellt die Restzeit auf 0 oder darunter, Timer beenden
durationLeft = moment.duration(0, 'seconds'); // Restzeit sauber auf 00:00, sonst bliebe der Timer trotz "ENDE" startbar
end(); end();
} else { } else {
durationLeft.add(value, 'second'); durationLeft.add(value, 'second');
@ -87,6 +81,9 @@ function getValues() {
}; };
} }
// duration, durationLeft und isPaused werden bewusst nicht exportiert: Exporte
// werden beim Laden des Moduls einmal ausgewertet und wuerden dauerhaft die
// Startwerte liefern. Der aktuelle Stand kommt ueber getValues().
module.exports = { module.exports = {
duration, durationLeft, isPaused, start, pause, reset, end, print, incDec, getValues start, pause, reset, end, print, incDec, getValues
} }

View file

@ -1,12 +1,17 @@
let wss = "ws://" + window.location.hostname + ":3001" // Build socketio endpoint from window.location // Socket.IO laeuft am selben Host und Port wie die Seite, io() ohne Argument
const socket = io(wss); // Connect to socketio server // verbindet sich dorthin zurueck.
const socket = io();
// ###################################################################################################################################################### // ######################################################################################################################################################
// Websockets event handler // Websockets event handler
// ###################################################################################################################################################### // ######################################################################################################################################################
socket.on('connected', (message) => { // Once client is connected to websocket // 'connect' feuert bei jeder Verbindung, auch nach einem Reconnect. Da der Pi das
console.log("Connected"); // WLAN selbst aufspannt, sind kurze Abbrueche normal: danach werden die Werte neu
// geholt, damit das Panel nicht auf einem veralteten Stand stehen bleibt.
socket.on('connect', () => {
console.log("Verbunden");
initialUpdate();
}); });
socket.on('timerDurationLeft', (message) => { socket.on('timerDurationLeft', (message) => {
@ -28,6 +33,22 @@ socket.on('score', (message) => {
// General functions // General functions
// ###################################################################################################################################################### // ######################################################################################################################################################
// Holt JSON von einem Endpunkt. Liefert null statt zu werfen, wenn der Server nicht
// erreichbar ist oder mit einem Fehler antwortet — sonst bricht der Aufrufer still ab.
async function fetchJson(url, options) {
try {
const response = await fetch(url, options);
if(!response.ok) {
console.error("Anfrage fehlgeschlagen:", url, response.status);
return null;
}
return await response.json();
} catch (err) {
console.error("Server nicht erreichbar:", url, err);
return null;
}
}
// Initial update gets called whenever page has been loaded // Initial update gets called whenever page has been loaded
async function initialUpdate() { async function initialUpdate() {
etcGetValues(); // Request new values for etc stuff etcGetValues(); // Request new values for etc stuff
@ -104,6 +125,19 @@ function updateScoreFrontend(values) {
} }
// Erzeugt einen Dropdown-Eintrag, der den Teamnamen in das angegebene Feld uebernimmt.
// Der Name wird als Text gesetzt und per Closure an den Handler gegeben, statt ihn in
// einen onclick-String zu schreiben: Namen mit Apostroph zerlegten sonst den Handler.
function createTeamDropdownItem(field, team) {
const li = document.createElement("li");
const a = document.createElement("a");
a.className = "dropdown-item";
a.textContent = team;
a.addEventListener("click", () => scoreInsertChosenName(field, team));
li.appendChild(a);
return li;
}
// Update DOM for all db contents with the passed values // Update DOM for all db contents with the passed values
function updateDbFrontend(values) { function updateDbFrontend(values) {
console.log(values); console.log(values);
@ -122,27 +156,17 @@ function updateDbFrontend(values) {
let selectDelete = document.getElementById("dbTeamToDelete"); // Select for team deletion from db let selectDelete = document.getElementById("dbTeamToDelete"); // Select for team deletion from db
selectDelete.innerHTML = "" // Clears all existing options from select selectDelete.innerHTML = "" // Clears all existing options from select
// <li><a class="dropdown-item" onclick="scoreInsertChosenName">Action</a></li> for(const team of values.teams) {
for(team of values.teams) {
// Create custom Dropdowns with different function arguments for team configuration // Create custom Dropdowns with different function arguments for team configuration
var li = document.createElement("li"); teamAnameDropdown.appendChild(createTeamDropdownItem("teamAname", team));
li.innerHTML = `<a class="dropdown-item" onclick="scoreInsertChosenName('teamAname','` + team +`')"> `+ team +`</a>`; teamAname2Dropdown.appendChild(createTeamDropdownItem("teamAname2", team));
teamAnameDropdown.appendChild(li); teamBnameDropdown.appendChild(createTeamDropdownItem("teamBname", team));
var li = document.createElement("li"); teamBname2Dropdown.appendChild(createTeamDropdownItem("teamBname2", team));
li.innerHTML = `<a class="dropdown-item" onclick="scoreInsertChosenName('teamAname2','` + team +`')"> `+ team +`</a>`;
teamAname2Dropdown.appendChild(li);
var li = document.createElement("li");
li.innerHTML = `<a class="dropdown-item" onclick="scoreInsertChosenName('teamBname','` + team +`')"> `+ team +`</a>`;
teamBnameDropdown.appendChild(li);
var li = document.createElement("li");
li.innerHTML = `<a class="dropdown-item" onclick="scoreInsertChosenName('teamBname2','` + team +`')"> `+ team +`</a>`;
teamBname2Dropdown.appendChild(li);
// Create select options for deletion in debugwindow // Create select options for deletion in debugwindow
let opt = document.createElement("option"); let opt = document.createElement("option");
opt.value = team; opt.value = team;
opt.innerHTML = team; opt.textContent = team;
selectDelete.appendChild(opt); selectDelete.appendChild(opt);
} }
} }
@ -184,16 +208,14 @@ async function killBrowser() {
// Request important values for etc stuff // Request important values for etc stuff
async function etcGetValues() { async function etcGetValues() {
const response = await fetch("/admin/etcGetValues"); // Call API Endpoint /admin/etcGetValues const data = await fetchJson("/admin/etcGetValues"); // Call API Endpoint /admin/etcGetValues
const data = await response.json(); // Wait for asynchronous transfer to complete if(data) updateEtcFrontend(data); // Update admin frontend
updateEtcFrontend(data); // Update admin frontend
} }
// Toggle index page to also show scoreboard // Toggle index page to also show scoreboard
async function etcToggleSplashscreen() { async function etcToggleSplashscreen() {
const response = await fetch("/admin/etcToggleSplashscreen");// Call API Endpoint /admin/etcToggleSplashscreen const data = await fetchJson("/admin/etcToggleSplashscreen");// Call API Endpoint /admin/etcToggleSplashscreen
const data = await response.json(); // Wait for asyncronous transfer to complete if(data) updateEtcFrontend(data); // Update admin frontend
updateEtcFrontend(data); // Update admin frontend
refreshMonitor(); // Refresh main monitor to display new frontend refreshMonitor(); // Refresh main monitor to display new frontend
} }
@ -203,9 +225,8 @@ async function etcToggleSplashscreen() {
// Request important timer values // Request important timer values
async function timerGetValues() { async function timerGetValues() {
const response = await fetch("/admin/timerGetValues"); // Call API Endpoint /admin/timerGetValues const data = await fetchJson("/admin/timerGetValues"); // Call API Endpoint /admin/timerGetValues
const data = await response.json(); // Wait for asynchronous transfer to complete if(data) updateTimerFrontend(data); // Update admin frontend
updateTimerFrontend(data); // Update admin frontend
} }
// Request to start the timer // Request to start the timer
@ -235,24 +256,22 @@ async function timerPause() {
// Request to reset the timer with the specified time // Request to reset the timer with the specified time
async function timerReset() { async function timerReset() {
let newDuration = document.getElementById("timerSelectDuration").value; let newDuration = document.getElementById("timerSelectDuration").value;
const response = await fetch("/admin/timerReset", { // Call API Endpoint /admin/timerStart with selected new duration in seconds const data = await fetchJson("/admin/timerReset", { // Call API Endpoint /admin/timerStart with selected new duration in seconds
method: 'POST', method: 'POST',
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({duration: newDuration}) body: JSON.stringify({duration: newDuration})
}); });
const data = await response.json(); // Wait for asyncronous transfer to complete if(data) updateTimerFrontend(data); // Update admin frontend
updateTimerFrontend(data); // Update admin frontend
} }
// Request to increase or decrease the timer depending on passed value in seconds // Request to increase or decrease the timer depending on passed value in seconds
async function timerIncDec(value) { async function timerIncDec(value) {
const response = await fetch("/admin/timerIncDec", { // Call API Endpoint /admin/timerIncDec with value in seconds to increase or decrease the timer const data = await fetchJson("/admin/timerIncDec", { // Call API Endpoint /admin/timerIncDec with value in seconds to increase or decrease the timer
method: 'POST', method: 'POST',
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({value: value}) body: JSON.stringify({value: value})
}); });
const data = await response.json(); // Wait for asyncronous transfer to complete if(data) updateTimerFrontend(data); // Update admin frontend
updateTimerFrontend(data); // Update admin frontend
} }
// ###################################################################################################################################################### // ######################################################################################################################################################
@ -261,49 +280,43 @@ async function timerIncDec(value) {
// Request important values for score // Request important values for score
async function scoreGetValues() { async function scoreGetValues() {
const response = await fetch("/admin/scoreGetValues"); // Call API Endpoint /admin/scoreGetValues const data = await fetchJson("/admin/scoreGetValues"); // Call API Endpoint /admin/scoreGetValues
const data = await response.json(); // Wait for asynchronous transfer to complete if(data) updateScoreFrontend(data); // Update admin frontend
updateScoreFrontend(data); // Update admin frontend
} }
// Toggle index page to also show scoreboard // Toggle index page to also show scoreboard
async function scoreToggle() { async function scoreToggle() {
const response = await fetch("/admin/scoreToggle"); // Call API Endpoint /admin/scoreToggle const data = await fetchJson("/admin/scoreToggle"); // Call API Endpoint /admin/scoreToggle
const data = await response.json(); // Wait for asyncronous transfer to complete if(data) updateScoreFrontend(data); // Update admin frontend
updateScoreFrontend(data); // Update admin frontend
refreshMonitor(); // Refresh main monitor to display new frontend refreshMonitor(); // Refresh main monitor to display new frontend
} }
// Alter the score of passed team in the specified direction // Alter the score of passed team in the specified direction
async function scoreAlterScore(team, dir) { async function scoreAlterScore(team, dir) {
const response = await fetch("/admin/scoreAlterScore", { // Call API Endpoint /admin/scoreAlterScore with the teamname to alter the score in the specified direction const data = await fetchJson("/admin/scoreAlterScore", { // Call API Endpoint /admin/scoreAlterScore with the teamname to alter the score in the specified direction
method: 'POST', method: 'POST',
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({team: team, dir: dir}) body: JSON.stringify({team: team, dir: dir})
}); });
const data = await response.json(); // Wait for asyncronous transfer to complete if(data) updateScoreFrontend(data); // Update admin frontend
updateScoreFrontend(data); // Update admin frontend
} }
// Switch sides // Switch sides
async function scoreToggleSideswitch() { async function scoreToggleSideswitch() {
const response = await fetch("/admin/scoreToggleSideswitch");// Call API Endpoint /admin/scoreSwitch const data = await fetchJson("/admin/scoreToggleSideswitch");// Call API Endpoint /admin/scoreSwitch
const data = await response.json(); // Wait for asyncronous transfer to complete if(data) updateScoreFrontend(data); // Update admin frontend
updateScoreFrontend(data); // Update admin frontend
} }
// Switch sides to mirror admin panel when sitting behind main monitor // Switch sides to mirror admin panel when sitting behind main monitor
async function scoreToggleReferenceMirrored() { async function scoreToggleReferenceMirrored() {
const response = await fetch("/admin/scoreToggleReferenceMirrored");// Call API Endpoint /admin/scoreSwitch const data = await fetchJson("/admin/scoreToggleReferenceMirrored");// Call API Endpoint /admin/scoreSwitch
const data = await response.json(); // Wait for asyncronous transfer to complete if(data) updateScoreFrontend(data); // Update admin frontend
updateScoreFrontend(data); // Update admin frontend
} }
// Clear score // Clear score
async function scoreClearScore() { async function scoreClearScore() {
const response = await fetch("/admin/scoreClearScore"); // Call API Endpoint /admin/scoreClearScore const data = await fetchJson("/admin/scoreClearScore"); // Call API Endpoint /admin/scoreClearScore
const data = await response.json(); // Wait for asynchronous transfer to complete if(data) updateScoreFrontend(data); // Update admin frontend
updateScoreFrontend(data); // Update admin frontend
} }
// Configure Teams // Configure Teams
@ -320,13 +333,12 @@ async function scoreConfigTeams() {
} }
console.log(teamA, teamB) console.log(teamA, teamB)
const response = await fetch("/admin/scoreConfigTeams", { // Call API Endpoint /admin/scoreAlterScore with the teamname to alter the score in the specified direction const data = await fetchJson("/admin/scoreConfigTeams", { // Call API Endpoint /admin/scoreAlterScore with the teamname to alter the score in the specified direction
method: 'POST', method: 'POST',
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({teamA: teamA, teamB: teamB}) body: JSON.stringify({teamA: teamA, teamB: teamB})
}); });
const data = await response.json(); // Wait for asyncronous transfer to complete if(data) updateScoreFrontend(data); // Update admin frontend
updateScoreFrontend(data); // Update admin frontend
refreshMonitor(); refreshMonitor();
} }
@ -336,31 +348,28 @@ async function scoreConfigTeams() {
// Request important values for db contents // Request important values for db contents
async function dbGetValues() { async function dbGetValues() {
const response = await fetch("/admin/dbGetValues"); // Call API Endpoint /admin/dbGetValues const data = await fetchJson("/admin/dbGetValues"); // Call API Endpoint /admin/dbGetValues
const data = await response.json(); // Wait for asynchronous transfer to complete if(data) updateDbFrontend(data); // Update admin frontend
updateDbFrontend(data); // Update admin frontend
} }
// Add team to DB // Add team to DB
async function dbAddTeam() { async function dbAddTeam() {
const response = await fetch("/admin/dbAddTeam", { // Call API Endpoint /admin/scoreAlterScore with the teamname to alter the score in the specified direction const data = await fetchJson("/admin/dbAddTeam", { // Call API Endpoint /admin/scoreAlterScore with the teamname to alter the score in the specified direction
method: 'POST', method: 'POST',
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({teamName: document.getElementById("dbTeamToAdd").value}) body: JSON.stringify({teamName: document.getElementById("dbTeamToAdd").value})
}); });
document.getElementById("dbTeamToAdd").value = ""; // Empty the text field after sending document.getElementById("dbTeamToAdd").value = ""; // Empty the text field after sending
const data = await response.json(); // Wait for asyncronous transfer to complete if(data) updateDbFrontend(data);
updateDbFrontend(data);
} }
// Delete team from DB // Delete team from DB
async function dbDeleteTeam() { async function dbDeleteTeam() {
let teamName = document.getElementById("dbTeamToDelete").value; let teamName = document.getElementById("dbTeamToDelete").value;
const response = await fetch("/admin/dbDeleteTeam", { // Call API Endpoint /admin/scoreAlterScore with the teamname to alter the score in the specified direction const data = await fetchJson("/admin/dbDeleteTeam", { // Call API Endpoint /admin/scoreAlterScore with the teamname to alter the score in the specified direction
method: 'POST', method: 'POST',
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({teamName: teamName}) body: JSON.stringify({teamName: teamName})
}); });
const data = await response.json(); // Wait for asyncronous transfer to complete if(data) updateDbFrontend(data);
updateDbFrontend(data);
} }

View file

@ -1,12 +1,17 @@
let wss = "ws://" + window.location.hostname + ":3001" // Build socketio endpoint from window.location // Socket.IO laeuft am selben Host und Port wie die Seite, io() ohne Argument
const socket = io(wss); // Connect to socketio server // verbindet sich dorthin zurueck.
const socket = io();
// ###################################################################################################################################################### // ######################################################################################################################################################
// Websockets event handler // Websockets event handler
// ###################################################################################################################################################### // ######################################################################################################################################################
socket.on('connected', (message) => { // 'connect' feuert bei jeder Verbindung, auch nach einem Reconnect. Da der Pi das WLAN
console.log("Connected"); // selbst aufspannt, sind kurze Abbrueche normal: danach werden Timer und Spielstand neu
// geholt, damit die Anzeige nicht auf einem veralteten Stand haengen bleibt.
socket.on('connect', () => {
console.log("Verbunden");
initialUpdate();
}); });
socket.on('timerDurationLeft', (message) => { socket.on('timerDurationLeft', (message) => {
@ -26,6 +31,22 @@ socket.on('refresh', (message) => {
// General functions // General functions
// ###################################################################################################################################################### // ######################################################################################################################################################
// Holt JSON von einem Endpunkt. Liefert null statt zu werfen, wenn der Server nicht
// erreichbar ist oder mit einem Fehler antwortet.
async function fetchJson(url) {
try {
const response = await fetch(url);
if(!response.ok) {
console.error("Anfrage fehlgeschlagen:", url, response.status);
return null;
}
return await response.json();
} catch (err) {
console.error("Server nicht erreichbar:", url, err);
return null;
}
}
// Update DOM timer elements from passed values // Update DOM timer elements from passed values
function updateTimerFrontend(values) { function updateTimerFrontend(values) {
document.getElementById("durationLeft").innerHTML = values.print; // Display durationLeft as prettyfied string document.getElementById("durationLeft").innerHTML = values.print; // Display durationLeft as prettyfied string
@ -42,7 +63,6 @@ async function initialUpdate() {
// Request important timer values // Request important timer values
async function timerGetValues() { async function timerGetValues() {
const response = await fetch("/admin/timerGetValues"); // Call API Endpoint /admin/timerGetValues const data = await fetchJson("/admin/timerGetValues"); // Call API Endpoint /admin/timerGetValues
const data = await response.json(); // Wait for asynchronous transfer to complete if(data) updateTimerFrontend(data); // Update admin frontend
updateTimerFrontend(data); // Update admin frontend
} }

View file

@ -1,12 +1,17 @@
let wss = "ws://" + window.location.hostname + ":3001" // Build socketio endpoint from window.location // Socket.IO laeuft am selben Host und Port wie die Seite, io() ohne Argument
const socket = io(wss); // Connect to socketio server // verbindet sich dorthin zurueck.
const socket = io();
// ###################################################################################################################################################### // ######################################################################################################################################################
// Websockets event handler // Websockets event handler
// ###################################################################################################################################################### // ######################################################################################################################################################
socket.on('connected', (message) => { // 'connect' feuert bei jeder Verbindung, auch nach einem Reconnect. Da der Pi das WLAN
console.log("Connected"); // selbst aufspannt, sind kurze Abbrueche normal: danach werden Timer und Spielstand neu
// geholt, damit die Anzeige nicht auf einem veralteten Stand haengen bleibt.
socket.on('connect', () => {
console.log("Verbunden");
initialUpdate();
}); });
socket.on('timerDurationLeft', (message) => { socket.on('timerDurationLeft', (message) => {
@ -35,6 +40,22 @@ socket.on('scoreSideswitch', (message) => {
// General functions // General functions
// ###################################################################################################################################################### // ######################################################################################################################################################
// Holt JSON von einem Endpunkt. Liefert null statt zu werfen, wenn der Server nicht
// erreichbar ist oder mit einem Fehler antwortet.
async function fetchJson(url) {
try {
const response = await fetch(url);
if(!response.ok) {
console.error("Anfrage fehlgeschlagen:", url, response.status);
return null;
}
return await response.json();
} catch (err) {
console.error("Server nicht erreichbar:", url, err);
return null;
}
}
// Update DOM timer elements from passed values // Update DOM timer elements from passed values
function updateTimerFrontend(values) { function updateTimerFrontend(values) {
document.getElementById("durationLeft").innerHTML = values.print; // Display durationLeft as prettyfied string document.getElementById("durationLeft").innerHTML = values.print; // Display durationLeft as prettyfied string
@ -87,9 +108,8 @@ async function initialUpdate() {
// Request important timer values // Request important timer values
async function timerGetValues() { async function timerGetValues() {
const response = await fetch("/admin/timerGetValues"); // Call API Endpoint /admin/timerGetValues const data = await fetchJson("/admin/timerGetValues"); // Call API Endpoint /admin/timerGetValues
const data = await response.json(); // Wait for asynchronous transfer to complete if(data) updateTimerFrontend(data); // Update admin frontend
updateTimerFrontend(data); // Update admin frontend
} }
// ###################################################################################################################################################### // ######################################################################################################################################################
@ -98,8 +118,7 @@ async function timerGetValues() {
// Request important values for score // Request important values for score
async function scoreGetValues() { async function scoreGetValues() {
const response = await fetch("/admin/scoreGetValues"); // Call API Endpoint /admin/scoreGetValues const data = await fetchJson("/admin/scoreGetValues"); // Call API Endpoint /admin/scoreGetValues
const data = await response.json(); // Wait for asynchronous transfer to complete and parse json (which got received by backend)
console.log(data); // Print received data console.log(data); // Print received data
updateScoreFrontend(data); // Update admin frontend with received values for scoreboard if(data) updateScoreFrontend(data); // Update admin frontend with received values for scoreboard
} }

View file

@ -1,14 +1,11 @@
let wss = "ws://" + window.location.hostname + ":3001" // Build socketio endpoint from window.location // Socket.IO laeuft am selben Host und Port wie die Seite, io() ohne Argument
const socket = io(wss); // Connect to socketio server // verbindet sich dorthin zurueck.
const socket = io();
// ###################################################################################################################################################### // ######################################################################################################################################################
// Websockets event handler // Websockets event handler
// ###################################################################################################################################################### // ######################################################################################################################################################
socket.on('connected', (message) => {
console.log("Connected");
});
socket.on('refresh', (message) => { socket.on('refresh', (message) => {
document.location.reload() // Reload page on received 'refresh' messagen document.location.reload() // Reload page on received 'refresh' messagen
}); });

View file

@ -1,6 +1,5 @@
var express = require('express'); var express = require('express');
var router = express.Router(); var router = express.Router();
let io = require('../controllers/socketio');
const score = require('../controllers/score'); const score = require('../controllers/score');
const etc = require('../controllers/etc'); const etc = require('../controllers/etc');
@ -18,15 +17,4 @@ 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"); // Begrüßungsnachricht an den neuen Client
// Eingehende Nachrichten vom Client loggen (aktuell nur für Debugging)
socket.on('message', (message) => {
console.log(message)
})
})
module.exports = router; module.exports = router;

View file

@ -298,7 +298,7 @@
<p>Achtung, eine Fehlbedienung kann unerwünschtes Verhalten hervorrufen.</p> <p>Achtung, eine Fehlbedienung kann unerwünschtes Verhalten hervorrufen.</p>
<div class="form-check form-switch"> <div class="form-check form-switch">
<input class="form-check-input" onclick="scoreToggleReferenceMirrored()" type="checkbox" role="switch" id="scoreSwitchReferenceMirrored"> <input class="form-check-input" onclick="scoreToggleReferenceMirrored()" type="checkbox" role="switch" id="scoreSwitchReferenceMirrored">
<label class="form-check-label" for="scoreSwitchEnable">Teamanzeige im Adminpanel spiegeln?</label> <label class="form-check-label" for="scoreSwitchReferenceMirrored">Teamanzeige im Adminpanel spiegeln?</label>
</div> </div>
<div class="form-check form-switch"> <div class="form-check form-switch">
<input class="form-check-input" onclick="scoreToggle()" type="checkbox" role="switch" id="scoreSwitchEnable"> <input class="form-check-input" onclick="scoreToggle()" type="checkbox" role="switch" id="scoreSwitchEnable">
@ -306,7 +306,7 @@
</div> </div>
<div class="form-check form-switch"> <div class="form-check form-switch">
<input class="form-check-input" onclick="etcToggleSplashscreen()" type="checkbox" role="switch" id="etcSwitchEnable"> <input class="form-check-input" onclick="etcToggleSplashscreen()" type="checkbox" role="switch" id="etcSwitchEnable">
<label class="form-check-label" for="scoreSwitchEnable">SG-Arheilgen Splashscreen anzeigen?</label> <label class="form-check-label" for="etcSwitchEnable">SG-Arheilgen Splashscreen anzeigen?</label>
</div> </div>
<button class="mb-2 btn btn-lg btn-warning" onclick="refreshMonitor()"><i class="fa-solid fa-rotate"></i> Monitor neu laden</button> <br> <button class="mb-2 btn btn-lg btn-warning" onclick="refreshMonitor()"><i class="fa-solid fa-rotate"></i> Monitor neu laden</button> <br>
<button class="mb-2 btn btn-lg btn-success" onclick="openBrowser()"><i class="fa-solid fa-rocket"></i> Browser öffnen</button> <br> <button class="mb-2 btn btn-lg btn-success" onclick="openBrowser()"><i class="fa-solid fa-rocket"></i> Browser öffnen</button> <br>
@ -316,7 +316,7 @@
<p class="fw-bold">Team Datenbank:</p> <p class="fw-bold">Team Datenbank:</p>
<div class="input-group mb-3"> <div class="input-group mb-3">
<span class="input-group-text"><i class="fa-solid fa-user-plus"></i></span> <span class="input-group-text"><i class="fa-solid fa-user-plus"></i></span>
<input type="text" class="form-control" id="dbTeamToAdd" aria-describedby="teamBnameHelp"> <input type="text" class="form-control" id="dbTeamToAdd">
<button class="btn btn-outline-success" type="button" onclick="dbAddTeam()"><i class="fa-solid fa-plus"></i></button> <button class="btn btn-outline-success" type="button" onclick="dbAddTeam()"><i class="fa-solid fa-plus"></i></button>
</div> </div>
<div class="input-group mb-3"> <div class="input-group mb-3">