mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-10-23 23:32:26 +00:00
feat: show which regions are actually used per channel in Scope Adoption by Channel
Scoped/Unscoped/Unknown counts answered "how much" but not "which regions" — a channel spanning multiple hashRegions areas looked identical to one using just one. New "Regions Used" column lists the distinct scope_name values seen on each channel's scoped messages, most-used first. Backend: GetChannelScopeAdoption now also queries per-(channel, scope_name) counts and attaches them as ChannelScopeAdoption.Regions. Non-fatal on query failure — adoption counts still render without it.
This commit is contained in:
1 parent
60716facb4
commit
524b6326fd
4 files changed
+118
-1
No files matched your search
@@ -3128,9 +3128,52 @@ func (db *DB) GetChannelScopeAdoption(window string) ([]ChannelScopeAdoption, er
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("channel scope adoption iteration: %w", err)
|
||||
}
|
||||
|
||||
regionsByChannel, err := db.getChannelScopeRegions(since)
|
||||
if err != nil {
|
||||
// Non-fatal: the adoption counts above are still useful without
|
||||
// the per-channel region breakdown.
|
||||
log.Printf("WARN getChannelScopeRegions: %v", err)
|
||||
} else {
|
||||
for i := range result {
|
||||
result[i].Regions = regionsByChannel[result[i].Channel]
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// getChannelScopeRegions answers "which regions" for GetChannelScopeAdoption's
|
||||
// "how many scoped messages" — for each channel, which distinct scope_name
|
||||
// values have actually been seen on its scoped messages, most-used first.
|
||||
func (db *DB) getChannelScopeRegions(since string) (map[string][]string, error) {
|
||||
rows, err := db.conn.Query(`
|
||||
SELECT
|
||||
COALESCE(NULLIF(channel_hash, ''), '(unknown channel)') AS channel,
|
||||
scope_name,
|
||||
COUNT(*) AS cnt
|
||||
FROM transmissions
|
||||
WHERE payload_type = 5 AND first_seen >= ? AND `+routeTypeTransportSQL+` AND scope_name IS NOT NULL AND scope_name != ''
|
||||
GROUP BY channel, scope_name
|
||||
ORDER BY channel, cnt DESC
|
||||
`, since)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("channel scope regions query: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
result := make(map[string][]string)
|
||||
for rows.Next() {
|
||||
var channel, region string
|
||||
var cnt int
|
||||
if err := rows.Scan(&channel, ®ion, &cnt); err != nil {
|
||||
continue
|
||||
}
|
||||
result[channel] = append(result[channel], region)
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
// GetMatchedRegionNames returns the set of scope_name values that have ever
|
||||
// matched at least one transmission still in retention (NULL and empty-string
|
||||
// "unknown" rows are excluded). Used to diff against the operator's
|
||||
|
||||
@@ -4398,10 +4398,74 @@ func TestHandleScopeStats_ChannelScopeAdoption(t *testing.T) {
|
||||
if test.Channel != "#test" || test.TotalMessages != 3 || test.Scoped != 2 || test.Unscoped != 1 {
|
||||
t.Errorf("channelScopeAdoption[0] = %+v, want {#test total=3 scoped=2 unscoped=1}", test)
|
||||
}
|
||||
if len(test.Regions) != 1 || test.Regions[0] != "#belgium" {
|
||||
t.Errorf("channelScopeAdoption[0].Regions = %+v, want [#belgium]", test.Regions)
|
||||
}
|
||||
wardriving := resp.ChannelScopeAdoption[1]
|
||||
if wardriving.Channel != "#wardriving" || wardriving.TotalMessages != 1 || wardriving.Scoped != 0 || wardriving.Unscoped != 1 {
|
||||
t.Errorf("channelScopeAdoption[1] = %+v, want {#wardriving total=1 scoped=0 unscoped=1}", wardriving)
|
||||
}
|
||||
if len(wardriving.Regions) != 0 {
|
||||
t.Errorf("channelScopeAdoption[1].Regions = %+v, want none — #wardriving has no scoped messages", wardriving.Regions)
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandleScopeStats_ChannelScopeAdoption_RegionsOrderedByUsage verifies
|
||||
// the Regions field on each channel: which distinct scope_name values its
|
||||
// scoped messages actually carry, most-used first — a channel with mixed
|
||||
// region traffic (a shared/public channel spanning several hashRegions
|
||||
// areas) must list every region it's seen, not just the top one.
|
||||
func TestHandleScopeStats_ChannelScopeAdoption_RegionsOrderedByUsage(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(`DELETE FROM transmissions`); err != nil {
|
||||
t.Fatalf("clear transmissions: %v", err)
|
||||
}
|
||||
|
||||
now := time.Now().UTC().Format(time.RFC3339)
|
||||
rows := []struct {
|
||||
hash, channel, scope string
|
||||
}{
|
||||
// #public: 3x #dk, 2x #dk-oj, 1x #se — #dk must sort first.
|
||||
{"h1", "#public", "#dk"}, {"h2", "#public", "#dk"}, {"h3", "#public", "#dk"},
|
||||
{"h4", "#public", "#dk-oj"}, {"h5", "#public", "#dk-oj"},
|
||||
{"h6", "#public", "#se"},
|
||||
}
|
||||
for _, r := range rows {
|
||||
if _, err := srv.db.conn.Exec(
|
||||
`INSERT INTO transmissions (raw_hex,hash,first_seen,route_type,payload_type,channel_hash,scope_name) VALUES (?,?,?,0,5,?,?)`,
|
||||
"aa", r.hash, now, r.channel, r.scope,
|
||||
); err != nil {
|
||||
t.Fatalf("seed row %s: %v", r.hash, err)
|
||||
}
|
||||
}
|
||||
|
||||
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.ChannelScopeAdoption) != 1 {
|
||||
t.Fatalf("channelScopeAdoption = %+v, want 1 entry", resp.ChannelScopeAdoption)
|
||||
}
|
||||
got := resp.ChannelScopeAdoption[0].Regions
|
||||
want := []string{"#dk", "#dk-oj", "#se"}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("Regions = %+v, want %+v", got, want)
|
||||
}
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Errorf("Regions[%d] = %q, want %q (most-used-first order) — full: %+v", i, got[i], want[i], got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandleScopeStats_ChannelScopeAdoption_Uncapped verifies the endpoint
|
||||
|
||||
@@ -124,6 +124,12 @@ type ChannelScopeAdoption struct {
|
||||
Scoped int `json:"scoped"`
|
||||
Unscoped int `json:"unscoped"`
|
||||
UnknownScope int `json:"unknownScope"`
|
||||
// Regions is which specific scope_name values have actually been seen
|
||||
// on this channel's scoped messages, most-used first. Distinct from
|
||||
// Scoped (a count) — this answers "which regions", not just "how
|
||||
// many scoped messages". Omitted when the channel has no scoped
|
||||
// messages in the window.
|
||||
Regions []string `json:"regions,omitempty"`
|
||||
}
|
||||
|
||||
type ScopeRegionCount struct {
|
||||
|
||||
+5
-1
@@ -4744,16 +4744,20 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf
|
||||
var adoptRows = filtered.map(function(ca) {
|
||||
var caOverall = ca.scoped + ca.unscoped;
|
||||
var label = isEncrypted(ca) ? 'Encrypted (0x' + ca.channel.slice(4).toUpperCase() + ')' : ca.channel;
|
||||
var regionsHtml = (ca.regions && ca.regions.length)
|
||||
? ca.regions.map(function(r) { return '<code>' + esc(r) + '</code>'; }).join(', ')
|
||||
: '<span class="text-muted">—</span>';
|
||||
return '<tr>' +
|
||||
'<td><code>' + esc(label) + '</code></td>' +
|
||||
'<td>' + ca.totalMessages.toLocaleString() + '</td>' +
|
||||
'<td>' + ca.scoped.toLocaleString() + ' (' + pct(ca.scoped, caOverall) + ')</td>' +
|
||||
'<td>' + ca.unscoped.toLocaleString() + '</td>' +
|
||||
'<td>' + ca.unknownScope.toLocaleString() + '</td>' +
|
||||
'<td>' + regionsHtml + '</td>' +
|
||||
'</tr>';
|
||||
}).join('');
|
||||
adoptBody = '<table class="data-table analytics-table">' +
|
||||
'<thead><tr><th>Channel</th><th>Messages</th><th>Scoped</th><th>Unscoped</th><th>Unknown</th></tr></thead>' +
|
||||
'<thead><tr><th>Channel</th><th>Messages</th><th>Scoped</th><th>Unscoped</th><th>Unknown</th><th>Regions Used</th></tr></thead>' +
|
||||
'<tbody>' + adoptRows + '</tbody>' +
|
||||
'</table>';
|
||||
} else if (adoption.length > 0) {
|
||||
|
||||
Reference in new issue
Block a user