23-01-2026
34
public/_cabinet/.htaccess.example
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# .htaccess für Cabinet Digital Signage
|
||||
# Umbenennen zu .htaccess um zu aktivieren
|
||||
|
||||
# Passwortschutz für Log-Viewer
|
||||
<Files "view-logs.php">
|
||||
AuthType Basic
|
||||
AuthName "Cabinet Logs - Restricted Access"
|
||||
AuthUserFile /var/www/html/public/_cabinet/.htpasswd
|
||||
Require valid-user
|
||||
</Files>
|
||||
|
||||
# CORS Headers für logger.php (falls nötig)
|
||||
<Files "logger.php">
|
||||
Header set Access-Control-Allow-Origin "*"
|
||||
Header set Access-Control-Allow-Methods "POST, OPTIONS"
|
||||
Header set Access-Control-Allow-Headers "Content-Type"
|
||||
</Files>
|
||||
|
||||
# Logs-Verzeichnis vor direktem Zugriff schützen
|
||||
<DirectoryMatch "^.*/logs/$">
|
||||
Order deny,allow
|
||||
Deny from all
|
||||
</DirectoryMatch>
|
||||
|
||||
# .htpasswd vor Zugriff schützen
|
||||
<Files ".htpasswd">
|
||||
Order allow,deny
|
||||
Deny from all
|
||||
</Files>
|
||||
|
||||
# PHP Error Logging (optional)
|
||||
php_flag display_errors Off
|
||||
php_flag log_errors On
|
||||
php_value error_log /var/www/html/public/_cabinet/logs/php_errors.log
|
||||
480
public/_cabinet/KIOSK_MODE_SETUP.md
Normal file
|
|
@ -0,0 +1,480 @@
|
|||
# Chrome Kiosk-Mode Setup für Android Digital Signage
|
||||
|
||||
## 🎯 Problem
|
||||
|
||||
Bei einem Page-Reload (z.B. nach 6h automatisch) wird der Vollbildmodus beendet - das ist eine Browser-Sicherheitsmaßnahme. Der Vollbildmodus kann nicht automatisch per Script reaktiviert werden.
|
||||
|
||||
## ✅ Die Lösung: Chrome Kiosk-Mode
|
||||
|
||||
Der **Kiosk-Mode** ist die professionelle Lösung für Digital Signage. Chrome läuft dabei permanent im Vollbild ohne Browser-UI.
|
||||
|
||||
---
|
||||
|
||||
## 📱 Option 1: Chrome Kiosk-Mode (Empfohlen für Android)
|
||||
|
||||
### Voraussetzungen:
|
||||
- Android-Gerät (Tablet/Display)
|
||||
- Chrome Browser installiert
|
||||
- ADB (Android Debug Bridge) für Setup
|
||||
|
||||
### Setup-Schritte:
|
||||
|
||||
#### 1. **Developer-Optionen aktivieren**
|
||||
|
||||
1. Gehe zu **Einstellungen** → **Über das Telefon/Tablet**
|
||||
2. Tippe **7x auf "Build-Nummer"**
|
||||
3. Developer-Optionen sind jetzt aktiv
|
||||
|
||||
#### 2. **USB-Debugging aktivieren**
|
||||
|
||||
1. Gehe zu **Einstellungen** → **Entwickleroptionen**
|
||||
2. Aktiviere **"USB-Debugging"**
|
||||
3. Verbinde Gerät per USB mit Computer
|
||||
|
||||
#### 3. **ADB installieren (auf Computer)**
|
||||
|
||||
**Windows:**
|
||||
```bash
|
||||
# Download Android Platform Tools
|
||||
https://developer.android.com/studio/releases/platform-tools
|
||||
|
||||
# Entpacken und zu PATH hinzufügen
|
||||
```
|
||||
|
||||
**macOS:**
|
||||
```bash
|
||||
brew install android-platform-tools
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
```bash
|
||||
sudo apt install android-tools-adb
|
||||
```
|
||||
|
||||
#### 4. **Chrome im Kiosk-Mode starten**
|
||||
|
||||
```bash
|
||||
# Verbindung testen
|
||||
adb devices
|
||||
|
||||
# Chrome beenden
|
||||
adb shell am force-stop com.android.chrome
|
||||
|
||||
# Chrome im Kiosk-Mode starten
|
||||
adb shell am start \
|
||||
-n com.android.chrome/com.google.android.apps.chrome.Main \
|
||||
-a android.intent.action.VIEW \
|
||||
-d "https://cabinet.b2in.eu" \
|
||||
--ez create_new_tab true \
|
||||
--activity-clear-task \
|
||||
--activity-clear-top \
|
||||
--activity-single-top
|
||||
|
||||
# Optional: Vollbild erzwingen
|
||||
adb shell settings put global policy_control immersive.full=*
|
||||
```
|
||||
|
||||
#### 5. **Auto-Start beim Boot (Optional)**
|
||||
|
||||
Erstelle eine Boot-App oder nutze Automate-Apps:
|
||||
|
||||
**Mit MacroDroid (kostenlose App):**
|
||||
1. Installiere MacroDroid aus Play Store
|
||||
2. Erstelle Macro:
|
||||
- **Trigger:** "Device Boot"
|
||||
- **Action:** "Launch Application" → Chrome
|
||||
- **Action:** "Load Webpage" → https://cabinet.b2in.eu
|
||||
3. Speichern und aktivieren
|
||||
|
||||
---
|
||||
|
||||
## 📱 Option 2: Dedicated Kiosk-Browser Apps
|
||||
|
||||
### Empfohlene Apps für Android Digital Signage:
|
||||
|
||||
### **1. Fully Kiosk Browser** (⭐ Empfohlen)
|
||||
|
||||
**Features:**
|
||||
- ✅ Echter Kiosk-Modus (keine UI, kein Zurück-Button)
|
||||
- ✅ Auto-Start beim Boot
|
||||
- ✅ Remote-Management
|
||||
- ✅ Screensaver-Funktion
|
||||
- ✅ Keep Screen On
|
||||
- ✅ Remote-Config via Web-Interface
|
||||
|
||||
**Installation:**
|
||||
```
|
||||
1. Download: https://www.fully-kiosk.com
|
||||
2. Installiere APK auf Android
|
||||
3. Öffne App
|
||||
4. Settings:
|
||||
- Start URL: https://cabinet.b2in.eu
|
||||
- Kiosk Mode: ON
|
||||
- Launch on Boot: ON
|
||||
- Hide System UI: ON
|
||||
- Keep Screen On: ON
|
||||
- Reload on Network Restore: ON
|
||||
5. Lock App (Admin Pin setzen)
|
||||
```
|
||||
|
||||
**Kosten:**
|
||||
- Kostenlos für Single-Device
|
||||
- Plus Version: ~€20 (einmalig) für erweiterte Features
|
||||
|
||||
### **2. Kiosk Browser Lockdown**
|
||||
|
||||
**Features:**
|
||||
- ✅ Einfaches Setup
|
||||
- ✅ Kiosk-Mode
|
||||
- ✅ Auto-Start
|
||||
- ✅ Kostenlos
|
||||
|
||||
**Installation:**
|
||||
```
|
||||
1. Play Store: "Kiosk Browser Lockdown"
|
||||
2. URL setzen: https://cabinet.b2in.eu
|
||||
3. Kiosk Mode aktivieren
|
||||
4. PIN setzen
|
||||
```
|
||||
|
||||
### **3. Chrome mit Custom Launcher**
|
||||
|
||||
**Features:**
|
||||
- ✅ Nutzt Chrome Engine
|
||||
- ✅ Custom Launcher ersetzt Home-Screen
|
||||
- ✅ Kostenlos
|
||||
|
||||
**Apps:**
|
||||
- "Screen On" (Play Store)
|
||||
- "Stay Alive!" (Play Store)
|
||||
- "Screen Alive" (Play Store)
|
||||
|
||||
---
|
||||
|
||||
## 💻 Option 3: Chrome Flags (Desktop/Android)
|
||||
|
||||
### Für Desktop-Testing:
|
||||
|
||||
**Chrome starten mit Flags:**
|
||||
|
||||
**Windows:**
|
||||
```powershell
|
||||
"C:\Program Files\Google\Chrome\Application\chrome.exe" ^
|
||||
--kiosk "https://cabinet.b2in.eu" ^
|
||||
--disable-session-crashed-bubble ^
|
||||
--disable-infobars ^
|
||||
--noerrdialogs ^
|
||||
--disable-translate ^
|
||||
--no-first-run ^
|
||||
--fast-start ^
|
||||
--disable-features=TranslateUI ^
|
||||
--disk-cache-dir=NUL ^
|
||||
--overscroll-history-navigation=0
|
||||
```
|
||||
|
||||
**macOS:**
|
||||
```bash
|
||||
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
|
||||
--kiosk "https://cabinet.b2in.eu" \
|
||||
--disable-session-crashed-bubble \
|
||||
--disable-infobars \
|
||||
--noerrdialogs
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
```bash
|
||||
google-chrome \
|
||||
--kiosk "https://cabinet.b2in.eu" \
|
||||
--disable-session-crashed-bubble \
|
||||
--disable-infobars \
|
||||
--noerrdialogs \
|
||||
--disable-translate \
|
||||
--no-first-run
|
||||
```
|
||||
|
||||
### Flags Erklärung:
|
||||
- `--kiosk` - Vollbild-Modus ohne UI
|
||||
- `--disable-session-crashed-bubble` - Keine "Chrome wurde nicht korrekt beendet" Meldung
|
||||
- `--disable-infobars` - Keine Info-Leisten
|
||||
- `--noerrdialogs` - Keine Error-Dialoge
|
||||
- `--disable-translate` - Keine Übersetzungs-Popups
|
||||
- `--no-first-run` - Kein First-Run-Dialog
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Option 4: Unsere Fallback-Lösung (bereits implementiert)
|
||||
|
||||
### Was haben wir implementiert:
|
||||
|
||||
#### 1. **LocalStorage-Tracking**
|
||||
```javascript
|
||||
// Beim Aktivieren merken
|
||||
localStorage.setItem('cabinet_fullscreen_was_active', 'true');
|
||||
|
||||
// Nach Reload prüfen
|
||||
if (wasFullscreen) {
|
||||
// Auffälliger Reminder anzeigen
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. **Visueller Reminder**
|
||||
- **Orange pulsierender Button** nach Reload
|
||||
- Text: "⚠️ Vollbild aktivieren!"
|
||||
- Automatisch sichtbar wenn Fullscreen vorher aktiv war
|
||||
|
||||
#### 3. **Auto-Retry (30s Delay)**
|
||||
```javascript
|
||||
// Nach 30s automatisch versuchen (falls Kiosk-Mode aktiv)
|
||||
setTimeout(() => {
|
||||
enterFullscreen(); // Wird ignoriert wenn keine User-Geste
|
||||
}, 30000);
|
||||
```
|
||||
|
||||
#### 4. **Logging**
|
||||
```javascript
|
||||
✅ "Fullscreen aktiviert"
|
||||
⚠️ "Fullscreen-Reminder angezeigt"
|
||||
ℹ️ "Fullscreen verlassen"
|
||||
```
|
||||
|
||||
### Vorteile:
|
||||
- ✅ Funktioniert ohne zusätzliche Apps
|
||||
- ✅ Visueller Hinweis dass Fullscreen reaktiviert werden muss
|
||||
- ✅ Nutzer wird "erinnert" nach Reload
|
||||
- ✅ Logging für Monitoring
|
||||
|
||||
### Nachteile:
|
||||
- ❌ Erfordert manuellen Klick nach Reload
|
||||
- ❌ Nicht vollautomatisch
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Empfohlenes Setup für Production
|
||||
|
||||
### Für Android Digital Signage Displays:
|
||||
|
||||
#### **Best Practice:**
|
||||
|
||||
```
|
||||
1. Hardware: Android Tablet/Display
|
||||
└─ Empfohlen: Android 9+ mit min 2GB RAM
|
||||
|
||||
2. Software: Fully Kiosk Browser (Plus)
|
||||
├─ Start URL: https://cabinet.b2in.eu
|
||||
├─ Kiosk Mode: ON
|
||||
├─ Launch on Boot: ON
|
||||
├─ Hide System UI: ON
|
||||
├─ Keep Screen On: ON
|
||||
├─ Reload on Network Restore: ON
|
||||
└─ Remote Management: ON
|
||||
|
||||
3. Network: Kabelgebunden (LAN)
|
||||
└─ Fallback: 5GHz WiFi mit statischer IP
|
||||
|
||||
4. Power: USV/Surge Protection
|
||||
└─ Auto Power-On nach Stromausfall
|
||||
|
||||
5. Monitoring:
|
||||
├─ Fully Kiosk Remote Admin
|
||||
└─ Unsere Logs: view-logs.php
|
||||
```
|
||||
|
||||
### Setup-Checklist:
|
||||
|
||||
- [ ] Android-Gerät vorbereitet
|
||||
- [ ] Fully Kiosk Browser installiert
|
||||
- [ ] Kiosk-Mode konfiguriert
|
||||
- [ ] Auto-Start aktiviert
|
||||
- [ ] System-UI versteckt
|
||||
- [ ] Keep-Screen-On aktiviert
|
||||
- [ ] Remote-Management aktiviert (Optional)
|
||||
- [ ] PIN-Schutz gesetzt
|
||||
- [ ] Display-Helligkeit eingestellt
|
||||
- [ ] Netzwerk getestet (LAN bevorzugt)
|
||||
- [ ] Power-Management konfiguriert
|
||||
- [ ] Test-Lauf 24h durchgeführt
|
||||
- [ ] Monitoring aktiv (Logs checken)
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Troubleshooting
|
||||
|
||||
### Problem: Kiosk-Mode wird nicht aktiviert
|
||||
|
||||
**Lösung 1: Developer-Optionen**
|
||||
```
|
||||
Settings → Developer Options → "Stay Awake" ON
|
||||
```
|
||||
|
||||
**Lösung 2: App-Berechtigungen**
|
||||
```
|
||||
Settings → Apps → Fully Kiosk → Permissions
|
||||
- Display over other apps: ALLOW
|
||||
- Auto-start: ALLOW
|
||||
```
|
||||
|
||||
**Lösung 3: Device Admin**
|
||||
```
|
||||
Settings → Security → Device Administrators
|
||||
- Fully Kiosk Browser: ENABLE
|
||||
```
|
||||
|
||||
### Problem: Display schaltet sich ab
|
||||
|
||||
**Lösung:**
|
||||
```
|
||||
1. Fully Kiosk Settings:
|
||||
- Keep Screen On: ON
|
||||
- Screen Saver: OFF
|
||||
- Prevent Sleep: ON
|
||||
|
||||
2. Android Settings:
|
||||
- Display → Sleep: NEVER
|
||||
- Display → Adaptive Brightness: OFF
|
||||
```
|
||||
|
||||
### Problem: Chrome Exit nach Reload
|
||||
|
||||
**Lösung:**
|
||||
```
|
||||
Nutze Fully Kiosk Browser statt Chrome!
|
||||
- Fully ist speziell für Kiosk designed
|
||||
- Kein ungewolltes Exit möglich
|
||||
- Automatischer Neustart bei Crash
|
||||
```
|
||||
|
||||
### Problem: Zurück-Button verlässt App
|
||||
|
||||
**Lösung:**
|
||||
```
|
||||
Fully Kiosk Settings:
|
||||
- Disable Back Button: ON
|
||||
- Disable Home Button: ON (Device Admin nötig)
|
||||
- Kiosk Mode: Advanced
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Vergleich der Optionen
|
||||
|
||||
| Option | Kosten | Komplexität | Zuverlässigkeit | Empfehlung |
|
||||
|--------|--------|-------------|-----------------|------------|
|
||||
| **Fully Kiosk** | €20 | ⭐⭐ | ⭐⭐⭐⭐⭐ | 🥇 **BEST** |
|
||||
| **Chrome Kiosk (ADB)** | Kostenlos | ⭐⭐⭐⭐ | ⭐⭐⭐ | OK für Tech-Versierte |
|
||||
| **Kiosk Browser Free** | Kostenlos | ⭐⭐ | ⭐⭐⭐ | OK für Testing |
|
||||
| **Unsere Fallback-Lösung** | Kostenlos | ⭐ | ⭐⭐ | Fallback |
|
||||
| **Chrome + Custom Launcher** | Kostenlos | ⭐⭐⭐ | ⭐⭐⭐ | Mittel |
|
||||
|
||||
---
|
||||
|
||||
## 🎬 Quick Start: Fully Kiosk (Empfohlen)
|
||||
|
||||
### 5-Minuten-Setup:
|
||||
|
||||
```bash
|
||||
1. Download: https://www.fully-kiosk.com
|
||||
└─ APK auf Android-Gerät installieren
|
||||
|
||||
2. App öffnen → Settings:
|
||||
Start URL: https://cabinet.b2in.eu
|
||||
|
||||
3. Advanced Settings:
|
||||
[x] Kiosk Mode
|
||||
[x] Launch on Boot
|
||||
[x] Hide System UI
|
||||
[x] Keep Screen On
|
||||
[x] Prevent Sleep
|
||||
|
||||
4. Lock Settings (+ Button):
|
||||
└─ PIN setzen (z.B. 1234)
|
||||
|
||||
5. ✅ Fertig! Display läuft 24/7 im Kiosk-Mode
|
||||
```
|
||||
|
||||
### Remote-Management aktivieren:
|
||||
|
||||
```
|
||||
1. Settings → Remote Administration:
|
||||
[x] Enable Remote Administration
|
||||
[x] Remote Admin from Local Network
|
||||
|
||||
2. Notiere IP-Adresse:
|
||||
z.B. http://192.168.1.100:2323
|
||||
|
||||
3. Öffne vom PC:
|
||||
http://192.168.1.100:2323
|
||||
└─ Password: (Dein PIN)
|
||||
|
||||
4. ✅ Remote-Control aktiv!
|
||||
- Screenshots
|
||||
- Reload
|
||||
- Settings ändern
|
||||
- Screen On/Off
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 Pro-Tipps
|
||||
|
||||
### 1. **Netzwerk-Stabilität**
|
||||
```
|
||||
- LAN bevorzugen (kein WiFi)
|
||||
- Statische IP vergeben
|
||||
- Router/Switch mit QoS
|
||||
- Fully Kiosk: "Reload on Network Restore" ON
|
||||
```
|
||||
|
||||
### 2. **Power-Management**
|
||||
```
|
||||
- USV verwenden
|
||||
- BIOS: "AC Power Recovery" → ON
|
||||
- Android: "Auto Power On" → ON
|
||||
- Fully Kiosk: "Restart on Crash" → ON
|
||||
```
|
||||
|
||||
### 3. **Display-Pflege**
|
||||
```
|
||||
- Bildschirmschoner nach 22:00 Uhr
|
||||
- Helligkeit reduzieren nachts
|
||||
- Pixel-Shift aktivieren (gegen Burn-In)
|
||||
- Display-Timeout: NEVER
|
||||
```
|
||||
|
||||
### 4. **Monitoring**
|
||||
```
|
||||
- Fully Kiosk Remote Admin
|
||||
- Unsere Logs: view-logs.php
|
||||
- Ping-Monitoring (Nagios/Zabbix)
|
||||
- Wöchentliche Checks
|
||||
```
|
||||
|
||||
### 5. **Security**
|
||||
```
|
||||
- Fully Kiosk mit PIN schützen
|
||||
- Device Administrator aktivieren
|
||||
- USB-Debugging OFF (nach Setup)
|
||||
- Unknown Sources OFF
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support
|
||||
|
||||
### Fully Kiosk Support:
|
||||
- Website: https://www.fully-kiosk.com
|
||||
- Forum: https://www.fully-kiosk.com/forum
|
||||
- Email: support@fully-kiosk.com
|
||||
|
||||
### Unsere Logs:
|
||||
- URL: https://cabinet.b2in.eu/view-logs.php
|
||||
- Check: Fullscreen-Events
|
||||
- Monitor: Memory & Errors
|
||||
|
||||
---
|
||||
|
||||
**Empfehlung:** Investiere die €20 für **Fully Kiosk Browser Plus** - es spart viele Stunden Troubleshooting und ist die stabilste Lösung für Digital Signage! 🎯
|
||||
|
||||
---
|
||||
|
||||
**Last Update:** 2026-01-19
|
||||
**Version:** 1.3
|
||||
215
public/_cabinet/LOGGING_README.md
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
# Cabinet Digital Signage - Logging System
|
||||
|
||||
## 📋 Übersicht
|
||||
|
||||
Dieses erweiterte Logging-System ermöglicht es dir, alle Fehler und Events von den Android-Displays remote zu überwachen, ohne physischen Zugriff auf die Geräte zu benötigen.
|
||||
|
||||
## 🎯 Features
|
||||
|
||||
### Automatisches Logging von:
|
||||
- ✅ **JavaScript-Fehler** (Runtime Errors)
|
||||
- ✅ **Unhandled Promise Rejections** (async/await Fehler)
|
||||
- ✅ **Console Errors & Warnings**
|
||||
- ✅ **Resource Loading Failures** (Videos, Bilder)
|
||||
- ✅ **Video Playback Errors** (mit Media Error Codes)
|
||||
- ✅ **Network Status** (Online/Offline Events)
|
||||
- ✅ **Video Stalling** (Buffering-Probleme)
|
||||
- ✅ **Configuration Loading** (API-Fehler)
|
||||
- ✅ **Heartbeat** (alle 5 Minuten - zeigt dass Display läuft)
|
||||
|
||||
### Log-Levels:
|
||||
- `FATAL` - Kritische JavaScript-Fehler
|
||||
- `ERROR` - Fehler (Video-Loading, Network, etc.)
|
||||
- `WARNING` - Warnungen (Buffering, Connection Lost)
|
||||
- `INFO` - Informationen (Heartbeat, Video Started, Config Loaded)
|
||||
|
||||
## 📂 Dateistruktur
|
||||
|
||||
```
|
||||
public/_cabinet/
|
||||
├── index.html # Haupt-Display-Datei (mit Logging)
|
||||
├── logger.php # Backend-Endpoint für Logs
|
||||
├── view-logs.php # Web-Interface zum Ansehen der Logs
|
||||
├── logs/ # Log-Verzeichnis (wird automatisch erstellt)
|
||||
│ ├── all_2026-01-19.log # Alle Logs des Tages
|
||||
│ ├── error_2026-01-19.log # Nur Errors
|
||||
│ ├── fatal_2026-01-19.log # Nur Fatal Errors
|
||||
│ ├── warning_2026-01-19.log # Nur Warnings
|
||||
│ ├── info_2026-01-19.log # Nur Info
|
||||
│ └── json_2026-01-19.log # JSON Format (für Parsing)
|
||||
└── LOGGING_README.md # Diese Datei
|
||||
```
|
||||
|
||||
## 🚀 Setup
|
||||
|
||||
### 1. Logs-Verzeichnis erstellen (falls nicht automatisch erstellt)
|
||||
```bash
|
||||
mkdir -p public/_cabinet/logs
|
||||
chmod 755 public/_cabinet/logs
|
||||
```
|
||||
|
||||
### 2. PHP-Konfiguration (falls nötig)
|
||||
Stelle sicher, dass PHP Schreibrechte auf das `logs/` Verzeichnis hat:
|
||||
```bash
|
||||
chown -R www-data:www-data public/_cabinet/logs
|
||||
# ODER
|
||||
chmod 777 public/_cabinet/logs # Nur für Development!
|
||||
```
|
||||
|
||||
### 3. Logs ansehen
|
||||
Öffne im Browser:
|
||||
```
|
||||
https://cabinet.b2in.eu/view-logs.php
|
||||
```
|
||||
|
||||
## 📊 Log-Viewer Features
|
||||
|
||||
Der `view-logs.php` bietet:
|
||||
- 📈 **Statistiken** (Anzahl Fatal/Error/Warning/Info)
|
||||
- 🎨 **Farbcodierung** nach Log-Level
|
||||
- 🔍 **Dateiauswahl** (verschiedene Log-Dateien)
|
||||
- ⚡ **Auto-Refresh** (alle 10 Sekunden)
|
||||
- 📏 **Zeilenanzahl** wählbar (50/100/500/1000/alle)
|
||||
|
||||
## 🔒 Sicherheit
|
||||
|
||||
**WICHTIG:** Der Log-Viewer sollte in Produktion geschützt werden!
|
||||
|
||||
### Option 1: .htaccess Passwortschutz
|
||||
```apache
|
||||
# In public/_cabinet/.htaccess
|
||||
<Files "view-logs.php">
|
||||
AuthType Basic
|
||||
AuthName "Restricted Access"
|
||||
AuthUserFile /pfad/zu/.htpasswd
|
||||
Require valid-user
|
||||
</Files>
|
||||
```
|
||||
|
||||
### Option 2: PHP Session-basiert
|
||||
Füge am Anfang von `view-logs.php` hinzu:
|
||||
```php
|
||||
<?php
|
||||
session_start();
|
||||
if (!isset($_SESSION['logged_in']) || $_SESSION['logged_in'] !== true) {
|
||||
// Login-Formular oder Redirect
|
||||
header('Location: /login.php');
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
```
|
||||
|
||||
## 📝 Kontext-Informationen
|
||||
|
||||
Jeder Log-Eintrag enthält automatisch:
|
||||
- **Timestamp** (ISO 8601)
|
||||
- **IP-Adresse** des Displays
|
||||
- **User Agent** (Browser/OS Info)
|
||||
- **Viewport** (Display-Auflösung)
|
||||
- **Connection Status** (online/offline)
|
||||
- **Aktuelles Video** (Dateiname)
|
||||
- **Video Index** (Position in Playlist)
|
||||
- **Footer Index** (Aktueller Footer-Content)
|
||||
- **Playlist-Länge** (Anzahl Videos)
|
||||
|
||||
## 🎮 Manuelle Logs
|
||||
|
||||
Du kannst auch manuell Logs aus dem JavaScript senden:
|
||||
|
||||
```javascript
|
||||
// Info Log
|
||||
window.displayLogger.log('Meine Info-Nachricht', {
|
||||
customData: 'wert'
|
||||
});
|
||||
|
||||
// Warning
|
||||
window.displayLogger.warn('Warnung', {
|
||||
reason: 'Irgendwas ist komisch'
|
||||
});
|
||||
|
||||
// Error
|
||||
window.displayLogger.error('Fehler aufgetreten', {
|
||||
errorCode: 123
|
||||
});
|
||||
|
||||
// Kontext setzen
|
||||
window.displayLogger.setContext('customKey', 'customValue');
|
||||
```
|
||||
|
||||
## 🔄 Log-Rotation
|
||||
|
||||
Logs werden automatisch rotiert:
|
||||
- Separate Dateien pro Tag
|
||||
- Automatische Löschung von Logs älter als 30 Tage
|
||||
- Verschiedene Dateien pro Log-Level
|
||||
|
||||
## 📱 Debugging-Workflow
|
||||
|
||||
1. **Problem entdeckt** → Öffne `view-logs.php`
|
||||
2. **Wähle Log-Datei** → z.B. `error_2026-01-19.log`
|
||||
3. **Schaue Statistiken** → Wie viele Fehler pro Level?
|
||||
4. **Analysiere Logs** → Welches Video? Welcher Footer? Welche IP?
|
||||
5. **Problem beheben** → Update CMS oder Code
|
||||
6. **Monitoring** → Aktiviere Auto-Refresh zum Live-Monitoring
|
||||
|
||||
## 🐛 Häufige Fehlertypen
|
||||
|
||||
### MEDIA_ERR_NETWORK (Code 2)
|
||||
- **Ursache:** Netzwerkprobleme beim Video-Laden
|
||||
- **Lösung:** Prüfe Internetverbindung, Video-URL, Server-Erreichbarkeit
|
||||
|
||||
### MEDIA_ERR_DECODE (Code 3)
|
||||
- **Ursache:** Video-Codec nicht unterstützt oder Datei korrupt
|
||||
- **Lösung:** Video neu encodieren (H.264, AAC)
|
||||
|
||||
### MEDIA_ERR_SRC_NOT_SUPPORTED (Code 4)
|
||||
- **Ursache:** Video-Format nicht unterstützt
|
||||
- **Lösung:** Format zu MP4/WebM ändern
|
||||
|
||||
### Promise Rejection
|
||||
- **Ursache:** Async-Fehler (meist API-Calls)
|
||||
- **Lösung:** Prüfe API-Endpoint, CORS-Settings
|
||||
|
||||
### Stalled Video
|
||||
- **Ursache:** Buffering-Probleme
|
||||
- **Lösung:** Video-Bitrate reduzieren, Netzwerk prüfen
|
||||
|
||||
## 💡 Best Practices
|
||||
|
||||
1. **Regelmäßig Logs checken** - Mindestens 1x pro Woche
|
||||
2. **Statistiken beachten** - Viele Errors/Warnings? → Aktion nötig
|
||||
3. **Heartbeat überwachen** - Kommt alle 5 Min? Display läuft
|
||||
4. **JSON-Logs für Analyse** - Nutze `json_*.log` für automatische Auswertung
|
||||
5. **Log-Level richtig nutzen**:
|
||||
- `FATAL` → Sofort reagieren
|
||||
- `ERROR` → Zeitnah prüfen
|
||||
- `WARNING` → Im Auge behalten
|
||||
- `INFO` → Für Debugging
|
||||
|
||||
## 🔗 Integration mit Monitoring-Tools
|
||||
|
||||
Die JSON-Logs können einfach in Monitoring-Tools integriert werden:
|
||||
|
||||
```bash
|
||||
# Alle Fatal Errors der letzten 24h
|
||||
grep '"level":"FATAL"' public/_cabinet/logs/json_*.log
|
||||
|
||||
# Errors nach IP gruppieren
|
||||
jq -r '.ip' public/_cabinet/logs/json_*.log | sort | uniq -c
|
||||
|
||||
# Häufigste Fehler
|
||||
jq -r '.message' public/_cabinet/logs/json_*.log | sort | uniq -c | sort -rn
|
||||
```
|
||||
|
||||
## 📞 Support
|
||||
|
||||
Bei Fragen oder Problemen:
|
||||
1. Prüfe die Logs in `view-logs.php`
|
||||
2. Schaue dir die Kontext-Informationen an
|
||||
3. Prüfe ob andere Displays gleichen Fehler haben
|
||||
4. Kontaktiere den Support mit Log-Auszug
|
||||
|
||||
---
|
||||
|
||||
**Version:** 1.2
|
||||
**Letztes Update:** 2026-01-19
|
||||
292
public/_cabinet/QUICK_START.md
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
# Cabinet Digital Signage - Quick Start Guide
|
||||
|
||||
## 🚀 Schnellstart nach Video-Optimierung
|
||||
|
||||
### Was wurde geändert?
|
||||
|
||||
Die Display-Software wurde **massiv optimiert** um das Problem mit schwarzen Bildschirmen nach längerer Laufzeit zu beheben.
|
||||
|
||||
### ✅ Version 1.3 Features:
|
||||
|
||||
1. **Video-Cleanup** - Speicher wird nach jedem Video freigegeben
|
||||
2. **Watchdog** - Überwacht ob Videos laufen und recovered automatisch
|
||||
3. **Start-Timeout** - Videos die nicht starten werden übersprungen
|
||||
4. **Error Recovery** - Automatischer Skip bei defekten Videos
|
||||
5. **Memory-Monitoring** - Speicherüberwachung alle 10 Minuten
|
||||
6. **Präventiver Reload** - Automatischer Neustart alle 6 Stunden
|
||||
7. **Performance-Boost** - CSS Hardware-Beschleunigung aktiviert
|
||||
|
||||
---
|
||||
|
||||
## 📱 Display Setup
|
||||
|
||||
### 1. Display aufrufen
|
||||
```
|
||||
https://cabinet.b2in.eu
|
||||
```
|
||||
|
||||
### 2. Vollbild aktivieren
|
||||
- Klicke auf **"V 1.3"** Button oben links
|
||||
- Display wechselt in Vollbildmodus
|
||||
- Button verschwindet automatisch
|
||||
|
||||
⚠️ **WICHTIG:** Bei einem Page-Reload (nach 6h oder bei Fehlern) wird der Vollbildmodus beendet. Du siehst dann einen **orange pulsierenden Button** mit "⚠️ Vollbild aktivieren!". Einfach erneut klicken.
|
||||
|
||||
💡 **Besser:** Nutze **Fully Kiosk Browser** für permanenten Vollbild ohne manuelles Klicken. Siehe `KIOSK_MODE_SETUP.md`
|
||||
|
||||
### 3. Laufen lassen
|
||||
- Display läuft jetzt automatisch 24/7
|
||||
- Automatischer Reload alle 6 Stunden
|
||||
- Watchdog überwacht Video-Playback
|
||||
- Bei Problemen: Automatische Self-Recovery
|
||||
|
||||
---
|
||||
|
||||
## 📊 Monitoring
|
||||
|
||||
### Log-Viewer öffnen:
|
||||
```
|
||||
https://cabinet.b2in.eu/view-logs.php
|
||||
```
|
||||
|
||||
### Was solltest du sehen:
|
||||
|
||||
#### ✅ Normale Logs (alles OK):
|
||||
```
|
||||
[INFO] Video started: video1.mp4
|
||||
[INFO] Video cleanup durchgeführt
|
||||
[INFO] Heartbeat - Display is running
|
||||
[INFO] Memory Status: 45% (230MB / 512MB)
|
||||
[INFO] Playlist-Loop abgeschlossen
|
||||
```
|
||||
|
||||
#### ⚠️ Warnungen (beobachten):
|
||||
```
|
||||
[WARNING] Video stalled (buffering)
|
||||
[WARNING] Hohe Speicherauslastung (85%)
|
||||
[WARNING] Überspringe zum nächsten Video
|
||||
```
|
||||
|
||||
#### ❌ Fehler (Action nötig):
|
||||
```
|
||||
[ERROR] Video start timeout
|
||||
[ERROR] Video Error: MEDIA_ERR_NETWORK
|
||||
[ERROR] Kritischer Zustand erkannt
|
||||
[FATAL] JavaScript Error
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎬 Video-Upload Checklist
|
||||
|
||||
Beim Hochladen neuer Videos beachten:
|
||||
|
||||
### ✅ Must-Have:
|
||||
- [ ] Format: **MP4** (H.264 + AAC)
|
||||
- [ ] Auflösung: **Max 1920x1080**
|
||||
- [ ] Bitrate: **5-10 Mbps**
|
||||
- [ ] Dateigröße: **Max 100 MB**
|
||||
- [ ] Länge: **15-60 Sekunden** (optimal)
|
||||
|
||||
### ⚠️ Vermeiden:
|
||||
- ❌ Zu große Dateien (>100MB)
|
||||
- ❌ Zu hohe Bitrate (>10 Mbps)
|
||||
- ❌ Zu lange Videos (>3 Min)
|
||||
- ❌ Exotische Formate (MOV, AVI, WMV)
|
||||
- ❌ 4K Videos (overkill für Display)
|
||||
|
||||
### 🔧 Video optimieren (FFmpeg):
|
||||
```bash
|
||||
ffmpeg -i input.mp4 \
|
||||
-c:v libx264 -preset slow -crf 23 \
|
||||
-c:a aac -b:a 128k \
|
||||
-vf scale=1920:1080 \
|
||||
-movflags +faststart \
|
||||
output.mp4
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Problem: Schwarzer Bildschirm
|
||||
|
||||
#### Schritt 1: Logs checken
|
||||
```
|
||||
→ Öffne view-logs.php
|
||||
→ Schaue nach ERROR oder FATAL Logs
|
||||
```
|
||||
|
||||
#### Schritt 2: Was sagt das Log?
|
||||
|
||||
**"Video start timeout"**
|
||||
- Video lädt nicht → Check Video-URL
|
||||
- Video zu groß → Komprimieren
|
||||
- Netzwerkproblem → Check Internet
|
||||
|
||||
**"Video Error: MEDIA_ERR_NETWORK"**
|
||||
- Netzwerk-Issue → Check Router/Internet
|
||||
- Server down → Check b2in.eu erreichbar
|
||||
|
||||
**"Video definitiv stuck"**
|
||||
- ✅ Watchdog hat recovered!
|
||||
- Video wurde übersprungen
|
||||
- Nächstes Video sollte laufen
|
||||
|
||||
**"Hohe Speicherauslastung"**
|
||||
- Videos zu groß → Komprimieren
|
||||
- Zu viele Videos → Playlist verkleinern
|
||||
- Warte auf automatischen Reload (alle 6h)
|
||||
|
||||
#### Schritt 3: Manuelle Actions
|
||||
|
||||
**Display neu laden:**
|
||||
```javascript
|
||||
// In Browser-Console (F12):
|
||||
location.reload();
|
||||
```
|
||||
|
||||
**Display komplett neustarten:**
|
||||
```
|
||||
1. Browser schließen
|
||||
2. Warten 10 Sekunden
|
||||
3. Browser neu öffnen
|
||||
4. URL aufrufen
|
||||
5. Vollbild aktivieren
|
||||
```
|
||||
|
||||
### Problem: Video buffert ständig
|
||||
|
||||
#### Ursachen:
|
||||
- Internetverbindung zu langsam
|
||||
- Video-Bitrate zu hoch
|
||||
- Netzwerk überlastet
|
||||
|
||||
#### Lösung:
|
||||
1. **Check Internet:** Speedtest machen
|
||||
2. **Videos optimieren:** Bitrate reduzieren (5 Mbps)
|
||||
3. **Playlist reduzieren:** Weniger Videos = weniger Daten
|
||||
4. **Router prüfen:** Neustart? Kabel OK?
|
||||
|
||||
### Problem: Footer läuft, Video nicht
|
||||
|
||||
#### Das war das Haupt-Problem! Jetzt gefixt durch:
|
||||
- ✅ Video-Cleanup nach jedem Video
|
||||
- ✅ Watchdog erkennt stuck Videos
|
||||
- ✅ Automatischer Skip/Recovery
|
||||
- ✅ Memory-Management
|
||||
- ✅ Präventiver Reload alle 6h
|
||||
|
||||
#### Falls es DOCH noch passiert:
|
||||
```
|
||||
1. Logs checken: view-logs.php
|
||||
2. Memory-Status prüfen
|
||||
3. Watchdog-Logs suchen
|
||||
4. Falls >3 Fehler: Display reload automatisch
|
||||
5. Falls nicht: Manuell reloaden
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support
|
||||
|
||||
### Bei Problemen:
|
||||
|
||||
1. **Logs sichern:**
|
||||
- Öffne view-logs.php
|
||||
- Download/Screenshot der Fehler
|
||||
- Besonders ERROR und FATAL Logs
|
||||
|
||||
2. **Info sammeln:**
|
||||
- Welches Display (IP/Standort)?
|
||||
- Wann trat Problem auf?
|
||||
- Was zeigen die Logs?
|
||||
- Memory-Status?
|
||||
|
||||
3. **Kontakt:**
|
||||
- Mit Log-Auszug
|
||||
- Screenshots
|
||||
- Display-Info
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Monitoring-Routine
|
||||
|
||||
### Täglich (optional):
|
||||
- [ ] Kurzer Blick auf Display (läuft es?)
|
||||
- [ ] Bei Problemen: Logs checken
|
||||
|
||||
### Wöchentlich:
|
||||
- [ ] Logs checken (view-logs.php)
|
||||
- [ ] Statistiken ansehen (Fatal/Error/Warning)
|
||||
- [ ] Memory-Status prüfen
|
||||
- [ ] Watchdog-Interventionen zählen
|
||||
|
||||
### Monatlich:
|
||||
- [ ] Alte Logs aufräumen (>30 Tage automatisch)
|
||||
- [ ] Video-Performance überprüfen
|
||||
- [ ] Playlist aktualisieren
|
||||
- [ ] Display-Uptime checken
|
||||
|
||||
---
|
||||
|
||||
## 💡 Tipps & Tricks
|
||||
|
||||
### Performance optimieren:
|
||||
- Halte Playlist klein (5-10 Videos)
|
||||
- Optimiere Videos vor Upload
|
||||
- Nutze konsistente Video-Auflösung
|
||||
- Vermeide sehr lange Videos (>2 Min)
|
||||
|
||||
### Zuverlässigkeit erhöhen:
|
||||
- Lass präventiven Reload aktiv (6h)
|
||||
- Check Logs wöchentlich
|
||||
- Halte Internet-Verbindung stabil
|
||||
- Nutze kabelgebundenes Netzwerk statt WiFi
|
||||
|
||||
### Memory sparen:
|
||||
- Videos komprimieren (H.264, CRF 23)
|
||||
- Playlist auf 10 Videos limitieren
|
||||
- Preload='metadata' (bereits aktiv)
|
||||
- Automatischer Cleanup (bereits aktiv)
|
||||
|
||||
---
|
||||
|
||||
## ✨ Neue Features nutzen
|
||||
|
||||
### Auto-Recovery:
|
||||
```
|
||||
Display recovered jetzt automatisch von:
|
||||
✅ Stuck Videos (Watchdog)
|
||||
✅ Video-Ladefehlern (Skip)
|
||||
✅ Start-Timeouts (Skip)
|
||||
✅ Memory-Problemen (Reload nach 6h)
|
||||
✅ Kritischen Fehlern (Reload)
|
||||
```
|
||||
|
||||
### Logging:
|
||||
```
|
||||
Alle Events werden geloggt:
|
||||
📊 Video-Start/Ende
|
||||
📊 Memory-Status
|
||||
📊 Fehler und Warnungen
|
||||
📊 Watchdog-Interventionen
|
||||
📊 Heartbeats (Display läuft)
|
||||
```
|
||||
|
||||
### Monitoring:
|
||||
```
|
||||
Live-Überwachung möglich:
|
||||
🔍 view-logs.php
|
||||
🔍 Auto-Refresh alle 10s
|
||||
🔍 Statistiken (Fatal/Error/Warning)
|
||||
🔍 Farbcodierung nach Schwere
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Version:** 1.3
|
||||
**Release:** 2026-01-19
|
||||
**Status:** ✅ Production Ready
|
||||
|
||||
🎉 **Viel Erfolg mit dem optimierten Display!**
|
||||
368
public/_cabinet/VIDEO_OPTIMIZATION_README.md
Normal file
|
|
@ -0,0 +1,368 @@
|
|||
# Cabinet Digital Signage - Video-Optimierung & Robustness
|
||||
|
||||
## 🔧 Problem
|
||||
|
||||
Nach einer gewissen Laufzeit wurden die Videos nicht mehr angezeigt (schwarzer Bildschirm), obwohl der Footer weiterhin funktionierte. Dies deutet auf Memory-Leaks oder einen fehlerhaften Video-Loop hin.
|
||||
|
||||
## ✅ Implementierte Lösungen
|
||||
|
||||
### 1. **Robuster Video-Cleanup** (Memory-Management)
|
||||
|
||||
#### Was wurde gemacht:
|
||||
- **Explizites Video-Cleanup** vor jedem neuen Video:
|
||||
```javascript
|
||||
videoElement.pause();
|
||||
videoElement.removeAttribute('src');
|
||||
videoElement.load(); // Triggert Garbage Collection
|
||||
```
|
||||
- **100ms Delay** nach Cleanup, bevor neues Video geladen wird
|
||||
- **Preload auf 'metadata'** gesetzt (statt 'auto') → weniger Memory-Verbrauch
|
||||
|
||||
#### Warum das hilft:
|
||||
- Browser gibt Speicher des alten Videos frei
|
||||
- Verhindert Memory-Leaks bei langen Laufzeiten
|
||||
- Reduziert gleichzeitig geladene Video-Daten
|
||||
|
||||
### 2. **Video Watchdog** (Überwachung)
|
||||
|
||||
#### Was wurde gemacht:
|
||||
- **Watchdog läuft alle 5 Sekunden**
|
||||
- Prüft ob Video noch läuft (vergleicht `currentTime`)
|
||||
- Erkennt wenn Video "stecken bleibt"
|
||||
- Automatischer Skip zum nächsten Video nach 2x "stuck"
|
||||
|
||||
#### Was wird überwacht:
|
||||
```javascript
|
||||
- currentTime (bewegt sich das Video?)
|
||||
- isPaused (ist Video pausiert?)
|
||||
- hasEnded (ist Video zu Ende?)
|
||||
- isStuck (currentTime ändert sich nicht)
|
||||
```
|
||||
|
||||
#### Warum das hilft:
|
||||
- Erkennt frozen Videos automatisch
|
||||
- Verhindert dass Display hängen bleibt
|
||||
- Selbstheilende Funktion ohne manuellen Eingriff
|
||||
|
||||
### 3. **Start-Timeout** (10 Sekunden)
|
||||
|
||||
#### Was wurde gemacht:
|
||||
- Timeout von 10 Sekunden für Video-Start
|
||||
- Falls Video nicht innerhalb von 10s startet → Skip zum nächsten
|
||||
- Timeout wird geclearet wenn Video erfolgreich startet
|
||||
|
||||
#### Warum das hilft:
|
||||
- Verhindert endloses Warten bei defekten Videos
|
||||
- Display bleibt nicht schwarz wenn Video nicht lädt
|
||||
- Automatische Recovery
|
||||
|
||||
### 4. **Error Recovery** (Fehlerbehandlung)
|
||||
|
||||
#### Was wurde gemacht:
|
||||
- **Automatischer Skip** bei Video-Fehlern
|
||||
- **Consecutive Error Tracking** (zählt aufeinanderfolgende Fehler)
|
||||
- **Max 3 aufeinanderfolgende Fehler** → dann Page-Reload nach 30s
|
||||
- **Error-Logging** mit detaillierten Media Error Codes
|
||||
|
||||
#### Error Codes:
|
||||
- `MEDIA_ERR_ABORTED` (1) - Video-Laden abgebrochen
|
||||
- `MEDIA_ERR_NETWORK` (2) - Netzwerkfehler
|
||||
- `MEDIA_ERR_DECODE` (3) - Dekodierungsfehler
|
||||
- `MEDIA_ERR_SRC_NOT_SUPPORTED` (4) - Format nicht unterstützt
|
||||
|
||||
#### Warum das hilft:
|
||||
- Display recovered automatisch von Fehlern
|
||||
- Verhindert dass ein defektes Video alles blockiert
|
||||
- Bei wiederholten Problemen: Komplett-Neustart
|
||||
|
||||
### 5. **Memory-Monitoring** (Performance-Überwachung)
|
||||
|
||||
#### Was wurde gemacht:
|
||||
- **Memory-Check alle 10 Minuten**
|
||||
- Loggt Speicherverbrauch in MB und Prozent
|
||||
- **Warnung bei >80% Speicherauslastung**
|
||||
- Loggt Video-Buffer-Status
|
||||
|
||||
#### Beispiel-Log:
|
||||
```json
|
||||
{
|
||||
"message": "Memory Status",
|
||||
"context": {
|
||||
"usedMB": 245,
|
||||
"limitMB": 512,
|
||||
"percentUsed": 48
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Warum das hilft:
|
||||
- Frühzeitiges Erkennen von Memory-Problemen
|
||||
- Daten für Troubleshooting und Optimierung
|
||||
- Proaktives Monitoring statt reaktives Debugging
|
||||
|
||||
### 6. **Präventive Maßnahmen**
|
||||
|
||||
#### A) Präventiver Page-Reload (alle 6 Stunden)
|
||||
```javascript
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 6 * 60 * 60 * 1000);
|
||||
```
|
||||
- Verhindert Memory-Leaks über sehr lange Laufzeit
|
||||
- Fresh Start alle 6 Stunden
|
||||
- Erfolgt automatisch im Hintergrund
|
||||
|
||||
#### B) Critical Error Check (alle 30 Sekunden)
|
||||
- Überwacht kritische Zustände
|
||||
- Bei 3 kritischen Fehlern → Reload nach 5s
|
||||
- Selbstheilende Funktion
|
||||
|
||||
#### C) CSS Performance-Optimierungen
|
||||
```css
|
||||
#video-player {
|
||||
will-change: transform;
|
||||
transform: translateZ(0);
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
```
|
||||
- Aktiviert Hardware-Beschleunigung
|
||||
- Reduziert Rendering-Last
|
||||
- Optimiert für Video-Playback
|
||||
|
||||
### 7. **Erweiterte Video-Events**
|
||||
|
||||
#### Neue Events die geloggt werden:
|
||||
- `ended` - Video zu Ende
|
||||
- `error` - Video-Fehler (mit Error Code)
|
||||
- `stalled` - Buffering (Video hängt)
|
||||
- `waiting` - Waiting for data
|
||||
- `playing` - Video spielt ab
|
||||
- `canplay` - Video kann abgespielt werden
|
||||
|
||||
#### Warum das hilft:
|
||||
- Vollständige Transparenz über Video-Status
|
||||
- Erkennen von Buffering-Problemen
|
||||
- Debuggen von Playback-Issues
|
||||
|
||||
## 📊 Monitoring & Debugging
|
||||
|
||||
### Was wird jetzt geloggt:
|
||||
|
||||
#### Erfolgreiche Events:
|
||||
```
|
||||
✅ Video cleanup durchgeführt
|
||||
✅ Video started: video1.mp4
|
||||
✅ Video läuft wieder normal
|
||||
✅ Playlist-Loop abgeschlossen, starte von vorne
|
||||
```
|
||||
|
||||
#### Warnungen:
|
||||
```
|
||||
⚠️ Video scheint stecken geblieben zu sein
|
||||
⚠️ Hohe Speicherauslastung (85%)
|
||||
⚠️ Video stalled (buffering)
|
||||
⚠️ Überspringe zum nächsten Video
|
||||
```
|
||||
|
||||
#### Fehler:
|
||||
```
|
||||
❌ Video start timeout (10s überschritten)
|
||||
❌ Video definitiv stuck - starte nächstes
|
||||
❌ Video Error: MEDIA_ERR_NETWORK
|
||||
❌ Kritischer Zustand erkannt
|
||||
```
|
||||
|
||||
### Log-Analyse:
|
||||
|
||||
#### Beispiel 1: Memory-Problem
|
||||
```
|
||||
[INFO] Memory Status: 420MB / 512MB (82%)
|
||||
[WARNING] Hohe Speicherauslastung
|
||||
→ Action: Beobachten, evtl. Videos optimieren
|
||||
```
|
||||
|
||||
#### Beispiel 2: Stuck Video
|
||||
```
|
||||
[WARNING] Video scheint stecken geblieben (2x)
|
||||
[ERROR] Video definitiv stuck - starte nächstes
|
||||
[WARNING] Überspringe zum nächsten Video: watchdog_stuck
|
||||
→ Action: Watchdog hat Recovery durchgeführt ✓
|
||||
```
|
||||
|
||||
#### Beispiel 3: Netzwerkprobleme
|
||||
```
|
||||
[ERROR] Video Error: MEDIA_ERR_NETWORK
|
||||
[WARNING] Überspringe zum nächsten Video: error_MEDIA_ERR_NETWORK
|
||||
[INFO] Video started: video2.mp4
|
||||
→ Action: Netzwerk kurz unterbrochen, automatisch recovered ✓
|
||||
```
|
||||
|
||||
## 🎯 Best Practices für Videos
|
||||
|
||||
### 1. **Video-Format**
|
||||
- **Container:** MP4 (H.264 + AAC)
|
||||
- **Codec:** H.264 (High Profile, Level 4.0)
|
||||
- **Audio:** AAC, 128-256 kbps
|
||||
- **Auflösung:** Max 1920x1080 (Full HD)
|
||||
- **Framerate:** 25 oder 30 fps
|
||||
- **Bitrate:** 5-10 Mbps (nicht höher!)
|
||||
|
||||
### 2. **Video-Länge**
|
||||
- **Optimal:** 15-60 Sekunden
|
||||
- **Maximum:** 2-3 Minuten
|
||||
- **Warum:** Kürzere Videos = weniger Memory-Verbrauch
|
||||
|
||||
### 3. **Dateigrößen**
|
||||
- **Optimal:** 10-50 MB pro Video
|
||||
- **Maximum:** 100 MB pro Video
|
||||
- **Warum:** Schnelleres Laden, weniger Buffering
|
||||
|
||||
### 4. **Playlist-Größe**
|
||||
- **Optimal:** 5-10 Videos
|
||||
- **Maximum:** 20 Videos
|
||||
- **Warum:** Übersichtlich, nicht zu viel Content im Loop
|
||||
|
||||
### 5. **Video-Optimierung**
|
||||
Nutze Tools wie:
|
||||
- **FFmpeg** für Re-Encoding
|
||||
- **HandBrake** für Kompression
|
||||
- **Adobe Media Encoder** für Profis
|
||||
|
||||
#### FFmpeg Beispiel:
|
||||
```bash
|
||||
ffmpeg -i input.mp4 \
|
||||
-c:v libx264 -preset slow -crf 23 \
|
||||
-c:a aac -b:a 128k \
|
||||
-vf scale=1920:1080 \
|
||||
-movflags +faststart \
|
||||
output.mp4
|
||||
```
|
||||
|
||||
## 🔍 Troubleshooting
|
||||
|
||||
### Problem: Videos werden nach einiger Zeit schwarz
|
||||
|
||||
#### Mögliche Ursachen:
|
||||
1. **Memory-Leak** → Check Memory-Logs
|
||||
2. **Video zu groß** → Komprimieren
|
||||
3. **Netzwerkprobleme** → Check Network-Logs
|
||||
4. **Browser-Cache voll** → Wird jetzt automatisch gecleart
|
||||
|
||||
#### Lösung:
|
||||
- ✅ Implementiert: Automatischer Cleanup
|
||||
- ✅ Implementiert: Watchdog erkennt Problem
|
||||
- ✅ Implementiert: Automatischer Skip/Recovery
|
||||
- ✅ Implementiert: Präventiver Reload nach 6h
|
||||
|
||||
### Problem: Video startet nicht
|
||||
|
||||
#### Check in Logs:
|
||||
```
|
||||
[ERROR] Video start timeout
|
||||
[ERROR] Video play failed: MEDIA_ERR_SRC_NOT_SUPPORTED
|
||||
```
|
||||
|
||||
#### Lösung:
|
||||
1. **Video-Format prüfen** (MP4 H.264?)
|
||||
2. **Video-Pfad prüfen** (erreichbar?)
|
||||
3. **Video neu encodieren**
|
||||
4. **Watchdog springt automatisch zum nächsten**
|
||||
|
||||
### Problem: Video buffert ständig
|
||||
|
||||
#### Check in Logs:
|
||||
```
|
||||
[WARNING] Video stalled (buffering)
|
||||
[WARNING] Video waiting (buffering)
|
||||
```
|
||||
|
||||
#### Lösung:
|
||||
1. **Netzwerkverbindung prüfen**
|
||||
2. **Video-Bitrate reduzieren**
|
||||
3. **Preload auf 'metadata'** (bereits implementiert)
|
||||
4. **Video komprimieren**
|
||||
|
||||
### Problem: Hohe Speicherauslastung
|
||||
|
||||
#### Check in Logs:
|
||||
```
|
||||
[WARNING] Hohe Speicherauslastung (85%)
|
||||
```
|
||||
|
||||
#### Lösung:
|
||||
1. **Videos komprimieren**
|
||||
2. **Playlist verkleinern**
|
||||
3. **Präventiver Reload** (bereits aktiv nach 6h)
|
||||
4. **Browser-Cache leeren** (manuell)
|
||||
|
||||
## 📈 Performance-Metriken
|
||||
|
||||
### Empfohlene Werte:
|
||||
- **Memory Usage:** < 70% des Heap-Limits
|
||||
- **Video Start Time:** < 2 Sekunden
|
||||
- **Buffering Events:** < 1 pro Stunde
|
||||
- **Consecutive Errors:** 0
|
||||
- **Watchdog Interventions:** < 1 pro Tag
|
||||
|
||||
### Critical Werte (Action required):
|
||||
- **Memory Usage:** > 85%
|
||||
- **Video Start Time:** > 10 Sekunden (Timeout!)
|
||||
- **Buffering Events:** > 5 pro Stunde
|
||||
- **Consecutive Errors:** ≥ 3
|
||||
- **Watchdog Interventions:** > 10 pro Tag
|
||||
|
||||
## 🚀 Testing
|
||||
|
||||
### 1. **Test im Browser**
|
||||
```
|
||||
1. Öffne https://cabinet.b2in.eu
|
||||
2. Öffne Developer Tools (F12)
|
||||
3. Console Tab öffnen
|
||||
4. Logs beobachten:
|
||||
- "Video cleanup durchgeführt"
|
||||
- "Video started: ..."
|
||||
- Memory-Status nach 30s
|
||||
```
|
||||
|
||||
### 2. **Stress-Test**
|
||||
```
|
||||
1. Lass Display 24h laufen
|
||||
2. Check Logs auf Probleme
|
||||
3. Memory-Status nach 24h prüfen
|
||||
4. Watchdog-Interventionen zählen
|
||||
```
|
||||
|
||||
### 3. **Network-Test**
|
||||
```
|
||||
1. Simuliere schlechte Verbindung (DevTools → Network → Throttling)
|
||||
2. Beobachte Error-Recovery
|
||||
3. Check ob automatischer Skip funktioniert
|
||||
```
|
||||
|
||||
### 4. **Memory-Test**
|
||||
```
|
||||
1. Lass Display 6h laufen
|
||||
2. Check Memory-Logs alle 10 Min
|
||||
3. Sollte < 70% bleiben
|
||||
4. Nach 6h: automatischer Reload
|
||||
```
|
||||
|
||||
## 📋 Changelog
|
||||
|
||||
### Version 1.3 (2026-01-19)
|
||||
- ✅ Video-Cleanup vor jedem neuen Video
|
||||
- ✅ Video Watchdog (5s Interval)
|
||||
- ✅ Start-Timeout (10s)
|
||||
- ✅ Error Recovery mit Consecutive Error Tracking
|
||||
- ✅ Memory-Monitoring (alle 10 Min)
|
||||
- ✅ Präventiver Reload (alle 6h)
|
||||
- ✅ Critical Error Check (alle 30s)
|
||||
- ✅ CSS Performance-Optimierungen
|
||||
- ✅ Erweiterte Video-Events (playing, canplay, waiting)
|
||||
- ✅ Detaillierte Error-Codes mit Logging
|
||||
|
||||
---
|
||||
|
||||
**Status:** ✅ Ready for Production
|
||||
**Tested on:** Chrome 120+ / Android 11+
|
||||
**Last Update:** 2026-01-19
|
||||
BIN
public/_cabinet/assets/fruehjahr_2024.mp4
Normal file
BIN
public/_cabinet/assets/fruehjahr_2025.mp4
Normal file
BIN
public/_cabinet/assets/herbst_2024.mp4
Normal file
BIN
public/_cabinet/assets/herbst_2025.mp4
Normal file
11
public/_cabinet/clicks.log
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
2025-12-04 10:04:05 - Scanned: pinterest
|
||||
2025-12-18 13:49:51 - Code: pbay2d - Headline: Beratung & Termin - URL: https://cabinet.b2in.eu/go.php?z=t
|
||||
2025-12-18 13:50:12 - Code: ihvb0j - Headline: Beratung vor Ort - URL: https://cabinet.b2in.eu/go.php?z=t1
|
||||
2025-12-18 13:50:12 - Code: ihvb0j - Headline: Beratung vor Ort - URL: https://cabinet.b2in.eu/go.php?z=t1
|
||||
2025-12-18 13:50:12 - Code: ihvb0j - Headline: Beratung vor Ort - URL: https://cabinet.b2in.eu/go.php?z=t1
|
||||
2025-12-18 13:50:12 - Code: ihvb0j - Headline: Beratung vor Ort - URL: https://cabinet.b2in.eu/go.php?z=t1
|
||||
2025-12-18 13:50:12 - Code: ihvb0j - Headline: Beratung vor Ort - URL: https://cabinet.b2in.eu/go.php?z=t1
|
||||
2025-12-18 13:51:01 - Code: k7wrsh - Headline: Instagram - URL: https://cabinet.b2in.eu/go.php?z=i
|
||||
2025-12-18 13:52:57 - Code: wao7uv - Headline: Beratung & Termin - URL: https://www.cabinet.de/bielefeld?utm_source=store_display&utm_medium=qr_code&utm_campaign=bielefeld_pos&utm_content=termin_buchung#c39393
|
||||
2025-12-18 13:53:33 - Code: wao7uv - Headline: Beratung & Termin - URL: https://www.cabinet.de/bielefeld?utm_source=store_display&utm_medium=qr_code&utm_campaign=bielefeld_pos&utm_content=termin_buchung#c39393
|
||||
2025-12-18 14:01:26 - Code: c59kjb - Headline: Beratung & Termin - URL: https://www.cabinet.de/bielefeld?utm_source=store_display&utm_medium=qr_code&utm_campaign=bielefeld_pos&utm_content=termin_buchung#c39393
|
||||
87
public/_cabinet/go.php
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
<?php
|
||||
/*
|
||||
Redirect-Script mit Datenbanklogging für Display-Footer-Inhalte
|
||||
Funktioniert sowohl in _display-b2in-eu als auch in _cabinet
|
||||
Aufruf via: https://cabinet.b2in.eu/go.php?z=abc123
|
||||
*/
|
||||
|
||||
// Datenbankverbindung direkt (ohne Laravel Bootstrap)
|
||||
// Lade .env Datei
|
||||
$envFile = __DIR__ . '/../../.env';
|
||||
if (file_exists($envFile)) {
|
||||
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
foreach ($lines as $line) {
|
||||
if (strpos(trim($line), '#') === 0) continue;
|
||||
if (strpos($line, '=') === false) continue;
|
||||
list($name, $value) = explode('=', $line, 2);
|
||||
$name = trim($name);
|
||||
$value = trim($value);
|
||||
if (!getenv($name)) {
|
||||
putenv("$name=$value");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$dbHost = getenv('DB_HOST') ?: 'mysql';
|
||||
$dbName = getenv('DB_DATABASE') ?: 'b2in';
|
||||
$dbUser = getenv('DB_USERNAME') ?: 'sail';
|
||||
$dbPass = getenv('DB_PASSWORD') ?: 'password';
|
||||
|
||||
$shortCode = isset($_GET['z']) ? $_GET['z'] : '';
|
||||
|
||||
if (empty($shortCode)) {
|
||||
http_response_code(404);
|
||||
echo "Kein Ziel angegeben.";
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
// PDO-Verbindung zur Datenbank
|
||||
$pdo = new PDO("mysql:host={$dbHost};dbname={$dbName};charset=utf8mb4", $dbUser, $dbPass, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]);
|
||||
|
||||
// Suche den Footer-Content mit diesem Short-Code
|
||||
$stmt = $pdo->prepare("SELECT * FROM display_footer_contents WHERE short_code = ? LIMIT 1");
|
||||
$stmt->execute([$shortCode]);
|
||||
$footerContent = $stmt->fetch();
|
||||
|
||||
if ($footerContent) {
|
||||
// Klicks erhöhen
|
||||
$updateStmt = $pdo->prepare("UPDATE display_footer_contents SET clicks = clicks + 1 WHERE id = ?");
|
||||
$updateStmt->execute([$footerContent['id']]);
|
||||
|
||||
// Optional: Logging in Datei
|
||||
$logEntry = date('Y-m-d H:i:s') . " - Code: {$shortCode} - Headline: {$footerContent['headline']} - URL: {$footerContent['url']}\n";
|
||||
@file_put_contents(__DIR__ . '/clicks.log', $logEntry, FILE_APPEND);
|
||||
|
||||
// Redirect zur Original-URL
|
||||
header("Location: " . $footerContent['url']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Fallback: Alte Codes für Rückwärtskompatibilität
|
||||
$alteCodes = [
|
||||
't' => 'https://www.cabinet.de/bielefeld?utm_source=store_display&utm_medium=qr_code&utm_campaign=bielefeld_pos&utm_content=termin_buchung#c39393',
|
||||
't1' => 'https://www.cabinet.de/bielefeld?utm_source=store_display&utm_medium=qr_code&utm_campaign=bielefeld_pos&utm_content=termin_buchung#c39393',
|
||||
'p' => 'https://de.pinterest.com/cabinet_AG/',
|
||||
'i' => 'https://www.instagram.com/cabinet_schranksysteme/',
|
||||
'f' => 'https://de-de.facebook.com/cabinetschranksysteme/'
|
||||
];
|
||||
|
||||
if (isset($alteCodes[$shortCode])) {
|
||||
$logEntry = date('Y-m-d H:i:s') . " - Legacy Code: {$shortCode}\n";
|
||||
@file_put_contents(__DIR__ . '/clicks.log', $logEntry, FILE_APPEND);
|
||||
|
||||
header("Location: " . $alteCodes[$shortCode]);
|
||||
exit;
|
||||
}
|
||||
|
||||
http_response_code(404);
|
||||
echo "Ziel nicht gefunden.";
|
||||
} catch (PDOException $e) {
|
||||
error_log("Display Go.php Database Error: " . $e->getMessage());
|
||||
http_response_code(500);
|
||||
echo "Ein Fehler ist aufgetreten.";
|
||||
}
|
||||
412
public/_cabinet/index copy.html
Normal file
|
|
@ -0,0 +1,412 @@
|
|||
<!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">
|
||||
<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;
|
||||
}
|
||||
|
||||
/* --- 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 src="" 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';
|
||||
|
||||
// API-URL für die Konfiguration (CORS ist aktiviert für cabinet.b2in.eu)
|
||||
const API_URL = BASE_URL + '/api/display/config';
|
||||
|
||||
/* ==============================================
|
||||
KONFIGURATION LADEN
|
||||
============================================== */
|
||||
|
||||
async function loadConfiguration() {
|
||||
try {
|
||||
const response = await fetch(API_URL);
|
||||
if (!response.ok) {
|
||||
throw new Error('Fehler beim Laden der Konfiguration');
|
||||
}
|
||||
|
||||
const config = await response.json();
|
||||
videoPlaylist = config.videoPlaylist || [];
|
||||
footerContent = config.footerContent || [];
|
||||
|
||||
console.log('Konfiguration geladen:', config);
|
||||
|
||||
// Überprüfe, ob Videos vorhanden sind
|
||||
if (videoPlaylist.length === 0) {
|
||||
console.warn('Keine Videos in der Playlist vorhanden');
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Konfiguration:', error);
|
||||
document.getElementById('headline').innerText = 'FEHLER';
|
||||
document.getElementById('subline').innerText = 'Konfiguration konnte nicht geladen werden';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==============================================
|
||||
PROGRAMM-LOGIK
|
||||
============================================== */
|
||||
|
||||
// --- VIDEO PLAYER LOGIC ---
|
||||
const videoElement = document.getElementById('video-player');
|
||||
let currentVideoIndex = 0;
|
||||
|
||||
function playNextVideo() {
|
||||
if (videoPlaylist.length === 0) return;
|
||||
|
||||
const video = videoPlaylist[currentVideoIndex];
|
||||
// Videos von b2in.eu laden (absolute URL)
|
||||
videoElement.src = BASE_URL + "/_cabinet/" + video.src;
|
||||
if(footerContentLength !== 0) {
|
||||
videoElement.style.objectPosition = `center ${video.position}%`;
|
||||
}
|
||||
videoElement.play().catch(e => console.log("Autoplay blocked/failed", e));
|
||||
|
||||
currentVideoIndex++;
|
||||
if (currentVideoIndex >= videoPlaylist.length) {
|
||||
currentVideoIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
videoElement.addEventListener('ended', playNextVideo);
|
||||
|
||||
// --- 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];
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
/* ==============================================
|
||||
INITIALISIERUNG
|
||||
============================================== */
|
||||
|
||||
async function initialize() {
|
||||
const success = await loadConfiguration();
|
||||
|
||||
if (success && videoPlaylist.length > 0) {
|
||||
// Start Video
|
||||
playNextVideo();
|
||||
|
||||
// Start Footer Loop
|
||||
if (footerContent.length > 0) {
|
||||
updateFooter();
|
||||
setInterval(updateFooter, 30000); // Alle 30.000 ms (30 sek) wechseln
|
||||
|
||||
// Progress Bar initial starten
|
||||
restartProgressBar();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Beim Laden der Seite initialisieren
|
||||
initialize();
|
||||
|
||||
// Optional: 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
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
371
public/_cabinet/index-dynamic.html
Normal file
|
|
@ -0,0 +1,371 @@
|
|||
<!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">
|
||||
<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;
|
||||
}
|
||||
|
||||
/* --- 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 src="" id="qr-image" class="qr-code-img" alt="QR Code">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ==============================================
|
||||
KONFIGURATION WIRD DYNAMISCH GELADEN
|
||||
============================================== */
|
||||
|
||||
let videoPlaylist = [];
|
||||
let footerContent = [];
|
||||
|
||||
// API-URL für die Konfiguration
|
||||
const API_URL = '/api/display/config';
|
||||
|
||||
/* ==============================================
|
||||
KONFIGURATION LADEN
|
||||
============================================== */
|
||||
|
||||
async function loadConfiguration() {
|
||||
try {
|
||||
const response = await fetch(API_URL);
|
||||
if (!response.ok) {
|
||||
throw new Error('Fehler beim Laden der Konfiguration');
|
||||
}
|
||||
|
||||
const config = await response.json();
|
||||
videoPlaylist = config.videoPlaylist || [];
|
||||
footerContent = config.footerContent || [];
|
||||
|
||||
console.log('Konfiguration geladen:', config);
|
||||
|
||||
// Überprüfe, ob Videos vorhanden sind
|
||||
if (videoPlaylist.length === 0) {
|
||||
console.warn('Keine Videos in der Playlist vorhanden');
|
||||
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');
|
||||
footerContent = [{
|
||||
headline: 'WILLKOMMEN',
|
||||
subline: 'Bitte fügen Sie Footer-Inhalte im CMS hinzu',
|
||||
url: 'https://cabinet.b2in.eu'
|
||||
}];
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Konfiguration:', error);
|
||||
document.getElementById('headline').innerText = 'FEHLER';
|
||||
document.getElementById('subline').innerText = 'Konfiguration konnte nicht geladen werden';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==============================================
|
||||
PROGRAMM-LOGIK
|
||||
============================================== */
|
||||
|
||||
// --- VIDEO PLAYER LOGIC ---
|
||||
const videoElement = document.getElementById('video-player');
|
||||
let currentVideoIndex = 0;
|
||||
|
||||
function playNextVideo() {
|
||||
if (videoPlaylist.length === 0) return;
|
||||
|
||||
const video = videoPlaylist[currentVideoIndex];
|
||||
videoElement.src = video.src;
|
||||
videoElement.style.objectPosition = `center ${video.position}%`;
|
||||
videoElement.play().catch(e => console.log("Autoplay blocked/failed", e));
|
||||
|
||||
currentVideoIndex++;
|
||||
if (currentVideoIndex >= videoPlaylist.length) {
|
||||
currentVideoIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
videoElement.addEventListener('ended', playNextVideo);
|
||||
|
||||
// --- 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];
|
||||
|
||||
// Text setzen
|
||||
headlineEl.innerText = content.headline;
|
||||
sublineEl.innerText = content.subline;
|
||||
|
||||
// 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;
|
||||
|
||||
// Index weiterschalten
|
||||
currentFooterIndex++;
|
||||
if (currentFooterIndex >= footerContent.length) {
|
||||
currentFooterIndex = 0;
|
||||
}
|
||||
|
||||
// 3. Einblenden
|
||||
qrImageEl.onload = () => {
|
||||
textArea.classList.remove('fade-out');
|
||||
qrArea.classList.remove('fade-out');
|
||||
};
|
||||
// Fallback falls Bild sofort da ist (Cache)
|
||||
setTimeout(() => {
|
||||
textArea.classList.remove('fade-out');
|
||||
qrArea.classList.remove('fade-out');
|
||||
}, 100);
|
||||
|
||||
}, 1000); // 1 Sekunde für Fade-Out Animation
|
||||
}
|
||||
|
||||
/* ==============================================
|
||||
INITIALISIERUNG
|
||||
============================================== */
|
||||
|
||||
async function initialize() {
|
||||
const success = await loadConfiguration();
|
||||
|
||||
if (success && videoPlaylist.length > 0) {
|
||||
// Start Video
|
||||
playNextVideo();
|
||||
|
||||
// Start Footer Loop
|
||||
if (footerContent.length > 0) {
|
||||
updateFooter();
|
||||
setInterval(updateFooter, 30000); // Alle 30.000 ms (30 sek) wechseln
|
||||
|
||||
// Progress Bar initial starten
|
||||
restartProgressBar();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Beim Laden der Seite initialisieren
|
||||
initialize();
|
||||
|
||||
// Optional: Auto-Reload alle 5 Minuten, um neue Inhalte zu laden
|
||||
setInterval(async () => {
|
||||
console.log('Prüfe auf neue Konfiguration...');
|
||||
await loadConfiguration();
|
||||
}, 5 * 60 * 1000); // 5 Minuten
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
331
public/_cabinet/index-static-backup.html
Normal file
|
|
@ -0,0 +1,331 @@
|
|||
<!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">
|
||||
<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;
|
||||
}
|
||||
|
||||
/* --- 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;
|
||||
}
|
||||
</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 src="" id="qr-image" class="qr-code-img" alt="QR Code">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ==============================================
|
||||
KONFIGURATION
|
||||
============================================== */
|
||||
|
||||
// 1. VIDEOS (Dateinamen und Position hier anpassen)
|
||||
// position: Prozentwert von 0% (ganz oben) bis 100% (ganz unten)
|
||||
const videoPlaylist = [
|
||||
{ src: "assets/herbst_2025.mp4", position: 25 },
|
||||
{ src: "assets/fruehjahr_2025.mp4", position: 10 },
|
||||
{ src: "assets/fruehjahr_2024.mp4", position: 25 },
|
||||
{ src: "assets/herbst_2024.mp4", position: 25 },
|
||||
];
|
||||
|
||||
// 2. INHALTE & LINKS
|
||||
// Ich habe deine Links hier eingetragen. Die Texte kannst du anpassen.
|
||||
const footerContent = [
|
||||
{
|
||||
headline: "Beratung & Termin",
|
||||
subline: "Jetzt Termin vereinbaren.",
|
||||
url: "https://cabinet.b2in.eu/go.php?z=t"
|
||||
},
|
||||
{
|
||||
headline: "Beratung vor Ort",
|
||||
subline: "Einfach reinkommen.",
|
||||
url: "https://cabinet.b2in.eu/go.php?z=t1"
|
||||
},
|
||||
{
|
||||
headline: "Pinterest",
|
||||
subline: "Inspirationen entdecken.",
|
||||
url: "https://cabinet.b2in.eu/go.php?z=p"
|
||||
},
|
||||
{
|
||||
headline: "Instagram",
|
||||
subline: "Tägliche Einblicke & Design.",
|
||||
url: "https://cabinet.b2in.eu/go.php?z=i"
|
||||
},
|
||||
{
|
||||
headline: "Facebook",
|
||||
subline: "News, Aktionen & Community.",
|
||||
url: "https://cabinet.b2in.eu/go.php?z=f"
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
/* ==============================================
|
||||
PROGRAMM-LOGIK (Ab hier nichts ändern)
|
||||
============================================== */
|
||||
|
||||
// --- VIDEO PLAYER LOGIC ---
|
||||
const videoElement = document.getElementById('video-player');
|
||||
let currentVideoIndex = 0;
|
||||
|
||||
function playNextVideo() {
|
||||
const video = videoPlaylist[currentVideoIndex];
|
||||
videoElement.src = video.src;
|
||||
videoElement.style.objectPosition = `center ${video.position}%`;
|
||||
videoElement.play().catch(e => console.log("Autoplay blocked/failed", e));
|
||||
|
||||
currentVideoIndex++;
|
||||
if (currentVideoIndex >= videoPlaylist.length) {
|
||||
currentVideoIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
videoElement.addEventListener('ended', playNextVideo);
|
||||
|
||||
// Start Video
|
||||
if(videoPlaylist.length > 0) playNextVideo();
|
||||
|
||||
// --- 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() {
|
||||
// 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];
|
||||
|
||||
// Text setzen
|
||||
headlineEl.innerText = content.headline;
|
||||
sublineEl.innerText = content.subline;
|
||||
|
||||
// QR Code generieren (API Aufruf)
|
||||
// Wir nutzen 'qrserver.com', eine schnelle und kostenlose API
|
||||
const qrSize = "300x300";
|
||||
const qrColor = "000000"; // Schwarz
|
||||
const qrBg = "ffffff"; // Weiß
|
||||
// encodeURIComponent sorgt dafür, dass Sonderzeichen im Link funktionieren
|
||||
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;
|
||||
|
||||
// Index weiterschalten
|
||||
currentFooterIndex++;
|
||||
if (currentFooterIndex >= footerContent.length) {
|
||||
currentFooterIndex = 0;
|
||||
}
|
||||
|
||||
// 3. Einblenden
|
||||
// Kurze Verzögerung damit das Bild Zeit hat zu laden (optisch schöner)
|
||||
qrImageEl.onload = () => {
|
||||
textArea.classList.remove('fade-out');
|
||||
qrArea.classList.remove('fade-out');
|
||||
};
|
||||
// Fallback falls Bild sofort da ist (Cache)
|
||||
setTimeout(() => {
|
||||
textArea.classList.remove('fade-out');
|
||||
qrArea.classList.remove('fade-out');
|
||||
}, 100);
|
||||
|
||||
}, 1000); // 1 Sekunde für Fade-Out Animation
|
||||
}
|
||||
|
||||
// Start Footer Loop
|
||||
updateFooter();
|
||||
setInterval(updateFooter, 30000); // Alle 30.000 ms (30 sek) wechseln
|
||||
|
||||
// Progress Bar initial starten
|
||||
restartProgressBar();
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
991
public/_cabinet/index.html
Normal file
|
|
@ -0,0 +1,991 @@
|
|||
<!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;
|
||||
}
|
||||
|
||||
/* Fullscreen Button */
|
||||
#fullscreen-btn {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
z-index: 1000;
|
||||
background-color: rgba(0, 159, 227, 0.8);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
font-family: 'IBM Plex Sans', sans-serif;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
#fullscreen-btn:hover {
|
||||
background-color: rgba(0, 159, 227, 1);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
#fullscreen-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* Button ausblenden wenn bereits im Fullscreen */
|
||||
#fullscreen-btn.hidden {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Fullscreen Reminder (nach Reload) */
|
||||
#fullscreen-btn.reminder {
|
||||
background-color: rgba(255, 152, 0, 0.95);
|
||||
animation: pulse 2s infinite;
|
||||
padding: 12px 20px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.08);
|
||||
box-shadow: 0 6px 12px rgba(255, 152, 0, 0.6);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- FULLSCREEN BUTTON -->
|
||||
<button id="fullscreen-btn" title="Vollbildmodus aktivieren">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align: middle;">
|
||||
<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/>
|
||||
</svg>
|
||||
<span style="vertical-align: middle;">V 1.3</span>
|
||||
</button>
|
||||
|
||||
<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 src="" id="qr-image" class="qr-code-img" alt="QR Code">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ==============================================
|
||||
FULLSCREEN BUTTON LOGIC MIT AUTO-REMINDER
|
||||
============================================== */
|
||||
|
||||
const fullscreenBtn = document.getElementById('fullscreen-btn');
|
||||
const FULLSCREEN_STATE_KEY = 'cabinet_fullscreen_was_active';
|
||||
|
||||
// Fullscreen aktivieren
|
||||
function enterFullscreen() {
|
||||
const elem = document.documentElement;
|
||||
|
||||
// Merken dass Fullscreen aktiviert wurde (für nach Reload)
|
||||
localStorage.setItem(FULLSCREEN_STATE_KEY, 'true');
|
||||
|
||||
if (elem.requestFullscreen) {
|
||||
elem.requestFullscreen();
|
||||
} else if (elem.webkitRequestFullscreen) { // Safari/Chrome
|
||||
elem.webkitRequestFullscreen();
|
||||
} else if (elem.mozRequestFullScreen) { // Firefox
|
||||
elem.mozRequestFullScreen();
|
||||
} else if (elem.msRequestFullscreen) { // IE/Edge
|
||||
elem.msRequestFullscreen();
|
||||
}
|
||||
|
||||
window.displayLogger?.log('Fullscreen aktiviert');
|
||||
}
|
||||
|
||||
// Button Event Listener
|
||||
fullscreenBtn.addEventListener('click', () => {
|
||||
enterFullscreen();
|
||||
// Reminder-Klasse entfernen falls vorhanden
|
||||
fullscreenBtn.classList.remove('reminder');
|
||||
});
|
||||
|
||||
// Prüfen ob Fullscreen vorher aktiv war (nach Reload)
|
||||
function checkFullscreenRestore() {
|
||||
const wasFullscreen = localStorage.getItem(FULLSCREEN_STATE_KEY);
|
||||
|
||||
if (wasFullscreen === 'true') {
|
||||
// Fullscreen war vorher aktiv → Auffälliger Reminder
|
||||
fullscreenBtn.classList.add('reminder');
|
||||
fullscreenBtn.innerHTML = `
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align: middle;">
|
||||
<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/>
|
||||
</svg>
|
||||
<span style="vertical-align: middle;">⚠️ Vollbild aktivieren!</span>
|
||||
`;
|
||||
|
||||
window.displayLogger?.warn('Fullscreen-Reminder angezeigt (war vorher aktiv)', {
|
||||
reason: 'Page reload',
|
||||
previousState: 'fullscreen'
|
||||
});
|
||||
|
||||
// Nach 30 Sekunden automatisch versuchen (falls Kiosk-Mode)
|
||||
setTimeout(() => {
|
||||
if (!document.fullscreenElement && !document.webkitFullscreenElement) {
|
||||
window.displayLogger?.log('Versuche Auto-Fullscreen (Kiosk-Mode?)');
|
||||
enterFullscreen();
|
||||
}
|
||||
}, 30000);
|
||||
}
|
||||
}
|
||||
|
||||
// Button ausblenden wenn bereits im Fullscreen
|
||||
document.addEventListener('fullscreenchange', () => {
|
||||
if (document.fullscreenElement) {
|
||||
fullscreenBtn.classList.add('hidden');
|
||||
fullscreenBtn.classList.remove('reminder');
|
||||
localStorage.setItem(FULLSCREEN_STATE_KEY, 'true');
|
||||
} else {
|
||||
fullscreenBtn.classList.remove('hidden');
|
||||
// Fullscreen wurde verlassen → State clearen
|
||||
localStorage.removeItem(FULLSCREEN_STATE_KEY);
|
||||
window.displayLogger?.log('Fullscreen verlassen');
|
||||
}
|
||||
});
|
||||
|
||||
// Webkit Fullscreen Change (Chrome/Safari)
|
||||
document.addEventListener('webkitfullscreenchange', () => {
|
||||
if (document.webkitFullscreenElement) {
|
||||
fullscreenBtn.classList.add('hidden');
|
||||
fullscreenBtn.classList.remove('reminder');
|
||||
localStorage.setItem(FULLSCREEN_STATE_KEY, 'true');
|
||||
} else {
|
||||
fullscreenBtn.classList.remove('hidden');
|
||||
localStorage.removeItem(FULLSCREEN_STATE_KEY);
|
||||
window.displayLogger?.log('Fullscreen verlassen (webkit)');
|
||||
}
|
||||
});
|
||||
|
||||
// Check beim Laden der Seite
|
||||
checkFullscreenRestore();
|
||||
|
||||
/* ==============================================
|
||||
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';
|
||||
|
||||
// API-URL für die Konfiguration (CORS ist aktiviert für cabinet.b2in.eu)
|
||||
const API_URL = BASE_URL + '/api/display/config';
|
||||
|
||||
/* ==============================================
|
||||
KONFIGURATION LADEN
|
||||
============================================== */
|
||||
|
||||
async function loadConfiguration() {
|
||||
try {
|
||||
window.displayLogger?.log('Lade Konfiguration...', { url: API_URL });
|
||||
|
||||
const response = await fetch(API_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;
|
||||
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() {
|
||||
// Wichtig: Stoppt Video und gibt Speicher frei
|
||||
try {
|
||||
videoElement.pause();
|
||||
videoElement.removeAttribute('src');
|
||||
videoElement.load(); // Triggert Garbage Collection des alten Videos
|
||||
|
||||
// Timeouts clearen
|
||||
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;
|
||||
|
||||
// Watchdog zurücksetzen
|
||||
lastVideoTime = 0;
|
||||
videoStuckCount = 0;
|
||||
|
||||
const video = videoPlaylist[currentVideoIndex];
|
||||
const videoSrc = BASE_URL + "/_cabinet/" + video.src;
|
||||
|
||||
// Kontext aktualisieren
|
||||
window.displayLogger?.setContext('currentVideo', video.src);
|
||||
window.displayLogger?.setContext('currentVideoIndex', currentVideoIndex);
|
||||
|
||||
// WICHTIG: Altes Video cleanup BEVOR neues geladen wird
|
||||
cleanupVideo();
|
||||
|
||||
// Kleiner Delay um Cleanup abzuschließen
|
||||
setTimeout(() => {
|
||||
try {
|
||||
// Neues Video laden
|
||||
videoElement.src = videoSrc;
|
||||
|
||||
if(footerContentLength !== 0 && video.position !== undefined) {
|
||||
videoElement.style.objectPosition = `center ${video.position}%`;
|
||||
}
|
||||
|
||||
// Timeout für Video-Start
|
||||
videoStartTimeout = setTimeout(() => {
|
||||
window.displayLogger?.error('Video start timeout', {
|
||||
video: video.src,
|
||||
timeout: VIDEO_START_TIMEOUT
|
||||
});
|
||||
// Nächstes Video probieren
|
||||
skipToNextVideo('timeout');
|
||||
}, VIDEO_START_TIMEOUT);
|
||||
|
||||
// Video abspielen
|
||||
videoElement.play()
|
||||
.then(() => {
|
||||
window.displayLogger?.log(`Video started: ${video.src}`);
|
||||
consecutiveErrors = 0; // Erfolg → Error-Counter zurücksetzen
|
||||
|
||||
// Start-Timeout clearen
|
||||
if (videoStartTimeout) {
|
||||
clearTimeout(videoStartTimeout);
|
||||
videoStartTimeout = null;
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
console.log("Autoplay blocked/failed", e);
|
||||
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
|
||||
});
|
||||
// Seite nach 30 Sekunden neu laden
|
||||
setTimeout(() => location.reload(), 30000);
|
||||
} else {
|
||||
// Nächstes Video probieren
|
||||
skipToNextVideo('play_failed');
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
window.displayLogger?.error('Exception beim Video-Laden', {
|
||||
error: e.message,
|
||||
stack: e.stack
|
||||
});
|
||||
skipToNextVideo('exception');
|
||||
}
|
||||
}, 100); // 100ms Delay für Cleanup
|
||||
|
||||
// Index weiterschalten
|
||||
currentVideoIndex++;
|
||||
if (currentVideoIndex >= videoPlaylist.length) {
|
||||
currentVideoIndex = 0;
|
||||
// Playlist-Loop abgeschlossen → Log für Monitoring
|
||||
window.displayLogger?.log('Playlist-Loop abgeschlossen, starte von vorne');
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
if (videoStuckCount >= 2) {
|
||||
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) => {
|
||||
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
|
||||
});
|
||||
|
||||
// Bei Fehler → Nächstes Video
|
||||
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();
|
||||
|
||||
// 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>
|
||||
|
||||
292
public/_cabinet/index_1.html
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
<!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">
|
||||
<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;
|
||||
}
|
||||
|
||||
/* --- FOOTER BEREICH (Unten - ca. 16.67% der Höhe = 320px bei 1920px) --- */
|
||||
#footer {
|
||||
height: 16.67vh;
|
||||
min-height: 200px;
|
||||
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;
|
||||
border-top: 0.26vh solid #009FE3; /* Cabinet Blau - 5px bei 1920px */
|
||||
font-size: 10px
|
||||
}
|
||||
|
||||
/* --- INHALTE IM FOOTER --- */
|
||||
.cta-text-container {
|
||||
width: 65%;
|
||||
opacity: 1;
|
||||
transition: opacity 1s ease-in-out;
|
||||
}
|
||||
|
||||
.cta-headline {
|
||||
font-size: 2.2em; /* 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.8em; /* Relativ zur Footer-Schriftgröße */
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.qr-container {
|
||||
width: 30%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
opacity: 1;
|
||||
transition: opacity 1s ease-in-out;
|
||||
}
|
||||
|
||||
.qr-code-img {
|
||||
width: 12em; /* Relativ zur Footer-Schriftgröße */
|
||||
height: auto;
|
||||
max-width: 200px;
|
||||
aspect-ratio: 1 / 1; /* Erzwingt quadratische Form */
|
||||
object-fit: contain;
|
||||
background-color: white; /* Weißer Hintergrund für Lesbarkeit */
|
||||
padding: 0.8em;
|
||||
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;
|
||||
}
|
||||
</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 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 src="" id="qr-image" class="qr-code-img" alt="QR Code">
|
||||
<span class="scan-hint">Scan Me</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ==============================================
|
||||
KONFIGURATION
|
||||
============================================== */
|
||||
|
||||
// 1. VIDEOS (Dateinamen und Position hier anpassen)
|
||||
// position: Prozentwert von 0% (ganz oben) bis 100% (ganz unten)
|
||||
const videoPlaylist = [
|
||||
{ src: "assets/video5.mp4", position: 10 },
|
||||
{ src: "assets/video5.mp4", position: 10 },
|
||||
{ src: "assets/video3.mp4", position: 15 },
|
||||
{ src: "assets/video4.mp4", position: 25 },
|
||||
|
||||
|
||||
|
||||
];
|
||||
|
||||
// 2. INHALTE & LINKS
|
||||
// Ich habe deine Links hier eingetragen. Die Texte kannst du anpassen.
|
||||
const footerContent = [
|
||||
{
|
||||
headline: "Beratung & Termin",
|
||||
subline: "Planen Sie Ihren Traumschrank.\nJetzt Termin vereinbaren.",
|
||||
url: "https://cabinet.b2in.eu/go.php?z=t"
|
||||
},
|
||||
{
|
||||
headline: "Inspiration",
|
||||
subline: "Entdecken Sie unsere Ideen-Pinnwände auf Pinterest.",
|
||||
url: "https://cabinet.b2in.eu/go.php?z=p"
|
||||
},
|
||||
{
|
||||
headline: "Instagram",
|
||||
subline: "Folgen Sie uns für tägliche Einblicke & Design.",
|
||||
url: "https://cabinet.b2in.eu/go.php?z=i"
|
||||
},
|
||||
{
|
||||
headline: "Facebook",
|
||||
subline: "News, Aktionen & Community.\nWerden Sie Fan.",
|
||||
url: "https://cabinet.b2in.eu/go.php?z=f"
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
/* ==============================================
|
||||
PROGRAMM-LOGIK (Ab hier nichts ändern)
|
||||
============================================== */
|
||||
|
||||
// --- VIDEO PLAYER LOGIC ---
|
||||
const videoElement = document.getElementById('video-player');
|
||||
let currentVideoIndex = 0;
|
||||
|
||||
function playNextVideo() {
|
||||
const video = videoPlaylist[currentVideoIndex];
|
||||
videoElement.src = video.src;
|
||||
videoElement.style.objectPosition = `center ${video.position}%`;
|
||||
videoElement.play().catch(e => console.log("Autoplay blocked/failed", e));
|
||||
|
||||
currentVideoIndex++;
|
||||
if (currentVideoIndex >= videoPlaylist.length) {
|
||||
currentVideoIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
videoElement.addEventListener('ended', playNextVideo);
|
||||
|
||||
// Start Video
|
||||
if(videoPlaylist.length > 0) playNextVideo();
|
||||
|
||||
|
||||
// --- 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');
|
||||
|
||||
function updateFooter() {
|
||||
// 1. Ausblenden
|
||||
textArea.classList.add('fade-out');
|
||||
qrArea.classList.add('fade-out');
|
||||
|
||||
// 2. Warten, Inhalt tauschen, Einblenden
|
||||
setTimeout(() => {
|
||||
const content = footerContent[currentFooterIndex];
|
||||
|
||||
// Text setzen
|
||||
headlineEl.innerText = content.headline;
|
||||
sublineEl.innerText = content.subline;
|
||||
|
||||
// QR Code generieren (API Aufruf)
|
||||
// Wir nutzen 'qrserver.com', eine schnelle und kostenlose API
|
||||
const qrSize = "300x300";
|
||||
const qrColor = "000000"; // Schwarz
|
||||
const qrBg = "ffffff"; // Weiß
|
||||
// encodeURIComponent sorgt dafür, dass Sonderzeichen im Link funktionieren
|
||||
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;
|
||||
|
||||
// Index weiterschalten
|
||||
currentFooterIndex++;
|
||||
if (currentFooterIndex >= footerContent.length) {
|
||||
currentFooterIndex = 0;
|
||||
}
|
||||
|
||||
// 3. Einblenden
|
||||
// Kurze Verzögerung damit das Bild Zeit hat zu laden (optisch schöner)
|
||||
qrImageEl.onload = () => {
|
||||
textArea.classList.remove('fade-out');
|
||||
qrArea.classList.remove('fade-out');
|
||||
};
|
||||
// Fallback falls Bild sofort da ist (Cache)
|
||||
setTimeout(() => {
|
||||
textArea.classList.remove('fade-out');
|
||||
qrArea.classList.remove('fade-out');
|
||||
}, 100);
|
||||
|
||||
}, 1000); // 1 Sekunde für Fade-Out Animation
|
||||
}
|
||||
|
||||
// Start Footer Loop
|
||||
updateFooter();
|
||||
setInterval(updateFooter, 30000); // Alle 30.000 ms (30 sek) wechseln
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
94
public/_cabinet/logger.php
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
<?php
|
||||
// logger.php - Optimiertes Logging für Cabinet Digital Signage
|
||||
// Erlaubt CORS
|
||||
header("Access-Control-Allow-Origin: *");
|
||||
header("Access-Control-Allow-Headers: Content-Type");
|
||||
header("Access-Control-Allow-Methods: POST, OPTIONS");
|
||||
header("Content-Type: application/json");
|
||||
|
||||
// Handle OPTIONS preflight request
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(200);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Nur POST akzeptieren
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$input = file_get_contents('php://input');
|
||||
$data = json_decode($input, true);
|
||||
|
||||
if ($data) {
|
||||
$level = strtoupper($data['level'] ?? 'INFO');
|
||||
$message = $data['message'] ?? 'n/a';
|
||||
$timestamp = $data['timestamp'] ?? date('c');
|
||||
$context = $data['context'] ?? [];
|
||||
$viewport = $data['viewport'] ?? 'unknown';
|
||||
$connection = $data['connection'] ?? 'unknown';
|
||||
|
||||
// Log-Eintrag mit strukturierten Daten
|
||||
$logEntry = [
|
||||
'timestamp' => date('Y-m-d H:i:s'),
|
||||
'iso_timestamp' => $timestamp,
|
||||
'level' => $level,
|
||||
'message' => $message,
|
||||
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
|
||||
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
|
||||
'viewport' => $viewport,
|
||||
'connection' => $connection,
|
||||
'context' => $context
|
||||
];
|
||||
|
||||
// Log-Datei basierend auf Level
|
||||
$logFile = __DIR__ . '/logs/' . strtolower($level) . '_' . date('Y-m-d') . '.log';
|
||||
$allLogsFile = __DIR__ . '/logs/all_' . date('Y-m-d') . '.log';
|
||||
|
||||
// Logs-Verzeichnis erstellen falls nicht vorhanden
|
||||
$logsDir = __DIR__ . '/logs';
|
||||
if (!is_dir($logsDir)) {
|
||||
mkdir($logsDir, 0755, true);
|
||||
}
|
||||
|
||||
// Formatierte Log-Zeile (human-readable)
|
||||
$logLine = sprintf(
|
||||
"[%s] [%s] %s\n",
|
||||
date('Y-m-d H:i:s'),
|
||||
str_pad($level, 8),
|
||||
$message
|
||||
);
|
||||
|
||||
// Zusätzliche Kontext-Informationen wenn vorhanden
|
||||
if (!empty($context)) {
|
||||
$logLine .= " Context: " . json_encode($context, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n";
|
||||
}
|
||||
|
||||
$logLine .= " IP: {$logEntry['ip']} | Viewport: {$viewport} | Connection: {$connection}\n";
|
||||
$logLine .= str_repeat('-', 80) . "\n";
|
||||
|
||||
// JSON Log für maschinelle Verarbeitung
|
||||
$jsonLogLine = json_encode($logEntry, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n";
|
||||
$jsonLogFile = __DIR__ . '/logs/json_' . date('Y-m-d') . '.log';
|
||||
|
||||
// In Dateien schreiben
|
||||
file_put_contents($logFile, $logLine, FILE_APPEND | LOCK_EX);
|
||||
file_put_contents($allLogsFile, $logLine, FILE_APPEND | LOCK_EX);
|
||||
file_put_contents($jsonLogFile, $jsonLogLine, FILE_APPEND | LOCK_EX);
|
||||
|
||||
// Log-Rotation: Dateien älter als 30 Tage löschen
|
||||
$files = glob($logsDir . '/*.log');
|
||||
$now = time();
|
||||
foreach ($files as $file) {
|
||||
if (is_file($file) && $now - filemtime($file) >= 30 * 24 * 3600) {
|
||||
unlink($file);
|
||||
}
|
||||
}
|
||||
|
||||
// Erfolgsantwort
|
||||
echo json_encode(['status' => 'success', 'message' => 'Log received']);
|
||||
} else {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Invalid JSON']);
|
||||
}
|
||||
} else {
|
||||
http_response_code(405);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Method not allowed']);
|
||||
}
|
||||
1
public/_cabinet/logs/test_2026-01-19.log
Normal file
|
|
@ -0,0 +1 @@
|
|||
[2026-01-19 08:45:35] [INFO] Logging-System wurde eingerichtet
|
||||
118
public/_cabinet/setup-logging.sh
Executable file
|
|
@ -0,0 +1,118 @@
|
|||
#!/bin/bash
|
||||
# Setup-Script für Cabinet Digital Signage Logging System
|
||||
# Führe dieses Script einmalig aus um das Logging zu aktivieren
|
||||
|
||||
echo "================================================"
|
||||
echo "Cabinet Digital Signage - Logging Setup"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
|
||||
# Farben für Output
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Arbeitsverzeichnis
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
echo "📁 Arbeitsverzeichnis: $SCRIPT_DIR"
|
||||
echo ""
|
||||
|
||||
# 1. Logs-Verzeichnis erstellen
|
||||
echo "1️⃣ Erstelle logs/ Verzeichnis..."
|
||||
if [ -d "logs" ]; then
|
||||
echo -e "${YELLOW} ⚠️ Verzeichnis existiert bereits${NC}"
|
||||
else
|
||||
mkdir -p logs
|
||||
echo -e "${GREEN} ✅ logs/ erstellt${NC}"
|
||||
fi
|
||||
|
||||
# 2. Berechtigungen setzen
|
||||
echo ""
|
||||
echo "2️⃣ Setze Berechtigungen..."
|
||||
|
||||
# Prüfe ob als root ausgeführt
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo -e "${YELLOW} ⚠️ Nicht als Root - verwende chmod 777 (nicht für Produktion!)${NC}"
|
||||
chmod 777 logs
|
||||
else
|
||||
echo -e "${GREEN} ✅ Als Root - setze www-data owner${NC}"
|
||||
chown -R www-data:www-data logs
|
||||
chmod 755 logs
|
||||
fi
|
||||
|
||||
# 3. Test-Log erstellen
|
||||
echo ""
|
||||
echo "3️⃣ Erstelle Test-Log..."
|
||||
TEST_LOG="logs/test_$(date +%Y-%m-%d).log"
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] Logging-System wurde eingerichtet" > "$TEST_LOG"
|
||||
|
||||
if [ -f "$TEST_LOG" ]; then
|
||||
echo -e "${GREEN} ✅ Test-Log erfolgreich erstellt: $TEST_LOG${NC}"
|
||||
else
|
||||
echo -e "${RED} ❌ Test-Log konnte nicht erstellt werden${NC}"
|
||||
echo -e "${YELLOW} Prüfe die Schreibrechte!${NC}"
|
||||
fi
|
||||
|
||||
# 4. .htaccess vorbereiten
|
||||
echo ""
|
||||
echo "4️⃣ Prüfe .htaccess..."
|
||||
if [ -f ".htaccess" ]; then
|
||||
echo -e "${YELLOW} ⚠️ .htaccess existiert bereits${NC}"
|
||||
echo -e " Möchtest du die Beispiel-.htaccess ansehen? → .htaccess.example"
|
||||
else
|
||||
echo -e "${GREEN} ℹ️ Keine .htaccess gefunden${NC}"
|
||||
echo -e " Für Passwortschutz: cp .htaccess.example .htaccess"
|
||||
fi
|
||||
|
||||
# 5. PHP-Konfiguration prüfen
|
||||
echo ""
|
||||
echo "5️⃣ Prüfe PHP-Konfiguration..."
|
||||
|
||||
if command -v php &> /dev/null; then
|
||||
PHP_VERSION=$(php -v | head -n 1)
|
||||
echo -e "${GREEN} ✅ PHP gefunden: $PHP_VERSION${NC}"
|
||||
|
||||
# Prüfe wichtige PHP-Settings
|
||||
FILE_UPLOADS=$(php -r "echo ini_get('file_uploads');")
|
||||
MAX_POST=$(php -r "echo ini_get('post_max_size');")
|
||||
|
||||
echo " 📋 file_uploads: $FILE_UPLOADS"
|
||||
echo " 📋 post_max_size: $MAX_POST"
|
||||
else
|
||||
echo -e "${RED} ❌ PHP nicht gefunden!${NC}"
|
||||
fi
|
||||
|
||||
# 6. Zusammenfassung
|
||||
echo ""
|
||||
echo "================================================"
|
||||
echo "✅ Setup abgeschlossen!"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
echo "📝 Nächste Schritte:"
|
||||
echo ""
|
||||
echo "1. Teste das Logging:"
|
||||
echo " → Öffne: https://cabinet.b2in.eu/test-logging.html"
|
||||
echo ""
|
||||
echo "2. Schaue die Logs an:"
|
||||
echo " → Öffne: https://cabinet.b2in.eu/view-logs.php"
|
||||
echo ""
|
||||
echo "3. Für Produktion (empfohlen):"
|
||||
echo " → Aktiviere Passwortschutz:"
|
||||
echo " cp .htaccess.example .htaccess"
|
||||
echo " htpasswd -c .htpasswd admin"
|
||||
echo ""
|
||||
echo "4. Dokumentation lesen:"
|
||||
echo " → cat LOGGING_README.md"
|
||||
echo ""
|
||||
echo "================================================"
|
||||
echo ""
|
||||
|
||||
# Verzeichnisstruktur anzeigen
|
||||
echo "📂 Aktuelle Struktur:"
|
||||
tree -L 2 -a . 2>/dev/null || ls -lah
|
||||
|
||||
echo ""
|
||||
echo "🎉 Fertig! Das Logging-System ist bereit."
|
||||
330
public/_cabinet/test-logging.html
Normal file
|
|
@ -0,0 +1,330 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Cabinet Logging Test</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 50px auto;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
h1 {
|
||||
color: #009FE3;
|
||||
border-bottom: 3px solid #009FE3;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.test-section {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
button {
|
||||
background-color: #009FE3;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
margin: 5px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #0082bd;
|
||||
}
|
||||
button.danger {
|
||||
background-color: #f44336;
|
||||
}
|
||||
button.danger:hover {
|
||||
background-color: #d32f2f;
|
||||
}
|
||||
button.warning {
|
||||
background-color: #ff9800;
|
||||
}
|
||||
button.warning:hover {
|
||||
background-color: #f57c00;
|
||||
}
|
||||
.log-output {
|
||||
background-color: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.log-entry {
|
||||
margin: 5px 0;
|
||||
padding: 5px;
|
||||
border-left: 3px solid #009FE3;
|
||||
}
|
||||
.success {
|
||||
color: #4caf50;
|
||||
}
|
||||
.error {
|
||||
color: #f44336;
|
||||
}
|
||||
.info {
|
||||
color: #009FE3;
|
||||
}
|
||||
code {
|
||||
background-color: #f0f0f0;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>🧪 Cabinet Logging System - Test Interface</h1>
|
||||
|
||||
<p><strong>Zweck:</strong> Diese Seite testet das Logging-System, bevor es auf den Displays eingesetzt wird.</p>
|
||||
<p><strong>Logs ansehen:</strong> <a href="view-logs.php" target="_blank">Log-Viewer öffnen →</a></p>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>📡 Verbindungstest</h2>
|
||||
<p>Teste ob der Logger-Endpoint erreichbar ist:</p>
|
||||
<button onclick="testConnection()">Verbindung testen</button>
|
||||
<div id="connection-output" class="log-output" style="display:none;"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>📝 Log-Level Tests</h2>
|
||||
<p>Sende verschiedene Log-Levels:</p>
|
||||
<button onclick="sendTestLog('INFO', 'Dies ist eine Test-Info-Nachricht')">INFO senden</button>
|
||||
<button class="warning" onclick="sendTestLog('WARNING', 'Dies ist eine Test-Warnung')">WARNING senden</button>
|
||||
<button class="danger" onclick="sendTestLog('ERROR', 'Dies ist ein Test-Fehler')">ERROR senden</button>
|
||||
<button class="danger" onclick="sendTestLog('FATAL', 'Dies ist ein FATAL-Test-Fehler')">FATAL senden</button>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>💥 Fehler-Simulation</h2>
|
||||
<p>Simuliere verschiedene Fehlertypen (erscheinen automatisch in Logs):</p>
|
||||
<button class="danger" onclick="triggerRuntimeError()">Runtime Error auslösen</button>
|
||||
<button class="danger" onclick="triggerPromiseRejection()">Promise Rejection auslösen</button>
|
||||
<button class="warning" onclick="triggerConsoleError()">Console.error auslösen</button>
|
||||
<button class="warning" onclick="triggerConsoleWarn()">Console.warn auslösen</button>
|
||||
<button class="danger" onclick="triggerResourceError()">Resource Loading Error</button>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>🎯 Kontext-Test</h2>
|
||||
<p>Teste Logging mit Kontext-Informationen:</p>
|
||||
<button onclick="testWithContext()">Log mit Kontext senden</button>
|
||||
<button onclick="testWithComplexContext()">Log mit komplexem Kontext</button>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>🔄 Performance-Test</h2>
|
||||
<p>Teste mehrere Logs gleichzeitig:</p>
|
||||
<button onclick="sendMultipleLogs(10)">10 Logs senden</button>
|
||||
<button onclick="sendMultipleLogs(50)">50 Logs senden</button>
|
||||
<button class="warning" onclick="sendMultipleLogs(100)">100 Logs senden (langsam!)</button>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>📊 Console Output</h2>
|
||||
<p>Lokale Log-Ausgabe (wird auch gesendet):</p>
|
||||
<div id="console-output" class="log-output"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Logger einbinden (vereinfachte Version für Tests)
|
||||
const LOG_URL = 'https://cabinet.b2in.eu/logger.php';
|
||||
const consoleOutput = document.getElementById('console-output');
|
||||
|
||||
function logToConsole(message, type = 'info') {
|
||||
const entry = document.createElement('div');
|
||||
entry.className = `log-entry ${type}`;
|
||||
entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
|
||||
consoleOutput.appendChild(entry);
|
||||
consoleOutput.scrollTop = consoleOutput.scrollHeight;
|
||||
}
|
||||
|
||||
async function sendLog(level, message, context = {}) {
|
||||
try {
|
||||
const response = await fetch(LOG_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
level: level,
|
||||
message: message,
|
||||
timestamp: new Date().toISOString(),
|
||||
context: context,
|
||||
viewport: `${window.innerWidth}x${window.innerHeight}`,
|
||||
connection: navigator.onLine ? 'online' : 'offline'
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
logToConsole(`✅ ${level}: ${message}`, 'success');
|
||||
return true;
|
||||
} else {
|
||||
logToConsole(`❌ Fehler beim Senden: ${response.status}`, 'error');
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
logToConsole(`❌ Network Error: ${error.message}`, 'error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Tests
|
||||
async function testConnection() {
|
||||
const output = document.getElementById('connection-output');
|
||||
output.style.display = 'block';
|
||||
output.innerHTML = '<div class="log-entry info">Teste Verbindung...</div>';
|
||||
|
||||
try {
|
||||
const response = await fetch(LOG_URL, {
|
||||
method: 'OPTIONS'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
output.innerHTML = `
|
||||
<div class="log-entry success">✅ Verbindung erfolgreich!</div>
|
||||
<div class="log-entry info">Status: ${response.status}</div>
|
||||
<div class="log-entry info">CORS: Aktiviert</div>
|
||||
`;
|
||||
} else {
|
||||
output.innerHTML = `
|
||||
<div class="log-entry error">❌ Verbindung fehlgeschlagen</div>
|
||||
<div class="log-entry error">Status: ${response.status}</div>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
output.innerHTML = `
|
||||
<div class="log-entry error">❌ Netzwerkfehler: ${error.message}</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function sendTestLog(level, message) {
|
||||
sendLog(level, message, {
|
||||
test: true,
|
||||
source: 'test-interface'
|
||||
});
|
||||
}
|
||||
|
||||
function triggerRuntimeError() {
|
||||
logToConsole('⚠️ Löse Runtime Error aus...', 'warning');
|
||||
setTimeout(() => {
|
||||
throw new Error('Test Runtime Error - Dies ist beabsichtigt!');
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function triggerPromiseRejection() {
|
||||
logToConsole('⚠️ Löse Promise Rejection aus...', 'warning');
|
||||
Promise.reject('Test Promise Rejection - Dies ist beabsichtigt!');
|
||||
}
|
||||
|
||||
function triggerConsoleError() {
|
||||
console.error('Test Console Error - Dies ist beabsichtigt!', {
|
||||
errorCode: 123,
|
||||
test: true
|
||||
});
|
||||
}
|
||||
|
||||
function triggerConsoleWarn() {
|
||||
console.warn('Test Console Warning - Dies ist beabsichtigt!', {
|
||||
warningCode: 456,
|
||||
test: true
|
||||
});
|
||||
}
|
||||
|
||||
function triggerResourceError() {
|
||||
logToConsole('⚠️ Löse Resource Loading Error aus...', 'warning');
|
||||
const img = document.createElement('img');
|
||||
img.src = 'https://example.com/nonexistent-image-12345.jpg';
|
||||
document.body.appendChild(img);
|
||||
setTimeout(() => img.remove(), 1000);
|
||||
}
|
||||
|
||||
function testWithContext() {
|
||||
sendLog('INFO', 'Test mit Kontext-Informationen', {
|
||||
testId: 'CTX-001',
|
||||
user: 'Tester',
|
||||
timestamp: Date.now(),
|
||||
browser: navigator.userAgent
|
||||
});
|
||||
}
|
||||
|
||||
function testWithComplexContext() {
|
||||
sendLog('INFO', 'Test mit komplexem Kontext', {
|
||||
testId: 'CTX-002',
|
||||
nested: {
|
||||
level1: {
|
||||
level2: {
|
||||
value: 'Tief verschachtelt'
|
||||
}
|
||||
}
|
||||
},
|
||||
array: [1, 2, 3, 4, 5],
|
||||
metadata: {
|
||||
display: 'Test-Display',
|
||||
location: 'Test-Raum',
|
||||
version: '1.2'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function sendMultipleLogs(count) {
|
||||
logToConsole(`🚀 Sende ${count} Logs...`, 'info');
|
||||
const startTime = Date.now();
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
await sendLog('INFO', `Performance Test Log ${i + 1}/${count}`, {
|
||||
test: 'performance',
|
||||
index: i,
|
||||
total: count
|
||||
});
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
logToConsole(`✅ ${count} Logs in ${duration}ms gesendet (${(duration/count).toFixed(2)}ms pro Log)`, 'success');
|
||||
}
|
||||
|
||||
// Global Error Handler für Tests
|
||||
window.onerror = function(msg, url, line, col, error) {
|
||||
sendLog('FATAL', `JavaScript Error: ${msg}`, {
|
||||
file: url,
|
||||
line: line,
|
||||
column: col,
|
||||
stack: error?.stack
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener('unhandledrejection', function(event) {
|
||||
sendLog('ERROR', `Unhandled Promise Rejection: ${event.reason}`, {
|
||||
promise: event.promise?.toString()
|
||||
});
|
||||
});
|
||||
|
||||
const originalError = console.error;
|
||||
console.error = function(...args) {
|
||||
sendLog('ERROR', `Console Error: ${args.join(' ')}`);
|
||||
originalError.apply(console, args);
|
||||
};
|
||||
|
||||
const originalWarn = console.warn;
|
||||
console.warn = function(...args) {
|
||||
sendLog('WARNING', `Console Warning: ${args.join(' ')}`);
|
||||
originalWarn.apply(console, args);
|
||||
};
|
||||
|
||||
// Initial Log
|
||||
logToConsole('🚀 Test-Interface geladen', 'success');
|
||||
sendLog('INFO', 'Test-Interface geöffnet', {
|
||||
userAgent: navigator.userAgent,
|
||||
viewport: `${window.innerWidth}x${window.innerHeight}`
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
319
public/_cabinet/view-logs.php
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
<?php
|
||||
// view-logs.php - Einfacher Log-Viewer für Cabinet Digital Signage
|
||||
// ACHTUNG: In Produktion mit Passwortschutz versehen!
|
||||
|
||||
$logsDir = __DIR__ . '/logs';
|
||||
$selectedFile = $_GET['file'] ?? null;
|
||||
$logLevel = $_GET['level'] ?? 'all';
|
||||
$lines = $_GET['lines'] ?? 100;
|
||||
|
||||
// Verfügbare Log-Dateien
|
||||
$logFiles = glob($logsDir . '/*.log');
|
||||
rsort($logFiles); // Neueste zuerst
|
||||
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Cabinet Logs Viewer</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Courier New', monospace;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: #252526;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #009FE3;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
select,
|
||||
button,
|
||||
input {
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #3e3e42;
|
||||
background-color: #2d2d30;
|
||||
color: #d4d4d4;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
background-color: #009FE3;
|
||||
border: none;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #0082bd;
|
||||
}
|
||||
|
||||
.log-container {
|
||||
background-color: #252526;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
overflow-x: auto;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.log-line {
|
||||
margin-bottom: 10px;
|
||||
padding: 5px;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
|
||||
.log-line.FATAL {
|
||||
border-left-color: #f44336;
|
||||
background-color: rgba(244, 67, 54, 0.1);
|
||||
}
|
||||
|
||||
.log-line.ERROR {
|
||||
border-left-color: #ff9800;
|
||||
background-color: rgba(255, 152, 0, 0.1);
|
||||
}
|
||||
|
||||
.log-line.WARNING {
|
||||
border-left-color: #ffeb3b;
|
||||
background-color: rgba(255, 235, 59, 0.1);
|
||||
}
|
||||
|
||||
.log-line.INFO {
|
||||
border-left-color: #4caf50;
|
||||
}
|
||||
|
||||
.level {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.level.FATAL {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.level.ERROR {
|
||||
background-color: #ff9800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.level.WARNING {
|
||||
background-color: #ffeb3b;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.level.INFO {
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
color: #858585;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.message {
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
.context {
|
||||
margin-left: 30px;
|
||||
color: #9cdcfe;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background-color: #252526;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 2em;
|
||||
font-weight: bold;
|
||||
color: #009FE3;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #858585;
|
||||
margin-top: 5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>📊 Cabinet Digital Signage - Log Viewer</h1>
|
||||
<div class="controls">
|
||||
<select name="file" id="file-select" onchange="location.href='?file=' + this.value + '&lines=<?= $lines ?>'">
|
||||
<option value="">-- Wähle Log-Datei --</option>
|
||||
<?php foreach ($logFiles as $file):
|
||||
$filename = basename($file);
|
||||
$selected = ($selectedFile === $file) ? 'selected' : '';
|
||||
?>
|
||||
<option value="<?= htmlspecialchars($file) ?>" <?= $selected ?>>
|
||||
<?= htmlspecialchars($filename) ?> (<?= formatBytes(filesize($file)) ?>)
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
|
||||
<select name="lines" id="lines-select" onchange="location.href='?file=<?= urlencode($selectedFile) ?>&lines=' + this.value">
|
||||
<option value="50" <?= $lines == 50 ? 'selected' : '' ?>>50 Zeilen</option>
|
||||
<option value="100" <?= $lines == 100 ? 'selected' : '' ?>>100 Zeilen</option>
|
||||
<option value="500" <?= $lines == 500 ? 'selected' : '' ?>>500 Zeilen</option>
|
||||
<option value="1000" <?= $lines == 1000 ? 'selected' : '' ?>>1000 Zeilen</option>
|
||||
<option value="-1" <?= $lines == -1 ? 'selected' : '' ?>>Alle</option>
|
||||
</select>
|
||||
|
||||
<button onclick="location.reload()">🔄 Aktualisieren</button>
|
||||
<button onclick="autoRefresh()">⏱️ Auto-Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
if ($selectedFile && file_exists($selectedFile)) {
|
||||
// Statistiken
|
||||
$allContent = file_get_contents($selectedFile);
|
||||
$stats = [
|
||||
'FATAL' => substr_count($allContent, '[FATAL]'),
|
||||
'ERROR' => substr_count($allContent, '[ERROR]'),
|
||||
'WARNING' => substr_count($allContent, '[WARNING]'),
|
||||
'INFO' => substr_count($allContent, '[INFO]')
|
||||
];
|
||||
?>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" style="color: #f44336;"><?= $stats['FATAL'] ?></div>
|
||||
<div class="stat-label">FATAL Errors</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" style="color: #ff9800;"><?= $stats['ERROR'] ?></div>
|
||||
<div class="stat-label">Errors</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" style="color: #ffeb3b;"><?= $stats['WARNING'] ?></div>
|
||||
<div class="stat-label">Warnings</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" style="color: #4caf50;"><?= $stats['INFO'] ?></div>
|
||||
<div class="stat-label">Info Messages</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="log-container">
|
||||
<?php
|
||||
// Log-Datei einlesen
|
||||
$logLines = file($selectedFile, FILE_IGNORE_NEW_LINES);
|
||||
|
||||
// Nur die letzten X Zeilen anzeigen
|
||||
if ($lines > 0) {
|
||||
$logLines = array_slice($logLines, -$lines);
|
||||
}
|
||||
|
||||
$currentEntry = '';
|
||||
foreach ($logLines as $line) {
|
||||
// Erkennen von Log-Level
|
||||
if (preg_match('/\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]\s+\[([A-Z]+)\s*\]\s+(.+)/', $line, $matches)) {
|
||||
if ($currentEntry) {
|
||||
echo "</div>";
|
||||
}
|
||||
$timestamp = $matches[1];
|
||||
$level = trim($matches[2]);
|
||||
$message = htmlspecialchars($matches[3]);
|
||||
|
||||
echo "<div class='log-line {$level}'>";
|
||||
echo "<span class='timestamp'>{$timestamp}</span>";
|
||||
echo "<span class='level {$level}'>{$level}</span>";
|
||||
echo "<span class='message'>{$message}</span>";
|
||||
$currentEntry = $level;
|
||||
} else {
|
||||
// Kontext-Zeilen
|
||||
echo "<div class='context'>" . htmlspecialchars($line) . "</div>";
|
||||
}
|
||||
}
|
||||
if ($currentEntry) {
|
||||
echo "</div>";
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
|
||||
<?php } else { ?>
|
||||
<div class="log-container">
|
||||
<p>Bitte wähle eine Log-Datei aus dem Dropdown-Menü.</p>
|
||||
<?php if (empty($logFiles)): ?>
|
||||
<p style="color: #ff9800;">⚠️ Keine Log-Dateien gefunden. Stelle sicher, dass das Logging aktiv ist und das Verzeichnis 'logs/' existiert.</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<script>
|
||||
let autoRefreshInterval = null;
|
||||
|
||||
function autoRefresh() {
|
||||
if (autoRefreshInterval) {
|
||||
clearInterval(autoRefreshInterval);
|
||||
autoRefreshInterval = null;
|
||||
alert('Auto-Refresh deaktiviert');
|
||||
} else {
|
||||
autoRefreshInterval = setInterval(() => {
|
||||
location.reload();
|
||||
}, 10000); // Alle 10 Sekunden
|
||||
alert('Auto-Refresh aktiviert (alle 10 Sekunden)');
|
||||
}
|
||||
}
|
||||
|
||||
// Automatisch zum Ende scrollen
|
||||
const logContainer = document.querySelector('.log-container');
|
||||
if (logContainer) {
|
||||
logContainer.scrollTop = logContainer.scrollHeight;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
<?php
|
||||
function formatBytes($bytes, $precision = 2)
|
||||
{
|
||||
$units = ['B', 'KB', 'MB', 'GB'];
|
||||
$bytes = max($bytes, 0);
|
||||
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
|
||||
$pow = min($pow, count($units) - 1);
|
||||
$bytes /= (1 << (10 * $pow));
|
||||
return round($bytes, $precision) . ' ' . $units[$pow];
|
||||
}
|
||||
?>
|
||||
13171
public/flux/flux.js
Normal file
BIN
public/img/logos/b2a-logo-negativ.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
public/img/logos/b2a-logo-positiv.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
public/img/logos/b2in-logo-negative.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
public/img/logos/b2in-logo-positive.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
public/img/logos/stileigentum-logo-negativ.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
public/img/logos/stileigentum-logo-positiv.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
public/img/logos/style2own-logo-negativ.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
public/img/logos/style2own-logo-positiv.png
Normal file
|
After Width: | Height: | Size: 46 KiB |