23-01-2026

This commit is contained in:
Kevin Adametz 2026-01-23 17:33:10 +01:00
parent 07959c0ba2
commit 854ce02bf6
166 changed files with 32909 additions and 1262 deletions

View 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

View 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

View 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

View 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!**

View 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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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
View 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.";
}

View 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>

View 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>

View 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
View 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>

View 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>

View 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']);
}

View file

@ -0,0 +1 @@
[2026-01-19 08:45:35] [INFO] Logging-System wurde eingerichtet

118
public/_cabinet/setup-logging.sh Executable file
View 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."

View 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>

View 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

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB