First version, for githup; UNSTABLE, DO NOT USE!

This commit is contained in:
Fabio Herzig
2026-04-12 21:25:44 +02:00
commit a51fd9dbeb
423 changed files with 58560 additions and 0 deletions

131
www/displays/audio.php Normal file
View File

@@ -0,0 +1,131 @@
<!DOCTYPE html>
<html>
<head>
<title>Remote Audioplayer</title>
<meta name="robots" content="noindex">
<style>
body {
background: #000 !important;
height: 100vh;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
font-family: sans-serif;
cursor: pointer;
}
h1 {
text-align: center;
color: #fff;
}
</style>
</head>
<body>
<script>
const userAccess = "audio";
let ws;
const RETRY_DELAY = 200; // ms
function startWebSocket() {
console.log("Attempting WebSocket connection...");
try {
ws = new WebSocket(`wss://` + window.location.hostname + `/ws/?access=${userAccess}`);
} catch (err) {
scheduleRetry();
return;
}
ws.onopen = () => {
console.log("WebSocket connected!");
document.querySelector('.errorws').style.display = 'none';
// SAFE: this executes only on successful connect
ws.send(JSON.stringify({
type: "SELF",
payload: {}
}));
};
ws.onerror = () => {
document.querySelector('h1').innerText = 'WS ERROR';
};
ws.onclose = () => {
document.querySelector('h1').innerText = 'WS DISCONECTED';
scheduleRetry();
};
}
function scheduleRetry() {
console.log(`Retrying in ${RETRY_DELAY}ms...`);
setTimeout(startWebSocket, RETRY_DELAY);
}
startWebSocket();
</script>
<h1>Der Audioplayer startet durch das Berühren des Displays</h1>
<audio id="peep" preload="auto"></audio>
<audio id="musicPlayer" preload="auto"></audio>
<script>
const peep = document.getElementById('peep');
const music = document.getElementById('musicPlayer');
let lastMusicUrl = null;
let pollingStarted = false; // ensure we start polling only once
async function fetchAndHandleMusic() {
try {
const response = await fetch('/displays/json/audio.json?t=' + Date.now(), { cache: 'no-store' });
const data = await response.json();
// Stop everything if musik is "stop"
if (!data.musik || data.musik === 'nan' || data.start == false) {
if (!music.paused) {
music.pause();
music.currentTime = 0;
}
if (!peep.paused) {
peep.pause();
peep.currentTime = 0;
}
lastMusicUrl = null;
return;
}
// Play only if new URL
if (data.musik !== lastMusicUrl) {
lastMusicUrl = data.musik;
// Play short peep first
peep.src = '/files/music/piep.mp3';
peep.play().then(() => {
// After 2 seconds, play the main music
setTimeout(() => {
music.src = data.musik;
music.play().catch(err => console.log('Music play error:', err));
}, 2000);
}).catch(err => console.log('Peep play error:', err));
}
} catch (err) {
console.error('Error fetching JSON:', err);
}
}
document.body.addEventListener('click', () => {
if (!pollingStarted) {
pollingStarted = true;
ws.addEventListener("message", () => {
fetchAndHandleMusic();
});
document.querySelector('h1').innerText = 'Player läuft'; // remove instruction
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,372 @@
:root {
--bg: #111827;
--panel: #020617;
--panel-soft: #0b1120;
--accent: #38bdf8;
--accent-soft: rgba(56, 189, 248, 0.15);
--text-main: #e5e7eb;
--text-muted: #9ca3af;
--danger: #ef4444;
--success: #22c55e;
--radius-lg: 18px;
--border-subtle: 1px solid rgba(148, 163, 184, 0.35);
--shadow-soft: 0 20px 45px rgba(15, 23, 42, 0.85);
--transition-fast: 180ms ease-out;
--colorStartDiv: #0b1120;
--panelBgLogo: #4a2f96;
--font-heading: clamp(1.5rem, 6vh, 4rem);
--font-sub: clamp(1rem, 3.5vh, 2.2rem);
--logo-size: clamp(150px, 40vh, 500px);
}
* {
box-sizing: border-box;
}
head > * {
pointer-events: none;
}
html,
body {
margin: 0;
padding: 0;
height: 100vh;
width: 100vw;
overflow: hidden;
position: fixed;
}
body {
display: flex;
justify-content: center;
align-items: stretch;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background-color: var(--bg);
color: var(--text-main);
overflow: hidden;
}
/* WebSocket error overlay */
.errorws {
position: fixed;
inset: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 16px;
padding: 5vh 5vw;
background: radial-gradient(circle at top, #020617 0, #020617 55%);
z-index: 10000;
}
.errorws .logoimg {
max-width: var(--logo-size);
height: auto;
margin-bottom: 2vh;
filter: drop-shadow(0 12px 30px rgba(0, 0, 0, 0.65));
}
.errortext {
margin: 0;
font-size: clamp(1.8rem, 8vw, 5rem);
line-height: 1.1;
max-width: 90vw;
font-weight: 500;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--text-main);
text-align: center;
}
.errortextSmall {
margin: 10px;
font-size: clamp(1rem, 3vw, 2rem);
opacity: 0.8;
font-weight: 300;
letter-spacing: 0.12em;
color: var(--text-main);
text-align: center;
}
/* Startup logo overlay */
.logobg {
padding: 4vh;
position: fixed;
inset: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 12px;
background-color: var(--panelBgLogo);
z-index: 1000;
transition: opacity 0.5s ease;
}
.logoimg {
width: auto;
max-height: 45vh;
max-width: 80vw;
object-fit: contain;
/* filter: drop-shadow(0 18px 40px rgba(15, 23, 42, 0.4)); */
}
.logotext {
margin: 0;
font-size: clamp(1.8rem, 7vh, 6rem);
text-align: center;
width: 90vw;
line-height: 1.2;
/* Forces single line for names/titles to prevent overflow */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.logotext, .logoctext {
color: var(--text-color) !important;
text-align: center;
font-weight: 400;
margin-top: 1vh;
}
.logoctext {
margin: 0;
font-size: clamp(2rem, 7vw, 12rem);
}
/* 4. Responsive adjustments for different screen ratios */
@media (aspect-ratio: 4/3), (aspect-ratio: 10/7) {
/* Tablet specific tweaks (iPad/Android Tabs) */
:root {
--logo-size: 30vh;
}
.logotext {
font-size: 5vh;
}
}
@media (min-width: 1921px) {
/* Ultra-large TV/4K tweaks */
.logotext {
letter-spacing: 0.25em; /* Better legibility at distance */
}
}
/* Main scoreboard container full screen */
.pagediv {
flex: 1;
height: 100%;
width: 100vw;
display: flex;
flex-direction: column;
gap: 2vh;
padding: 2.2vh 2.4vw;
box-sizing: border-box;
background:
radial-gradient(circle at top left, var(--bg-soft), transparent 60%),
radial-gradient(circle at bottom right, var(--bg-soft), transparent 60%);
}
.pagediv.manuel {
padding-right: clamp(60px, 2.4vw, 2.4vw);
}
/* Common row styling */
.display-row {
flex: 1;
display: flex;
align-items: center;
padding: 30px;
border-radius: var(--radius-lg);
background: linear-gradient(135deg, var(--panel-soft), var(--panel));
border-bottom: var(--border-subtle);
position: relative;
overflow: hidden;
box-shadow: var(--shadow-soft);
}
.display-row::before {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(circle at top left, var(--bg-soft), transparent 55%);
opacity: 0.5;
pointer-events: none;
}
.row1text,
.row2text p,
.row3 > p,
.start_text {
margin: 0;
padding: 0;
line-height: 1.1;
}
/* Row 1: athlete name */
.row1text {
font-size: min(8vh, 4.6vw);
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
white-space: nowrap;
}
/* Row 2: club, program, start/stop */
.row2 {
justify-content: space-between;
gap: 2vw;
}
.row2::after {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(circle at bottom right, var(--colorStartDiv), transparent 55%);
opacity: 0.5;
pointer-events: none;
}
.row2text {
display: flex;
flex-direction: column;
gap: 0.8vh;
width: 50vw;
height: 100%;
}
.row2_1text {
color: var(--text-muted);
}
.row2_2text {
font-weight: 600;
}
/* Start / stop pill */
.start_div {
width: 35vw;
height: 100%;
display: flex;
justify-content: end;
align-items: center;
}
.start_text {
display: flex;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
}
/* Row 3: D-score and total */
.row3 {
justify-content: space-between;
gap: 2vw;
}
.row3 > p {
font-size: min(9vh, 5vw);
font-weight: 700;
}
.row3_1text {
color: var(--color-note-l);
}
.row3_2text {
color: var(--color-note-r);
}
/* Existing #score and char/row styles if you use them elsewhere */
#score {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
.char {
display: flex;
justify-content: center;
align-items: center;
font-size: 40vw;
line-height: 1;
}
.row {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
font-size: 20vw;
}
/* Landscape / portrait logo scaling */
@media (orientation: landscape) {
.logoimg {
max-height: 60vh;
}
}
@media (orientation: portrait) {
.logoimg {
max-width: 60vw;
}
}
/* Small screens keep everything readable */
@media (max-width: 768px) {
.pagediv {
padding: 1.6vh 3vw;
gap: 1.4vh;
}
.display-row {
padding: 1.4vh 3vw;
}
.row2 {
flex-direction: column;
align-items: flex-start;
}
.start_div {
align-self: flex-end;
min-width: 40vw;
}
}
.noWsConnection {
position: fixed;
bottom: 20px;
right: 20px;
transform: rotate(270deg);
transform-origin: right bottom;
}
.rotator {
display: flex;
align-items: center;
gap: 6px;
transform: translateX(100%);
}
.noWsConnection img {
width: 20px;
height: 20px;
object-fit: contain;
}
.noWsConnection p {
font-size: 20px;
color: var(--text-muted);
margin: 0;
letter-spacing: 1.1px;
}

441
www/displays/display.php Normal file
View File

@@ -0,0 +1,441 @@
<?php
/*$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https://" : "http://";
$fullDomain = $protocol . $_SERVER['HTTP_HOST'];*/
$lastSegment = strtolower($_GET['geraet']) ?? '';
$baseDir = $_SERVER['DOCUMENT_ROOT'];
$token = "QQa2UMbEYW8oOL7wz9DjtqECVCikSZsDuSdmzxiadEXFsKyujEUyQOW1AYMD2OqU8VXxClIRweRuWLzvBrZpPYL41e89Rs96tM7Lq1KpjA5E2mg2UfgvztheGRV";
require_once $baseDir . '/../scripts/db/db-verbindung-script-guest.php';
require_once $baseDir . '/../scripts/db/db-functions.php';
require_once $baseDir . '/../scripts/db/db-tables.php';
$stmt = $guest->prepare("SELECT `name` FROM $tableGeraete ORDER BY start_index ASC");
if (!$stmt->execute()) {
http_response_code(500);
exit;
}
$result = $stmt->get_result();
$disciplines = array_map(
'strtolower',
array_column($result->fetch_all(MYSQLI_ASSOC), 'name')
);
$stmt->close();
// Define a small helper function to keep the code DRY (Don't Repeat Yourself)
function sanitize_hex($color) {
return preg_replace('/[^0-9a-fA-F#]/', '', $color);
}
// Fetch and Sanitize Brand Colors
$wkName = db_get_var($guest, "SELECT `value` FROM $tableVar WHERE `name` = ?", ['wkName']);
$cleanColor = sanitize_hex(db_get_var($guest, "SELECT `value` FROM $tableVar WHERE `name` = ?", ['displayColourLogo']));
$cleanColorText = sanitize_hex(db_get_var($guest, "SELECT `value` FROM $tableVar WHERE `name` = ?", ['displayTextColourLogo']));
// Fetch and Sanitize Layout Colors
$displayColorScoringBg = sanitize_hex(db_get_var($guest, "SELECT `value` FROM $tableVar WHERE `name` = ?", ['displayColorScoringBg']));
$displayColorScoringBgSoft = sanitize_hex(db_get_var($guest, "SELECT `value` FROM $tableVar WHERE `name` = ?", ['displayColorScoringBgSoft']));
$displayColorScoringPanel = sanitize_hex(db_get_var($guest, "SELECT `value` FROM $tableVar WHERE `name` = ?", ['displayColorScoringPanel']));
$displayColorScoringPanelSoft = sanitize_hex(db_get_var($guest, "SELECT `value` FROM $tableVar WHERE `name` = ?", ['displayColorScoringPanelSoft']));
// Fetch and Sanitize Text Colors
$displayColorScoringPanelText = sanitize_hex(db_get_var($guest, "SELECT `value` FROM $tableVar WHERE `name` = ?", ['displayColorScoringPanelText']));
$displayColorScoringPanelTextSoft = sanitize_hex(db_get_var($guest, "SELECT `value` FROM $tableVar WHERE `name` = ?", ['displayColorScoringPanelTextSoft']));
// Fetch and Sanitize Accent Colors (Note: fixed 'diplay' typo here)
$displayColorScoringPanelTextNoteL = sanitize_hex(db_get_var($guest, "SELECT `value` FROM $tableVar WHERE `name` = ?", ['displayColorScoringPanelTextNoteL']));
$displayColorScoringPanelTextNoteR = sanitize_hex(db_get_var($guest, "SELECT `value` FROM $tableVar WHERE `name` = ?", ['displayColorScoringPanelTextNoteR']));
$guest->close();
if (!isset($lastSegment) || !in_array($lastSegment, $disciplines)){
echo 'kein Gerät';
exit;
}
$jsonUrlconfig = '/displays/json/config.json';
$jsonUrl = '/displays/json/display_' . $lastSegment . '.json';
?>
<!DOCTYPE html>
<html>
<head>
<title><?= $wkName ?> Anzeigen</title>
<meta name="robots" content="noindex">
<link rel="stylesheet" href="/displays/css/display.css">
<style>
:root {
/* Brand */
--panelBgLogo: <?= $cleanColor ?>;
--text-color: <?= $cleanColorText ?>;
/* Backgrounds */
--bg: <?= $displayColorScoringBg ?>;
--bg-soft: <?= $displayColorScoringBgSoft ?>26;
--panel: <?= $displayColorScoringPanel ?>;
--panel-soft: <?= $displayColorScoringPanelSoft ?>;
/* Typography */
--text-main: <?= $displayColorScoringPanelText ?>;
--text-muted: <?= $displayColorScoringPanelTextSoft ?>;
/* Noten */
--color-note-l: <?= $displayColorScoringPanelTextNoteL ?>;
--color-note-r: <?= $displayColorScoringPanelTextNoteR ?>;
}
</style>
</head>
<body>
<div class="errorws">
<img class="logoimg" src="https://cdn-icons-png.freepik.com/512/12890/12890341.png?ga=GA1.1.991281105.1761199359">
<p class="errortext">Keine WebSocket Verbindung</p>
<p class="errortextSmall">Versuche Verbindung... (Versuch <span id="counterTries"></span> / <span id="counterMaxTries"></span>)</p>
</div>
<div class="noWsConnection">
<div class="rotator">
<img src="https://cdn-icons-png.freepik.com/512/12890/12890341.png?ga=GA1.1.991281105.1761199359">
<p>Keine WebSocket Verbindung, Syncronisation via FETCH</p>
</div>
</div>
<div class="logobg">
<img id="jsImgLogo" class="logoimg" src="/intern/img/logo-normal.png">
<p class="logotext"><?= $wkName ?></p>
<p class="logoctext"></p>
</div>
<div class="pagediv">
<div class="display-row row1">
<p class="row1text"></p>
</div>
<div class="display-row row2">
<div class="row2text">
<p class="row2_1text sds"></p>
<p class="row2_2text"></p>
</div>
<div class="start_div">
<p class="start_text"></p>
</div>
</div>
<div class="display-row row3">
<p class="row3_1text"></p>
<p class="row3_2text"></p>
</div>
</div>
<script>
const userAccess = "<?php echo $lastSegment; ?>_display";
const jsonUrl = "<?php echo $jsonUrl; ?>";
const jsonUrlconfig = "<?php echo $jsonUrlconfig; ?>";
// --- State Management ---
let ws;
let filedConectCount = 1;
const fallbackConectCount = 10;
const RETRY_DELAY = 1000;
const FALLBACK_POLL_INTERVAL = 3000; // Fetch every 3 seconds if WS is dead
let fallbackTimer = null;
// Hold our current data in memory
let displayConfig = { type: 'logo', ctext: '' };
let currentScore = {};
let lastUniqueId = null;
// UI Elements
const counterTriesEl = document.getElementById('counterTries');
const counterMaxTriesEl = document.getElementById('counterMaxTries');
if(counterTriesEl) counterTriesEl.innerHTML = filedConectCount;
if(counterMaxTriesEl) counterMaxTriesEl.innerHTML = fallbackConectCount;
// --- Fullscreen Listener ---
const fs = document.documentElement;
document.body.addEventListener('click', () => {
if (fs.requestFullscreen) fs.requestFullscreen();
else if (fs.webkitRequestFullscreen) fs.webkitRequestFullscreen();
else if (fs.mozRequestFullScreen) fs.mozRequestFullScreen();
else if (fs.msRequestFullscreen) fs.msRequestFullscreen();
});
// --- WebSocket Logic ---
function startWebSocket() {
console.log("Attempting WebSocket connection...");
try {
ws = new WebSocket(`wss://${window.location.hostname}/ws/?access=${userAccess}`);
} catch (err) {
scheduleRetry();
return;
}
ws.onopen = () => {
console.log("WebSocket connected!");
document.querySelector('.errorws').style.display = 'none';
document.querySelector('.noWsConnection').style.display = 'none';
document.querySelector('.pagediv').classList.remove("manuel");
filedConectCount = 1;
if(counterTriesEl) counterTriesEl.innerHTML = filedConectCount;
// Stop fallback polling since WS is alive
stopFallbackPolling();
// Do ONE initial fetch to sync current state
fetchFullState();
};
ws.onerror = () => {
document.querySelector('.errorws').style.display = 'flex';
};
ws.onclose = () => {
document.querySelector('.errorws').style.display = 'flex';
scheduleRetry();
};
ws.addEventListener("message", msg => {
let msgJSON;
try {
msgJSON = JSON.parse(msg.data);
} catch (error) {
return; // Ignore malformed messages
}
// Route the incoming push data
switch (msgJSON.type) {
case "EINSTELLUNGEN_DISPLAY_UPDATE":
updateSettings(msgJSON.payload.key, msgJSON.payload.value);
break;
case "UPDATE_DISPLAYCONTROL":
// Expecting payload: { type: 'logo'|'ctext'|'scoring', ctext: '...' }
displayConfig = msgJSON.payload;
renderDOM();
break;
case "UPDATE_SCORE":
// Expecting payload: the exact same structure as your jsonUrl outputs
currentScore = msgJSON.payload;
renderDOM();
break;
}
});
}
function scheduleRetry() {
console.log(`Retrying in ${RETRY_DELAY}ms...`);
if (filedConectCount >= fallbackConectCount) {
// MAX RETRIES REACHED -> Enter Fallback Mode
document.querySelector('.errorws').style.display = 'none';
document.querySelector('.noWsConnection').style.display = 'flex';
document.querySelector('.pagediv').classList.add("manuel");
startFallbackPolling();
} else {
if(counterTriesEl) counterTriesEl.innerHTML = filedConectCount;
setTimeout(startWebSocket, RETRY_DELAY);
filedConectCount++;
}
}
// --- Fallback Polling Logic ---
function startFallbackPolling() {
if (fallbackTimer === null) {
console.warn("Starting JSON fallback polling...");
fetchFullState(); // Fetch immediately once
fallbackTimer = setInterval(fetchFullState, FALLBACK_POLL_INTERVAL);
// Optionally, keep trying to reconnect the WS slowly in the background
setTimeout(startWebSocket, 10000);
}
}
function stopFallbackPolling() {
if (fallbackTimer !== null) {
clearInterval(fallbackTimer);
fallbackTimer = null;
console.log("Stopped JSON fallback polling.");
}
}
// --- Data Fetching (Initial Sync & Fallback) ---
async function fetchFullState() {
try {
// Fetch both config and score simultaneously
const [resConfig, resScore] = await Promise.all([
fetch(jsonUrlconfig + '?t=' + Date.now(), { cache: "no-store" }),
fetch(jsonUrl + '?t=' + Date.now(), { cache: "no-store" })
]);
if (resConfig.ok) displayConfig = await resConfig.json();
if (resScore.ok) currentScore = await resScore.json();
renderDOM();
} catch (err) {
console.error("Error fetching JSON:", err);
const container = document.getElementById('score');
if(container) {
container.innerHTML = "";
container.style.backgroundColor = "black";
}
}
}
// --- The Master Renderer ---
function renderDOM() {
const logobg = document.querySelector('.logobg');
const ctext = document.querySelector('.logoctext');
if (displayConfig.type === 'logo' || displayConfig.type === 'ctext') {
// LOGO OR CUSTOM TEXT MODE
if (logobg) logobg.style.opacity = "1";
if (ctext) ctext.innerText = (displayConfig.type === 'ctext') ? (displayConfig.ctext || '') : '';
} else if (displayConfig.type === 'scoring') {
// SCORING MODE
if (logobg) logobg.style.opacity = "0";
if (currentScore.uniqueid !== lastUniqueId) {
lastUniqueId = currentScore.uniqueid;
// Reset any animation/repeat logic here if needed
}
const safeText = (selector, text) => {
const el = document.querySelector(selector);
if (el) el.innerText = text !== undefined ? text : '';
};
safeText('.row1text', `${currentScore.vorname || ''} ${currentScore.name || ''}`);
safeText('.row2_1text', currentScore.verein ? `${currentScore.verein}, ` : '');
safeText('.row2_2text', currentScore.programm || '');
const starttext = document.querySelector('.start_text');
const row2El = document.querySelector('.row2');
if (starttext && row2El) {
const rootStyles = getComputedStyle(document.documentElement);
const dangerColor = rootStyles.getPropertyValue('--danger').trim();
const successColor = rootStyles.getPropertyValue('--success').trim();
if (currentScore.start === true) {
row2El.style.setProperty('--colorStartDiv', successColor);
starttext.innerHTML = 'Start';
} else {
row2El.style.setProperty('--colorStartDiv', dangerColor);
starttext.innerHTML = 'Stop';
}
}
safeText('.row3_1text', currentScore.noteLinks);
safeText('.row3_2text', currentScore.noteRechts);
}
fitTextAll();
}
// --- Settings & UI Handlers ---
function updateSettings(type, value) {
const sanitizeHex = (val) => val.replace(/[^0-9a-fA-F#]/g, '');
switch (type) {
case 'wkName':
const logotext = document.querySelector('.logotext');
if (logotext) logotext.innerHTML = value;
break;
case 'displayColourLogo':
document.documentElement.style.setProperty('--panelBgLogo', sanitizeHex(value));
break;
case 'displayTextColourLogo':
document.documentElement.style.setProperty('--text-color', sanitizeHex(value));
break;
case 'displayColorScoringBg':
document.documentElement.style.setProperty('--bg', sanitizeHex(value));
break;
case 'displayColorScoringBgSoft':
document.documentElement.style.setProperty('--bg-soft', sanitizeHex(value) + "26");
break;
case 'displayColorScoringPanel':
document.documentElement.style.setProperty('--panel', sanitizeHex(value));
break;
case 'displayColorScoringPanelSoft':
document.documentElement.style.setProperty('--panel-soft', sanitizeHex(value));
break;
case 'displayColorScoringPanelText':
document.documentElement.style.setProperty('--text-main', sanitizeHex(value));
break;
case 'displayColorScoringPanelTextSoft':
document.documentElement.style.setProperty('--text-muted', sanitizeHex(value));
break;
case 'displayColorScoringPanelTextNoteL': // Matching your PHP typo
document.documentElement.style.setProperty('--color-note-l', sanitizeHex(value));
break;
case 'displayColorScoringPanelTextNoteR': // Matching your PHP typo
document.documentElement.style.setProperty('--color-note-r', sanitizeHex(value));
break;
case 'logo-normal':
const jsImgLogo = document.getElementById('jsImgLogo');
if(jsImgLogo) jsImgLogo.src = '/intern/img/logo-normal.png?' + Date.now();
break;
}
}
// --- Text Resizing Engine ---
function isOverflown(parent, elem, heightscale, widthscale, paddingtext) {
return (
(elem.scrollWidth + paddingtext) > (parent.clientWidth / widthscale) ||
(elem.scrollHeight + paddingtext) > (parent.clientHeight / heightscale)
);
}
function fitTextElement(elem, { minSize = 10, maxSize = 1000, step = 1, unit = 'px' } = {}) {
if (!elem) return;
const parent = elem.parentElement;
if (!parent) return;
// FIXED: Declare variables properly so they don't leak into the global scope
let heightscale = 1;
let widthscale = 1;
let paddingtext = 60;
if (parent.classList.contains('row2text')) {
heightscale = 2;
paddingtext = 0;
}
if (parent.classList.contains('row3')) {
widthscale = 2;
}
let size = minSize;
elem.style.whiteSpace = 'nowrap';
elem.style.fontSize = size + unit;
while (size < maxSize && !isOverflown(parent, elem, heightscale, widthscale, paddingtext)) {
size += step;
elem.style.fontSize = size + unit;
}
elem.style.fontSize = (size - step) + unit;
}
function fitTextAll() {
const paragraphs = document.querySelectorAll('.pagediv p');
paragraphs.forEach(p => fitTextElement(p));
}
window.addEventListener('resize', fitTextAll);
// --- Initialize ---
startWebSocket();
</script>
</body>
</html>