chat can be minimized. Added direct connections heatmap.

This commit is contained in:
lklynet
2026-01-03 11:26:21 -05:00
parent 884ed1b302
commit 0ca471ab29
6 changed files with 233 additions and 6 deletions
+142 -1
View File
@@ -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: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>',
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(`<b>Node</b> ${peer.id.slice(-8)}<br>${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) {
+12 -1
View File
@@ -5,6 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="/favicon.ico">
<link rel="stylesheet" href="/style.css">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
</head>
<body>
<canvas id="network"></canvas>
@@ -17,7 +19,15 @@
<div class="debug">
ID: {{ID}}<br>
Direct Connections: <span id="direct">{{DIRECT}}</span><br>
<span class="debug-link" onclick="openDiagnostics()">diagnostics</span>
<span class="debug-link" onclick="openDiagnostics()">diagnostics</span> |
<span class="debug-link" onclick="openMap()">map</span>
</div>
</div>
<div id="mapModal" class="modal">
<div class="modal-content map-content">
<button class="close-btn" onclick="closeMap()">×</button>
<div id="map"></div>
</div>
</div>
@@ -66,6 +76,7 @@
</div>
<div id="terminal" class="terminal hidden">
<button id="terminal-toggle" class="terminal-toggle" title="Toggle Chat"></button>
<div id="terminal-output" class="terminal-output"></div>
<div class="terminal-input-line">
<span class="prompt">&gt;</span>
+53
View File
@@ -16,6 +16,10 @@ body.chat-active {
padding-bottom: 250px;
}
body.chat-collapsed {
padding-bottom: 40px;
}
.container { text-align: center; position: relative; z-index: 10; }
#network { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; }
.count { font-size: 8rem; font-weight: bold; color: #4ade80; transition: color 0.2s; visibility: hidden; }
@@ -53,6 +57,22 @@ a { color: #4b5563; text-decoration: none; border-bottom: 1px dotted #4b5563; }
width: 90%;
position: relative;
}
.modal-content.map-content {
max-width: 800px;
width: 90%;
height: 80vh;
padding: 0;
overflow: hidden;
display: flex;
flex-direction: column;
}
#map {
width: 100%;
height: 100%;
background: #222;
}
.modal-title {
font-size: 0.9rem;
color: #666;
@@ -111,12 +131,45 @@ a { color: #4b5563; text-decoration: none; border-bottom: 1px dotted #4b5563; }
color: #4ade80;
font-size: 12px;
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.3);
transition: transform 0.3s ease;
}
.terminal.hidden {
display: none;
}
.terminal.collapsed {
transform: translateX(-50%) translateY(100%);
}
.terminal-toggle {
position: absolute;
top: -24px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.9);
border: 1px solid #333;
border-bottom: none;
border-radius: 8px 8px 0 0;
color: #4ade80;
cursor: pointer;
font-family: monospace;
font-weight: bold;
font-size: 14px;
z-index: 101;
padding: 2px 15px;
height: 24px;
line-height: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.terminal-toggle:hover {
color: #fff;
background: #222;
}
.terminal-output {
flex: 1;
overflow-y: auto;
+9 -1
View File
@@ -62,7 +62,15 @@ class MessageHandler {
sourceSocket.peerId = id;
}
const wasNew = this.peerManager.addOrUpdatePeer(id, seq);
const getIp = (sock) => {
if (sock.remoteAddress) return sock.remoteAddress;
if (sock.rawStream && sock.rawStream.remoteHost) return sock.rawStream.remoteHost;
if (sock.rawStream && sock.rawStream.remoteAddress) return sock.rawStream.remoteAddress;
return null;
};
const ip = (hops === 0) ? getIp(sourceSocket) : null;
const wasNew = this.peerManager.addOrUpdatePeer(id, seq, key, ip);
if (wasNew) {
this.diagnostics.increment("newPeersAdded");
+13 -1
View File
@@ -9,7 +9,7 @@ class PeerManager {
this.mySeq = 0;
}
addOrUpdatePeer(id, seq) {
addOrUpdatePeer(id, seq, key, ip = null) {
const stored = this.seenPeers.get(id);
const wasNew = !stored;
@@ -19,6 +19,8 @@ class PeerManager {
this.seenPeers.set(id, {
seq,
lastSeen: Date.now(),
key,
ip: ip || (stored ? stored.ip : null),
});
return wasNew;
@@ -70,6 +72,16 @@ class PeerManager {
getSeq() {
return this.mySeq;
}
getPeersWithIps() {
const peers = [];
for (const [id, data] of this.seenPeers.entries()) {
if (data.ip) {
peers.push({ id, ip: data.ip });
}
}
return peers;
}
}
module.exports = { PeerManager };
+4 -2
View File
@@ -37,7 +37,8 @@ const setupRoutes = (app, identity, peerManager, swarm, sseManager, diagnostics)
direct: swarm.getSwarm().connections.size,
id: identity.id,
diagnostics: diagnostics.getStats(),
chatEnabled: ENABLE_CHAT
chatEnabled: ENABLE_CHAT,
peers: peerManager.getPeersWithIps()
});
res.write(`data: ${data}\n\n`);
@@ -53,7 +54,8 @@ const setupRoutes = (app, identity, peerManager, swarm, sseManager, diagnostics)
direct: swarm.getSwarm().connections.size,
id: identity.id,
diagnostics: diagnostics.getStats(),
chatEnabled: ENABLE_CHAT
chatEnabled: ENABLE_CHAT,
peers: peerManager.getPeersWithIps()
});
});