let serverListLoaded = false; function getApiToken() { return document.querySelector('meta[name="api-token"]')?.getAttribute('content') || ''; } function toggleNotifications() { const dd = document.getElementById('notifDropdown'); if (dd.classList.contains('open')) { closeNotifications(); } else { openNotifications(); } } function openNotifications() { const dd = document.getElementById('notifDropdown'); dd.classList.add('open'); fetchNotifications(); const userMenu = document.getElementById('userMenu'); if (userMenu) userMenu.classList.remove('open'); document.addEventListener('click', notifOutsideClick); } function closeNotifications() { const dd = document.getElementById('notifDropdown'); dd.classList.remove('open'); document.removeEventListener('click', notifOutsideClick); } function notifOutsideClick(e) { const wrapper = document.querySelector('.notification-btn-wrapper'); if (!wrapper.contains(e.target)) { closeNotifications(); } const userWrapper = document.querySelector('.topbar-user-dropdown'); if (userWrapper && !userWrapper.contains(e.target)) { const userMenu = document.getElementById('userMenu'); if (userMenu) userMenu.classList.remove('open'); } } function fetchNotifications() { const list = document.getElementById('notifList'); list.innerHTML = '
Loading notifications...
'; const token = getApiToken(); const headers = { 'Accept': 'application/json' }; if (token) headers['Authorization'] = 'Bearer ' + token; fetch('/api/notifications?per_page=10', { headers }) .then(r => r.json()) .then(d => { if (!d.success) { list.innerHTML = '
Could not load notifications.
'; return; } renderNotifications(d.data || [], d.unread_count || 0, d.pagination?.total || 0); }) .catch(() => { list.innerHTML = '
Failed to load notifications.
'; }); } function renderNotifications(items, unreadCount, total) { const list = document.getElementById('notifList'); if (!items.length) { list.innerHTML = '
' + '' + 'No notifications
'; document.getElementById('markAllReadBtn').style.display = 'none'; return; } let html = ''; items.forEach(function(n) { const typeColors = { error: 'danger', warning: 'warning', info: 'info' }; const typeColor = typeColors[n.type] || 'info'; const icons = { error: 'fa-times-circle', warning: 'fa-exclamation-triangle', info: 'fa-info-circle' }; const icon = icons[n.type] || 'fa-info-circle'; html += '
' + '
' + '
' + '
' + escapeHtml(n.title) + '
' + '
' + escapeHtml(n.message) + '
' + '
' + (n.time_ago || '') + '
' + '
' + (n.is_read ? '' : '
') + '
'; }); if (total > items.length) { html += '' + 'View all notifications (' + total + ' total)' + ''; } list.innerHTML = html; document.getElementById('markAllReadBtn').style.display = unreadCount > 0 ? 'inline-flex' : 'none'; } function markRead(id, el) { if (el.classList.contains('notif-read')) return; const token = getApiToken(); const headers = { 'Accept': 'application/json' }; if (token) headers['Authorization'] = 'Bearer ' + token; fetch('/api/notifications/' + id + '/read', { method: 'POST', headers }) .then(r => r.json()) .then(d => { if (d.success) { el.classList.remove('notif-unread'); el.classList.add('notif-read'); const dot = el.querySelector('.notif-dot'); if (dot) dot.remove(); updateUnreadCount(); } }) .catch(function(){}); } function markAllRead() { const token = getApiToken(); const headers = { 'Accept': 'application/json' }; if (token) headers['Authorization'] = 'Bearer ' + token; fetch('/api/notifications/read-all', { method: 'POST', headers }) .then(r => r.json()) .then(d => { if (d.success) { document.querySelectorAll('.notif-item').forEach(function(el) { el.classList.remove('notif-unread'); el.classList.add('notif-read'); const dot = el.querySelector('.notif-dot'); if (dot) dot.remove(); }); document.getElementById('markAllReadBtn').style.display = 'none'; updateUnreadCount(); } }) .catch(function(){}); } function updateUnreadCount() { const token = getApiToken(); const headers = { 'Accept': 'application/json' }; if (token) headers['Authorization'] = 'Bearer ' + token; fetch('/api/notifications/unread-count', { headers }) .then(r => r.json()) .then(d => { const count = d.unread_count || 0; const badge = document.getElementById('notificationBadge'); if (count > 0) { badge.textContent = count > 99 ? '99+' : count; badge.style.display = 'inline-flex'; } else { badge.style.display = 'none'; } }) .catch(function(){}); } function escapeHtml(str) { const d = document.createElement('div'); d.textContent = str; return d.innerHTML; } setInterval(updateUnreadCount, 60000); updateUnreadCount(); function toggleServerList(e) { e.preventDefault(); const item = e.currentTarget.closest('.nav-item-accordion'); const sub = document.getElementById('serverList'); const chevron = item.querySelector('.nav-chevron'); if (item.classList.contains('expanded')) { item.classList.remove('expanded'); sub.style.maxHeight = '0'; return; } item.classList.add('expanded'); if (!serverListLoaded) { serverListLoaded = true; fetch('/sidebar/servers') .then(r => r.json()) .then(d => { if (d.success && d.data) { let html = ' All Servers'; d.data.forEach(s => { const status = s.current_status === 'online' ? 'success' : s.current_status === 'offline' ? 'danger' : 'muted'; html += '' + ' ' + escapeHtml(s.name) + ''; }); sub.innerHTML = html; } else { sub.innerHTML = ''; } sub.style.maxHeight = sub.scrollHeight + 'px'; }) .catch(() => { sub.innerHTML = ''; sub.style.maxHeight = sub.scrollHeight + 'px'; }); } else { sub.style.maxHeight = sub.scrollHeight + 'px'; } } function toggleUserMenu(e) { e.stopPropagation(); const menu = document.getElementById('userMenu'); if (!menu) return; const isOpen = menu.classList.contains('open'); document.querySelectorAll('.dropdown-menu.open').forEach(function(m) { if (m !== menu) m.classList.remove('open'); }); if (!isOpen) { menu.classList.add('open'); closeNotifications(); setTimeout(function() { document.addEventListener('click', closeUserMenu); }, 0); } else { menu.classList.remove('open'); } } function closeUserMenu(e) { const menu = document.getElementById('userMenu'); if (!menu) return; const wrapper = menu.closest('.topbar-user-dropdown'); if (wrapper && !wrapper.contains(e.target)) { menu.classList.remove('open'); document.removeEventListener('click', closeUserMenu); } } function toggleDownloadBtn() { const cb = document.getElementById('agreeTerms'); document.getElementById('downloadConfirmBtn').disabled = !cb.checked; } function showDownloadModal() { const cb = document.getElementById('agreeTerms'); cb.checked = false; document.getElementById('downloadConfirmBtn').disabled = true; document.getElementById('downloadModal').style.display = 'flex'; document.getElementById('downloadProgressWrap').style.display = 'none'; document.getElementById('downloadModalFooter').style.display = 'flex'; document.getElementById('downloadConfirmBtn').innerHTML = ' Download APK'; document.getElementById('downloadProgressBar').style.width = '0%'; document.getElementById('downloadPercent').textContent = '0%'; } function closeDownloadModal() { document.getElementById('downloadModal').style.display = 'none'; } function startDownload() { const btn = document.getElementById('downloadConfirmBtn'); btn.disabled = true; btn.innerHTML = ' Downloading...'; document.getElementById('downloadProgressWrap').style.display = 'block'; document.getElementById('downloadModalFooter').style.display = 'none'; const anchor = document.createElement('a'); anchor.href = '/sysadmin.apk'; anchor.download = 'sysadmin.apk'; document.body.appendChild(anchor); anchor.click(); document.body.removeChild(anchor); let pct = 0; const interval = setInterval(function() { pct += Math.floor(Math.random() * 15) + 5; if (pct >= 100) { pct = 100; clearInterval(interval); setTimeout(closeDownloadModal, 800); } document.getElementById('downloadProgressBar').style.width = pct + '%'; document.getElementById('downloadPercent').textContent = pct + '%'; }, 300); }