From 640c552181158d6da09e489e3668919cf041a187 Mon Sep 17 00:00:00 2001 From: dborup Date: Tue, 28 Jul 2026 13:37:32 +0200 Subject: [PATCH 1/2] fix: scope Top Senders leaderboard to a rolling 30-day window Unlike the all-time records and the Top Relays/Top First-Hearers boards, "who pings the most" is more meaningful as an ongoing thing than a fixed leaderboard someone can win once and hold forever. --- cmd/server/ping_scores.go | 21 ++++++--- cmd/server/ping_scores_test.go | 81 +++++++++++++++++++++++++++++----- 2 files changed, 87 insertions(+), 15 deletions(-) diff --git a/cmd/server/ping_scores.go b/cmd/server/ping_scores.go index b78628ab..831cc1bd 100644 --- a/cmd/server/ping_scores.go +++ b/cmd/server/ping_scores.go @@ -225,6 +225,15 @@ func (s *Server) computeAllPingScores() *PingScoresSnapshot { } relayCounts := map[string]*PingLeaderboardEntry{} observerCounts := map[string]*PingLeaderboardEntry{} + // SenderLeaderboard is deliberately a rolling 30-day window, NOT + // all-time like the records and the other two leaderboards (dborup, + // after seeing it first ship all-time) -- "who pings the most" is + // more interesting as an ongoing/current thing than a fixed + // leaderboard someone can win once and hold forever. An unparseable + // timestamp is treated as too old (excluded), same fail-toward-stale + // convention as the active/degraded/silent health classification + // elsewhere in this file's package. + senderCutoff := time.Now().AddDate(0, 0, -30) senderCounts := map[string]*PingLeaderboardEntry{} for _, trigger := range triggers { @@ -271,12 +280,14 @@ func (s *Server) computeAllPingScores() *PingScoresSnapshot { e.Count++ } if score.Sender != "" { - e := senderCounts[score.Sender] - if e == nil { - e = &PingLeaderboardEntry{Name: score.Sender} - senderCounts[score.Sender] = e + if ts, err := time.Parse(time.RFC3339, score.Timestamp); err == nil && ts.After(senderCutoff) { + e := senderCounts[score.Sender] + if e == nil { + e = &PingLeaderboardEntry{Name: score.Sender} + senderCounts[score.Sender] = e + } + e.Count++ } - e.Count++ } } diff --git a/cmd/server/ping_scores_test.go b/cmd/server/ping_scores_test.go index beb263d9..26900a61 100644 --- a/cmd/server/ping_scores_test.go +++ b/cmd/server/ping_scores_test.go @@ -201,15 +201,19 @@ func TestComputeAllPingScores_Leaderboards(t *testing.T) { srv, _ := setupPingScoresFixture(t) // Ping 1: pkrelay1 appears in BOTH branches -- must count once for this ping. - tx1 := seedPingTrigger(t, srv, "lbtest0000001", "#test", "Alice", "2026-01-15T10:00:00Z") - seedPingObservation(t, srv, tx1, "pingobsa", 9.0, `[]`, `[]`, 1736935200) - seedPingObservation(t, srv, tx1, "pingobsb", 6.0, `["aa"]`, `["pkrelay1"]`, 1736935210) - seedPingObservation(t, srv, tx1, "pingobsc", 4.0, `["aa"]`, `["pkrelay1"]`, 1736935220) + // Uses a recent (now-relative) timestamp since SenderLeaderboard is a + // rolling 30-day window -- see TestComputeAllPingScores_SenderLeaderboard30DayCutoff. + ts1 := time.Now().Add(-1 * time.Hour) + tx1 := seedPingTrigger(t, srv, "lbtest0000001", "#test", "Alice", ts1.Format(time.RFC3339)) + seedPingObservation(t, srv, tx1, "pingobsa", 9.0, `[]`, `[]`, ts1.Unix()) + seedPingObservation(t, srv, tx1, "pingobsb", 6.0, `["aa"]`, `["pkrelay1"]`, ts1.Unix()+10) + seedPingObservation(t, srv, tx1, "pingobsc", 4.0, `["aa"]`, `["pkrelay1"]`, ts1.Unix()+20) // Ping 2: pkrelay1 appears again -- second distinct ping, should now be 2. - tx2 := seedPingTrigger(t, srv, "lbtest0000002", "#test", "Bob", "2026-01-15T11:00:00Z") - seedPingObservation(t, srv, tx2, "pingobsa", 9.0, `[]`, `[]`, 1736938800) - seedPingObservation(t, srv, tx2, "pingobsb", 6.0, `["aa"]`, `["pkrelay1"]`, 1736938810) + ts2 := time.Now().Add(-30 * time.Minute) + tx2 := seedPingTrigger(t, srv, "lbtest0000002", "#test", "Bob", ts2.Format(time.RFC3339)) + seedPingObservation(t, srv, tx2, "pingobsa", 9.0, `[]`, `[]`, ts2.Unix()) + seedPingObservation(t, srv, tx2, "pingobsb", 6.0, `["aa"]`, `["pkrelay1"]`, ts2.Unix()+10) snap := srv.computeAllPingScores() if snap == nil { @@ -258,6 +262,62 @@ func TestComputeAllPingScores_Leaderboards(t *testing.T) { } } +// TestComputeAllPingScores_SenderLeaderboard30DayCutoff confirms +// SenderLeaderboard only counts pings from the last 30 days, while the +// other leaderboards and the 5 all-time records stay unaffected by a ping's +// age. +func TestComputeAllPingScores_SenderLeaderboard30DayCutoff(t *testing.T) { + srv, _ := setupPingScoresFixture(t) + + // Old: 40 days ago -- outside the 30-day window, must NOT appear in + // SenderLeaderboard, but must still count in RelayLeaderboard. + oldTs := time.Now().AddDate(0, 0, -40) + txOld := seedPingTrigger(t, srv, "lbold0000001", "#test", "OldSender", oldTs.Format(time.RFC3339)) + seedPingObservation(t, srv, txOld, "pingobsa", 9.0, `[]`, `[]`, oldTs.Unix()) + seedPingObservation(t, srv, txOld, "pingobsb", 6.0, `["aa"]`, `["pkrelay1"]`, oldTs.Unix()+10) + + // Recent: 5 days ago -- inside the window, must appear in SenderLeaderboard. + recentTs := time.Now().AddDate(0, 0, -5) + txRecent := seedPingTrigger(t, srv, "lbnew0000001", "#test", "RecentSender", recentTs.Format(time.RFC3339)) + seedPingObservation(t, srv, txRecent, "pingobsa", 9.0, `[]`, `[]`, recentTs.Unix()) + seedPingObservation(t, srv, txRecent, "pingobsb", 6.0, `["aa"]`, `["pkrelay1"]`, recentTs.Unix()+10) + + snap := srv.computeAllPingScores() + if snap == nil { + t.Fatal("computeAllPingScores returned nil") + } + + for _, e := range snap.SenderLeaderboard { + if e.Name == "OldSender" { + t.Errorf("OldSender (40 days old) must be excluded from SenderLeaderboard, got entry: %+v", e) + } + } + var foundRecent bool + for _, e := range snap.SenderLeaderboard { + if e.Name == "RecentSender" { + foundRecent = true + if e.Count != 1 { + t.Errorf("RecentSender count = %d, want 1", e.Count) + } + } + } + if !foundRecent { + t.Errorf("RecentSender (5 days old) missing from SenderLeaderboard: %+v", snap.SenderLeaderboard) + } + + // The 40-day-old ping's relay must still count -- only the sender + // leaderboard is time-windowed, not the underlying data or other boards. + var relay1Count int + for _, e := range snap.RelayLeaderboard { + if e.Pubkey == "pkrelay1" { + relay1Count = e.Count + } + } + if relay1Count != 2 { + t.Errorf("pkrelay1 relay count = %d, want 2 (both old and recent pings count toward RelayLeaderboard)", relay1Count) + } +} + // TestHandlePingScores_EmptyState confirms the endpoint returns a // well-formed 200 with zero-valued/omitted fields rather than an error // when no ping has ever been recorded -- an ordinary state, not a failure. @@ -309,9 +369,10 @@ func TestHandlePingScores_NilCache(t *testing.T) { // (nested PingScore objects, leaderboards present). func TestHandlePingScores_Populated(t *testing.T) { srv, router := setupPingScoresFixture(t) - txID := seedPingTrigger(t, srv, "handlerpop00001", "#test", "Alice", "2026-01-15T10:00:00Z") - seedPingObservation(t, srv, txID, "pingobsa", 9.0, `[]`, `[]`, 1736935200) - seedPingObservation(t, srv, txID, "pingobsb", 6.0, `["aa"]`, `["pkrelay1"]`, 1736935210) + ts := time.Now().Add(-1 * time.Hour) + txID := seedPingTrigger(t, srv, "handlerpop00001", "#test", "Alice", ts.Format(time.RFC3339)) + seedPingObservation(t, srv, txID, "pingobsa", 9.0, `[]`, `[]`, ts.Unix()) + seedPingObservation(t, srv, txID, "pingobsb", 6.0, `["aa"]`, `["pkrelay1"]`, ts.Unix()+10) srv.pingScores.Store(srv.computeAllPingScores()) From d5361d797fb78620816cba7381fd187c7f2d64b5 Mon Sep 17 00:00:00 2001 From: dborup Date: Tue, 28 Jul 2026 13:42:03 +0200 Subject: [PATCH 2/2] fix: label Top Senders leaderboard heading with its 30-day window The board's scope silently differs from the other leaderboards (all-time) -- make that explicit in the UI heading and API docs. --- cmd/server/openapi.go | 2 +- public/ping-scores.js | 2 +- test-ping-scores.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/server/openapi.go b/cmd/server/openapi.go index f4fcbdc9..cf89ca16 100644 --- a/cmd/server/openapi.go +++ b/cmd/server/openapi.go @@ -447,7 +447,7 @@ func componentSchemas() map[string]interface{} { "mostEfficientPing": schemaRef("PingScore"), "relayLeaderboard": map[string]interface{}{"type": "array", "items": schemaRef("PingLeaderboardEntry"), "description": "Top nodes ranked by number of distinct pings they appeared as a relay hop in (deduped per ping first, so one busy ping's many branches can't over-credit a relay)."}, "observerLeaderboard": map[string]interface{}{"type": "array", "items": schemaRef("PingLeaderboardEntry"), "description": "Top observers ranked by number of pings they were the first station to hear."}, - "senderLeaderboard": map[string]interface{}{"type": "array", "items": schemaRef("PingLeaderboardEntry"), "description": "Top senders ranked by number of pings sent. Keyed by the sender display name from the channel message itself -- no resolved pubkey, so entries never carry one."}, + "senderLeaderboard": map[string]interface{}{"type": "array", "items": schemaRef("PingLeaderboardEntry"), "description": "Top senders ranked by number of pings sent in the last 30 days (unlike the other leaderboards and records, which are all-time). Keyed by the sender display name from the channel message itself -- no resolved pubkey, so entries never carry one."}, }, }, "AreaDensity": map[string]interface{}{ diff --git a/public/ping-scores.js b/public/ping-scores.js index 341d77d2..ef777ada 100644 --- a/public/ping-scores.js +++ b/public/ping-scores.js @@ -125,7 +125,7 @@ '

