PSmartplace> <style> @import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500;600&family=DM+Mono:wght@400;500&display=swap'); *{box-sizing:border-box;margin:0;padding:0;} body{font-family:'DM Sans',sans-serif;background:#0d0f14;color:#e8e6e0;min-height:100vh;} ::-webkit-scrollbar{width:4px;} ::-webkit-scrollbar-thumb{background:#2a2d36;border-radius:2px;} @keyframes fadeIn{from{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:translateY(0)}} @keyframes pulse{0%,100%{opacity:1}50%{opacity:0.4}} @keyframes spin{to{transform:rotate(360deg)}} /* LOGIN */ #login-screen{display:flex;align-items:center;justify-content:center;min-height:100vh;background:#0d0f14;} .login-box{background:#10131a;border:1px solid #1e2130;border-radius:20px;padding:40px;width:360px;animation:fadeIn .3s ease;} .login-logo{display:flex;align-items:center;gap:10px;margin-bottom:32px;} .login-logo .icon{width:36px;height:36px;background:linear-gradient(135deg,#4a7aff,#7f5af0);border-radius:10px;display:flex;align-items:center;justify-content:center;} .login-logo span{font-size:18px;font-weight:600;} .login-box input{width:100%;background:#1a1d24;border:1px solid #2a2d36;border-radius:10px;padding:12px 16px;color:#e8e6e0;font-family:inherit;font-size:14px;margin-bottom:12px;outline:none;} .login-box input:focus{border-color:#4a7aff;} .login-box button{width:100%;background:linear-gradient(135deg,#4a7aff,#7f5af0);border:none;border-radius:10px;padding:13px;color:white;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit;margin-top:4px;} .login-error{color:#E24B4A;font-size:12px;margin-top:8px;text-align:center;} /* MAIN APP */ #app{display:none;flex-direction:column;min-height:100vh;} /* TOPBAR */ .topbar{background:#10131a;border-bottom:1px solid #1e2130;padding:0 28px;display:flex;align-items:center;justify-content:space-between;height:56px;flex-shrink:0;} .topbar-logo{display:flex;align-items:center;gap:10px;} .topbar-logo .icon{width:28px;height:28px;background:linear-gradient(135deg,#4a7aff,#7f5af0);border-radius:8px;display:flex;align-items:center;justify-content:center;} .topbar-logo span{font-weight:600;font-size:15px;} .topbar-right{display:flex;align-items:center;gap:12px;} .online-badge{display:flex;align-items:center;gap:6px;font-size:12px;color:#6b7080;} .online-dot{width:7px;height:7px;border-radius:50%;background:#1D9E75;animation:pulse 2s infinite;} .logout-btn{background:none;border:1px solid #2a2d36;border-radius:8px;color:#6b7080;font-size:12px;padding:6px 12px;cursor:pointer;font-family:inherit;} .logout-btn:hover{color:#e8e6e0;border-color:#4a7aff;} /* LAYOUT */ .layout{display:flex;flex:1;overflow:hidden;} /* SIDEBAR */ .sidebar{width:200px;background:#10131a;border-right:1px solid #1e2130;padding:20px 12px;display:flex;flex-direction:column;gap:4px;} .nav-item{display:flex;align-items:center;gap:10px;padding:9px 12px;border-radius:8px;cursor:pointer;font-size:14px;color:#6b7080;border:none;background:none;font-family:inherit;width:100%;text-align:left;transition:all .15s;} .nav-item:hover{background:#1a1d24;color:#e8e6e0;} .nav-item.active{background:#1a1d24;color:#e8e6e0;font-weight:500;} .sidebar-section{font-size:11px;color:#3a3d4a;text-transform:uppercase;letter-spacing:.8px;padding:16px 12px 6px;margin-top:auto;border-top:1px solid #1e2130;} /* CONTENT */ .content{flex:1;overflow:auto;padding:28px;} .page{display:none;animation:fadeIn .2s ease;} .page.active{display:block;} .page-header{margin-bottom:24px;} .page-header h1{font-size:22px;font-weight:600;letter-spacing:-.5px;margin-bottom:4px;} .page-header p{color:#6b7080;font-size:13px;} .page-header-row{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:24px;} /* STATS */ .stats-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-bottom:24px;} .stat-card{border-radius:12px;padding:16px 20px;border:1px solid;} .stat-card .value{font-size:28px;font-weight:600;font-family:'DM Mono',monospace;} .stat-card .label{font-size:12px;color:#6b7080;margin-top:2px;} /* TABLE */ .table-wrap{background:#10131a;border:1px solid #1e2130;border-radius:14px;overflow:hidden;} .table-header{display:grid;padding:10px 18px;border-bottom:1px solid #1e2130;} .table-header span{font-size:11px;color:#3a3d4a;text-transform:uppercase;letter-spacing:.8px;} .table-row{display:grid;padding:13px 18px;border-bottom:1px solid #1a1d24;cursor:pointer;align-items:center;transition:background .15s;} .table-row:hover{background:#16191f;} .table-row:last-child{border-bottom:none;} .cell-main{font-size:13px;font-weight:500;} .cell-sub{font-size:11px;color:#6b7080;font-family:'DM Mono',monospace;} .cell-secondary{font-size:13px;color:#9ba0b0;} .cell-small{font-size:12px;color:#6b7080;font-family:'DM Mono',monospace;} /* STATUS BADGE */ .status-badge{display:inline-flex;align-items:center;gap:5px;font-size:12px;padding:3px 10px;border-radius:20px;} .status-online{color:#1D9E75;background:#0d2d1f;} .status-offline{color:#E24B4A;background:#2d0d0d;} /* BUTTONS */ .btn{border:none;border-radius:8px;padding:9px 18px;font-size:13px;font-weight:500;cursor:pointer;font-family:inherit;transition:all .15s;} .btn:hover{opacity:.85;transform:scale(.97);} .btn-primary{background:linear-gradient(135deg,#4a7aff,#7f5af0);color:white;} .btn-secondary{background:#1a1d24;border:1px solid #2a2d36;color:#e8e6e0;} .btn-danger{background:#2d0d0d;border:1px solid #E24B4A44;color:#E24B4A;} .btn-sm{padding:6px 12px;font-size:12px;} /* MODAL */ .modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.7);display:flex;align-items:center;justify-content:center;z-index:1000;backdrop-filter:blur(4px);} .modal{background:#10131a;border:1px solid #1e2130;border-radius:16px;padding:28px;width:460px;animation:fadeIn .2s ease;} .modal h2{font-size:18px;font-weight:600;margin-bottom:20px;} .modal-footer{display:flex;gap:8px;justify-content:flex-end;margin-top:20px;} .form-group{margin-bottom:16px;} .form-group label{display:block;font-size:12px;color:#6b7080;margin-bottom:6px;text-transform:uppercase;letter-spacing:.5px;} .form-group input, .form-group select{width:100%;background:#1a1d24;border:1px solid #2a2d36;border-radius:8px;padding:10px 14px;color:#e8e6e0;font-family:inherit;font-size:13px;outline:none;} .form-group input:focus, .form-group select:focus{border-color:#4a7aff;} .form-group select option{background:#1a1d24;} /* NOTIFICATION */ .notif{position:fixed;top:20px;right:20px;padding:10px 18px;border-radius:10px;font-size:13px;animation:fadeIn .2s ease;z-index:2000;} .notif-success{background:#0f3d2a;border:1px solid #1D9E75;color:#5DCAA5;} .notif-error{background:#3d1010;border:1px solid #E24B4A;color:#F09595;} /* PLAYLIST CARDS */ .cards-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:14px;} .pl-card{background:#10131a;border:1px solid #1e2130;border-radius:14px;overflow:hidden;cursor:pointer;transition:background .15s;} .pl-card:hover{background:#16191f;} .pl-thumb{height:90px;display:flex;align-items:center;justify-content:center;position:relative;} .pl-thumb-label{position:absolute;bottom:8px;left:8px;background:rgba(0,0,0,.6);border-radius:5px;padding:2px 7px;font-size:11px;color:white;} .pl-thumb-duration{position:absolute;bottom:8px;right:8px;background:rgba(0,0,0,.6);border-radius:5px;padding:2px 7px;font-size:11px;color:white;font-family:'DM Mono',monospace;} .pl-body{padding:14px 16px;} .pl-name{font-size:14px;font-weight:500;margin-bottom:10px;} .pl-actions{display:flex;gap:8px;} /* MEDIA */ .media-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:12px;} .media-card{background:#10131a;border:1px solid #1e2130;border-radius:12px;overflow:hidden;cursor:pointer;transition:background .15s;} .media-card:hover{background:#16191f;} .media-thumb{height:100px;display:flex;align-items:center;justify-content:center;position:relative;} .media-type{position:absolute;top:8px;right:8px;background:rgba(0,0,0,.6);border-radius:4px;padding:2px 6px;font-size:10px;color:white;} .media-info{padding:10px 12px;} .media-name{font-size:12px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;} .media-size{font-size:11px;color:#6b7080;margin-top:2px;} /* UPLOAD ZONE */ .upload-zone{border:2px dashed #2a2d36;border-radius:14px;padding:32px 24px;text-align:center;cursor:pointer;margin-bottom:20px;transition:all .2s;} .upload-zone:hover{border-color:#4a7aff;background:#131620;} .upload-zone p{font-size:14px;color:#6b7080;margin-top:8px;} .upload-zone span{color:#4a7aff;} /* SETTINGS */ .settings-card{background:#10131a;border:1px solid #1e2130;border-radius:14px;padding:22px;margin-bottom:16px;} .settings-card h3{font-size:15px;font-weight:500;margin-bottom:16px;} .setting-row{display:flex;justify-content:space-between;align-items:center;padding:12px 0;border-bottom:1px solid #1a1d24;} .setting-row:last-child{border-bottom:none;} .setting-label{font-size:13px;} .setting-desc{font-size:12px;color:#6b7080;margin-top:2px;} .toggle{width:40px;height:22px;background:#2a2d36;border-radius:11px;cursor:pointer;position:relative;transition:background .2s;border:none;} .toggle.on{background:#4a7aff;} .toggle::after{content:'';position:absolute;width:16px;height:16px;background:white;border-radius:50%;top:3px;left:3px;transition:left .2s;} .toggle.on::after{left:21px;} /* BRANDING */ .branding-preview{background:#000;border-radius:10px;height:120px;display:flex;align-items:flex-end;justify-content:flex-end;padding:10px;margin-top:12px;position:relative;} .branding-badge{background:rgba(0,0,0,.7);border-radius:6px;padding:4px 10px;font-size:11px;color:rgba(255,255,255,.8);} /* SCHEDULE */ .schedule-grid{display:grid;grid-template-columns:100px 1fr 1fr;gap:8px;align-items:center;margin-bottom:8px;} .schedule-day{font-size:13px;color:#9ba0b0;} .schedule-time{background:#1a1d24;border:1px solid #2a2d36;border-radius:6px;padding:6px 10px;color:#e8e6e0;font-family:inherit;font-size:12px;width:100%;outline:none;} </style> </head> <body> <!-- LOGIN --> <div id="login-screen"> <div class="login-box"> <div class="login-logo"> <div class="icon"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg> </div> <span>PSmartplace</span> </div> <input type="text" id="login-user" placeholder="Benutzername" value="admin"> <input type="password" id="login-pass" placeholder="Passwort" value="admin123"> <button onclick="doLogin()">Anmelden</button> <div class="login-error" id="login-error"></div> </div> </div> <!-- APP --> <div id="app"> <!-- Topbar --> <div class="topbar"> <div class="topbar-logo"> <div class="icon"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg> </div> <span>PSmartplace Signage</span> </div> <div class="topbar-right"> <div class="online-badge"><div class="online-dot"></div><span id="online-count">0 online</span></div> <button class="logout-btn" onclick="doLogout()">Abmelden</button> </div> </div> <div class="layout"> <!-- Sidebar --> <div class="sidebar"> <button class="nav-item active" onclick="showPage('screens',this)"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg> Screens </button> <button class="nav-item" onclick="showPage('playlists',this)"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15V19M15 15V19M3 8H21M3 12H21M3 16H9M3 20H9M3 4H21"/></svg> Playlists </button> <button class="nav-item" onclick="showPage('media',this)"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg> Medien </button> <button class="nav-item" onclick="showPage('settings',this)"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg> Einstellungen </button> </div> <!-- Content --> <div class="content"> <!-- SCREENS PAGE --> <div class="page active" id="page-screens"> <div class="page-header-row"> <div class="page-header"> <h1>Screens</h1> <p>Verwalte alle angeschlossenen Displays</p> </div> <button class="btn btn-primary" onclick="openModal('screen')">+ Neuer Screen</button> </div> <div class="stats-grid"> <div class="stat-card" style="background:#0d2d1f;border-color:#1D9E7522;"> <div class="value" style="color:#1D9E75" id="stat-online">0</div> <div class="label">Online</div> </div> <div class="stat-card" style="background:#2d0d0d;border-color:#E24B4A22;"> <div class="value" style="color:#E24B4A" id="stat-offline">0</div> <div class="label">Offline</div> </div> <div class="stat-card" style="background:#1a1d24;border-color:#2a2d36;"> <div class="value" style="color:#e8e6e0" id="stat-total">0</div> <div class="label">Gesamt</div> </div> </div> <div class="table-wrap"> <div class="table-header" style="grid-template-columns:1fr 1fr 1fr 100px 80px;"> <span>Screen</span><span>Kunde</span><span>Playlist</span><span>Status</span><span></span> </div> <div id="screens-list"></div> </div> </div> <!-- PLAYLISTS PAGE --> <div class="page" id="page-playlists"> <div class="page-header-row"> <div class="page-header"> <h1>Playlists</h1> <p>Erstelle und verwalte deine Abspielfolgen</p> </div> <button class="btn btn-primary" onclick="openModal('playlist')">+ Neue Playlist</button> </div> <div class="cards-grid" id="playlists-list"></div> </div> <!-- MEDIA PAGE --> <div class="page" id="page-media"> <div class="page-header"> <h1>Medien</h1> <p>Bilder und Videos für deine Playlists</p> </div> <div class="upload-zone" onclick="document.getElementById('file-input').click()" ondragover="event.preventDefault();this.style.borderColor='#4a7aff'" ondragleave="this.style.borderColor='#2a2d36'" ondrop="handleDrop(event)"> <input type="file" id="file-input" style="display:none" multiple accept="image/*,video/*" onchange="handleUpload(this.files)"> <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#3a3d4a" stroke-width="1.5" style="margin:0 auto"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg> <p>Dateien hier ablegen oder <span>klicken zum Hochladen</span></p> </div> <div class="media-grid" id="media-list"></div> </div> <!-- SETTINGS PAGE --> <div class="page" id="page-settings"> <div class="page-header"> <h1>Einstellungen</h1> <p>Globale Systemeinstellungen</p> </div> <!-- Branding --> <div class="settings-card"> <h3>Branding</h3> <div class="setting-row"> <div> <div class="setting-label">„Powered by PSmartplace" anzeigen</div> <div class="setting-desc">Wird unten rechts auf allen Screens angezeigt</div> </div> <button class="toggle on" id="branding-toggle" onclick="toggleSetting('branding',this)"></button> </div> <div class="setting-row"> <div> <div class="setting-label">Branding-Text</div> </div> <input type="text" value="Powered by PSmartplace" style="background:#1a1d24;border:1px solid #2a2d36;border-radius:6px;padding:6px 10px;color:#e8e6e0;font-size:12px;width:220px;outline:none;"> </div> <div class="branding-preview"> <div class="branding-badge">Powered by PSmartplace</div> </div> </div> <!-- Zeitschaltuhr --> <div class="settings-card"> <h3>Standard-Betriebszeiten</h3> <div class="setting-row"> <div> <div class="setting-label">Zeitschaltuhr aktivieren</div> <div class="setting-desc">Screens schalten sich außerhalb der Zeiten ab</div> </div> <button class="toggle" id="schedule-toggle" onclick="toggleSetting('schedule',this)"></button> </div> <div style="margin-top:16px;"> <div class="schedule-grid" style="margin-bottom:8px;"> <span style="font-size:11px;color:#3a3d4a;">TAG</span> <span style="font-size:11px;color:#3a3d4a;">VON</span> <span style="font-size:11px;color:#3a3d4a;">BIS</span> </div> ${['Montag','Dienstag','Mittwoch','Donnerstag','Freitag','Samstag','Sonntag'].map((d,i) => ` <div class="schedule-grid"> <span class="schedule-day">${d}</span> <input type="time" class="schedule-time" value="${i<5?'08:00':'09:00'}"> <input type="time" class="schedule-time" value="${i<5?'18:00':i===5?'14:00':''}"> </div>`).join('')} </div> <button class="btn btn-primary btn-sm" style="margin-top:16px;" onclick="showNotif('Zeiten gespeichert!','success')">Speichern</button> </div> <!-- Admin --> <div class="settings-card"> <h3>Admin-Zugangsdaten ändern</h3> <div class="form-group"> <label>Neues Passwort</label> <input type="password" placeholder="Neues Passwort eingeben"> </div> <button class="btn btn-primary btn-sm" onclick="showNotif('Passwort geändert!','success')">Speichern</button> </div> </div> </div> </div> </div> <!-- MODALS --> <div id="modal-overlay" class="modal-overlay" style="display:none" onclick="if(event.target===this)closeModal()"> <!-- Screen Modal --> <div class="modal" id="modal-screen" style="display:none"> <h2>Neuer Screen</h2> <div class="form-group"><label>Screen-Name</label><input id="s-name" placeholder="z.B. Lobby Eingang"></div> <div class="form-group"><label>Kunde</label><input id="s-client" placeholder="z.B. Müller GmbH"></div> <div class="form-group"><label>Standort</label><input id="s-location" placeholder="z.B. Frankfurt"></div> <div class="form-group"><label>Screen-ID</label><input id="s-id" placeholder="z.B. SCR001"></div> <div class="modal-footer"> <button class="btn btn-secondary" onclick="closeModal()">Abbrechen</button> <button class="btn btn-primary" onclick="saveScreen()">Speichern</button> </div> </div> <!-- Playlist Modal --> <div class="modal" id="modal-playlist" style="display:none"> <h2>Neue Playlist</h2> <div class="form-group"><label>Name</label><input id="p-name" placeholder="z.B. Sommer-Promo"></div> <div class="form-group"><label>Hintergrundfarbe (Demo-Slide)</label><input id="p-color" type="color" value="#1a3a6a" style="height:40px;cursor:pointer;"></div> <div class="form-group"><label>Text auf Demo-Slide</label><input id="p-label" placeholder="z.B. Willkommen!"></div> <div class="modal-footer"> <button class="btn btn-secondary" onclick="closeModal()">Abbrechen</button> <button class="btn btn-primary" onclick="savePlaylist()">Speichern</button> </div> </div> </div> <!-- NOTIFICATION --> <div id="notif" style="display:none"></div> <script> const SERVER = window.location.origin; let currentUser = null; let screens = [], playlists = [], mediaFiles = []; // ===== AUTH ===== async function doLogin() { const username = document.getElementById('login-user').value; const password = document.getElementById('login-pass').value; try { const res = await fetch(`${SERVER}/api/login`, { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({username, password}) }); const data = await res.json(); if (data.success) { currentUser = data.user; document.getElementById('login-screen').style.display = 'none'; document.getElementById('app').style.display = 'flex'; loadAll(); setInterval(loadScreens, 30000); } else { document.getElementById('login-error').textContent = 'Falsche Zugangsdaten'; } } catch(e) { document.getElementById('login-error').textContent = 'Server nicht erreichbar'; } } function doLogout() { currentUser = null; document.getElementById('login-screen').style.display = 'flex'; document.getElementById('app').style.display = 'none'; } // ===== NAVIGATION ===== function showPage(name, btn) { document.querySelectorAll('.page').forEach(p => p.classList.remove('active')); document.querySelectorAll('.nav-item').forEach(b => b.classList.remove('active')); document.getElementById('page-' + name).classList.add('active'); if (btn) btn.classList.add('active'); } // ===== LOAD DATA ===== async function loadAll() { await Promise.all([loadScreens(), loadPlaylists()]); } async function loadScreens() { try { const res = await fetch(`${SERVER}/api/screens`); screens = await res.json(); renderScreens(); } catch(e) {} } async function loadPlaylists() { try { const res = await fetch(`${SERVER}/api/playlists`); playlists = await res.json(); renderPlaylists(); } catch(e) {} } // ===== RENDER SCREENS ===== function renderScreens() { const online = screens.filter(s => s.status === 'online').length; const offline = screens.filter(s => s.status === 'offline').length; document.getElementById('stat-online').textContent = online; document.getElementById('stat-offline').textContent = offline; document.getElementById('stat-total').textContent = screens.length; document.getElementById('online-count').textContent = `${online} online`; const list = document.getElementById('screens-list'); if (!screens.length) { list.innerHTML = '<div style="padding:24px;text-align:center;color:#6b7080;font-size:13px;">Noch keine Screens — klicke "+ Neuer Screen"</div>'; return; } list.innerHTML = screens.map(s => { const pl = playlists.find(p => p.id === s.playlistId); const ago = s.lastSeen ? timeAgo(s.lastSeen) : 'Nie'; return `<div class="table-row" style="grid-template-columns:1fr 1fr 1fr 100px 80px;"> <div><div class="cell-main">${s.name}</div><div class="cell-sub">${s.id}</div></div> <div class="cell-secondary">${s.client || '—'}</div> <div class="cell-small">${pl ? pl.name : '—'}</div> <div><span class="status-badge ${s.status === 'online' ? 'status-online' : 'status-offline'}">${s.status === 'online' ? '● Online' : '○ Offline'}</span></div> <div><button class="btn btn-secondary btn-sm" onclick="restartScreen('${s.id}')">Neustart</button></div> </div>`; }).join(''); } // ===== RENDER PLAYLISTS ===== function renderPlaylists() { const list = document.getElementById('playlists-list'); const colors = ['#E8593C','#3B8BD4','#1D9E75','#BA7517','#7F77DD','#D85A30']; if (!playlists.length) { list.innerHTML = '<div style="color:#6b7080;font-size:13px;">Noch keine Playlists</div>'; return; } list.innerHTML = playlists.map((p, i) => ` <div class="pl-card"> <div class="pl-thumb" style="background:${colors[i % colors.length]}"> <div class="pl-thumb-label">${p.items ? p.items.length : 0} Slides</div> </div> <div class="pl-body"> <div class="pl-name">${p.name}</div> <div class="pl-actions"> <button class="btn btn-secondary btn-sm" style="flex:1" onclick="showNotif('Bearbeiten kommt bald!','success')">Bearbeiten</button> <button class="btn btn-secondary btn-sm" onclick="showNotif('Dupliziert!','success')">⎘</button> </div> </div> </div>`).join(''); } // ===== SAVE ===== async function saveScreen() { const screen = { id: document.getElementById('s-id').value || 'SCR' + Date.now(), name: document.getElementById('s-name').value, client: document.getElementById('s-client').value, location: document.getElementById('s-location').value, }; await fetch(`${SERVER}/api/screens`, { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(screen) }); closeModal(); await loadScreens(); showNotif('Screen gespeichert!', 'success'); } async function savePlaylist() { const playlist = { name: document.getElementById('p-name').value, items: [{ id: '1', type: 'color', content: document.getElementById('p-color').value, duration: 5, transition: 'fade', label: document.getElementById('p-label').value }] }; await fetch(`${SERVER}/api/playlists`, { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(playlist) }); closeModal(); await loadPlaylists(); showNotif('Playlist gespeichert!', 'success'); } // ===== MODAL ===== function openModal(type) { document.getElementById('modal-overlay').style.display = 'flex'; document.querySelectorAll('.modal').forEach(m => m.style.display = 'none'); document.getElementById('modal-' + type).style.display = 'block'; } function closeModal() { document.getElementById('modal-overlay').style.display = 'none'; } // ===== HELPERS ===== function timeAgo(ts) { const s = Math.floor((Date.now() - ts) / 1000); if (s < 60) return `vor ${s}s`; if (s < 3600) return `vor ${Math.floor(s/60)}m`; return `vor ${Math.floor(s/3600)}h`; } function restartScreen(id) { showNotif('Neustart-Signal gesendet!', 'success'); } function showNotif(msg, type) { const el = document.getElementById('notif'); el.textContent = msg; el.className = `notif notif-${type}`; el.style.display = 'block'; setTimeout(() => el.style.display = 'none', 3000); } function toggleSetting(key, btn) { btn.classList.toggle('on'); showNotif('Einstellung gespeichert!', 'success'); } function handleUpload(files) { showNotif(`${files.length} Datei(en) hochgeladen!`, 'success'); } function handleDrop(e) { e.preventDefault(); e.currentTarget.style.borderColor = '#2a2d36'; handleUpload(e.dataTransfer.files); } // Enter-Taste im Login document.getElementById('login-pass').addEventListener('keydown', e => { if(e.key === 'Enter') doLogin(); }); </script> </body> </html>