(() => { let map = null; let socket = null; let activeDots = null; let animateInterval = null; let statsInterval = null; let refreshMapSize = null; const TABLE_LIMIT = 20; function initConnectionsPage() { const mapEl = document.getElementById('map'); if (!mapEl || mapEl.dataset.bound) return; if (typeof L === 'undefined' || typeof io === 'undefined') return; mapEl.dataset.bound = '1'; activeDots = new Map(); // Initialize the map centered on Europe map = L.map('map').setView([50.0, 10.0], 5); refreshMapSize = () => setTimeout(() => map && map.invalidateSize(), 150); refreshMapSize(); window.addEventListener('resize', refreshMapSize); window.addEventListener('orientationchange', refreshMapSize); // Add OpenStreetMap tile layer L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors', maxZoom: 19 }).addTo(map); // Custom pulsing circle marker const PulsingMarker = L.CircleMarker.extend({ options: { radius: 10, fillColor: '#ff4444', color: '#ff0000', weight: 2, opacity: 1, fillOpacity: 0.8 } }); function updatePopup(dot) { const location = dot.connections[0]; const content = ` ${location.city}, ${location.country}
Connections: ${dot.mass}
Latest: ${new Date(location.timestamp).toLocaleString()} `; dot.marker.bindPopup(content); } function getTimeAgo(timestamp) { const seconds = Math.floor((Date.now() - timestamp) / 1000); if (seconds < 60) return `${seconds}s ago`; const minutes = Math.floor(seconds / 60); if (minutes < 60) return `${minutes}m ago`; const hours = Math.floor(minutes / 60); return `${hours}h ago`; } function updateMapStats() { let mostRecent = null; activeDots.forEach(dot => { if (!mostRecent || dot.lastUpdate > mostRecent) { mostRecent = dot.lastUpdate; } }); if (mostRecent) { const timeAgo = getTimeAgo(mostRecent); const lastConnection = document.getElementById('last-connection'); if (lastConnection) lastConnection.textContent = timeAgo; } } function addOrUpdateDot(lat, lon, city, country, timestamp) { const key = `${lat},${lon}`; const now = Date.now(); if (activeDots.has(key)) { const dot = activeDots.get(key); dot.mass += 1; dot.lastUpdate = now; dot.connections.push({ city, country, timestamp }); const newRadius = 10 + Math.log(dot.mass) * 5; dot.marker.setRadius(newRadius); updatePopup(dot); } else { const marker = new PulsingMarker([lat, lon], { radius: 10 }).addTo(map); const dot = { marker: marker, mass: 1, createdAt: now, lastUpdate: now, lat: lat, lon: lon, connections: [{ city, country, timestamp }] }; activeDots.set(key, dot); updatePopup(dot); } updateMapStats(); } function formatBytes(bytes) { if (!bytes && bytes !== 0) return '-'; const units = ['B', 'KB', 'MB', 'GB', 'TB']; let num = bytes; let unitIndex = 0; while (num >= 1024 && unitIndex < units.length - 1) { num /= 1024; unitIndex++; } const rounded = num % 1 === 0 ? num.toFixed(0) : num.toFixed(1); return `${rounded} ${units[unitIndex]}`; } function toBool(val) { if (val === true || val === 1) return true; if (typeof val === 'string') { const lower = val.toLowerCase(); return lower === 'true' || lower === '1' || lower === 'yes'; } return false; } function animateDots() { if (!map) return; const now = Date.now(); const fadeTime = 10 * 60 * 1000; // 10 minutes activeDots.forEach((dot, key) => { const age = now - dot.lastUpdate; if (age > fadeTime) { map.removeLayer(dot.marker); activeDots.delete(key); } else { const fadeProgress = age / fadeTime; const baseRadius = 10 + Math.log(dot.mass) * 5; const currentRadius = baseRadius * (1 - fadeProgress * 0.8); dot.marker.setRadius(Math.max(currentRadius, 2)); const opacity = 1 - fadeProgress * 0.7; dot.marker.setStyle({ fillOpacity: opacity * 0.8, opacity: opacity }); } }); updateMapStats(); } animateInterval = setInterval(animateDots, 100); statsInterval = setInterval(() => { if (activeDots.size > 0) { updateMapStats(); } }, 1000); // WebSocket connection // Force polling to avoid websocket upgrade issues behind some proxies. socket = io({ transports: ['polling'], upgrade: false }); socket.on('connect', () => { socket.emit('request_initial_data'); socket.emit('request_map_data'); }); socket.on('map_initial_data', function(data) { data.connections.forEach(conn => { if (conn.lat && conn.lon) { addOrUpdateDot(conn.lat, conn.lon, conn.city, conn.country, conn.timestamp); } }); }); socket.on('new_connection', function(data) { if (data.lat && data.lon) { addOrUpdateDot(data.lat, data.lon, data.city, data.country, data.timestamp); } }); function updateStats(data) { const headerCount = document.getElementById('connectionsTotalHeader'); if (headerCount) headerCount.textContent = `${data.length} Verbindungen`; const totalConnections = document.getElementById('totalConnections'); if (totalConnections) totalConnections.textContent = data.length; const totalBytes = data.reduce((sum, rec) => { const val = parseFloat(rec.filesize); return sum + (isNaN(val) ? 0 : val); }, 0); const totalSize = document.getElementById('totalSize'); if (totalSize) totalSize.textContent = formatBytes(totalBytes); const cachedCount = data.reduce((sum, rec) => sum + (toBool(rec.cached) ? 1 : 0), 0); const pct = data.length ? ((cachedCount / data.length) * 100).toFixed(0) : 0; const cachedPercent = document.getElementById('cachedPercent'); if (cachedPercent) cachedPercent.textContent = `${pct}%`; } function createRow(record, animate = true) { const tr = document.createElement('tr'); tr.dataset.timestamp = record.timestamp; if (animate) { tr.classList.add('slide-in'); tr.addEventListener('animationend', () => tr.classList.remove('slide-in'), { once: true }); } tr.innerHTML = ` ${record.timestamp} ${record.location} ${record.user_agent} ${record.full_path} ${record.filesize_human || record.filesize} ${record.mime_typ} ${record.cached} `; return tr; } function updateTable(data) { const tbody = document.getElementById('connectionsTableBody'); if (!tbody) return; const existing = {}; Array.from(tbody.children).forEach(r => { existing[r.dataset.timestamp] = r; }); const frag = document.createDocumentFragment(); data.forEach(rec => { let row = existing[rec.timestamp]; if (row) { row.innerHTML = createRow(rec, false).innerHTML; row.classList.remove('slide-out'); } else { row = createRow(rec, true); } frag.appendChild(row); }); tbody.innerHTML = ''; tbody.appendChild(frag); } function animateTableWithNewRow(data) { const tbody = document.getElementById('connectionsTableBody'); if (!tbody) return; const currentTs = new Set([...tbody.children].map(r => r.dataset.timestamp)); const newRecs = data.filter(r => !currentTs.has(r.timestamp)); if (newRecs.length) { const temp = createRow(newRecs[0], false); temp.style.visibility = 'hidden'; tbody.appendChild(temp); const h = temp.getBoundingClientRect().height; temp.remove(); tbody.style.transform = `translateY(${h}px)`; setTimeout(() => { updateTable(data); tbody.style.transition = 'none'; tbody.style.transform = 'translateY(0)'; void tbody.offsetWidth; tbody.style.transition = 'transform 0.5s ease-out'; }, 500); } else { updateTable(data); } } socket.on('recent_connections', data => { updateStats(data); const tableData = data.slice(0, TABLE_LIMIT); animateTableWithNewRow(tableData); }); } function destroyConnectionsPage() { if (socket) { socket.off(); socket.disconnect(); socket = null; } if (animateInterval) { clearInterval(animateInterval); animateInterval = null; } if (statsInterval) { clearInterval(statsInterval); statsInterval = null; } if (refreshMapSize) { window.removeEventListener('resize', refreshMapSize); window.removeEventListener('orientationchange', refreshMapSize); refreshMapSize = null; } if (map) { map.remove(); map = null; } activeDots = null; } if (window.PageRegistry && typeof window.PageRegistry.register === 'function') { window.PageRegistry.register('connections', { init: initConnectionsPage, destroy: destroyConnectionsPage }); } })();