mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-10-23 12:32:34 +00:00
## Summary Implements the **Traffic axis** of the repeater usefulness score (#672). Does NOT close #672 — Bridge, Coverage, and Redundancy axes are deferred to follow-up PRs. Adds `usefulness_score` (0..1) to repeater/room node API responses representing what fraction of non-advert traffic passes through this repeater as a relay hop. ## Why traffic-axis-first The issue proposes a 4-axis composite (Bridge, Coverage, Traffic, Redundancy). Bridge/Coverage/Redundancy require betweenness centrality and neighbor graph infrastructure (#773 Neighbor Graph V2). Traffic axis can ship independently using existing path-hop data. ## Remaining work for #672 - Bridge axis (betweenness centrality — depends on #773) - Coverage axis (observer reach comparison) - Redundancy axis (node-removal simulation — depends on #687) - Composite score combining all 4 axes Partial fix for #672. --------- Co-authored-by: meshcore-bot <bot@meshcore.local>
This commit is contained in:
1 parent
d05e468598
commit
f33801ecb4
4 files changed
+181
No files matched your search
@@ -0,0 +1,64 @@
|
||||
package main
|
||||
|
||||
import "strings"
|
||||
|
||||
// GetRepeaterUsefulnessScore returns a 0..1 score representing what
|
||||
// fraction of non-advert traffic in the store passes through this
|
||||
// repeater as a relay hop. Issue #672 (Traffic axis only — bridge,
|
||||
// coverage, and redundancy axes are deferred to follow-up work).
|
||||
//
|
||||
// Numerator: count of non-advert StoreTx entries indexed under
|
||||
// pubkey in byPathHop.
|
||||
// Denominator: total non-advert StoreTx entries in the store
|
||||
// (sum of byPayloadType for all keys != payloadTypeAdvert).
|
||||
//
|
||||
// Returns 0 when there is no non-advert traffic, the pubkey is empty,
|
||||
// or the repeater never appears as a relay hop. Scores are clamped to
|
||||
// [0,1] for defensive bounds.
|
||||
//
|
||||
// Cost: O(N) over byPayloadType keys (typically <20) plus the per-hop
|
||||
// slice for pubkey. Cheap relative to the per-request enrichment loop
|
||||
// in handleNodes; if it ever shows up in profiles, denominator can be
|
||||
// memoized off store invalidation.
|
||||
func (s *PacketStore) GetRepeaterUsefulnessScore(pubkey string) float64 {
|
||||
if pubkey == "" {
|
||||
return 0
|
||||
}
|
||||
key := strings.ToLower(pubkey)
|
||||
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
// Denominator: total non-advert packets.
|
||||
totalNonAdvert := 0
|
||||
for pt, list := range s.byPayloadType {
|
||||
if pt == payloadTypeAdvert {
|
||||
continue
|
||||
}
|
||||
totalNonAdvert += len(list)
|
||||
}
|
||||
if totalNonAdvert == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Numerator: this repeater's non-advert hop appearances.
|
||||
relayed := 0
|
||||
for _, tx := range s.byPathHop[key] {
|
||||
if tx == nil {
|
||||
continue
|
||||
}
|
||||
if tx.PayloadType != nil && *tx.PayloadType == payloadTypeAdvert {
|
||||
continue
|
||||
}
|
||||
relayed++
|
||||
}
|
||||
|
||||
score := float64(relayed) / float64(totalNonAdvert)
|
||||
if score < 0 {
|
||||
return 0
|
||||
}
|
||||
if score > 1 {
|
||||
return 1
|
||||
}
|
||||
return score
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestRepeaterUsefulness_BasicShare verifies that usefulness_score is
|
||||
// relay_count_24h / total_non_advert_traffic_24h. With 1 of 4 relayed
|
||||
// packets going through the repeater, score should be 0.25.
|
||||
//
|
||||
// Issue #672. We are intentionally implementing the *traffic share*
|
||||
// dimension of the composite score from the issue body — bridge,
|
||||
// coverage, redundancy are deferred to follow-up work. This is the
|
||||
// "Traffic" axis of the table in #672.
|
||||
func TestRepeaterUsefulness_BasicShare(t *testing.T) {
|
||||
db := setupCapabilityTestDB(t)
|
||||
defer db.conn.Close()
|
||||
|
||||
pubkey := "aabbccdd11223344"
|
||||
store := NewPacketStore(db, nil)
|
||||
|
||||
// 4 non-advert packets total in last hour. The repeater appears in
|
||||
// the resolved path of exactly one of them.
|
||||
pt := 1
|
||||
for i := 0; i < 4; i++ {
|
||||
tx := &StoreTx{RawHex: "0100", PayloadType: &pt, FirstSeen: recentTS(0)}
|
||||
// Only first packet has our repeater in its path.
|
||||
if i == 0 {
|
||||
store.mu.Lock()
|
||||
tx.ID = len(store.packets) + 1
|
||||
tx.Hash = "uf-hit"
|
||||
store.packets = append(store.packets, tx)
|
||||
store.byHash[tx.Hash] = tx
|
||||
store.byTxID[tx.ID] = tx
|
||||
store.byPayloadType[pt] = append(store.byPayloadType[pt], tx)
|
||||
store.byPathHop[pubkey] = append(store.byPathHop[pubkey], tx)
|
||||
store.mu.Unlock()
|
||||
} else {
|
||||
addTestPacket(store, tx)
|
||||
}
|
||||
}
|
||||
|
||||
score := store.GetRepeaterUsefulnessScore(pubkey)
|
||||
// 1 relay / 4 total = 0.25
|
||||
if score < 0.24 || score > 0.26 {
|
||||
t.Errorf("expected usefulness ~0.25, got %f", score)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRepeaterUsefulness_NoTraffic verifies score is 0 when there is
|
||||
// no non-advert traffic to share.
|
||||
func TestRepeaterUsefulness_NoTraffic(t *testing.T) {
|
||||
db := setupCapabilityTestDB(t)
|
||||
defer db.conn.Close()
|
||||
store := NewPacketStore(db, nil)
|
||||
score := store.GetRepeaterUsefulnessScore("deadbeefcafebabe")
|
||||
if score != 0 {
|
||||
t.Errorf("expected 0 for empty store, got %f", score)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRepeaterUsefulness_AdvertsExcluded verifies that ADVERT packets
|
||||
// (payload_type=4) are excluded from both numerator and denominator —
|
||||
// adverts don't count as forwarded traffic.
|
||||
func TestRepeaterUsefulness_AdvertsExcluded(t *testing.T) {
|
||||
db := setupCapabilityTestDB(t)
|
||||
defer db.conn.Close()
|
||||
|
||||
pubkey := "11aa22bb33cc44dd"
|
||||
store := NewPacketStore(db, nil)
|
||||
|
||||
// 2 non-advert packets, both with our repeater in path → score = 1.0
|
||||
pt := 1
|
||||
for i := 0; i < 2; i++ {
|
||||
tx := &StoreTx{RawHex: "0100", PayloadType: &pt, FirstSeen: recentTS(0)}
|
||||
store.mu.Lock()
|
||||
tx.ID = len(store.packets) + 1
|
||||
tx.Hash = "uf-non-advert"
|
||||
if i == 1 {
|
||||
tx.Hash = "uf-non-advert-2"
|
||||
}
|
||||
store.packets = append(store.packets, tx)
|
||||
store.byHash[tx.Hash] = tx
|
||||
store.byTxID[tx.ID] = tx
|
||||
store.byPayloadType[pt] = append(store.byPayloadType[pt], tx)
|
||||
store.byPathHop[pubkey] = append(store.byPathHop[pubkey], tx)
|
||||
store.mu.Unlock()
|
||||
}
|
||||
// Add 100 adverts — these must be ignored.
|
||||
advertPT := payloadTypeAdvert
|
||||
for i := 0; i < 100; i++ {
|
||||
tx := &StoreTx{RawHex: "0400", PayloadType: &advertPT, FirstSeen: recentTS(0)}
|
||||
addTestPacket(store, tx)
|
||||
}
|
||||
|
||||
score := store.GetRepeaterUsefulnessScore(pubkey)
|
||||
if score < 0.99 || score > 1.01 {
|
||||
t.Errorf("expected usefulness ~1.0 (adverts excluded), got %f", score)
|
||||
}
|
||||
}
|
||||
@@ -1110,6 +1110,7 @@ func (s *Server) handleNodes(w http.ResponseWriter, r *http.Request) {
|
||||
node["relay_active"] = info.RelayActive
|
||||
node["relay_count_1h"] = info.RelayCount1h
|
||||
node["relay_count_24h"] = info.RelayCount24h
|
||||
node["usefulness_score"] = s.store.GetRepeaterUsefulnessScore(pk)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1217,6 +1218,7 @@ func (s *Server) handleNodeDetail(w http.ResponseWriter, r *http.Request) {
|
||||
node["relay_window_hours"] = info.WindowHours
|
||||
node["relay_count_1h"] = info.RelayCount1h
|
||||
node["relay_count_24h"] = info.RelayCount24h
|
||||
node["usefulness_score"] = s.store.GetRepeaterUsefulnessScore(pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -532,6 +532,21 @@
|
||||
<tr><td>Status</td><td><span title="${si.statusTooltip}">${statusLabel}</span> <span style="font-size:11px;color:var(--text-muted);margin-left:4px">${statusExplanation}</span></td></tr>
|
||||
<tr><td>Last Heard</td><td>${renderNodeTimestampHtml(lastHeard || n.last_seen)}</td></tr>
|
||||
${(n.role === 'repeater' || n.role === 'room') ? `<tr><td title="Last time this repeater appeared as a relay hop in a non-advert packet observed by the network. Distinct from 'Last Heard' (which counts the repeater's own adverts). See issue #662.">Last Relayed</td><td>${n.last_relayed ? renderNodeTimestampHtml(n.last_relayed) + ' ' + (n.relay_active ? '<span style="color:var(--status-green);font-size:11px">🟢 actively relaying</span>' : '<span style="color:var(--status-yellow);font-size:11px">🟡 alive (idle)</span>') : '<span style="color:var(--text-muted)">never observed as relay hop</span> <span style="color:var(--status-yellow);font-size:11px">🟡 alive (idle)</span>'}${(n.relay_count_1h != null || n.relay_count_24h != null) ? ` <span style="color:var(--text-muted);font-size:11px;margin-left:4px">(${n.relay_count_1h || 0} relays/hr, ${n.relay_count_24h || 0} relays/24h)</span>` : ''}</td></tr>` : ''}
|
||||
${(n.role === 'repeater' || n.role === 'room') && n.usefulness_score != null ? (() => {
|
||||
const s = Number(n.usefulness_score) || 0;
|
||||
const pct = (s * 100).toFixed(1);
|
||||
// Visual indicator: width % bar with green→yellow→red color by score.
|
||||
// Per issue #672 classification table: 0.8+ Critical, 0.6+ Valuable,
|
||||
// 0.3+ Moderate, 0.1+ Marginal, else Redundant.
|
||||
let label, color;
|
||||
if (s >= 0.8) { label = 'Critical'; color = 'var(--status-green, #2ecc71)'; }
|
||||
else if (s >= 0.6) { label = 'Valuable'; color = 'var(--status-green, #2ecc71)'; }
|
||||
else if (s >= 0.3) { label = 'Moderate'; color = 'var(--status-yellow, #f1c40f)'; }
|
||||
else if (s >= 0.1) { label = 'Marginal'; color = 'var(--status-orange, #e67e22)'; }
|
||||
else { label = 'Redundant'; color = 'var(--status-red, #e74c3c)'; }
|
||||
const barWidth = Math.max(2, Math.round(s * 100));
|
||||
return `<tr id="row-usefulness-score" data-usefulness-score="${s.toFixed(4)}"><td title="Fraction of non-advert traffic in the network observed by CoreScope that this repeater carries as a relay hop (Traffic axis of issue #672). Range 0–1; higher = forwards more of the mesh's actual traffic.">Usefulness</td><td><span style="display:inline-block;vertical-align:middle;width:80px;height:8px;background:var(--bg-secondary,#333);border-radius:4px;overflow:hidden;margin-right:6px"><span style="display:block;width:${barWidth}%;height:100%;background:${color}"></span></span><span style="color:${color};font-weight:600">${pct}%</span> <span style="color:var(--text-muted);font-size:11px;margin-left:4px">${label}</span></td></tr>`;
|
||||
})() : ''}
|
||||
<tr><td>First Seen</td><td>${renderNodeTimestampHtml(n.first_seen)}</td></tr>
|
||||
<tr><td>Total Packets</td><td>${stats.totalTransmissions || stats.totalPackets || n.advert_count || 0}${stats.totalObservations && stats.totalObservations !== (stats.totalTransmissions || stats.totalPackets) ? ' <span class="text-muted" style="font-size:0.85em">(seen ' + stats.totalObservations + '×)</span>' : ''}</td></tr>
|
||||
<tr><td>Packets Today</td><td>${stats.packetsToday || 0}</td></tr>
|
||||
|
||||
Reference in new issue
Block a user