(() => {
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 = `