const markers = {}; const nodeData = {}; const statusDot = document.getElementById('statusDot'); const statusText = document.getElementById('statusText'); const nodeCount = document.getElementById('nodeCount'); let currentUser = null; let ws; // Auth check fetch('/api/auth/me').then(r => r.ok ? r.json() : null).then(u => { currentUser = u; updateNavbar(); updateSidebar(); }); function updateNavbar() { if (currentUser) { document.getElementById('userName').textContent = currentUser.name; document.getElementById('userMenu').classList.remove('d-none'); document.getElementById('loginBtn').classList.add('d-none'); } else { document.getElementById('userMenu').classList.add('d-none'); document.getElementById('loginBtn').classList.remove('d-none'); } } function updateSidebar() { const isAdmin = currentUser && currentUser.role === 'admin'; document.querySelectorAll('.sidebar-admin').forEach(el => { el.style.display = isAdmin ? '' : 'none'; }); } const hopColors = { 0: '#2196F3', 1: '#4CAF50', 2: '#FF9800', 3: '#F44336', 4: '#9C27B0', 5: '#795548', }; const hopColorDefault = '#9E9E9E'; function getHopColor(hopCount) { if (hopCount == null) return hopColorDefault; if (hopCount >= 6) return hopColorDefault; return hopColors[hopCount] || hopColorDefault; } function createIcon(color) { return L.divIcon({ className: '', html: ` `, iconSize: [24, 24], iconAnchor: [12, 12], popupAnchor: [0, -14], tooltipAnchor: [14, 0] }); } function nodeTooltip(node) { const name = node.long_name || node.short_name || node.node_id; const hops = node.hop_count != null ? node.hop_count : '?'; const bat = node.battery != null ? `${node.battery}%` : '-'; const snr = node.snr != null ? `${node.snr.toFixed(1)} dB` : '-'; const hw = node.hw_model || '-'; const alt = node.alt != null ? `${Math.round(node.alt)} m` : '-'; const lastSeen = node.last_seen ? new Date(node.last_seen * 1000).toLocaleString('de-DE') : '-'; return `
${escapeHtml(name)}
Hardware: ${escapeHtml(hw)}
Hops: ${hops}
SNR: ${snr}
Batterie: ${bat}
Hoehe: ${alt}
Zuletzt: ${lastSeen}
`; } function updateMarker(node) { if (!node.lat || !node.lon) return; const id = node.node_id; nodeData[id] = node; const icon = createIcon(getHopColor(node.hop_count)); if (markers[id]) { markers[id].setLatLng([node.lat, node.lon]); markers[id].setIcon(icon); markers[id].setTooltipContent(nodeTooltip(node)); } else { markers[id] = L.marker([node.lat, node.lon], { icon }) .addTo(map) .bindTooltip(nodeTooltip(node), { direction: 'right', className: 'node-tooltip-wrap' }); } } function fitBounds() { const coords = Object.values(markers).map(m => m.getLatLng()); if (coords.length > 0) { map.invalidateSize(); map.fitBounds(L.latLngBounds(coords).pad(0.1), { maxZoom: 14 }); } } function connectWebSocket() { const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; ws = new WebSocket(`${proto}//${location.host}/ws`); ws.onopen = () => { statusDot.classList.add('connected'); statusText.textContent = 'Verbunden'; }; ws.onclose = () => { statusDot.classList.remove('connected'); statusText.textContent = 'Reconnect...'; setTimeout(connectWebSocket, 3000); }; ws.onerror = () => { ws.close(); }; ws.onmessage = (event) => { const msg = JSON.parse(event.data); switch (msg.type) { case 'initial': msg.data.forEach(node => updateMarker(node)); nodeCount.textContent = `${Object.keys(markers).length} Nodes`; fitBounds(); break; case 'node_update': updateMarker(msg.data); nodeCount.textContent = `${Object.keys(markers).length} Nodes`; break; } }; } function escapeHtml(str) { if (!str) return ''; const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } // Legend const legend = L.control({ position: 'bottomright' }); legend.onAdd = function () { const div = L.DomUtil.create('div', 'legend'); div.innerHTML = 'Hops'; const entries = [ [0, 'Direkt'], [1, '1 Hop'], [2, '2 Hops'], [3, '3 Hops'], [4, '4 Hops'], [5, '5+ Hops'], [null, 'Unbekannt'], ]; entries.forEach(([hop, label]) => { const color = hop != null ? (hopColors[hop] || hopColorDefault) : hopColorDefault; div.innerHTML += `
${label}
`; }); return div; }; // Theme toggle const themeToggle = document.getElementById('themeToggle'); const themeIcon = document.getElementById('themeIcon'); function applyTheme(theme) { document.documentElement.setAttribute('data-bs-theme', theme); themeIcon.className = theme === 'dark' ? 'bi bi-sun-fill' : 'bi bi-moon-fill'; localStorage.setItem('theme', theme); } applyTheme(localStorage.getItem('theme') || 'dark'); themeToggle.addEventListener('click', () => { const current = document.documentElement.getAttribute('data-bs-theme'); applyTheme(current === 'dark' ? 'light' : 'dark'); }); // Sidebar toggle (mobile) const sidebarToggle = document.getElementById('sidebarToggle'); const sidebar = document.getElementById('sidebar'); const sidebarBackdrop = document.getElementById('sidebarBackdrop'); if (sidebarToggle) { sidebarToggle.addEventListener('click', () => sidebar.classList.toggle('open')); sidebarBackdrop.addEventListener('click', () => sidebar.classList.remove('open')); } // Init map after layout is ready const map = L.map('map').setView([51.1657, 10.4515], 6); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap', maxZoom: 19 }).addTo(map); legend.addTo(map); // Invalidate size after short delay so sidebar layout settles setTimeout(() => { map.invalidateSize(); }, 200); connectWebSocket();