612 lines
22 KiB
HTML
612 lines
22 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||
<title>CABINET – Store Info</title>
|
||
<link rel="apple-touch-icon" sizes="57x57" href="../../favicon/apple-icon-57x57.png">
|
||
<link rel="apple-touch-icon" sizes="60x60" href="../../favicon/apple-icon-60x60.png">
|
||
<link rel="apple-touch-icon" sizes="72x72" href="../../favicon/apple-icon-72x72.png">
|
||
<link rel="apple-touch-icon" sizes="76x76" href="../../favicon/apple-icon-76x76.png">
|
||
<link rel="apple-touch-icon" sizes="114x114" href="../../favicon/apple-icon-114x114.png">
|
||
<link rel="apple-touch-icon" sizes="120x120" href="../../favicon/apple-icon-120x120.png">
|
||
<link rel="apple-touch-icon" sizes="144x144" href="../../favicon/apple-icon-144x144.png">
|
||
<link rel="apple-touch-icon" sizes="152x152" href="../../favicon/apple-icon-152x152.png">
|
||
<link rel="apple-touch-icon" sizes="180x180" href="../../favicon/apple-icon-180x180.png">
|
||
<link rel="icon" type="image/png" sizes="192x192" href="../../favicon/android-icon-192x192.png">
|
||
<link rel="icon" type="image/png" sizes="96x96" href="../../favicon/favicon-96x96.png">
|
||
<link rel="icon" type="image/png" sizes="32x32" href="../../favicon/favicon-32x32.png">
|
||
<link rel="icon" type="image/png" sizes="16x16" href="../../favicon/favicon-16x16.png">
|
||
<link rel="shortcut icon" href="../../favicon/favicon.ico">
|
||
<link rel="manifest" href="../../favicon/manifest.json">
|
||
<meta name="msapplication-TileColor" content="#ffffff">
|
||
<meta name="msapplication-TileImage" content="../../favicon/ms-icon-144x144.png">
|
||
<meta name="theme-color" content="#ffffff">
|
||
<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;500;600;700&display=swap" rel="stylesheet">
|
||
<link rel="stylesheet" href="./info-styles.css">
|
||
<style>
|
||
/* Schützt vor versehentlicher Textauswahl / Kopier-Pop-up beim Berühren des Tablets */
|
||
body {
|
||
user-select: none;
|
||
-webkit-user-select: none;
|
||
-webkit-touch-callout: none;
|
||
}
|
||
.touch-guard {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 9999;
|
||
pointer-events: auto;
|
||
user-select: none;
|
||
-webkit-user-select: none;
|
||
-webkit-touch-callout: none;
|
||
touch-action: none;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="touch-guard" aria-hidden="true"></div>
|
||
<main class="screen">
|
||
|
||
<!-- 1. HEADER: Logo + Date -->
|
||
<header class="header" id="header">
|
||
<div class="brand">
|
||
<img src="../logo-cabinet-300.png" alt="CABINET" class="brand-logo">
|
||
</div>
|
||
<div class="header-date">
|
||
<div class="header-weekday" id="weekday">–</div>
|
||
<div class="header-datestring" id="datestring">–</div>
|
||
<div class="header-updated">Akt: <span id="last-updated">–</span></div>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- 2. STATUS BANNER -->
|
||
<section class="status-banner" id="status-banner" data-status="open">
|
||
<div class="status-icon" id="status-icon"></div>
|
||
<div class="status-text">
|
||
<div class="status-headline" id="status-headline">Laden...</div>
|
||
<div class="status-subtext" id="status-subtext"></div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 3. OPENING HOURS -->
|
||
<section class="hours-section">
|
||
<div class="hours-title">Öffnungszeiten</div>
|
||
<div class="hours-list" id="hours-list">
|
||
<div class="hours-row" data-day="monday">
|
||
<span class="hours-day">Montag</span>
|
||
<span class="hours-time" id="hours-monday">–</span>
|
||
</div>
|
||
<div class="hours-row" data-day="tuesday">
|
||
<span class="hours-day">Dienstag</span>
|
||
<span class="hours-time" id="hours-tuesday">–</span>
|
||
</div>
|
||
<div class="hours-row" data-day="wednesday">
|
||
<span class="hours-day">Mittwoch</span>
|
||
<span class="hours-time" id="hours-wednesday">–</span>
|
||
</div>
|
||
<div class="hours-row" data-day="thursday">
|
||
<span class="hours-day">Donnerstag</span>
|
||
<span class="hours-time" id="hours-thursday">–</span>
|
||
</div>
|
||
<div class="hours-row" data-day="friday">
|
||
<span class="hours-day">Freitag</span>
|
||
<span class="hours-time" id="hours-friday">–</span>
|
||
</div>
|
||
<div class="hours-row" data-day="saturday">
|
||
<span class="hours-day">Samstag</span>
|
||
<span class="hours-time" id="hours-saturday">–</span>
|
||
</div>
|
||
<div class="hours-row" data-day="sunday">
|
||
<span class="hours-day">Sonntag</span>
|
||
<span class="hours-time" id="hours-sunday">–</span>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 4. APPOINTMENT CARD -->
|
||
<section class="appointment-card" id="appointment">
|
||
<div class="appointment-icon">📅</div>
|
||
<div class="appointment-text">
|
||
<div class="appointment-label">Nächster freier Termin</div>
|
||
<div class="appointment-date" id="appointment-date">–</div>
|
||
<div class="appointment-note">Beratung – ca. 45 Min.</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 5. FOOTER: Contact + QR -->
|
||
<footer class="info-footer" id="footer">
|
||
<div class="contact-block">
|
||
<div class="contact-item">
|
||
<span class="contact-icon">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||
<path d="M2.25 6.75C2.25 15.0343 8.96573 21.75 17.25 21.75H19.5C20.7426 21.75 21.75 20.7426 21.75 19.5V18.1284C21.75 17.6121 21.3987 17.1622 20.8979 17.037L16.4747 15.9312C16.0355 15.8214 15.5734 15.9855 15.3018 16.3476L14.3316 17.6412C14.05 18.0166 13.563 18.1827 13.1223 18.0212C9.81539 16.8098 7.19015 14.1846 5.97876 10.8777C5.81734 10.437 5.98336 9.94998 6.3588 9.6684L7.65242 8.69818C8.01453 8.4266 8.17861 7.96445 8.06883 7.52533L6.96304 3.10215C6.83783 2.60133 6.38785 2.25 5.87163 2.25H4.5C3.25736 2.25 2.25 3.25736 2.25 4.5V6.75Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||
</svg>
|
||
</span>
|
||
<span id="contact-phone">–</span>
|
||
</div>
|
||
<div class="contact-item">
|
||
<span class="contact-icon">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||
<path d="M21.75 6.75V17.25C21.75 18.4926 20.7426 19.5 19.5 19.5H4.5C3.25736 19.5 2.25 18.4926 2.25 17.25V6.75M21.75 6.75C21.75 5.50736 20.7426 4.5 19.5 4.5H4.5C3.25736 4.5 2.25 5.50736 2.25 6.75M21.75 6.75V6.99271C21.75 7.77405 21.3447 8.49945 20.6792 8.90894L13.1792 13.5243C12.4561 13.9694 11.5439 13.9694 10.8208 13.5243L3.32078 8.90894C2.65535 8.49945 2.25 7.77405 2.25 6.99271V6.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||
</svg>
|
||
</span>
|
||
<span id="contact-email">–</span>
|
||
</div>
|
||
</div>
|
||
<div class="footer-qr">
|
||
<img id="qr-code" alt="QR Code">
|
||
<span class="footer-qr-label">Website</span>
|
||
</div>
|
||
</footer>
|
||
</main>
|
||
|
||
<!-- Offline Badge -->
|
||
<div class="offline-badge" id="offline-badge">Stand: <span id="offline-time">–</span></div>
|
||
|
||
<script>
|
||
/**
|
||
* CABINET Info-Tablet App
|
||
* Polling-based display for store info in shop window.
|
||
*/
|
||
|
||
class InfoTabletApp {
|
||
constructor() {
|
||
// Configuration
|
||
this.BASE_URL = this.detectBaseUrl();
|
||
this.API_STATUS = this.BASE_URL + '/api/cabinet-tablet/status';
|
||
this.API_CHECK = this.BASE_URL + '/api/cabinet-tablet/check';
|
||
this.QR_TARGET = 'https://cabinet-bielefeld.de';
|
||
|
||
this.POLL_INTERVAL = 30000; // 30 seconds
|
||
this.FILE_CHECK_INTERVAL = 120000; // 2 minutes
|
||
this.RELOAD_INTERVAL = 6 * 3600000; // 6 hours
|
||
this.MAX_FAILURES = 3;
|
||
this.RECOVERY_WAIT = 300000; // 5 minutes
|
||
this.OFFLINE_RELOAD = 1800000; // 30 minutes
|
||
|
||
// State
|
||
this.cachedTimestamp = null;
|
||
this.fileVersion = null;
|
||
this.failureCount = 0;
|
||
this.lastSuccessTime = Date.now();
|
||
this.isRecovering = false;
|
||
this.currentData = null;
|
||
|
||
// Day mapping
|
||
this.DAYS = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
|
||
this.DAY_NAMES = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'];
|
||
}
|
||
|
||
detectBaseUrl() {
|
||
const hostname = window.location.hostname;
|
||
if (hostname === 'cabinet.b2in.eu' || hostname.includes('b2in.eu')) {
|
||
return 'https://b2in.eu';
|
||
}
|
||
// Dev/test: same origin
|
||
return window.location.origin;
|
||
}
|
||
|
||
async init() {
|
||
console.log('[InfoTablet] Initializing...');
|
||
|
||
// Set date immediately
|
||
this.updateDate();
|
||
|
||
// Generate QR code
|
||
this.generateQR();
|
||
|
||
// Try to load from cache first
|
||
const cached = this.loadFromCache();
|
||
if (cached) {
|
||
console.log('[InfoTablet] Loaded from cache');
|
||
this.updateDOM(cached);
|
||
}
|
||
|
||
// Fetch fresh data
|
||
await this.fetchFullStatus();
|
||
|
||
// Record initial file version for change detection
|
||
await this.recordFileVersion();
|
||
|
||
// Start polling
|
||
this.startPolling();
|
||
|
||
// Schedule auto-reload
|
||
this.scheduleAutoReload();
|
||
|
||
// Schedule midnight update
|
||
this.scheduleMidnightUpdate();
|
||
|
||
console.log('[InfoTablet] Ready!');
|
||
}
|
||
|
||
// ========================================
|
||
// POLLING
|
||
// ========================================
|
||
|
||
startPolling() {
|
||
setInterval(() => this.checkForUpdates(), this.POLL_INTERVAL);
|
||
setInterval(() => this.checkFileVersion(), this.FILE_CHECK_INTERVAL);
|
||
}
|
||
|
||
async checkForUpdates() {
|
||
try {
|
||
const response = await fetch(this.API_CHECK);
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP ${response.status}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
this.failureCount = 0;
|
||
this.lastSuccessTime = Date.now();
|
||
this.hideOfflineBadge();
|
||
|
||
// Fetch full status when settings changed OR when open/closed state transitioned
|
||
const settingsChanged = data.updated_at !== this.cachedTimestamp;
|
||
const statusChanged = this.currentData && data.store_status !== this.currentData.store_status;
|
||
|
||
if (settingsChanged || statusChanged) {
|
||
console.log('[InfoTablet] Change detected (' + (settingsChanged ? 'settings' : 'status transition') + '), fetching full status...');
|
||
await this.fetchFullStatus();
|
||
}
|
||
} catch (error) {
|
||
this.handleFetchError(error);
|
||
}
|
||
}
|
||
|
||
async recordFileVersion() {
|
||
try {
|
||
const response = await fetch(window.location.href, {
|
||
method: 'HEAD',
|
||
cache: 'no-store',
|
||
});
|
||
this.fileVersion = response.headers.get('ETag') || response.headers.get('Last-Modified');
|
||
} catch (e) {
|
||
// Ignore – wird beim nächsten Check erneut versucht
|
||
}
|
||
}
|
||
|
||
async 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 (this.fileVersion && version && version !== this.fileVersion) {
|
||
console.log('[InfoTablet] Datei geändert – Seite wird neu geladen');
|
||
location.reload();
|
||
}
|
||
} catch (e) {
|
||
// Offline – ignorieren
|
||
}
|
||
}
|
||
|
||
async fetchFullStatus() {
|
||
try {
|
||
const response = await fetch(this.API_STATUS);
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP ${response.status}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
this.currentData = data;
|
||
this.cachedTimestamp = data.updated_at;
|
||
this.failureCount = 0;
|
||
this.lastSuccessTime = Date.now();
|
||
this.isRecovering = false;
|
||
|
||
this.updateDOM(data);
|
||
this.saveToCache(data);
|
||
this.hideOfflineBadge();
|
||
this.updateLastUpdated();
|
||
|
||
console.log('[InfoTablet] Status updated');
|
||
} catch (error) {
|
||
this.handleFetchError(error);
|
||
}
|
||
}
|
||
|
||
handleFetchError(error) {
|
||
this.failureCount++;
|
||
console.warn(`[InfoTablet] Fetch error (${this.failureCount}/${this.MAX_FAILURES}):`, error.message);
|
||
|
||
const offlineDuration = Date.now() - this.lastSuccessTime;
|
||
|
||
// Show offline badge
|
||
this.showOfflineBadge();
|
||
|
||
// After 30 min offline: reload page
|
||
if (offlineDuration >= this.OFFLINE_RELOAD) {
|
||
console.error('[InfoTablet] Offline for 30+ min, reloading...');
|
||
location.reload();
|
||
return;
|
||
}
|
||
|
||
// After MAX_FAILURES: enter recovery mode (wait longer)
|
||
if (this.failureCount >= this.MAX_FAILURES && !this.isRecovering) {
|
||
this.isRecovering = true;
|
||
console.warn('[InfoTablet] Entering recovery mode, waiting 5 min...');
|
||
setTimeout(() => {
|
||
this.isRecovering = false;
|
||
this.failureCount = 0;
|
||
this.fetchFullStatus();
|
||
}, this.RECOVERY_WAIT);
|
||
}
|
||
}
|
||
|
||
// ========================================
|
||
// DOM UPDATES
|
||
// ========================================
|
||
|
||
updateDOM(data) {
|
||
this.updateStatusBanner(data.store_status, data.notice_headline, data.notice_subtext, data.today_close, data.next_open);
|
||
this.updateHours(data.hours, data.override_open_today, data.override_close_today);
|
||
this.updateAppointment(data.next_appointment);
|
||
this.updateContact(data.contact);
|
||
}
|
||
|
||
updateStatusBanner(status, headline, subtext, todayClose, nextOpen) {
|
||
const banner = document.getElementById('status-banner');
|
||
const iconEl = document.getElementById('status-icon');
|
||
const headlineEl = document.getElementById('status-headline');
|
||
const subtextEl = document.getElementById('status-subtext');
|
||
|
||
banner.setAttribute('data-status', status);
|
||
|
||
if (status === 'open') {
|
||
iconEl.innerHTML = '✓';
|
||
headlineEl.textContent = 'Geöffnet';
|
||
subtextEl.textContent = todayClose ? `Heute bis ${todayClose} Uhr für Sie da.` : '';
|
||
} else if (status === 'notice') {
|
||
iconEl.innerHTML = '!';
|
||
headlineEl.textContent = headline || 'Hinweis';
|
||
subtextEl.textContent = subtext || '';
|
||
} else if (status === 'warning') {
|
||
iconEl.innerHTML = '!';
|
||
headlineEl.textContent = headline || 'Wichtiger Hinweis';
|
||
subtextEl.textContent = subtext || '';
|
||
} else {
|
||
// closed
|
||
iconEl.innerHTML = '✕';
|
||
headlineEl.textContent = 'Geschlossen';
|
||
if (nextOpen) {
|
||
subtextEl.textContent = `Ab ${nextOpen.label}, ${nextOpen.time} Uhr wieder für Sie da.`;
|
||
} else if (subtext) {
|
||
subtextEl.textContent = subtext;
|
||
} else {
|
||
subtextEl.textContent = '';
|
||
}
|
||
}
|
||
}
|
||
|
||
updateHours(hours, overrideOpen, overrideClose) {
|
||
if (!hours) {
|
||
return;
|
||
}
|
||
|
||
const todayIndex = this.getBerlinDate().getDay();
|
||
const todayKey = this.DAYS[todayIndex];
|
||
|
||
for (const [day, time] of Object.entries(hours)) {
|
||
const el = document.getElementById(`hours-${day}`);
|
||
if (!el) {
|
||
continue;
|
||
}
|
||
|
||
const row = el.closest('.hours-row');
|
||
|
||
// Check if today
|
||
row.classList.remove('today', 'override');
|
||
if (day === todayKey) {
|
||
row.classList.add('today');
|
||
|
||
// Apply override if exists
|
||
if (overrideOpen || overrideClose) {
|
||
row.classList.add('override');
|
||
const openTime = overrideOpen || time.split('–')[0]?.trim() || '';
|
||
const closeTime = overrideClose || time.split('–')[1]?.trim() || '';
|
||
el.textContent = `${openTime} – ${closeTime}`;
|
||
} else {
|
||
el.textContent = time;
|
||
}
|
||
} else {
|
||
el.textContent = time;
|
||
}
|
||
}
|
||
}
|
||
|
||
updateAppointment(appointment) {
|
||
const card = document.getElementById('appointment');
|
||
const dateEl = document.getElementById('appointment-date');
|
||
|
||
if (!appointment || !appointment.date) {
|
||
card.classList.add('hidden');
|
||
return;
|
||
}
|
||
|
||
card.classList.remove('hidden');
|
||
|
||
const date = new Date(appointment.date + 'T00:00:00');
|
||
const dayName = this.DAY_NAMES[new Date(date.toLocaleString('en-US', { timeZone: 'Europe/Berlin' })).getDay()];
|
||
const formatted = date.toLocaleDateString('de-DE', {
|
||
timeZone: 'Europe/Berlin',
|
||
day: '2-digit',
|
||
month: '2-digit',
|
||
year: 'numeric',
|
||
});
|
||
const time = appointment.time || '';
|
||
|
||
dateEl.textContent = `${dayName}, ${formatted}` + (time ? ` – ${time} Uhr` : '');
|
||
}
|
||
|
||
updateContact(contact) {
|
||
if (!contact) {
|
||
return;
|
||
}
|
||
|
||
const phoneEl = document.getElementById('contact-phone');
|
||
const emailEl = document.getElementById('contact-email');
|
||
|
||
if (contact.phone) {
|
||
phoneEl.textContent = contact.phone;
|
||
}
|
||
if (contact.email) {
|
||
emailEl.textContent = contact.email;
|
||
}
|
||
}
|
||
|
||
// ========================================
|
||
// DATE
|
||
// ========================================
|
||
|
||
/**
|
||
* Gibt ein Date-Objekt zurück, dessen .getDay()/.getHours() etc.
|
||
* die Berliner Lokalzeit widerspiegeln – unabhängig von der Systemzeitzone
|
||
* des Geräts (z.B. UTC auf Android-Kiosk-Displays).
|
||
*/
|
||
getBerlinDate() {
|
||
return new Date(new Date().toLocaleString('en-US', { timeZone: 'Europe/Berlin' }));
|
||
}
|
||
|
||
updateDate() {
|
||
const now = new Date();
|
||
const berlinNow = this.getBerlinDate();
|
||
const weekdayEl = document.getElementById('weekday');
|
||
const dateEl = document.getElementById('datestring');
|
||
|
||
weekdayEl.textContent = this.DAY_NAMES[berlinNow.getDay()];
|
||
dateEl.textContent = now.toLocaleDateString('de-DE', {
|
||
timeZone: 'Europe/Berlin',
|
||
day: '2-digit',
|
||
month: '2-digit',
|
||
year: 'numeric'
|
||
});
|
||
}
|
||
|
||
updateLastUpdated() {
|
||
const el = document.getElementById('last-updated');
|
||
if (el) {
|
||
el.textContent = new Date().toLocaleTimeString('de-DE', {
|
||
timeZone: 'Europe/Berlin',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
}) + ' Uhr';
|
||
}
|
||
}
|
||
|
||
scheduleMidnightUpdate() {
|
||
// Mitternacht in Berliner Zeit berechnen, nicht in Geräte-UTC
|
||
const berlinNow = this.getBerlinDate();
|
||
const berlinMidnight = new Date(berlinNow);
|
||
berlinMidnight.setDate(berlinMidnight.getDate() + 1);
|
||
berlinMidnight.setHours(0, 0, 5, 0); // 00:00:05 Berliner Zeit
|
||
|
||
const msUntilMidnight = berlinMidnight.getTime() - berlinNow.getTime();
|
||
|
||
setTimeout(() => {
|
||
console.log('[InfoTablet] Midnight update');
|
||
this.updateDate();
|
||
|
||
// Re-highlight today in hours
|
||
if (this.currentData) {
|
||
this.updateHours(this.currentData.hours, null, null);
|
||
}
|
||
|
||
// Fetch fresh data (overrides may have been reset)
|
||
this.fetchFullStatus();
|
||
|
||
// Schedule next midnight
|
||
this.scheduleMidnightUpdate();
|
||
}, msUntilMidnight);
|
||
}
|
||
|
||
// ========================================
|
||
// QR CODE
|
||
// ========================================
|
||
|
||
generateQR() {
|
||
const size = '200x200';
|
||
const color = '000000';
|
||
const bg = 'ffffff';
|
||
const url = `https://api.qrserver.com/v1/create-qr-code/?size=${size}&color=${color}&bgcolor=${bg}&margin=6&data=${encodeURIComponent(this.QR_TARGET)}`;
|
||
|
||
const img = document.getElementById('qr-code');
|
||
if (img) {
|
||
img.src = url;
|
||
}
|
||
}
|
||
|
||
// ========================================
|
||
// CACHE
|
||
// ========================================
|
||
|
||
saveToCache(data) {
|
||
try {
|
||
localStorage.setItem('cabinet-tablet-data', JSON.stringify(data));
|
||
localStorage.setItem('cabinet-tablet-time', new Date().toISOString());
|
||
} catch (e) {
|
||
// localStorage may be full or unavailable
|
||
}
|
||
}
|
||
|
||
loadFromCache() {
|
||
try {
|
||
const raw = localStorage.getItem('cabinet-tablet-data');
|
||
if (raw) {
|
||
return JSON.parse(raw);
|
||
}
|
||
} catch (e) {
|
||
// Corrupted cache
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// ========================================
|
||
// OFFLINE BADGE
|
||
// ========================================
|
||
|
||
showOfflineBadge() {
|
||
const badge = document.getElementById('offline-badge');
|
||
const timeEl = document.getElementById('offline-time');
|
||
const cachedTime = localStorage.getItem('cabinet-tablet-time');
|
||
|
||
if (cachedTime) {
|
||
const date = new Date(cachedTime);
|
||
timeEl.textContent = date.toLocaleTimeString('de-DE', {
|
||
timeZone: 'Europe/Berlin',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
}) + ' Uhr';
|
||
}
|
||
|
||
badge.classList.add('visible');
|
||
}
|
||
|
||
hideOfflineBadge() {
|
||
document.getElementById('offline-badge').classList.remove('visible');
|
||
}
|
||
|
||
// ========================================
|
||
// AUTO-RELOAD
|
||
// ========================================
|
||
|
||
scheduleAutoReload() {
|
||
setTimeout(() => {
|
||
console.log('[InfoTablet] Auto-reload after 6 hours');
|
||
location.reload();
|
||
}, this.RELOAD_INTERVAL);
|
||
}
|
||
}
|
||
|
||
// Initialize
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const app = new InfoTabletApp();
|
||
app.init();
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|