diff --git a/routing_table.go b/routing_table.go index 3d38869..5803650 100644 --- a/routing_table.go +++ b/routing_table.go @@ -168,6 +168,25 @@ func (rt *RoutingTable) GetRoutingTableSize() int { return len(rt.GetAllRoutingTableNodes()) } +// GetAllRoutingTableNodesWithMeta returns active peers with their cache metadata +func (rt *RoutingTable) GetAllRoutingTableNodesWithMeta() []*CachedSystem { + rt.cacheMu.RLock() + defer rt.cacheMu.RUnlock() + + verificationCutoff := time.Now().Add(-VerificationCutoff) + result := make([]*CachedSystem, 0) + + for _, cached := range rt.systemCache { + // "Active peer" = verified recently and not failing + if cached.Verified && !cached.LastVerified.IsZero() && + cached.LastVerified.After(verificationCutoff) && + cached.FailCount < MaxFailCount { + result = append(result, cached) + } + } + return result +} + // === System Cache Methods === // CacheSystem adds a system to the cache @@ -300,6 +319,18 @@ func (rt *RoutingTable) GetAllCachedSystems() []*System { return result } +// GetAllCachedSystemsWithMeta returns all cached systems with their metadata +func (rt *RoutingTable) GetAllCachedSystemsWithMeta() []*CachedSystem { + rt.cacheMu.RLock() + defer rt.cacheMu.RUnlock() + + result := make([]*CachedSystem, 0, len(rt.systemCache)) + for _, cached := range rt.systemCache { + result = append(result, cached) + } + return result +} + // GetVerifiedCachedSystems returns only systems verified within maxAge func (rt *RoutingTable) GetVerifiedCachedSystems(maxAge time.Duration) []*System { rt.cacheMu.RLock() diff --git a/web-interface.go b/web-interface.go index 30ffa3b..900f856 100644 --- a/web-interface.go +++ b/web-interface.go @@ -18,15 +18,28 @@ type WebInterface struct { addr string } +// KnownSystemData holds system info plus metadata for the template +type KnownSystemData struct { + System *System + LearnedAt int64 +} + +// PeerData holds peer info plus metadata for the template +type PeerData struct { + System *System + LearnedAt int64 + IsNew bool // Discovered within last 24 hours +} + // WebInterfaceData holds data for the web template type WebInterfaceData struct { System *System - Peers []*System + Peers []PeerData PeerIDs []string PeerCount int MaxPeers int PeerCapacityDesc string - KnownSystems []*System + KnownSystems []KnownSystemData TotalSystems int ProtocolVersion string AttestationCount int @@ -117,11 +130,27 @@ func (w *WebInterface) buildTemplateData() WebInterfaceData { sys := w.dht.GetLocalSystem() rt := w.dht.GetRoutingTable() - // Get routing table nodes (active peers) - peers := rt.GetAllRoutingTableNodes() + // Get routing table nodes (active peers) with metadata + cachedPeers := rt.GetAllRoutingTableNodesWithMeta() + oneDayAgo := time.Now().Add(-24 * time.Hour) + peers := make([]PeerData, 0, len(cachedPeers)) + for _, cached := range cachedPeers { + peers = append(peers, PeerData{ + System: cached.System, + LearnedAt: cached.LearnedAt.Unix(), + IsNew: cached.LearnedAt.After(oneDayAgo), + }) + } - // Get all cached systems (known galaxy) - allSystems := rt.GetAllCachedSystems() + // Get all cached systems (known galaxy) with metadata + cachedSystems := rt.GetAllCachedSystemsWithMeta() + knownSystems := make([]KnownSystemData, 0, len(cachedSystems)) + for _, cached := range cachedSystems { + knownSystems = append(knownSystems, KnownSystemData{ + System: cached.System, + LearnedAt: cached.LearnedAt.Unix(), + }) + } // Get attestation count (use GetDatabaseStats) dbStats, _ := w.storage.GetDatabaseStats() @@ -190,7 +219,7 @@ func (w *WebInterface) buildTemplateData() WebInterfaceData { // Build peer ID list for JS peerIDs := make([]string, len(peers)) for i, p := range peers { - peerIDs[i] = p.ID.String() + peerIDs[i] = p.System.ID.String() } return WebInterfaceData{ @@ -200,8 +229,8 @@ func (w *WebInterface) buildTemplateData() WebInterfaceData { PeerCount: rtSize, MaxPeers: sys.GetMaxPeers(), PeerCapacityDesc: capacityDesc, - KnownSystems: allSystems, - TotalSystems: len(allSystems) + 1, // +1 for self + KnownSystems: knownSystems, + TotalSystems: len(knownSystems) + 1, // +1 for self ProtocolVersion: CurrentProtocolVersion.String(), AttestationCount: attestationCount, DatabaseSize: dbSizeStr, @@ -230,16 +259,48 @@ func (w *WebInterface) handleSystemAPI(rw http.ResponseWriter, r *http.Request) json.NewEncoder(rw).Encode(w.dht.GetLocalSystem()) } +// PeerResponse includes peer data plus cache metadata for API +type PeerResponse struct { + *System + LearnedAt int64 `json:"learned_at"` // Unix timestamp +} + func (w *WebInterface) handlePeersAPI(rw http.ResponseWriter, r *http.Request) { - peers := w.dht.GetRoutingTable().GetAllRoutingTableNodes() + cachedPeers := w.dht.GetRoutingTable().GetAllRoutingTableNodesWithMeta() + + // Build response with learned_at timestamps + response := make([]PeerResponse, 0, len(cachedPeers)) + for _, cached := range cachedPeers { + response = append(response, PeerResponse{ + System: cached.System, + LearnedAt: cached.LearnedAt.Unix(), + }) + } + rw.Header().Set("Content-Type", "application/json") - json.NewEncoder(rw).Encode(peers) + json.NewEncoder(rw).Encode(response) +} + +// KnownSystemResponse includes system data plus cache metadata +type KnownSystemResponse struct { + *System + LearnedAt int64 `json:"learned_at"` // Unix timestamp } func (w *WebInterface) handleKnownSystemsAPI(rw http.ResponseWriter, r *http.Request) { - systems := w.dht.GetRoutingTable().GetAllCachedSystems() + cachedSystems := w.dht.GetRoutingTable().GetAllCachedSystemsWithMeta() + + // Build response with learned_at timestamps + response := make([]KnownSystemResponse, 0, len(cachedSystems)) + for _, cached := range cachedSystems { + response = append(response, KnownSystemResponse{ + System: cached.System, + LearnedAt: cached.LearnedAt.Unix(), + }) + } + rw.Header().Set("Content-Type", "application/json") - json.NewEncoder(rw).Encode(systems) + json.NewEncoder(rw).Encode(response) } func (w *WebInterface) handleStatsAPI(rw http.ResponseWriter, r *http.Request) { @@ -449,6 +510,7 @@ const indexTemplate = ` border-radius: 8px; } .peer-name { font-weight: 500; color: #60a5fa; } + .new-badge { background: #22c55e; color: #000; font-size: 9px; padding: 1px 4px; border-radius: 3px; margin-left: 4px; font-weight: 600; } .peer-id { font-size: 0.8em; color: #666; font-family: monospace; } .star-display { display: flex; align-items: center; gap: 10px; margin: 10px 0; } .star { @@ -734,9 +796,9 @@ const indexTemplate = `
No peers in routing table
@@ -891,9 +953,16 @@ const indexTemplate = ` const knownSystems = [ {{range .KnownSystems}} - {id: "{{.ID}}", name: "{{.Name}}", x: {{.X}}, y: {{.Y}}, z: {{.Z}}, color: "{{.Stars.Primary.Color}}", starClass: "{{.Stars.Primary.Class}}", starDesc: "{{.Stars.Primary.Description}}"}, + {id: "{{.System.ID}}", name: "{{.System.Name}}", x: {{.System.X}}, y: {{.System.Y}}, z: {{.System.Z}}, color: "{{.System.Stars.Primary.Color}}", starClass: "{{.System.Stars.Primary.Class}}", starDesc: "{{.System.Stars.Primary.Description}}", learnedAt: {{.LearnedAt}}}, {{end}} ]; + + // Check if a system was learned within the last 24 hours + function isNewSystem(learnedAt) { + if (!learnedAt) return false; + const oneDayAgo = Math.floor(Date.now() / 1000) - (24 * 60 * 60); + return learnedAt > oneDayAgo; + } const selfSystem = { id: "{{.System.ID}}", name: "{{.System.Name}}", @@ -1132,7 +1201,8 @@ const indexTemplate = ` // Create HTML label const label = document.createElement('div'); - label.textContent = sys.name; + const isNew = !isSelf && isNewSystem(sys.learnedAt); + label.innerHTML = sys.name + (isNew ? ' NEW' : ''); label.style.cssText = 'position:absolute;font-size:11px;white-space:nowrap;transform:translateX(-50%);'; if (isSelf) { label.style.color = '#60a5fa'; @@ -1167,7 +1237,8 @@ const indexTemplate = ` }); } - // Add connection lines + // Add connection lines - ONLY show our direct peer connections by default + // Other connections are shown on hover const processedEdges = new Set(); if (cachedConnections && cachedConnections.length > 0) { cachedConnections.forEach(conn => { @@ -1180,6 +1251,9 @@ const indexTemplate = ` if (processedEdges.has(edgeKey)) return; processedEdges.add(edgeKey); + // Only draw lines involving ourselves (direct peers) + const involvesUs = conn.from_id === selfSystem.id || conn.to_id === selfSystem.id; + const reciprocal = isReciprocal(conn.from_id, conn.to_id); const points = [ new THREE.Vector3(from.x, from.y, from.z), @@ -1192,21 +1266,27 @@ const indexTemplate = ` const material = new THREE.LineBasicMaterial({ color: 0x64c8ff, transparent: true, - opacity: 0.35 + opacity: involvesUs ? 0.5 : 0 }); line = new THREE.Line(geometry, material); } else { const material = new THREE.LineDashedMaterial({ color: 0xffaa44, transparent: true, - opacity: 0.3, + opacity: involvesUs ? 0.4 : 0, dashSize: 30, gapSize: 20 }); line = new THREE.Line(geometry, material); line.computeLineDistances(); } - line.userData = { fromId: conn.from_id, toId: conn.to_id, reciprocal: reciprocal }; + line.userData = { + fromId: conn.from_id, + toId: conn.to_id, + reciprocal: reciprocal, + involvesUs: involvesUs, + baseOpacity: involvesUs ? (reciprocal ? 0.5 : 0.4) : 0 + }; scene.add(line); connectionLines.push(line); }); @@ -1262,8 +1342,8 @@ const indexTemplate = ` 'No peers in routing table
'; } else { - peerListEl.innerHTML = peers.map(p => - '