diff --git a/public/index.html b/public/index.html index 8d5ae27..9345212 100644 --- a/public/index.html +++ b/public/index.html @@ -89,7 +89,7 @@ - + diff --git a/public/live.js b/public/live.js index 8bce9df..642ed50 100644 --- a/public/live.js +++ b/public/live.js @@ -1192,28 +1192,72 @@ if (heatLayer) { map.removeLayer(heatLayer); heatLayer = null; } } - function getLiveFavorites() { - try { return new Set(JSON.parse(localStorage.getItem('meshcore-favorites') || '[]')); } catch { return new Set(); } + function getFavoritePubkeys() { + let favs = []; + try { favs = favs.concat(JSON.parse(localStorage.getItem('meshcore-favorites') || '[]')); } catch {} + try { favs = favs.concat(JSON.parse(localStorage.getItem('meshcore-my-nodes') || '[]').map(n => n.pubkey)); } catch {} + return favs.filter(Boolean); } - function getLiveMyNodes() { - try { return new Set(JSON.parse(localStorage.getItem('meshcore-my-nodes') || '[]')); } catch { return new Set(); } - } - function isNodeFavorited(pubkey) { - const favs = getLiveFavorites(); - const mine = getLiveMyNodes(); - return favs.has(pubkey) || mine.has(pubkey); - } - function applyFavoritesFilter() { - // All markers always visible — favorites filter only affects packet animations - Object.keys(nodeMarkers).forEach(key => { - const marker = nodeMarkers[key]; - if (!marker) return; - if (!nodesLayer.hasLayer(marker)) { marker.addTo(nodesLayer); if (marker._glowMarker) marker._glowMarker.addTo(nodesLayer); } - }); - const _el2 = document.getElementById('liveNodeCount'); - if (_el2) { - _el2.textContent = Object.keys(nodeMarkers).length; + + function packetInvolvesFavorite(pkt) { + const favs = getFavoritePubkeys(); + if (favs.length === 0) return false; + const decoded = pkt.decoded || {}; + const payload = decoded.payload || {}; + const hops = decoded.path?.hops || []; + + // Full pubkeys: sender + if (payload.pubKey && favs.some(f => f === payload.pubKey)) return true; + + // Observer: may be name or pubkey + const obs = pkt.observer_name || pkt.observer || ''; + if (obs) { + if (favs.some(f => f === obs)) return true; + for (const nd of Object.values(nodeData)) { + if ((nd.name === obs || nd.public_key === obs) && favs.some(f => f === nd.public_key)) return true; + } } + + // Hops are truncated hex prefixes — match by prefix in either direction + for (const hop of hops) { + const h = (hop.id || hop.public_key || hop).toString().toLowerCase(); + if (favs.some(f => f.toLowerCase().startsWith(h) || h.startsWith(f.toLowerCase()))) return true; + } + + return false; + } + + function isNodeFavorited(pubkey) { + return getFavoritePubkeys().some(f => f === pubkey); + } + + function rebuildFeedList() { + const feed = document.getElementById('liveFeed'); + if (!feed) return; + // Remove all feed items but keep the hide button and resize handle + feed.querySelectorAll('.live-feed-item').forEach(el => el.remove()); + // Re-add from VCR buffer (most recent first, up to 25) + const entries = VCR.buffer.slice(-100).reverse(); + let count = 0; + for (const entry of entries) { + if (count >= 25) break; + const pkt = entry.pkt; + if (showOnlyFavorites && !packetInvolvesFavorite(pkt)) continue; + const decoded = pkt.decoded || {}; + const header = decoded.header || {}; + const payload = decoded.payload || {}; + const typeName = header.payloadTypeName || 'UNKNOWN'; + const icon = PAYLOAD_ICONS[typeName] || '📦'; + const hops = decoded.path?.hops || []; + const color = TYPE_COLORS[typeName] || '#6b7280'; + addFeedItemDOM(icon, typeName, payload, hops, color, pkt, feed); + count++; + } + } + + function applyFavoritesFilter() { + // Node markers always stay visible — only rebuild the feed list + rebuildFeedList(); } function addNodeMarker(n) { @@ -1304,12 +1348,8 @@ playSound(typeName); addFeedItem(icon, typeName, payload, hops, color, pkt); - // Favorites filter: skip animation if no involved nodes are favorited - if (showOnlyFavorites) { - const involvedKeys = hops.map(h => h.id || h.public_key).filter(Boolean); - if (payload.pubKey) involvedKeys.push(payload.pubKey); - if (!involvedKeys.some(k => isNodeFavorited(k))) return; - } + // Favorites filter: skip animation if packet doesn't involve a favorited node + if (showOnlyFavorites && !packetInvolvesFavorite(pkt)) return; // If ADVERT, ensure node appears on map if (typeName === 'ADVERT' && payload.pubKey) { @@ -1342,6 +1382,9 @@ pktTimestamps.push(Date.now()); const _el = document.getElementById('livePktCount'); if (_el) _el.textContent = packetCount; + // Favorites filter: skip if none of the packets involve a favorite + if (showOnlyFavorites && !packets.some(p => packetInvolvesFavorite(p))) return; + playSound(typeName); // Ensure ADVERT nodes appear @@ -1697,10 +1740,34 @@ if (heatLayer) { map.removeLayer(heatLayer); heatLayer = null; } } + function addFeedItemDOM(icon, typeName, payload, hops, color, pkt, feed) { + const text = payload.text || payload.name || ''; + const preview = text ? ' ' + (text.length > 35 ? text.slice(0, 35) + '…' : text) : ''; + const hopStr = hops.length ? `${hops.length}⇢` : ''; + const obsBadge = pkt.observation_count > 1 ? `👁 ${pkt.observation_count}` : ''; + const item = document.createElement('div'); + item.className = 'live-feed-item'; + item.setAttribute('tabindex', '0'); + item.setAttribute('role', 'button'); + item.style.cursor = 'pointer'; + item.innerHTML = ` + ${icon} + ${typeName} + ${hopStr}${obsBadge} + ${escapeHtml(preview)} + ${new Date(pkt._ts || Date.now()).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit',second:'2-digit'})} + `; + item.addEventListener('click', () => showFeedCard(item, pkt, color)); + feed.appendChild(item); + } + function addFeedItem(icon, typeName, payload, hops, color, pkt) { const feed = document.getElementById('liveFeed'); if (!feed) return; + // Favorites filter: skip feed item if packet doesn't involve a favorite + if (showOnlyFavorites && !packetInvolvesFavorite(pkt)) return; + const text = payload.text || payload.name || ''; const preview = text ? ' ' + (text.length > 35 ? text.slice(0, 35) + '…' : text) : ''; const hopStr = hops.length ? `${hops.length}⇢` : '';