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>
374 lines
No EOL
19 KiB
JavaScript
374 lines
No EOL
19 KiB
JavaScript
let wss = "ws://" + window.location.hostname + ":3001" // Build socketio endpoint from window.location
|
|
const socket = io(wss); // Connect to socketio server
|
|
|
|
// ######################################################################################################################################################
|
|
// Websockets event handler
|
|
// ######################################################################################################################################################
|
|
|
|
// 'connect' feuert bei jeder Verbindung, auch nach einem Reconnect. Da der Pi das
|
|
// 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) => {
|
|
console.log(message); // Log durationLeft in client console
|
|
document.getElementById("durationLeft").innerHTML = message; // Display durationLeft in corresponding html Element
|
|
});
|
|
|
|
socket.on('timerEnded', (message) => {
|
|
initialUpdate(); // Update admin frontend to set correct buttonstatus
|
|
});
|
|
|
|
socket.on('score', (message) => {
|
|
console.log(message); // Log score in client console
|
|
scoreGetValues();
|
|
// document.getElementById("score").innerHTML = message; // Display score in corresponding html Element
|
|
});
|
|
|
|
// ######################################################################################################################################################
|
|
// 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
|
|
async function initialUpdate() {
|
|
etcGetValues(); // Request new values for etc stuff
|
|
timerGetValues(); // Request new values for timer
|
|
scoreGetValues(); // Request new values for score
|
|
dbGetValues(); // Request new values for db contents
|
|
}
|
|
|
|
// Update DOM timer elements from passed values
|
|
function updateEtcFrontend(values) {
|
|
console.log(values);
|
|
document.getElementById("etcSwitchEnable").checked = values.splashscreenEnabled;// Set switch status to received score enabled value
|
|
}
|
|
|
|
// Update DOM timer elements from passed values
|
|
function updateTimerFrontend(values) {
|
|
console.log(values); // Print received data
|
|
values.duration = moment.duration(values.duration); // Create moment object from ISO 8601 string
|
|
values.durationLeft = moment.duration(values.durationLeft); // Create moment object from ISO 8601 string
|
|
document.getElementById("durationLeft").innerHTML = values.print; // Display durationLeft as prettyfied string
|
|
|
|
if(values.isPaused) { // Set button state depending on timer status
|
|
document.getElementById("timerStartBtn").disabled = false;
|
|
document.getElementById("timerPauseBtn").disabled = true;
|
|
} else {
|
|
document.getElementById("timerStartBtn").disabled = true;
|
|
document.getElementById("timerPauseBtn").disabled = false;
|
|
}
|
|
}
|
|
|
|
// Build colname to show Teams on Adminpanel based on isSpielgemeinschaft
|
|
function buildColFromIsSpielgemeinschaft(team) {
|
|
if(team.isSpielgemeinschaft) {
|
|
return team.name + "<br>" + team.name2;
|
|
} else {
|
|
return team.name + "<br> ";
|
|
}
|
|
}
|
|
|
|
// Update DOM score elements from passed values
|
|
function updateScoreFrontend(values) {
|
|
console.log(values); // Print received data
|
|
document.getElementById("scoreSwitchEnable").checked = values.enabled; // Set switch status to received score enabled value
|
|
document.getElementById("scoreSwitchReferenceMirrored").checked = values.referenceMirrored; // Set switch status to received score enabled value
|
|
|
|
// Config Teams - Input current teams into text fields
|
|
document.getElementById("teamAname").value = values.teamA.name;
|
|
document.getElementById("teamAname2").value = values.teamA.name2;
|
|
document.getElementById("teamAisSpielgemeinschaft").checked = values.teamA.isSpielgemeinschaft;
|
|
|
|
document.getElementById("teamBname").value = values.teamB.name;
|
|
document.getElementById("teamBname2").value = values.teamB.name2;
|
|
document.getElementById("teamBisSpielgemeinschaft").checked = values.teamB.isSpielgemeinschaft;
|
|
|
|
// Show current Teams on admin panel
|
|
if(!values.referenceMirrored && !values.sideswitch || values.referenceMirrored && values.sideswitch) { // If not mirrored on adminpanel, show like main frontend while considering sideswitch state
|
|
document.getElementById("score").innerHTML = values.teamA.score + ':' + values.teamB.score;
|
|
document.getElementById("teamAcolName").innerHTML = buildColFromIsSpielgemeinschaft(values.teamA);
|
|
document.getElementById("teamBcolName").innerHTML = buildColFromIsSpielgemeinschaft(values.teamB);
|
|
document.getElementById("teamAinc").setAttribute("onclick", "scoreAlterScore('teamA', 'inc')");
|
|
document.getElementById("teamAdec").setAttribute("onclick", "scoreAlterScore('teamA', 'dec')");
|
|
document.getElementById("teamBinc").setAttribute("onclick", "scoreAlterScore('teamB', 'inc')");
|
|
document.getElementById("teamBdec").setAttribute("onclick", "scoreAlterScore('teamB', 'dec')");
|
|
|
|
} else if(!values.referenceMirrored && values.sideswitch || values.referenceMirrored && !values.sideswitch) { // Else, swap posistions
|
|
document.getElementById("score").innerHTML = values.teamB.score + ':' + values.teamA.score;
|
|
document.getElementById("teamAcolName").innerHTML = buildColFromIsSpielgemeinschaft(values.teamB);
|
|
document.getElementById("teamBcolName").innerHTML = buildColFromIsSpielgemeinschaft(values.teamA);
|
|
document.getElementById("teamAinc").setAttribute("onclick", "scoreAlterScore('teamB', 'inc')");
|
|
document.getElementById("teamAdec").setAttribute("onclick", "scoreAlterScore('teamB', 'dec')");
|
|
document.getElementById("teamBinc").setAttribute("onclick", "scoreAlterScore('teamA', 'inc')");
|
|
document.getElementById("teamBdec").setAttribute("onclick", "scoreAlterScore('teamA', 'dec')");
|
|
}
|
|
|
|
}
|
|
|
|
// 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
|
|
function updateDbFrontend(values) {
|
|
console.log(values);
|
|
|
|
let teamAnameDropdown = document.getElementById("teamAnameDropdown"); // Dropdown for TeamA name
|
|
let teamAname2Dropdown = document.getElementById("teamAname2Dropdown"); // Dropdown for TeamA name2
|
|
let teamBnameDropdown = document.getElementById("teamBnameDropdown"); // Dropdown for TeamB name
|
|
let teamBname2Dropdown = document.getElementById("teamBname2Dropdown"); // Dropdown for TeamB name2
|
|
|
|
// Clear innerHTML of dropdowns
|
|
teamAnameDropdown.innerHTML = "";
|
|
teamAname2Dropdown.innerHTML = "";
|
|
teamBnameDropdown.innerHTML = "";
|
|
teamBname2Dropdown.innerHTML = "";
|
|
|
|
let selectDelete = document.getElementById("dbTeamToDelete"); // Select for team deletion from db
|
|
selectDelete.innerHTML = "" // Clears all existing options from select
|
|
|
|
for(const team of values.teams) {
|
|
// Create custom Dropdowns with different function arguments for team configuration
|
|
teamAnameDropdown.appendChild(createTeamDropdownItem("teamAname", team));
|
|
teamAname2Dropdown.appendChild(createTeamDropdownItem("teamAname2", team));
|
|
teamBnameDropdown.appendChild(createTeamDropdownItem("teamBname", team));
|
|
teamBname2Dropdown.appendChild(createTeamDropdownItem("teamBname2", team));
|
|
|
|
// Create select options for deletion in debugwindow
|
|
let opt = document.createElement("option");
|
|
opt.value = team;
|
|
opt.textContent = team;
|
|
selectDelete.appendChild(opt);
|
|
}
|
|
}
|
|
|
|
// Inserts the passed name into the passed name text input
|
|
async function scoreInsertChosenName(field, name) {
|
|
switch(field) {
|
|
case 'teamAname':
|
|
document.getElementById("teamAname").value = name;
|
|
break;
|
|
case 'teamAname2':
|
|
document.getElementById("teamAname2").value = name;
|
|
break;
|
|
case 'teamBname':
|
|
document.getElementById("teamBname").value = name;
|
|
break;
|
|
case 'teamBname2':
|
|
document.getElementById("teamBname2").value = name;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Request monitor refresh for index frontend
|
|
async function refreshMonitor() {
|
|
const response = await fetch("/admin/refreshMonitor"); // Call API Endpoint /admin/timerStart
|
|
}
|
|
|
|
async function openBrowser() {
|
|
const response = await fetch("/admin/openBrowser"); // Call API Endpoint /admin/openBrowser
|
|
}
|
|
|
|
async function killBrowser() {
|
|
const response = await fetch("/admin/killBrowser"); // Call API Endpoint /admin/killBrowser
|
|
}
|
|
|
|
// ######################################################################################################################################################
|
|
// Etc endpoints
|
|
// ######################################################################################################################################################
|
|
|
|
// Request important values for etc stuff
|
|
async function etcGetValues() {
|
|
const data = await fetchJson("/admin/etcGetValues"); // Call API Endpoint /admin/etcGetValues
|
|
if(data) updateEtcFrontend(data); // Update admin frontend
|
|
}
|
|
|
|
// Toggle index page to also show scoreboard
|
|
async function etcToggleSplashscreen() {
|
|
const data = await fetchJson("/admin/etcToggleSplashscreen");// Call API Endpoint /admin/etcToggleSplashscreen
|
|
if(data) updateEtcFrontend(data); // Update admin frontend
|
|
refreshMonitor(); // Refresh main monitor to display new frontend
|
|
}
|
|
|
|
// ######################################################################################################################################################
|
|
// Timerfunctions
|
|
// ######################################################################################################################################################
|
|
|
|
// Request important timer values
|
|
async function timerGetValues() {
|
|
const data = await fetchJson("/admin/timerGetValues"); // Call API Endpoint /admin/timerGetValues
|
|
if(data) updateTimerFrontend(data); // Update admin frontend
|
|
}
|
|
|
|
// Request to start the timer
|
|
async function timerStart() {
|
|
const response = await fetch("/admin/timerStart"); // Call API Endpoint /admin/timerStart
|
|
if(response.status == 200) { // If request successfull
|
|
const data = await response.json(); // Wait for asyncronous transfer to complete
|
|
updateTimerFrontend(data); // Update admin frontend
|
|
} else { // Timer already finished
|
|
const timerStartErrorToast = bootstrap.Toast.getOrCreateInstance(document.getElementById("timerStartErrorToast")); // Create toast instance byId timerStartErrorToast
|
|
timerStartErrorToast.show(); // Show error toast
|
|
}
|
|
}
|
|
|
|
// Request to pause the timer
|
|
async function timerPause() {
|
|
const response = await fetch("/admin/timerPause"); // Call API Endpoint /admin/timerStart
|
|
if(response.status == 200) {
|
|
const data = await response.json(); // Wait for asyncronous transfer to complete
|
|
updateTimerFrontend(data); // Update admin frontend
|
|
} else { // Timer already paused
|
|
const timerPauseErrorToast = bootstrap.Toast.getOrCreateInstance(document.getElementById("timerPauseErrorToast")); // Create toast instance byId timerPauseErrorToast
|
|
timerPauseErrorToast.show(); // Show error toast
|
|
}
|
|
}
|
|
|
|
// Request to reset the timer with the specified time
|
|
async function timerReset() {
|
|
let newDuration = document.getElementById("timerSelectDuration").value;
|
|
const data = await fetchJson("/admin/timerReset", { // Call API Endpoint /admin/timerStart with selected new duration in seconds
|
|
method: 'POST',
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({duration: newDuration})
|
|
});
|
|
if(data) updateTimerFrontend(data); // Update admin frontend
|
|
}
|
|
|
|
// Request to increase or decrease the timer depending on passed value in seconds
|
|
async function timerIncDec(value) {
|
|
const data = await fetchJson("/admin/timerIncDec", { // Call API Endpoint /admin/timerIncDec with value in seconds to increase or decrease the timer
|
|
method: 'POST',
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({value: value})
|
|
});
|
|
if(data) updateTimerFrontend(data); // Update admin frontend
|
|
}
|
|
|
|
// ######################################################################################################################################################
|
|
// Scoreboardfunctions
|
|
// ######################################################################################################################################################
|
|
|
|
// Request important values for score
|
|
async function scoreGetValues() {
|
|
const data = await fetchJson("/admin/scoreGetValues"); // Call API Endpoint /admin/scoreGetValues
|
|
if(data) updateScoreFrontend(data); // Update admin frontend
|
|
}
|
|
|
|
// Toggle index page to also show scoreboard
|
|
async function scoreToggle() {
|
|
const data = await fetchJson("/admin/scoreToggle"); // Call API Endpoint /admin/scoreToggle
|
|
if(data) updateScoreFrontend(data); // Update admin frontend
|
|
refreshMonitor(); // Refresh main monitor to display new frontend
|
|
}
|
|
|
|
// Alter the score of passed team in the specified direction
|
|
async function scoreAlterScore(team, dir) {
|
|
const data = await fetchJson("/admin/scoreAlterScore", { // Call API Endpoint /admin/scoreAlterScore with the teamname to alter the score in the specified direction
|
|
method: 'POST',
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({team: team, dir: dir})
|
|
});
|
|
if(data) updateScoreFrontend(data); // Update admin frontend
|
|
}
|
|
|
|
// Switch sides
|
|
async function scoreToggleSideswitch() {
|
|
const data = await fetchJson("/admin/scoreToggleSideswitch");// Call API Endpoint /admin/scoreSwitch
|
|
if(data) updateScoreFrontend(data); // Update admin frontend
|
|
}
|
|
|
|
// Switch sides to mirror admin panel when sitting behind main monitor
|
|
async function scoreToggleReferenceMirrored() {
|
|
const data = await fetchJson("/admin/scoreToggleReferenceMirrored");// Call API Endpoint /admin/scoreSwitch
|
|
if(data) updateScoreFrontend(data); // Update admin frontend
|
|
}
|
|
|
|
// Clear score
|
|
async function scoreClearScore() {
|
|
const data = await fetchJson("/admin/scoreClearScore"); // Call API Endpoint /admin/scoreClearScore
|
|
if(data) updateScoreFrontend(data); // Update admin frontend
|
|
}
|
|
|
|
// Configure Teams
|
|
async function scoreConfigTeams() {
|
|
teamA = {
|
|
name: document.getElementById("teamAname").value,
|
|
name2: document.getElementById("teamAname2").value,
|
|
isSpielgemeinschaft: document.getElementById("teamAisSpielgemeinschaft").checked
|
|
};
|
|
teamB = {
|
|
name: document.getElementById("teamBname").value,
|
|
name2: document.getElementById("teamBname2").value,
|
|
isSpielgemeinschaft: document.getElementById("teamBisSpielgemeinschaft").checked
|
|
}
|
|
console.log(teamA, teamB)
|
|
|
|
const data = await fetchJson("/admin/scoreConfigTeams", { // Call API Endpoint /admin/scoreAlterScore with the teamname to alter the score in the specified direction
|
|
method: 'POST',
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({teamA: teamA, teamB: teamB})
|
|
});
|
|
if(data) updateScoreFrontend(data); // Update admin frontend
|
|
refreshMonitor();
|
|
}
|
|
|
|
// ######################################################################################################################################################
|
|
// DB functions
|
|
// ######################################################################################################################################################
|
|
|
|
// Request important values for db contents
|
|
async function dbGetValues() {
|
|
const data = await fetchJson("/admin/dbGetValues"); // Call API Endpoint /admin/dbGetValues
|
|
if(data) updateDbFrontend(data); // Update admin frontend
|
|
}
|
|
|
|
// Add team to DB
|
|
async function dbAddTeam() {
|
|
const data = await fetchJson("/admin/dbAddTeam", { // Call API Endpoint /admin/scoreAlterScore with the teamname to alter the score in the specified direction
|
|
method: 'POST',
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({teamName: document.getElementById("dbTeamToAdd").value})
|
|
});
|
|
document.getElementById("dbTeamToAdd").value = ""; // Empty the text field after sending
|
|
if(data) updateDbFrontend(data);
|
|
}
|
|
|
|
// Delete team from DB
|
|
async function dbDeleteTeam() {
|
|
let teamName = document.getElementById("dbTeamToDelete").value;
|
|
const data = await fetchJson("/admin/dbDeleteTeam", { // Call API Endpoint /admin/scoreAlterScore with the teamname to alter the score in the specified direction
|
|
method: 'POST',
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({teamName: teamName})
|
|
});
|
|
if(data) updateDbFrontend(data);
|
|
} |