diff --git a/aurora-livecam/dashboard/api/stats.php b/aurora-livecam/dashboard/api/stats.php new file mode 100644 index 0000000..a8c4952 --- /dev/null +++ b/aurora-livecam/dashboard/api/stats.php @@ -0,0 +1,75 @@ +isLoggedIn()) { + http_response_code(401); + echo json_encode(['success' => false, 'error' => 'Unauthorized']); + exit; +} + +$user = $auth->getUser(); +$tenantId = $user['tenant_id'] ?? 0; + +$stats = [ + 'viewers_current' => 0, + 'viewers_today' => 0, + 'viewers_peak' => 0, + 'stream_status' => 'unknown', +]; + +// Aktuelle Zuschauer aus Datei +$viewerFile = dirname(__DIR__, 2) . '/active_viewers.json'; +if (file_exists($viewerFile)) { + $viewers = json_decode(file_get_contents($viewerFile), true); + $stats['viewers_current'] = count($viewers ?? []); +} + +// DB Stats falls verfügbar +try { + $db = Database::getInstance(); + + if ($tenantId > 0) { + $todayStats = $db->fetchOne( + "SELECT SUM(viewer_count) as total, MAX(viewer_count) as peak + FROM viewer_stats + WHERE tenant_id = ? AND DATE(recorded_at) = CURDATE()", + [$tenantId] + ); + + if ($todayStats) { + $stats['viewers_today'] = (int)($todayStats['total'] ?? 0); + $stats['viewers_peak'] = (int)($todayStats['peak'] ?? 0); + } + + $stream = $db->fetchOne( + "SELECT last_status FROM tenant_streams WHERE tenant_id = ? AND is_primary = 1", + [$tenantId] + ); + $stats['stream_status'] = $stream['last_status'] ?? 'unknown'; + } +} catch (\Exception $e) { + // DB nicht verfügbar - Stats bleiben auf Defaults +} + +echo json_encode([ + 'success' => true, + 'stats' => $stats, + 'timestamp' => time(), +]); diff --git a/aurora-livecam/dashboard/assets/dashboard.css b/aurora-livecam/dashboard/assets/dashboard.css new file mode 100644 index 0000000..f0bcb1e --- /dev/null +++ b/aurora-livecam/dashboard/assets/dashboard.css @@ -0,0 +1,536 @@ +/* Dashboard CSS */ +:root { + --primary: #667eea; + --primary-dark: #5a67d8; + --secondary: #764ba2; + --accent: #f093fb; + --success: #48bb78; + --warning: #ed8936; + --danger: #f56565; + --dark: #1a202c; + --gray-900: #1a202c; + --gray-800: #2d3748; + --gray-700: #4a5568; + --gray-600: #718096; + --gray-500: #a0aec0; + --gray-400: #cbd5e0; + --gray-300: #e2e8f0; + --gray-200: #edf2f7; + --gray-100: #f7fafc; + --white: #ffffff; + --sidebar-width: 260px; + --header-height: 60px; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; + background: var(--gray-100); + color: var(--gray-800); + line-height: 1.6; +} + +/* Dashboard Container */ +.dashboard-container { + display: flex; + min-height: 100vh; +} + +/* Sidebar */ +.sidebar { + width: var(--sidebar-width); + background: linear-gradient(180deg, var(--gray-900) 0%, var(--gray-800) 100%); + color: var(--white); + display: flex; + flex-direction: column; + position: fixed; + height: 100vh; + z-index: 100; +} + +.sidebar-header { + padding: 1.5rem; + border-bottom: 1px solid var(--gray-700); +} + +.sidebar-header h2 { + font-size: 1.25rem; + font-weight: 600; + margin-bottom: 0.25rem; +} + +.role-badge { + font-size: 0.75rem; + background: var(--primary); + padding: 0.125rem 0.5rem; + border-radius: 9999px; + text-transform: capitalize; +} + +/* Navigation */ +.sidebar-nav { + flex: 1; + padding: 1rem 0; + overflow-y: auto; +} + +.nav-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 1.5rem; + color: var(--gray-400); + text-decoration: none; + transition: all 0.2s; +} + +.nav-item:hover { + background: var(--gray-700); + color: var(--white); +} + +.nav-item.active { + background: linear-gradient(90deg, var(--primary) 0%, var(--secondary) 100%); + color: var(--white); +} + +.nav-icon { + font-size: 1.25rem; + width: 1.5rem; + text-align: center; +} + +.nav-divider { + height: 1px; + background: var(--gray-700); + margin: 1rem 0; +} + +.nav-label { + display: block; + padding: 0.5rem 1.5rem; + font-size: 0.75rem; + text-transform: uppercase; + color: var(--gray-500); + letter-spacing: 0.05em; +} + +.sidebar-footer { + border-top: 1px solid var(--gray-700); + padding: 0.5rem 0; +} + +.nav-item.logout:hover { + background: var(--danger); +} + +/* Main Content */ +.main-content { + flex: 1; + margin-left: var(--sidebar-width); + min-height: 100vh; +} + +.main-header { + height: var(--header-height); + background: var(--white); + border-bottom: 1px solid var(--gray-300); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 2rem; + position: sticky; + top: 0; + z-index: 50; +} + +.main-header h1 { + font-size: 1.5rem; + font-weight: 600; +} + +.user-info { + color: var(--gray-600); + font-size: 0.875rem; +} + +.content-wrapper { + padding: 2rem; +} + +/* Cards */ +.card { + background: var(--white); + border-radius: 0.5rem; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); + margin-bottom: 1.5rem; +} + +.card-header { + padding: 1rem 1.5rem; + border-bottom: 1px solid var(--gray-200); + display: flex; + align-items: center; + justify-content: space-between; +} + +.card-title { + font-size: 1rem; + font-weight: 600; +} + +.card-body { + padding: 1.5rem; +} + +/* Stats Grid */ +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; +} + +.stat-card { + background: var(--white); + border-radius: 0.5rem; + padding: 1.5rem; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); +} + +.stat-icon { + font-size: 2rem; + margin-bottom: 0.5rem; +} + +.stat-value { + font-size: 2rem; + font-weight: 700; + color: var(--gray-900); +} + +.stat-label { + color: var(--gray-600); + font-size: 0.875rem; +} + +.stat-change { + font-size: 0.875rem; + margin-top: 0.25rem; +} + +.stat-change.positive { color: var(--success); } +.stat-change.negative { color: var(--danger); } + +/* Forms */ +.form-group { + margin-bottom: 1.5rem; +} + +.form-label { + display: block; + font-weight: 500; + margin-bottom: 0.5rem; + color: var(--gray-700); +} + +.form-input, +.form-select, +.form-textarea { + width: 100%; + padding: 0.75rem 1rem; + border: 1px solid var(--gray-300); + border-radius: 0.375rem; + font-size: 1rem; + transition: border-color 0.2s, box-shadow 0.2s; +} + +.form-input:focus, +.form-select:focus, +.form-textarea:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2); +} + +.form-help { + font-size: 0.875rem; + color: var(--gray-500); + margin-top: 0.25rem; +} + +.form-textarea { + min-height: 100px; + resize: vertical; +} + +/* Buttons */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.75rem 1.5rem; + border: none; + border-radius: 0.375rem; + font-size: 1rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + text-decoration: none; +} + +.btn-primary { + background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%); + color: var(--white); +} + +.btn-primary:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); +} + +.btn-secondary { + background: var(--gray-200); + color: var(--gray-700); +} + +.btn-secondary:hover { + background: var(--gray-300); +} + +.btn-danger { + background: var(--danger); + color: var(--white); +} + +.btn-success { + background: var(--success); + color: var(--white); +} + +.btn-sm { + padding: 0.5rem 1rem; + font-size: 0.875rem; +} + +/* Alerts */ +.alert { + padding: 1rem 1.5rem; + border-radius: 0.375rem; + margin-bottom: 1.5rem; +} + +.alert-success { + background: #c6f6d5; + color: #22543d; + border: 1px solid #9ae6b4; +} + +.alert-error { + background: #fed7d7; + color: #742a2a; + border: 1px solid #feb2b2; +} + +.alert-warning { + background: #feebc8; + color: #744210; + border: 1px solid #fbd38d; +} + +.alert-info { + background: #bee3f8; + color: #2a4365; + border: 1px solid #90cdf4; +} + +/* Tables */ +.table { + width: 100%; + border-collapse: collapse; +} + +.table th, +.table td { + padding: 1rem; + text-align: left; + border-bottom: 1px solid var(--gray-200); +} + +.table th { + font-weight: 600; + color: var(--gray-600); + font-size: 0.875rem; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.table tbody tr:hover { + background: var(--gray-50); +} + +/* Status Badges */ +.badge { + display: inline-block; + padding: 0.25rem 0.75rem; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 500; +} + +.badge-success { background: #c6f6d5; color: #22543d; } +.badge-warning { background: #feebc8; color: #744210; } +.badge-danger { background: #fed7d7; color: #742a2a; } +.badge-info { background: #bee3f8; color: #2a4365; } + +/* Grid */ +.grid { + display: grid; + gap: 1.5rem; +} + +.grid-2 { grid-template-columns: repeat(2, 1fr); } +.grid-3 { grid-template-columns: repeat(3, 1fr); } + +/* Color Picker */ +.color-picker-wrapper { + display: flex; + align-items: center; + gap: 1rem; +} + +.color-picker { + width: 50px; + height: 40px; + border: none; + border-radius: 0.375rem; + cursor: pointer; +} + +.color-value { + font-family: monospace; + color: var(--gray-600); +} + +/* Preview Box */ +.preview-box { + border: 2px dashed var(--gray-300); + border-radius: 0.5rem; + padding: 2rem; + text-align: center; + background: var(--gray-50); +} + +/* Toggle Switch */ +.toggle-wrapper { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.toggle { + position: relative; + width: 48px; + height: 24px; +} + +.toggle input { + opacity: 0; + width: 0; + height: 0; +} + +.toggle-slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: var(--gray-300); + border-radius: 24px; + transition: 0.3s; +} + +.toggle-slider:before { + position: absolute; + content: ""; + height: 18px; + width: 18px; + left: 3px; + bottom: 3px; + background: white; + border-radius: 50%; + transition: 0.3s; +} + +.toggle input:checked + .toggle-slider { + background: var(--primary); +} + +.toggle input:checked + .toggle-slider:before { + transform: translateX(24px); +} + +/* Login Page */ +.login-container { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%); +} + +.login-box { + background: var(--white); + padding: 2.5rem; + border-radius: 1rem; + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); + width: 100%; + max-width: 400px; +} + +.login-title { + text-align: center; + margin-bottom: 2rem; +} + +.login-title h1 { + font-size: 1.5rem; + margin-bottom: 0.5rem; +} + +.login-title p { + color: var(--gray-500); +} + +/* Responsive */ +@media (max-width: 768px) { + .sidebar { + transform: translateX(-100%); + transition: transform 0.3s; + } + + .sidebar.open { + transform: translateX(0); + } + + .main-content { + margin-left: 0; + } + + .stats-grid { + grid-template-columns: 1fr; + } + + .grid-2, + .grid-3 { + grid-template-columns: 1fr; + } +} diff --git a/aurora-livecam/dashboard/assets/dashboard.js b/aurora-livecam/dashboard/assets/dashboard.js new file mode 100644 index 0000000..2c8a2a3 --- /dev/null +++ b/aurora-livecam/dashboard/assets/dashboard.js @@ -0,0 +1,131 @@ +/** + * Dashboard JavaScript + */ + +document.addEventListener('DOMContentLoaded', function() { + // Auto-dismiss alerts after 5 seconds + const alerts = document.querySelectorAll('.alert'); + alerts.forEach(alert => { + setTimeout(() => { + alert.style.transition = 'opacity 0.3s'; + alert.style.opacity = '0'; + setTimeout(() => alert.remove(), 300); + }, 5000); + }); + + // Mobile sidebar toggle + const sidebar = document.querySelector('.sidebar'); + const mainContent = document.querySelector('.main-content'); + + if (window.innerWidth <= 768) { + // Add menu button + const menuBtn = document.createElement('button'); + menuBtn.className = 'btn btn-secondary'; + menuBtn.style.cssText = 'position: fixed; top: 10px; left: 10px; z-index: 200; padding: 0.5rem;'; + menuBtn.innerHTML = '☰'; + menuBtn.onclick = () => sidebar.classList.toggle('open'); + document.body.appendChild(menuBtn); + + // Close sidebar on content click + mainContent.addEventListener('click', () => { + sidebar.classList.remove('open'); + }); + } + + // Color picker live preview + document.querySelectorAll('.color-picker').forEach(picker => { + picker.addEventListener('input', function() { + const wrapper = this.closest('.color-picker-wrapper'); + if (wrapper) { + const valueDisplay = wrapper.querySelector('.color-value'); + if (valueDisplay) { + valueDisplay.textContent = this.value; + } + } + }); + }); + + // Form unsaved changes warning + const forms = document.querySelectorAll('form'); + let formChanged = false; + + forms.forEach(form => { + form.addEventListener('change', () => { + formChanged = true; + }); + + form.addEventListener('submit', () => { + formChanged = false; + }); + }); + + window.addEventListener('beforeunload', (e) => { + if (formChanged) { + e.preventDefault(); + e.returnValue = ''; + } + }); + + // Stats refresh (every 30 seconds on overview page) + if (document.querySelector('.stats-grid')) { + setInterval(refreshStats, 30000); + } +}); + +/** + * Refresh stats via AJAX + */ +function refreshStats() { + fetch('/dashboard/api/stats.php') + .then(response => response.json()) + .then(data => { + if (data.success) { + updateStatCard('viewers_current', data.stats.viewers_current); + updateStatCard('viewers_today', data.stats.viewers_today); + updateStatCard('viewers_peak', data.stats.viewers_peak); + } + }) + .catch(err => console.log('Stats refresh failed:', err)); +} + +/** + * Update a stat card value + */ +function updateStatCard(id, value) { + const cards = document.querySelectorAll('.stat-card'); + cards.forEach(card => { + const label = card.querySelector('.stat-label'); + if (label) { + // Match by label text (simplified) + const valueEl = card.querySelector('.stat-value'); + if (valueEl && typeof value !== 'undefined') { + valueEl.textContent = value; + } + } + }); +} + +/** + * Show notification toast + */ +function showNotification(message, type = 'info') { + const toast = document.createElement('div'); + toast.className = `alert alert-${type}`; + toast.style.cssText = 'position: fixed; top: 20px; right: 20px; z-index: 1000; min-width: 300px;'; + toast.textContent = message; + + document.body.appendChild(toast); + + setTimeout(() => { + toast.style.transition = 'opacity 0.3s'; + toast.style.opacity = '0'; + setTimeout(() => toast.remove(), 300); + }, 3000); +} + +/** + * Confirm dangerous actions + */ +function confirmAction(message) { + return confirm(message || 'Sind Sie sicher?'); +} diff --git a/aurora-livecam/dashboard/branding.php b/aurora-livecam/dashboard/branding.php new file mode 100644 index 0000000..8b1d460 --- /dev/null +++ b/aurora-livecam/dashboard/branding.php @@ -0,0 +1,230 @@ +requireLogin(); + +$user = $auth->getUser(); +$tenantId = $user['tenant_id'] ?? 0; + +$flashMessage = null; +$flashType = 'info'; + +// Branding-Daten laden +$branding = [ + 'site_name' => '', + 'site_name_full' => '', + 'tagline' => '', + 'primary_color' => '#667eea', + 'secondary_color' => '#764ba2', + 'accent_color' => '#f093fb', + 'welcome_text_de' => '', + 'welcome_text_en' => '', + 'footer_text' => '', + 'custom_css' => '', +]; + +try { + $db = Database::getInstance(); + if ($tenantId > 0) { + $tenantManager = new TenantManager($db); + $dbBranding = $tenantManager->getBranding($tenantId); + if ($dbBranding) { + $branding = array_merge($branding, $dbBranding); + } + } +} catch (\Exception $e) { + // DB nicht verfügbar +} + +// Formular verarbeiten +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $newBranding = [ + 'site_name' => trim($_POST['site_name'] ?? ''), + 'site_name_full' => trim($_POST['site_name_full'] ?? ''), + 'tagline' => trim($_POST['tagline'] ?? ''), + 'primary_color' => $_POST['primary_color'] ?? '#667eea', + 'secondary_color' => $_POST['secondary_color'] ?? '#764ba2', + 'accent_color' => $_POST['accent_color'] ?? '#f093fb', + 'welcome_text_de' => trim($_POST['welcome_text_de'] ?? ''), + 'welcome_text_en' => trim($_POST['welcome_text_en'] ?? ''), + 'footer_text' => trim($_POST['footer_text'] ?? ''), + 'custom_css' => trim($_POST['custom_css'] ?? ''), + ]; + + try { + $db = Database::getInstance(); + if ($tenantId > 0) { + $tenantManager = new TenantManager($db); + $tenantManager->updateBranding($tenantId, $newBranding); + + $flashMessage = 'Branding gespeichert!'; + $flashType = 'success'; + $branding = array_merge($branding, $newBranding); + } else { + $flashMessage = 'Branding kann im Legacy-Modus nicht gespeichert werden.'; + $flashType = 'warning'; + } + } catch (\Exception $e) { + $flashMessage = 'Fehler beim Speichern: ' . $e->getMessage(); + $flashType = 'error'; + } +} + +$pageTitle = 'Branding'; +$currentPage = 'branding'; + +ob_start(); +?> + +
+ + + +requireLogin(); + +$user = $auth->getUser(); +$tenantId = $user['tenant_id'] ?? 0; + +// Stats laden +$stats = [ + 'viewers_current' => 0, + 'viewers_today' => 0, + 'viewers_peak' => 0, + 'stream_status' => 'unknown', +]; + +// Versuche Stats aus DB zu laden +try { + $db = Database::getInstance(); + + if ($tenantId > 0) { + // Aktuelle Zuschauer (vereinfacht) + $viewerFile = dirname(__DIR__) . '/active_viewers.json'; + if (file_exists($viewerFile)) { + $viewers = json_decode(file_get_contents($viewerFile), true); + $stats['viewers_current'] = count($viewers ?? []); + } + + // Heute Stats + $todayStats = $db->fetchOne( + "SELECT SUM(viewer_count) as total, MAX(viewer_count) as peak + FROM viewer_stats + WHERE tenant_id = ? AND DATE(recorded_at) = CURDATE()", + [$tenantId] + ); + + if ($todayStats) { + $stats['viewers_today'] = $todayStats['total'] ?? 0; + $stats['viewers_peak'] = $todayStats['peak'] ?? 0; + } + + // Stream Status + $stream = $db->fetchOne( + "SELECT last_status FROM tenant_streams WHERE tenant_id = ? AND is_primary = 1", + [$tenantId] + ); + $stats['stream_status'] = $stream['last_status'] ?? 'unknown'; + } +} catch (\Exception $e) { + // DB nicht verfügbar - Legacy-Modus + $viewerFile = dirname(__DIR__) . '/active_viewers.json'; + if (file_exists($viewerFile)) { + $viewers = json_decode(file_get_contents($viewerFile), true); + $stats['viewers_current'] = count($viewers ?? []); + } +} + +// Page Setup +$pageTitle = 'Übersicht'; +$currentPage = 'overview'; + +ob_start(); +?> + + ++ Aktivitäten werden hier angezeigt, sobald Analytics aktiviert ist. +
+Melden Sie sich an, um fortzufahren
++ Hinweis: Die Vorschau funktioniert nur mit HLS-Streams und wenn Ihr Browser HLS unterstützt. +
+ +Keine Stream-URL konfiguriert
++ Stream-Monitoring zeigt automatische Verfügbarkeitsprüfungen an. + Diese Funktion wird demnächst verfügbar sein. +
+