b2in/public/_cabinet/index.html
2026-04-10 17:18:17 +02:00

871 lines
32 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cabinet Digital Signage Bielefeld</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;600;700&display=swap" rel="stylesheet">
<script>
(function() {
const LOG_URL = 'https://cabinet.b2in.eu/logger.php';
// Kontext-Informationen für besseres Debugging
let appContext = {
currentVideo: null,
currentFooter: null,
videoPlaylistLength: 0,
footerContentLength: 0,
lastActivity: Date.now()
};
// Logging-Funktion mit Kontext
function sendLog(level, message, additionalData = {}) {
try {
const logData = {
level: level,
message: String(message),
timestamp: new Date().toISOString(),
context: {
...appContext,
...additionalData
},
viewport: `${window.innerWidth}x${window.innerHeight}`,
connection: navigator.onLine ? 'online' : 'offline'
};
fetch(LOG_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(logData),
keepalive: true // Wichtig für Logs beim Verlassen der Seite
}).catch(() => {}); // Fehler beim Loggen ignorieren
} catch (e) {}
}
// Globale Fehler abfangen
window.onerror = function(msg, url, line, col, error) {
sendLog('FATAL', `JavaScript Error: ${msg}`, {
file: url,
line: line,
column: col,
stack: error?.stack
});
return false;
};
// Unhandled Promise Rejections (sehr wichtig für async/await!)
window.addEventListener('unhandledrejection', function(event) {
sendLog('ERROR', `Unhandled Promise Rejection: ${event.reason}`, {
promise: event.promise?.toString()
});
});
// Console.error überschreiben
const originalError = console.error;
console.error = function(...args) {
sendLog('ERROR', `Console Error: ${args.join(' ')}`);
originalError.apply(console, args);
};
// Console.warn überschreiben (für Warnungen)
const originalWarn = console.warn;
console.warn = function(...args) {
sendLog('WARNING', `Console Warning: ${args.join(' ')}`);
originalWarn.apply(console, args);
};
// Resource Loading Errors (z.B. Videos, Bilder)
window.addEventListener('error', function(event) {
if (event.target !== window) {
const element = event.target;
const tagName = element.tagName;
const src = element.src || element.href;
sendLog('ERROR', `Resource Failed to Load: ${tagName}`, {
src: src,
type: tagName
});
}
}, true); // useCapture = true, um alle Events zu fangen
// Online/Offline Status überwachen
window.addEventListener('online', () => {
sendLog('INFO', 'Connection restored');
});
window.addEventListener('offline', () => {
sendLog('WARNING', 'Connection lost');
});
// Heartbeat: Alle 5 Minuten ein "alive" Signal senden
setInterval(() => {
sendLog('INFO', 'Heartbeat - Display is running', {
uptime: Math.floor((Date.now() - appContext.lastActivity) / 1000) + 's'
});
}, 5 * 60 * 1000); // Alle 5 Minuten
// Initial Log beim Start
sendLog('INFO', 'Display started', {
userAgent: navigator.userAgent,
screen: `${screen.width}x${screen.height}`,
url: window.location.href
});
// Export für andere Scripts
window.displayLogger = {
log: (msg, data) => sendLog('INFO', msg, data),
warn: (msg, data) => sendLog('WARNING', msg, data),
error: (msg, data) => sendLog('ERROR', msg, data),
setContext: (key, value) => { appContext[key] = value; }
};
})();
</script>
<style>
/* --- GRUNDGERÜST --- */
body, html {
margin: 0;
padding: 0;
width: 100vw;
height: 100vh;
overflow: hidden;
font-family: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background-color: #000;
display: flex;
justify-content: center;
align-items: center;
}
#main-container {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
max-width: 100vw;
max-height: 100vh;
/* Seitenverhältnis 9:16 (1080:1920) beibehalten */
aspect-ratio: 9 / 16;
}
/* Wenn Bildschirm breiter als 9:16 ist, nach Höhe skalieren */
@media (min-aspect-ratio: 9/16) {
#main-container {
width: auto;
height: 100vh;
}
}
/* Wenn Bildschirm höher als 9:16 ist, nach Breite skalieren */
@media (max-aspect-ratio: 9/16) {
#main-container {
width: 100vw;
height: auto;
}
}
/* --- VIDEO BEREICH (Oben) --- */
#video-wrapper {
flex-grow: 1;
position: relative;
background: #000;
overflow: hidden;
}
#video-player {
width: 100%;
height: 100%;
object-fit: cover; /* Video füllt den Bereich randlos */
object-position: center 15%; /* Fallback-Position, wird per JavaScript überschrieben */
display: block;
/* Performance-Optimierungen für Video */
will-change: transform; /* Hint für Browser-Optimierung */
transform: translateZ(0); /* Hardware-Beschleunigung aktivieren */
backface-visibility: hidden; /* Reduziert Rendering-Last */
-webkit-backface-visibility: hidden;
}
/* --- FOOTER BEREICH (Unten - ca. 16.67% der Höhe = 320px bei 1920px) --- */
#footer {
height: 9.67vh;
min-height: 100px;
background-color: #1a1a1a; /* Dunkelgrau */
color: #ffffff;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px; /* 60px bei 1080px Breite */
box-sizing: border-box;
font-size: 10px;
position: relative;
}
/* Progress Bar am oberen Rand des Footers */
#progress-bar {
position: absolute;
top: 0;
left: 0;
height: 3px;
background-color: #009FE3; /* Cabinet Blau */
width: 0%;
transition: none;
}
#progress-bar.animate {
animation: progressAnimation 30s linear;
}
@keyframes progressAnimation {
from {
width: 0%;
}
to {
width: 100%;
}
}
/* --- INHALTE IM FOOTER --- */
.cta-text-container {
width: 75%;
opacity: 1;
transition: opacity 1s ease-in-out;
}
.cta-headline {
font-size: 2.0em; /* Relativ zur Footer-Schriftgröße */
font-weight: 300;
margin-bottom: 0.3em;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #bbb;
}
.cta-subline {
font-size: 2.4em; /* Relativ zur Footer-Schriftgröße */
font-weight: 700;
line-height: 1.1;
}
.qr-container {
width: 25%;
display: flex;
flex-direction: column;
align-items: center;
opacity: 1;
transition: opacity 1s ease-in-out;
}
.qr-code-img {
width: 8em; /* Relativ zur Footer-Schriftgröße */
height: auto;
max-width: 100px;
aspect-ratio: 1 / 1; /* Erzwingt quadratische Form */
object-fit: contain;
background-color: white; /* Weißer Hintergrund für Lesbarkeit */
padding: 0.4em;
border-radius: 0.6em;
box-sizing: border-box;
}
.scan-hint {
margin-top: 0.8em;
font-size: 1.3em; /* Relativ zur Footer-Schriftgröße */
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: 600;
color: #009FE3; /* Akzentfarbe */
}
/* Hilfsklasse für den Überblend-Effekt */
.fade-out {
opacity: 0;
}
/* Loading Indicator */
.loading {
text-align: center;
padding: 2em;
color: #fff;
}
</style>
</head>
<body>
<div id="main-container">
<!-- VIDEO BEREICH -->
<div id="video-wrapper">
<!-- Videos werden hier geladen. 'muted' ist oft nötig für Autoplay -->
<video id="video-player" autoplay muted playsinline></video>
</div>
<!-- FOOTER BEREICH -->
<div id="footer">
<div id="progress-bar"></div>
<div class="cta-text-container" id="text-area">
<div class="cta-headline" id="headline">LADEN...</div>
<div class="cta-subline" id="subline">Inhalte werden geladen</div>
</div>
<div class="qr-container" id="qr-area">
<img id="qr-image" class="qr-code-img" alt="QR Code">
</div>
</div>
</div>
<script>
/* ==============================================
KONFIGURATION WIRD DYNAMISCH GELADEN
============================================== */
let videoPlaylist = [];
let footerContent = [];
let footerContentLength = 0;
// Basis-URL für Assets und API (b2in.eu Server)
const BASE_URL = 'https://b2in.eu';
// Lokale JSON-Datei statt API (Übergangsweise bis neues Display-Modul freigegeben)
const CONFIG_URL = 'default.json';
/* ==============================================
KONFIGURATION LADEN
============================================== */
async function loadConfiguration() {
try {
window.displayLogger?.log('Lade Konfiguration...', { url: CONFIG_URL });
const response = await fetch(CONFIG_URL);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const config = await response.json();
videoPlaylist = config.videoPlaylist || [];
footerContent = config.footerContent || [];
window.displayLogger?.setContext('videoPlaylistLength', videoPlaylist.length);
window.displayLogger?.setContext('footerContentLength', footerContent.length);
window.displayLogger?.log('Konfiguration erfolgreich geladen', {
videos: videoPlaylist.length,
footerItems: footerContent.length
});
console.log('Konfiguration geladen:', config);
// Überprüfe, ob Videos vorhanden sind
if (videoPlaylist.length === 0) {
console.warn('Keine Videos in der Playlist vorhanden');
window.displayLogger?.warn('Keine Videos in Playlist');
document.getElementById('headline').innerText = 'KEINE VIDEOS';
document.getElementById('subline').innerText = 'Bitte fügen Sie Videos im CMS hinzu';
return false;
}
// Überprüfe, ob Footer-Inhalte vorhanden sind
if (footerContent.length === 0) {
console.warn('Keine Footer-Inhalte vorhanden - Footer wird ausgeblendet');
footerContentLength = 0;
// Footer ausblenden
const footer = document.getElementById('footer');
if (footer) {
footer.style.display = 'none';
}
// Video-Wrapper auf 100% Höhe setzen
const videoWrapper = document.getElementById('video-wrapper');
if (videoWrapper) {
videoWrapper.style.flexGrow = '1';
videoWrapper.style.height = '100%';
}
} else {
// Footer anzeigen, falls er zuvor ausgeblendet wurde
const footer = document.getElementById('footer');
if (footer) {
footer.style.display = 'flex';
}
footerContentLength = 1;
}
return true;
} catch (error) {
console.error('Fehler beim Laden der Konfiguration:', error);
window.displayLogger?.error('Konfiguration konnte nicht geladen werden', {
error: error.message,
stack: error.stack
});
document.getElementById('headline').innerText = 'FEHLER';
document.getElementById('subline').innerText = 'Konfiguration konnte nicht geladen werden';
return false;
}
}
/* ==============================================
PROGRAMM-LOGIK
============================================== */
// --- ROBUSTER VIDEO PLAYER MIT MEMORY-MANAGEMENT ---
const videoElement = document.getElementById('video-player');
let currentVideoIndex = 0;
let videoStartTimeout = null;
let videoWatchdogInterval = null;
let lastVideoTime = 0;
let videoStuckCount = 0;
let consecutiveErrors = 0;
let isTransitioning = false;
const MAX_CONSECUTIVE_ERRORS = 3;
const VIDEO_START_TIMEOUT = 10000; // 10 Sekunden
const VIDEO_WATCHDOG_INTERVAL = 5000; // Alle 5 Sekunden prüfen
// Video-Element optimieren für Memory-Management
videoElement.setAttribute('preload', 'metadata'); // Nur Metadaten vorladen, nicht ganzes Video
function cleanupVideo() {
isTransitioning = true;
try {
videoElement.pause();
videoElement.removeAttribute('src');
videoElement.load();
if (videoStartTimeout) {
clearTimeout(videoStartTimeout);
videoStartTimeout = null;
}
window.displayLogger?.log('Video cleanup durchgeführt');
} catch (e) {
window.displayLogger?.error('Video cleanup error', { error: e.message });
}
}
function playNextVideo() {
if (videoPlaylist.length === 0) return;
// Verhindert gleichzeitige, sich überschneidende Übergänge
if (isTransitioning) {
window.displayLogger?.log('playNextVideo übersprungen - Übergang läuft noch');
return;
}
lastVideoTime = 0;
videoStuckCount = 0;
const video = videoPlaylist[currentVideoIndex];
const videoSrc = BASE_URL + "/_cabinet/" + video.src;
window.displayLogger?.setContext('currentVideo', video.src);
window.displayLogger?.setContext('currentVideoIndex', currentVideoIndex);
// Index VOR dem asynchronen Cleanup weiterschalten
currentVideoIndex++;
if (currentVideoIndex >= videoPlaylist.length) {
currentVideoIndex = 0;
window.displayLogger?.log('Playlist-Loop abgeschlossen, starte von vorne');
}
// Altes Video stoppen und Speicher freigeben (setzt isTransitioning = true)
cleanupVideo();
// Delay damit Cleanup vollständig abgeschlossen ist
setTimeout(() => {
isTransitioning = false;
try {
videoElement.src = videoSrc;
if (footerContentLength !== 0 && video.position !== undefined) {
videoElement.style.objectPosition = `center ${video.position}%`;
}
videoStartTimeout = setTimeout(() => {
window.displayLogger?.error('Video start timeout', {
video: video.src,
timeout: VIDEO_START_TIMEOUT
});
skipToNextVideo('timeout');
}, VIDEO_START_TIMEOUT);
videoElement.play()
.then(() => {
window.displayLogger?.log(`Video started: ${video.src}`);
consecutiveErrors = 0;
if (videoStartTimeout) {
clearTimeout(videoStartTimeout);
videoStartTimeout = null;
}
})
.catch(e => {
// AbortError = play() wurde absichtlich durch pause()/load() unterbrochen.
// Das ist kein echter Fehler, sondern erwartetes Verhalten beim Cleanup.
if (e.name === 'AbortError') {
window.displayLogger?.log(`Video play absichtlich unterbrochen: ${video.src}`);
return;
}
window.displayLogger?.error(`Video play failed: ${video.src}`, {
error: e.message
});
consecutiveErrors++;
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
window.displayLogger?.error('Zu viele aufeinanderfolgende Fehler', {
count: consecutiveErrors
});
setTimeout(() => location.reload(), 30000);
} else {
skipToNextVideo('play_failed');
}
});
} catch (e) {
window.displayLogger?.error('Exception beim Video-Laden', {
error: e.message,
stack: e.stack
});
skipToNextVideo('exception');
}
}, 200); // 200ms für zuverlässiges Cleanup
}
function skipToNextVideo(reason) {
window.displayLogger?.warn('Überspringe zum nächsten Video', { reason: reason });
playNextVideo();
}
// Video Watchdog: Prüft ob Video wirklich läuft
function startVideoWatchdog() {
if (videoWatchdogInterval) {
clearInterval(videoWatchdogInterval);
}
videoWatchdogInterval = setInterval(() => {
if (videoPlaylist.length === 0) return;
const currentTime = videoElement.currentTime;
const isPaused = videoElement.paused;
const hasEnded = videoElement.ended;
const isStuck = (currentTime === lastVideoTime && !isPaused && !hasEnded);
// Debug-Log
if (isStuck) {
videoStuckCount++;
window.displayLogger?.warn('Video scheint stecken geblieben zu sein', {
currentTime: currentTime,
isPaused: isPaused,
hasEnded: hasEnded,
stuckCount: videoStuckCount,
src: videoElement.src
});
// Wenn 2x hintereinander stuck → Recovery (nur wenn kein Übergang läuft)
if (videoStuckCount >= 2 && !isTransitioning) {
window.displayLogger?.error('Video definitiv stuck - starte nächstes', {
currentTime: currentTime,
src: videoElement.src
});
skipToNextVideo('watchdog_stuck');
}
} else {
// Video läuft normal → Counter zurücksetzen
if (videoStuckCount > 0) {
window.displayLogger?.log('Video läuft wieder normal');
}
videoStuckCount = 0;
}
lastVideoTime = currentTime;
}, VIDEO_WATCHDOG_INTERVAL);
}
// Video Events
videoElement.addEventListener('ended', () => {
window.displayLogger?.log('Video ended', {
src: videoElement.src
});
playNextVideo();
});
videoElement.addEventListener('error', (e) => {
// Fehler während eines laufenden Übergangs ignorieren (z.B. nach removeAttribute('src'))
if (isTransitioning) return;
const error = videoElement.error;
const errorCode = error?.code;
const errorMessage = {
1: 'MEDIA_ERR_ABORTED',
2: 'MEDIA_ERR_NETWORK',
3: 'MEDIA_ERR_DECODE',
4: 'MEDIA_ERR_SRC_NOT_SUPPORTED'
}[errorCode] || 'UNKNOWN';
window.displayLogger?.error('Video Error Event', {
code: errorCode,
message: error?.message,
src: videoElement.src,
mediaError: errorMessage
});
consecutiveErrors++;
skipToNextVideo(`error_${errorMessage}`);
});
videoElement.addEventListener('stalled', () => {
window.displayLogger?.warn('Video stalled (buffering)', {
src: videoElement.src,
currentTime: videoElement.currentTime
});
});
videoElement.addEventListener('waiting', () => {
window.displayLogger?.warn('Video waiting (buffering)', {
src: videoElement.src,
currentTime: videoElement.currentTime
});
});
videoElement.addEventListener('playing', () => {
window.displayLogger?.log('Video playing event', {
src: videoElement.src,
currentTime: videoElement.currentTime
});
});
videoElement.addEventListener('canplay', () => {
window.displayLogger?.log('Video canplay event', {
src: videoElement.src
});
});
// --- FOOTER ROTATION LOGIC ---
let currentFooterIndex = 0;
const textArea = document.getElementById('text-area');
const qrArea = document.getElementById('qr-area');
const headlineEl = document.getElementById('headline');
const sublineEl = document.getElementById('subline');
const qrImageEl = document.getElementById('qr-image');
const progressBar = document.getElementById('progress-bar');
function restartProgressBar() {
// Animation zurücksetzen und neu starten
progressBar.classList.remove('animate');
void progressBar.offsetWidth; // Force reflow
progressBar.classList.add('animate');
}
function updateFooter() {
if (footerContent.length === 0) return;
// 1. Ausblenden
textArea.classList.add('fade-out');
qrArea.classList.add('fade-out');
// Progress Bar neu starten
restartProgressBar();
// 2. Warten, Inhalt tauschen, Einblenden
setTimeout(() => {
const content = footerContent[currentFooterIndex];
// Kontext aktualisieren
window.displayLogger?.setContext('currentFooter', currentFooterIndex);
// Text setzen
headlineEl.innerText = content.headline;
sublineEl.innerText = content.subline;
// QR Code nur generieren wenn URL vorhanden
if (content.url) {
// QR Code generieren (API Aufruf)
const qrSize = "300x300";
const qrColor = "000000"; // Schwarz
const qrBg = "ffffff"; // Weiß
const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=${qrSize}&color=${qrColor}&bgcolor=${qrBg}&margin=10&data=${encodeURIComponent(content.url)}`;
qrImageEl.src = qrUrl;
qrArea.style.display = 'flex'; // QR-Bereich anzeigen
} else {
// Kein QR-Code - QR-Bereich ausblenden
qrArea.style.display = 'none';
// Text-Container auf volle Breite
textArea.style.width = '100%';
}
// Index weiterschalten
currentFooterIndex++;
if (currentFooterIndex >= footerContent.length) {
currentFooterIndex = 0;
}
// 3. Einblenden
if (content.url) {
qrImageEl.onload = () => {
textArea.classList.remove('fade-out');
qrArea.classList.remove('fade-out');
};
}
// Fallback falls Bild sofort da ist (Cache) oder kein QR-Code
setTimeout(() => {
textArea.classList.remove('fade-out');
if (content.url) {
qrArea.classList.remove('fade-out');
}
}, 100);
}, 1000); // 1 Sekunde für Fade-Out Animation
}
/* ==============================================
MEMORY MANAGEMENT & PERFORMANCE
============================================== */
// Memory-Optimierung: Regelmäßig Browser aufräumen
function performMemoryOptimization() {
try {
// Performance-Metriken loggen falls verfügbar
if (performance.memory) {
const memUsed = Math.round(performance.memory.usedJSHeapSize / 1048576);
const memLimit = Math.round(performance.memory.jsHeapSizeLimit / 1048576);
const memPercent = Math.round((memUsed / memLimit) * 100);
window.displayLogger?.log('Memory Status', {
usedMB: memUsed,
limitMB: memLimit,
percentUsed: memPercent
});
// Warnung wenn Speicher über 80%
if (memPercent > 80) {
window.displayLogger?.warn('Hohe Speicherauslastung', {
percentUsed: memPercent,
usedMB: memUsed
});
}
}
// Cache-Infos loggen
const cacheInfo = {
videoBuffered: videoElement.buffered.length,
videoDuration: videoElement.duration,
videoReadyState: videoElement.readyState
};
window.displayLogger?.log('Video Cache Status', cacheInfo);
} catch (e) {
window.displayLogger?.error('Memory optimization error', {
error: e.message
});
}
}
// Automatischer Page-Reload bei kritischen Problemen (Failsafe)
let criticalErrorCount = 0;
function checkCriticalErrors() {
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
criticalErrorCount++;
window.displayLogger?.error('Kritischer Zustand erkannt', {
consecutiveErrors: consecutiveErrors,
criticalErrorCount: criticalErrorCount
});
if (criticalErrorCount >= 3) {
window.displayLogger?.error('Zu viele kritische Fehler - Seite wird neu geladen');
setTimeout(() => location.reload(), 5000);
}
} else {
criticalErrorCount = 0; // Zurücksetzen wenn alles normal läuft
}
}
/* ==============================================
INITIALISIERUNG
============================================== */
async function initialize() {
const success = await loadConfiguration();
if (success && videoPlaylist.length > 0) {
// Start Video
playNextVideo();
// Start Video Watchdog (überwacht ob Videos laufen)
startVideoWatchdog();
window.displayLogger?.log('Video Watchdog gestartet');
// Start Footer Loop
if (footerContent.length > 0) {
updateFooter();
setInterval(updateFooter, 30000); // Alle 30.000 ms (30 sek) wechseln
// Progress Bar initial starten
restartProgressBar();
}
// Memory-Optimierung alle 10 Minuten
setInterval(performMemoryOptimization, 10 * 60 * 1000);
window.displayLogger?.log('Memory Optimizer gestartet (alle 10 Min)');
// Critical Error Check alle 30 Sekunden
setInterval(checkCriticalErrors, 30 * 1000);
// Initial Memory Check nach 30 Sekunden
setTimeout(performMemoryOptimization, 30000);
}
}
// Beim Laden der Seite initialisieren
initialize();
// Datei-Versions-Check: Erkennt ob index.html auf dem Server geändert wurde
let fileVersion = null;
async function checkFileVersion() {
try {
const response = await fetch(window.location.href, {
method: 'HEAD',
cache: 'no-store',
});
const version = response.headers.get('ETag') || response.headers.get('Last-Modified');
if (fileVersion === null) {
fileVersion = version;
} else if (version && version !== fileVersion) {
window.displayLogger?.log('Datei geändert Seite wird neu geladen');
location.reload();
}
} catch (e) {
// Offline ignorieren
}
}
checkFileVersion(); // Initiale Version merken
setInterval(checkFileVersion, 2 * 60 * 1000); // Alle 2 Minuten prüfen
// Auto-Reload alle 5 Minuten, um neue Inhalte zu laden
setInterval(async () => {
console.log('Prüfe auf neue Konfiguration...');
const oldFooterCount = footerContent.length;
await loadConfiguration();
// Wenn Footer-Inhalte hinzugefügt oder entfernt wurden, Seite neu laden
if ((oldFooterCount === 0 && footerContent.length > 0) ||
(oldFooterCount > 0 && footerContent.length === 0)) {
console.log('Footer-Status hat sich geändert - Seite wird neu geladen');
location.reload();
}
}, 5 * 60 * 1000); // 5 Minuten
// Präventiver Page-Reload alle 6 Stunden (verhindert Memory-Leaks über lange Zeit)
setTimeout(() => {
window.displayLogger?.log('Präventiver Reload nach 6 Stunden');
location.reload();
}, 6 * 60 * 60 * 1000); // 6 Stunden
</script>
</body>
</html>