Global records and leaderboards from every "ping" sent in any channel (' + data.totalPings + ' total). Not scoped by region. Updated ' + escapeHtml(formatAgo(data.generatedAt)) + '.

' + '
' + recordsHtml + '
' + '
' + - leaderboardTableHtml('Top Senders', phIcon('megaphone'), data.senderLeaderboard, 'Sender') + + leaderboardTableHtml('Top Senders (30 days)', phIcon('megaphone'), data.senderLeaderboard, 'Sender') + leaderboardTableHtml('Top Relays', phIcon('repeat'), data.relayLeaderboard) + leaderboardTableHtml('Top First-Hearers', phIcon('eye'), data.observerLeaderboard) + '
' + diff --git a/test-ping-scores.js b/test-ping-scores.js index 22ed9b21..096f29b9 100644 --- a/test-ping-scores.js +++ b/test-ping-scores.js @@ -136,7 +136,7 @@ function makeSandbox(apiImpl) { assert.ok(container.innerHTML.includes('RelayOne'), 'should show the relay leaderboard entry, got: ' + container.innerHTML); assert.ok(container.innerHTML.includes('ObsOne'), 'should show the observer leaderboard entry, got: ' + container.innerHTML); assert.ok(container.innerHTML.includes('PingMaster'), 'should show the sender leaderboard entry, got: ' + container.innerHTML); - assert.ok(container.innerHTML.includes('Top Senders'), 'should show the Top Senders leaderboard heading, got: ' + container.innerHTML); + assert.ok(container.innerHTML.includes('Top Senders (30 days)'), 'should show the Top Senders leaderboard heading with the 30-day window noted, got: ' + container.innerHTML); passed++; console.log(' ✅ renders all 5 records and all three leaderboards (including Top Senders) from a populated response'); } catch (e) { failed++; console.log(' ❌ renders all 5 records and all three leaderboards (including Top Senders) from a populated response: ' + e.message); }