diff --git a/public/app.js b/public/app.js index 0e5ebc7..d39ee3e 100644 --- a/public/app.js +++ b/public/app.js @@ -96,16 +96,145 @@ document.getElementById('diagnosticsModal').addEventListener('click', (e) => { document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { closeDiagnostics(); + closeMap(); } }); +// Map Logic +let map = null; +let mapInitialized = false; +let peerMarkers = {}; // id -> marker +let ipCache = {}; // ip -> { lat, lon } +let lastPeerData = []; + +const openMap = () => { + document.getElementById('mapModal').classList.add('active'); + if (!mapInitialized) { + initMap(); + } else { + setTimeout(() => { + map.invalidateSize(); + }, 100); + } + if (lastPeerData.length > 0) { + updateMap(lastPeerData); + } +} + +const closeMap = () => { + document.getElementById('mapModal').classList.remove('active'); +} + +document.getElementById('mapModal').addEventListener('click', (e) => { + if (e.target.id === 'mapModal') { + closeMap(); + } +}); + +const initMap = () => { + if (mapInitialized) return; + + map = L.map('map').setView([20, 0], 2); + + L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { + attribution: '© OpenStreetMap contributors © CARTO', + subdomains: 'abcd', + maxZoom: 19 + }).addTo(map); + + mapInitialized = true; + + setTimeout(() => { + map.invalidateSize(); + }, 100); +} + +const fetchLocation = async (ip) => { + if (ipCache[ip]) return ipCache[ip]; + + // Skip local IPs + if (ip === '127.0.0.1' || ip === '::1' || ip.startsWith('192.168.') || ip.startsWith('10.') || ip.startsWith('172.16.')) { + return null; + } + + try { + const res = await fetch(`https://ipwho.is/${ip}`); + const data = await res.json(); + if (data.success) { + const loc = { lat: data.latitude, lon: data.longitude, city: data.city, country: data.country }; + ipCache[ip] = loc; + return loc; + } + } catch (e) { + console.error('Geo fetch failed', e); + } + return null; +} + +const updateMap = async (peers) => { + if (!mapInitialized || !peers) return; + + const currentIds = new Set(peers.map(p => p.id)); + + // Remove old markers + for (const id in peerMarkers) { + if (!currentIds.has(id)) { + map.removeLayer(peerMarkers[id]); + delete peerMarkers[id]; + } + } + + // Add/Update markers + for (const peer of peers) { + if (!peer.ip) continue; + + if (!peerMarkers[peer.id]) { + const loc = await fetchLocation(peer.ip); + if (loc) { + const marker = L.circleMarker([loc.lat, loc.lon], { + radius: 5, + fillColor: "#4ade80", + color: "#fff", + weight: 1, + opacity: 1, + fillOpacity: 0.8 + }).addTo(map); + + marker.bindPopup(`Node ${peer.id.slice(-8)}
${loc.city}, ${loc.country}`); + peerMarkers[peer.id] = marker; + } + } + } +} + const terminal = document.getElementById('terminal'); const terminalOutput = document.getElementById('terminal-output'); const terminalInput = document.getElementById('terminal-input'); +const terminalToggle = document.getElementById('terminal-toggle'); const promptEl = document.querySelector('.prompt'); let myId = null; let myChatHistory = []; +terminalToggle.addEventListener('click', (e) => { + e.stopPropagation(); + toggleChat(); +}); + +const toggleChat = () => { + terminal.classList.toggle('collapsed'); + const isCollapsed = terminal.classList.contains('collapsed'); + terminalToggle.innerText = isCollapsed ? '▲' : '▼'; + + if (isCollapsed) { + document.body.classList.remove('chat-active'); + document.body.classList.add('chat-collapsed'); + } else { + document.body.classList.add('chat-active'); + document.body.classList.remove('chat-collapsed'); + terminalOutput.scrollTop = terminalOutput.scrollHeight; + } +} + const updatePromptStatus = () => { const now = Date.now(); myChatHistory = myChatHistory.filter(t => now - t < 10000); @@ -199,14 +328,26 @@ evtSource.onmessage = (event) => { if (data.chatEnabled) { terminal.classList.remove('hidden'); - document.body.classList.add('chat-active'); + if (terminal.classList.contains('collapsed')) { + document.body.classList.add('chat-collapsed'); + } else { + document.body.classList.add('chat-active'); + } } else { terminal.classList.add('hidden'); document.body.classList.remove('chat-active'); + document.body.classList.remove('chat-collapsed'); } if (data.id) myId = data.id; + if (data.peers) { + lastPeerData = data.peers; + if (mapInitialized && document.getElementById('mapModal').classList.contains('active')) { + updateMap(data.peers); + } + } + updateParticles(data.count); if (countEl.innerText != data.count) { diff --git a/public/index.html b/public/index.html index 8c4314b..2aaae41 100644 --- a/public/index.html +++ b/public/index.html @@ -5,6 +5,8 @@ + + @@ -17,7 +19,15 @@
ID: {{ID}}
Direct Connections: {{DIRECT}}
- diagnostics + diagnostics | + map +
+ + + @@ -66,6 +76,7 @@