MeshDD-Bot/static/js/map.js
ppfeiffer 0d6b26f4f8 feat: v0.5.0 - Benutzerverwaltung mit Session-Authentifizierung
Rollen-basiertes Zugriffsystem (public/user/admin), Registrierung mit
E-Mail-Verifikation, bcrypt Passwort-Hashing, Admin-Benutzerverwaltung.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 19:38:17 +01:00

211 lines
6.6 KiB
JavaScript

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: `<svg width="24" height="24" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="8" fill="${color}" stroke="#fff" stroke-width="2" opacity="0.9"/>
</svg>`,
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 `<div class="node-tooltip">
<strong>${escapeHtml(name)}</strong><br>
<span class="label">Hardware:</span> ${escapeHtml(hw)}<br>
<span class="label">Hops:</span> ${hops}<br>
<span class="label">SNR:</span> ${snr}<br>
<span class="label">Batterie:</span> ${bat}<br>
<span class="label">Hoehe:</span> ${alt}<br>
<span class="label">Zuletzt:</span> ${lastSeen}
</div>`;
}
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 = '<strong>Hops</strong>';
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 += `<div class="legend-item"><span class="legend-dot" style="background:${color}"></span>${label}</div>`;
});
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: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
maxZoom: 19
}).addTo(map);
legend.addTo(map);
// Invalidate size after short delay so sidebar layout settles
setTimeout(() => { map.invalidateSize(); }, 200);
connectWebSocket();