diff --git a/app/public/api/live-now.php b/app/public/api/live-now.php
index ea5d99b..aac8c8e 100644
--- a/app/public/api/live-now.php
+++ b/app/public/api/live-now.php
@@ -25,6 +25,14 @@ $items = $service->getCurrentProgrammes($data, $config);
$now = new DateTimeImmutable('now', new DateTimeZone($timezone));
+require_once '/var/www/src/Services/LogoService.php';
+
+foreach ($items as &$item) {
+ $item['logo_url'] = LogoService::getLogoUrl($item['channel_id']);
+}
+
+unset($item);
+
ApiResponse::json([
'ok' => true,
'timestamp' => $now->getTimestamp(),
diff --git a/app/public/assets/js/live-now.js b/app/public/assets/js/live-now.js
index e2aad63..1c962e8 100644
--- a/app/public/assets/js/live-now.js
+++ b/app/public/assets/js/live-now.js
@@ -160,3 +160,106 @@ document.addEventListener('DOMContentLoaded', () => {
liveNow.start();
});
+
+function dashboardCreateCard(item) {
+ const button = document.createElement('button');
+ button.className = 'dashboard-programme-card dashboard-programme';
+ button.type = 'button';
+ button.dataset.programmeId = `${item.channel_id}-${item.start}-${item.title}`;
+ button.dataset.dashboardKey = item.channel_id;
+
+ button.innerHTML = `
+
+
+
+
+ `;
+
+ dashboardUpdateCard(button, item);
+ return button;
+}
+
+function dashboardUpdateCard(card, item) {
+ card.querySelector('img').src = item.logo_url || '/assets/logos/default.svg';
+ card.querySelector('strong').textContent = item.title || '';
+ card.querySelector('span').textContent =
+ `${item.channel_name || ''} · noch ${TVGuideLive.calcRemainingMinutes(item.stop)} Min.`;
+
+ const bar = card.querySelector('.dashboard-progress div');
+ if (bar) {
+ bar.style.width = `${TVGuideLive.calcPercent(item.start, item.stop)}%`;
+ }
+
+ card.dataset.programmeId = `${item.channel_id}-${item.start}-${item.title}`;
+}
+
+function dashboardRender(grid, items) {
+ if (!grid) return;
+
+ // Beim ersten Live-Refresh vorhandene serverseitige Karten entfernen
+ if (!grid.dataset.dashboardInitialized) {
+ grid.innerHTML = '';
+ grid.dataset.dashboardInitialized = '1';
+ }
+
+ const seen = new Set();
+
+ items.slice(0, 6).forEach(item => {
+ const key = item.channel_id;
+ seen.add(key);
+
+ let card = grid.querySelector(`[data-dashboard-key="${CSS.escape(key)}"]`);
+
+ if (!card) {
+ card = dashboardCreateCard(item);
+ grid.appendChild(card);
+ } else {
+ dashboardUpdateCard(card, item);
+ }
+ });
+
+ grid.querySelectorAll('[data-dashboard-key]').forEach(card => {
+ if (!seen.has(card.dataset.dashboardKey)) {
+ card.remove();
+ }
+ });
+
+ if (items.length === 0) {
+ grid.innerHTML = 'Aktuell keine Sendungen.
';
+ }
+}
+
+async function dashboardRefreshNow() {
+ const nowGrid = document.querySelector('[data-dashboard-now]');
+ const favGrid = document.querySelector('[data-dashboard-favorites]');
+
+ if (!nowGrid && !favGrid) return;
+
+ try {
+ const data = await TVGuideLive.fetchJson('/api/live-now.php');
+ const items = data.items || [];
+
+ dashboardRender(nowGrid, items);
+
+ dashboardRender(
+ favGrid,
+ items.filter(item => item.favorite)
+ );
+ } catch (error) {
+ console.error('Dashboard konnte nicht aktualisiert werden:', error);
+ }
+}
+
+function dashboardTick() {
+ document.querySelectorAll('[data-dashboard-key]').forEach(card => {
+ const key = card.dataset.dashboardKey;
+ // vollständiger Refresh kommt alle 30 Sekunden; hier nur Balken/Restzeit über vorhandene DOM-Daten nicht möglich
+ });
+}
+
+document.addEventListener('DOMContentLoaded', () => {
+ dashboardRefreshNow();
+ setInterval(dashboardRefreshNow, 30000);
+});
diff --git a/app/templates/home.php b/app/templates/home.php
index 5398a0e..f06b4ad 100644
--- a/app/templates/home.php
+++ b/app/templates/home.php
@@ -110,7 +110,7 @@ $favoritesNow = array_values(array_filter($currentProgrammes, fn($item) => !empt
🔥 Jetzt läuft
-
+