mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-09-26 15:47:54 +00:00
feat: bridge-repeaters breakdown on the Scopes analytics tab
Adds bridgeRepeaters to /api/scope-stats: RepeatersByRegion inverted into pubkey -> regions, keeping only repeaters that have relayed traffic for MORE than one region. These are the mesh's literal backbone nodes connecting otherwise-separate regional communities — losing one is a more consequential failure than losing a single-region repeater. Computed inline while building RepeatersByRegion (reuses the same byRegion map and role-filtered names lookup, no extra queries). Frontend renders a small table under "Repeaters by Region": repeater name (linked to its node detail page), region count, and the region list.
This commit is contained in:
@@ -3568,6 +3568,35 @@ func (s *Server) handleScopeStats(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
sort.Slice(repeaters, func(i, j int) bool { return repeaters[i].Count > repeaters[j].Count })
|
||||
resp.RepeatersByRegion = repeaters
|
||||
|
||||
// Bridge repeaters: invert byRegion into pubkey -> regions and
|
||||
// keep only repeaters relaying for MORE than one region — the
|
||||
// mesh's literal backbone nodes connecting separate regional
|
||||
// communities. Same `names` existence-filter as above.
|
||||
pubkeyRegions := make(map[string][]string)
|
||||
for region, pks := range byRegion {
|
||||
for _, pk := range pks {
|
||||
if _, ok := names[pk]; !ok {
|
||||
continue
|
||||
}
|
||||
pubkeyRegions[pk] = append(pubkeyRegions[pk], region)
|
||||
}
|
||||
}
|
||||
bridges := make([]BridgeRepeater, 0)
|
||||
for pk, regions := range pubkeyRegions {
|
||||
if len(regions) < 2 {
|
||||
continue
|
||||
}
|
||||
sort.Strings(regions)
|
||||
bridges = append(bridges, BridgeRepeater{Name: names[pk], PublicKey: pk, Regions: regions, Count: len(regions)})
|
||||
}
|
||||
sort.Slice(bridges, func(i, j int) bool {
|
||||
if bridges[i].Count != bridges[j].Count {
|
||||
return bridges[i].Count > bridges[j].Count
|
||||
}
|
||||
return bridges[i].Name < bridges[j].Name
|
||||
})
|
||||
resp.BridgeRepeaters = bridges
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4424,6 +4424,74 @@ func TestHandleScopeStats_RepeatersByRegion(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandleScopeStats_BridgeRepeaters verifies the RepeatersByRegion
|
||||
// inversion: a repeater that relayed traffic for TWO distinct regions must
|
||||
// appear in BridgeRepeaters with both region names, while a repeater that
|
||||
// only ever relayed one region must be excluded.
|
||||
func TestHandleScopeStats_BridgeRepeaters(t *testing.T) {
|
||||
srv, _ := setupTestServer(t)
|
||||
if _, err := srv.db.conn.Exec(`ALTER TABLE transmissions ADD COLUMN scope_name TEXT DEFAULT NULL`); err != nil {
|
||||
t.Fatalf("add scope_name column: %v", err)
|
||||
}
|
||||
srv.db.hasScopeName = true
|
||||
|
||||
if _, err := srv.db.conn.Exec(
|
||||
`INSERT INTO nodes (public_key, name, role) VALUES ('bbbbccdd0011', 'BridgeNode', 'repeater')`,
|
||||
); err != nil {
|
||||
t.Fatalf("seed bridge node: %v", err)
|
||||
}
|
||||
if _, err := srv.db.conn.Exec(
|
||||
`INSERT INTO nodes (public_key, name, role) VALUES ('ccccccdd0011', 'SingleRegionNode', 'repeater')`,
|
||||
); err != nil {
|
||||
t.Fatalf("seed single-region node: %v", err)
|
||||
}
|
||||
|
||||
pt5 := 5
|
||||
txBelgium := &StoreTx{ID: 1, Hash: "tx1", FirstSeen: time.Now().UTC().Add(-5 * time.Minute).Format(time.RFC3339Nano), PayloadType: &pt5, ScopeName: "#belgium"}
|
||||
txFrance := &StoreTx{ID: 2, Hash: "tx2", FirstSeen: time.Now().UTC().Add(-5 * time.Minute).Format(time.RFC3339Nano), PayloadType: &pt5, ScopeName: "#france"}
|
||||
txBelgium2 := &StoreTx{ID: 3, Hash: "tx3", FirstSeen: time.Now().UTC().Add(-5 * time.Minute).Format(time.RFC3339Nano), PayloadType: &pt5, ScopeName: "#belgium"}
|
||||
|
||||
srv.store = &PacketStore{
|
||||
byPathHop: map[string][]*StoreTx{
|
||||
"bbbbccdd0011": {txBelgium, txFrance}, // relayed BOTH regions — a bridge
|
||||
"ccccccdd0011": {txBelgium2}, // relayed only #belgium — not a bridge
|
||||
},
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/scope-stats?window=24h", nil)
|
||||
w := httptest.NewRecorder()
|
||||
srv.handleScopeStats(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200; body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var resp ScopeStatsResponse
|
||||
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if len(resp.BridgeRepeaters) != 1 {
|
||||
t.Fatalf("bridgeRepeaters = %v, want 1 entry", resp.BridgeRepeaters)
|
||||
}
|
||||
br := resp.BridgeRepeaters[0]
|
||||
if br.Name != "BridgeNode" || br.PublicKey != "bbbbccdd0011" || br.Count != 2 {
|
||||
t.Errorf("bridgeRepeaters[0] = %+v, want {BridgeNode bbbbccdd0011 2 [...]}", br)
|
||||
}
|
||||
wantRegions := []string{"#belgium", "#france"}
|
||||
if len(br.Regions) != len(wantRegions) {
|
||||
t.Fatalf("bridgeRepeaters[0].Regions = %v, want %v", br.Regions, wantRegions)
|
||||
}
|
||||
for i, r := range wantRegions {
|
||||
if br.Regions[i] != r {
|
||||
t.Errorf("bridgeRepeaters[0].Regions[%d] = %q, want %q", i, br.Regions[i], r)
|
||||
}
|
||||
}
|
||||
for _, b := range resp.BridgeRepeaters {
|
||||
if b.PublicKey == "ccccccdd0011" {
|
||||
t.Errorf("SingleRegionNode should not appear in bridgeRepeaters (only relayed 1 region)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandleScopeStats_OriginatingNodesByRegion verifies the complementary
|
||||
// breakdown to RepeatersByRegion: nodes whose OWN default_scope (#899) is a
|
||||
// given region, i.e. nodes actually configured with/running that region
|
||||
|
||||
@@ -154,6 +154,11 @@ type ScopeStatsResponse struct {
|
||||
// channel chat specifically (payload_type=5), window-scoped like
|
||||
// Summary above — see ChannelScopeStats doc.
|
||||
ChannelMessages *ChannelScopeStats `json:"channelMessages,omitempty"`
|
||||
// BridgeRepeaters is the RepeatersByRegion data inverted: repeaters
|
||||
// that have relayed traffic for MORE than one region are the mesh's
|
||||
// literal backbone nodes connecting separate regional communities.
|
||||
// All-time, like RepeatersByRegion (same source data, same caveats).
|
||||
BridgeRepeaters []BridgeRepeater `json:"bridgeRepeaters,omitempty"`
|
||||
}
|
||||
|
||||
type RepeaterRef struct {
|
||||
@@ -167,6 +172,13 @@ type ScopeRegionRepeaters struct {
|
||||
Repeaters []RepeaterRef `json:"repeaters"`
|
||||
}
|
||||
|
||||
type BridgeRepeater struct {
|
||||
Name string `json:"name"`
|
||||
PublicKey string `json:"publicKey"`
|
||||
Regions []string `json:"regions"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
// ─── Health ────────────────────────────────────────────────────────────────────
|
||||
|
||||
type MemoryStats struct {
|
||||
|
||||
@@ -4493,6 +4493,7 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _analyticsData =
|
||||
'<div id="scopes-chart"></div>' +
|
||||
'<div id="scopes-utilization" style="margin-top:16px"></div>' +
|
||||
'<div id="scopes-repeaters" style="margin-top:16px"></div>' +
|
||||
'<div id="scopes-bridges" style="margin-top:16px"></div>' +
|
||||
'<div id="scopes-origin-nodes" style="margin-top:16px"></div>';
|
||||
|
||||
// Attach window-button click listeners (once)
|
||||
@@ -4711,6 +4712,34 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _analyticsData =
|
||||
'All-time, not limited to the window above — which repeaters have relayed traffic carrying each region scope. A region carried by only 1 repeater is a single point of failure for that area.',
|
||||
d.repeatersByRegion, 'repeater');
|
||||
|
||||
// Bridge repeaters: RepeatersByRegion inverted — repeaters relaying
|
||||
// for MORE than one region are the mesh's literal backbone nodes.
|
||||
var bridgeEl = document.getElementById('scopes-bridges');
|
||||
if (bridgeEl) {
|
||||
var bridges = d.bridgeRepeaters || [];
|
||||
if (bridges.length > 0) {
|
||||
var bridgeRows = bridges.map(function(b) {
|
||||
var regionList = b.regions.map(function(r) { return '<code>' + esc(r) + '</code>'; }).join(', ');
|
||||
return '<tr>' +
|
||||
'<td><a href="#/nodes/' + encodeURIComponent(b.publicKey) + '">' + esc(b.name) + '</a></td>' +
|
||||
'<td>' + b.count + '</td>' +
|
||||
'<td>' + regionList + '</td>' +
|
||||
'</tr>';
|
||||
}).join('');
|
||||
bridgeEl.innerHTML =
|
||||
'<h4 style="margin:0 0 4px">Bridge Repeaters</h4>' +
|
||||
'<p class="text-muted" style="margin:0 0 8px;font-size:0.85em">' +
|
||||
'All-time — repeaters that have relayed traffic for more than one region. These connect otherwise-separate regional communities; losing one can split the mesh\'s regional coverage.' +
|
||||
'</p>' +
|
||||
'<table class="data-table analytics-table">' +
|
||||
'<thead><tr><th>Repeater</th><th># Regions</th><th>Regions</th></tr></thead>' +
|
||||
'<tbody>' + bridgeRows + '</tbody>' +
|
||||
'</table>';
|
||||
} else {
|
||||
bridgeEl.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
renderRegionNodeGroups('scopes-origin-nodes', 'Nodes Running This Region',
|
||||
'All-time — nodes whose OWN configured scope is this region (not just relaying it for others). This is a much smaller, more specific set than "Repeaters by Region" above.',
|
||||
d.originatingNodesByRegion, 'node');
|
||||
|
||||
Reference in New Issue
Block a user