diff --git a/cmd/server/db.go b/cmd/server/db.go index 85b07f85..2862722c 100644 --- a/cmd/server/db.go +++ b/cmd/server/db.go @@ -1540,13 +1540,6 @@ type PacketPathPoint struct { // those neighbors -- 0 (and omitted) with a single contributor; // larger means the neighbors disagree more about where "nearby" is. ApproxSpreadKm *float64 `json:"approxSpreadKm,omitempty"` - // IsBridge is true when this node has been confirmed relaying - // traffic for 2+ distinct region scopes -- the same "Bridge" badge - // definition as the Foreign Traffic tab (ScopeStatsResponse. - // bridgeRepeaters). Set by the handler (routes.go), not GetPacketPath - // itself, since the underlying data lives in the in-memory store, not - // SQL. Absent/false when the store isn't available. - IsBridge bool `json:"isBridge,omitempty"` } // PacketPathObserver is the station that produced a given branch's @@ -1558,9 +1551,8 @@ type PacketPathPoint struct { type PacketPathObserver struct { // PublicKey is the observer's mesh pubkey (observers.id for v3, // observer_id for legacy), when it has one -- lets callers link out - // to the node detail page or cross-reference other per-node data - // (e.g. IsBridge, filled in by the handler). Empty for an observer - // whose id never resembled a pubkey. + // to the node detail page. Empty for an observer whose id never + // resembled a pubkey. PublicKey string `json:"publicKey,omitempty"` Name string `json:"name"` IATA string `json:"iata,omitempty"` @@ -1573,8 +1565,6 @@ type PacketPathObserver struct { Approx bool `json:"approx,omitempty"` ApproxNeighborCount int `json:"approxNeighborCount,omitempty"` ApproxSpreadKm *float64 `json:"approxSpreadKm,omitempty"` - // IsBridge -- see PacketPathPoint.IsBridge. - IsBridge bool `json:"isBridge,omitempty"` } // PacketPathBranch is one station's route to a packet: how far it diff --git a/cmd/server/openapi.go b/cmd/server/openapi.go index 37642972..44dfab65 100644 --- a/cmd/server/openapi.go +++ b/cmd/server/openapi.go @@ -345,7 +345,6 @@ func componentSchemas() map[string]interface{} { "approx": map[string]interface{}{"type": "boolean", "description": "True when lat/lon are not this node's own position but a count-weighted centroid of its positioned neighbor_edges neighbors instead -- a last-resort stand-in, not a real fix."}, "approxNeighborCount": map[string]interface{}{"type": "integer", "description": "Present only when approx=true. How many positioned neighbors fed the centroid -- a rough confidence signal, higher is more confident."}, "approxSpreadKm": map[string]interface{}{"type": "number", "nullable": true, "description": "Present only when approx=true and approxNeighborCount>1. Widest distance (km) between any two contributing neighbors -- larger means they disagree more about where 'nearby' is."}, - "isBridge": map[string]interface{}{"type": "boolean", "description": "True when this node has been confirmed relaying traffic for 2+ distinct region scopes -- same definition/data source as the Foreign Traffic tab's Bridge badge. Set by the handler from the in-memory store; always false when that store isn't available."}, }, }, "PacketPathObserver": map[string]interface{}{ @@ -361,7 +360,6 @@ func componentSchemas() map[string]interface{} { "approx": map[string]interface{}{"type": "boolean", "description": "True when lat/lon are not this station's own position but a count-weighted centroid of its positioned neighbors instead -- a last-resort stand-in, not a real fix."}, "approxNeighborCount": map[string]interface{}{"type": "integer", "description": "Present only when approx=true. See PacketPathPoint.approxNeighborCount."}, "approxSpreadKm": map[string]interface{}{"type": "number", "nullable": true, "description": "Present only when approx=true and approxNeighborCount>1. See PacketPathPoint.approxSpreadKm."}, - "isBridge": map[string]interface{}{"type": "boolean", "description": "See PacketPathPoint.isBridge."}, }, }, "PacketPathBranch": map[string]interface{}{ diff --git a/cmd/server/packet_path_bridge_test.go b/cmd/server/packet_path_bridge_test.go deleted file mode 100644 index 05a128c8..00000000 --- a/cmd/server/packet_path_bridge_test.go +++ /dev/null @@ -1,76 +0,0 @@ -package main - -import ( - "testing" - "time" -) - -// TestMarkBridgeRepeaters_NilStore covers the graceful no-op when the -// in-memory store isn't available (s.store can legitimately be nil -- -// see handlePacketPath). Must not panic and must leave every IsBridge -// at its zero value (false). -func TestMarkBridgeRepeaters_NilStore(t *testing.T) { - resp := &PacketPathResponse{ - Hash: "deadbeef", - Branches: []PacketPathBranch{ - {Points: []PacketPathPoint{{PublicKey: "pk1"}}, Observer: &PacketPathObserver{PublicKey: "obs1"}}, - }, - } - markBridgeRepeaters(resp, nil, &Config{}) - if resp.Branches[0].Points[0].IsBridge { - t.Errorf("Points[0].IsBridge = true, want false -- store is nil, nothing should be marked") - } - if resp.Branches[0].Observer.IsBridge { - t.Errorf("Observer.IsBridge = true, want false -- store is nil, nothing should be marked") - } -} - -// TestMarkBridgeRepeaters_TwoScopesIsBridge covers the actual -// determination: a pubkey relaying 2+ distinct region scopes (the same -// definition/data source as the Foreign Traffic tab's "Bridge" badge, -// ScopeStatsResponse.bridgeRepeaters) gets IsBridge=true on every point -// and observer entry referencing it; a pubkey with 0 or 1 scope, or one -// missing from the relay map entirely, stays false. -func TestMarkBridgeRepeaters_TwoScopesIsBridge(t *testing.T) { - store := &PacketStore{ - repeaterRelayCache: map[string]RepeaterRelayInfo{ - "bridgepk": {TransportedScopes: []string{"#dk", "#se"}}, - "regionalpk": {TransportedScopes: []string{"#dk"}}, - "unknownscopepk": {TransportedScopes: nil}, - }, - repeaterRelayCacheWin: 24, - repeaterRelayAt: time.Now(), - } - resp := &PacketPathResponse{ - Hash: "deadbeef", - Branches: []PacketPathBranch{ - { - Points: []PacketPathPoint{ - {PublicKey: "bridgepk"}, - {PublicKey: "regionalpk"}, - {PublicKey: "unknownscopepk"}, - {PublicKey: "notinrelaymap"}, - }, - Observer: &PacketPathObserver{PublicKey: "BridgePK"}, // case-insensitive match - }, - }, - } - markBridgeRepeaters(resp, store, &Config{}) - - pts := resp.Branches[0].Points - if !pts[0].IsBridge { - t.Errorf("Points[0] (bridgepk, 2 scopes) IsBridge = false, want true") - } - if pts[1].IsBridge { - t.Errorf("Points[1] (regionalpk, 1 scope) IsBridge = true, want false") - } - if pts[2].IsBridge { - t.Errorf("Points[2] (unknownscopepk, 0 scopes) IsBridge = true, want false") - } - if pts[3].IsBridge { - t.Errorf("Points[3] (notinrelaymap, absent from relay map) IsBridge = true, want false") - } - if !resp.Branches[0].Observer.IsBridge { - t.Errorf("Observer (BridgePK, case-insensitive match on bridgepk) IsBridge = false, want true") - } -} diff --git a/cmd/server/routes.go b/cmd/server/routes.go index 2e42da72..d88fca6c 100644 --- a/cmd/server/routes.go +++ b/cmd/server/routes.go @@ -3164,46 +3164,9 @@ func (s *Server) handlePacketPath(w http.ResponseWriter, r *http.Request) { writeError(w, 500, err.Error()) return } - markBridgeRepeaters(resp, s.store, s.cfg) writeJSON(w, resp) } -// markBridgeRepeaters sets IsBridge on every point/observer in resp whose -// pubkey has been confirmed relaying traffic for 2+ distinct region -// scopes -- the same "Bridge" definition/data source as the Foreign -// Traffic tab's badge (ScopeStatsResponse.bridgeRepeaters), just applied -// to whichever handful of pubkeys this one packet's path touches instead -// of scanning the whole mesh. No-op when the in-memory store isn't -// available (IsBridge stays false/omitted on every entry). -func markBridgeRepeaters(resp *PacketPathResponse, store *PacketStore, cfg *Config) { - if resp == nil || store == nil || cfg == nil { - return - } - relayMap := store.GetRepeaterRelayInfoMap(cfg.GetHealthThresholds().RelayActiveHours) - isBridge := func(pubkey string) bool { - if pubkey == "" { - return false - } - info, ok := relayMap[strings.ToLower(pubkey)] - return ok && len(info.TransportedScopes) >= 2 - } - mark := func(b *PacketPathBranch) { - if b == nil { - return - } - for i := range b.Points { - b.Points[i].IsBridge = isBridge(b.Points[i].PublicKey) - } - if b.Observer != nil { - b.Observer.IsBridge = isBridge(b.Observer.PublicKey) - } - } - for i := range resp.Branches { - mark(&resp.Branches[i]) - } - mark(resp.First) -} - var iataCoords = map[string]IataCoord{ "SJC": {Lat: 37.3626, Lon: -121.929}, "SFO": {Lat: 37.6213, Lon: -122.379}, diff --git a/public/packet-path-map.js b/public/packet-path-map.js index b73e0923..a6fca65f 100644 --- a/public/packet-path-map.js +++ b/public/packet-path-map.js @@ -100,7 +100,7 @@ var chain = located.map(function (p, hi) { return { lat: p.lat, lon: p.lon, name: p.name, label: 'hop ' + (hi + 1) + ' of ' + b.hops, approx: !!p.approx, - approxNeighborCount: p.approxNeighborCount, approxSpreadKm: p.approxSpreadKm, role: p.role, isBridge: !!p.isBridge, + approxNeighborCount: p.approxNeighborCount, approxSpreadKm: p.approxSpreadKm, role: p.role, publicKey: p.publicKey, }; }); @@ -112,7 +112,7 @@ lat: b.observer.lat, lon: b.observer.lon, name: b.observer.name, label: observerLabel, isObserver: true, approx: !!b.observer.approx, approxNeighborCount: b.observer.approxNeighborCount, approxSpreadKm: b.observer.approxSpreadKm, - role: b.observer.role, isBridge: !!b.observer.isBridge, publicKey: b.observer.publicKey, + role: b.observer.role, publicKey: b.observer.publicKey, }); } return { chain: chain, missing: (b.points || []).length - located.length }; @@ -129,7 +129,7 @@ '' + '

Relay Path

' + - '

How far and how wide this packet spread. The highlighted route is the farthest-traveled branch; every other station that heard it is shown too. The green ring marks whoever heard it first. Dashed markers are approximate -- estimated from nearby positioned neighbors, not the station\'s own position. A bold purple outline marks a confirmed bridge repeater. Click a marker to open that node\'s detail page.

' + + '

How far and how wide this packet spread. The highlighted route is the farthest-traveled branch; every other station that heard it is shown too. The green ring marks whoever heard it first. Dashed markers are approximate -- estimated from nearby positioned neighbors, not the station\'s own position. Click a marker to open that node\'s detail page.

' + '
' + '
Loading…
' + ''; @@ -217,24 +217,15 @@ fillColor: color, fillOpacity: approxFillOpacity(pt.approxNeighborCount), dashArray: '5,4', } : { radius: radius, color: outline, weight: p.primary ? 2 : 1, fillColor: color, fillOpacity: p.primary ? 1 : 0.8 }; - if (pt.isBridge) { - // Bridge repeaters (confirmed relaying for 2+ regions -- same - // definition as the Foreign Traffic tab's badge) get a bold - // purple outline on top of whatever primary/approx styling - // already applies, so they stand out as the mesh's backbone - // nodes regardless of which branch they're in. - markerOpts = Object.assign({}, markerOpts, { color: cssVar('--status-purple'), weight: markerOpts.weight + 1 }); - } var approxNote = ''; if (pt.approx) { approxNote = ', approx. position'; if (pt.approxNeighborCount) approxNote += ' from ' + pt.approxNeighborCount + ' neighbor' + (pt.approxNeighborCount === 1 ? '' : 's'); } - var bridgeNote = pt.isBridge ? ', bridge repeater' : ''; var clickNote = pt.publicKey ? ' — click for node detail' : ''; var marker = L.circleMarker([pt.lat, pt.lon], markerOpts) .addTo(map) - .bindTooltip(roleIcon(pt.role) + escapeHtml(pt.name) + ' (' + pt.label + approxNote + bridgeNote + ')' + clickNote, { className: 'packet-path-tooltip' }); + .bindTooltip(roleIcon(pt.role) + escapeHtml(pt.name) + ' (' + pt.label + approxNote + ')' + clickNote, { className: 'packet-path-tooltip' }); if (pt.publicKey) { // Same #/nodes/{pubkey} hash route the rest of the app already // links to (see e.g. public/channels.js's node-detail links). diff --git a/public/style.css b/public/style.css index 1a300c56..d1cdd903 100644 --- a/public/style.css +++ b/public/style.css @@ -2645,10 +2645,10 @@ button.ch-item:hover .ch-icon-btn { opacity: 1; } .leaflet-popup-content { color: var(--text) !important; font-size: 13px !important; } /* packet-path-map.js: tooltips got long once they started carrying role, - approx/confidence, bridge, and distance/timing info together -- - Leaflet's default tooltip is white-space:nowrap, which stretched a - long one into an unreadable single line spanning the whole map. - Wrap it onto multiple lines within a fixed width instead. */ + approx/confidence, and distance/timing info together -- Leaflet's + default tooltip is white-space:nowrap, which stretched a long one + into an unreadable single line spanning the whole map. Wrap it onto + multiple lines within a fixed width instead. */ .leaflet-tooltip.packet-path-tooltip { white-space: normal; max-width: 220px; diff --git a/test-packet-path-map.js b/test-packet-path-map.js index d3e4c53f..8d4aac5b 100644 --- a/test-packet-path-map.js +++ b/test-packet-path-map.js @@ -40,7 +40,7 @@ test('escapes node/observer names before interpolating into tooltip HTML (operat assert.ok(/escapeHtml\(pt\.name\)/.test(src), 'point tooltips must escape the name'); }); -test('tooltips use a wrapping CSS class -- Leaflet\'s default nowrap tooltip becomes unreadable once role/approx/bridge/distance/timing info are all combined', () => { +test('tooltips use a wrapping CSS class -- Leaflet\'s default nowrap tooltip becomes unreadable once role/approx/distance/timing info are all combined', () => { const bindCalls = (src.match(/\.bindTooltip\(/g) || []).length; const classNameUses = (src.match(/className:\s*'packet-path-tooltip'/g) || []).length; assert.ok(bindCalls > 0, 'expected at least one bindTooltip call'); @@ -454,46 +454,6 @@ function makeSandbox(apiImpl) { } catch (e) { failed++; console.log(' ❌ nodes with a known role get a role icon in their tooltip: ' + e.message); } })(); - await (async () => { - try { - // isBridge=true should get a bold purple outline (overriding the - // normal stroke color/weight) and a "bridge repeater" tooltip note. - const ctx = makeSandbox(() => Promise.resolve({ - hash: 'deadbeef', - branches: [ - { - hops: 1, - points: [ - { publicKey: 'pk1', name: 'PlainRepeater', lat: 56.0, lon: 10.0, isBridge: false }, - { publicKey: 'pk2', name: 'BridgeRepeater', lat: 56.1, lon: 10.1, isBridge: true }, - ], - observer: null, - }, - ], - })); - - const optsByTooltip = {}; - ctx.L = { - map: () => ({ setView() { return this; }, fitBounds() {}, invalidateSize() {}, remove() {} }), - tileLayer: () => ({ addTo() { return this; } }), - circleMarker: (latlng, opts) => ({ addTo() { return this; }, bindTooltip(t) { optsByTooltip[t] = opts; return this; }, on() { return this; } }), - polyline: () => ({ addTo() { return this; } }), - }; - - await ctx.window.PacketPathMap.open('deadbeef'); - const plainKey = Object.keys(optsByTooltip).find((k) => k.includes('PlainRepeater')); - const bridgeKey = Object.keys(optsByTooltip).find((k) => k.includes('BridgeRepeater')); - assert.ok(plainKey, 'expected a tooltip for PlainRepeater'); - assert.ok(bridgeKey, 'expected a tooltip for BridgeRepeater'); - assert.ok(!plainKey.includes('bridge repeater'), 'PlainRepeater tooltip should not mention bridge, got: ' + plainKey); - assert.ok(bridgeKey.includes('bridge repeater'), 'BridgeRepeater tooltip should mention bridge, got: ' + bridgeKey); - assert.notStrictEqual(optsByTooltip[bridgeKey].color, optsByTooltip[plainKey].color, 'expected the bridge marker to use a distinct outline color'); - assert.ok(optsByTooltip[bridgeKey].weight > optsByTooltip[plainKey].weight, 'expected the bridge marker outline to be thicker'); - passed++; - console.log(' ✅ bridge repeaters get a distinct outline and tooltip note'); - } catch (e) { failed++; console.log(' ❌ bridge repeaters get a distinct outline and tooltip note: ' + e.message); } - })(); - await (async () => { try { // A marker with a publicKey should register a click handler that