diff --git a/cmd/server/openapi.go b/cmd/server/openapi.go index cf89ca16..cbe61806 100644 --- a/cmd/server/openapi.go +++ b/cmd/server/openapi.go @@ -426,6 +426,17 @@ func componentSchemas() map[string]interface{} { "kmPerSecondAirtime": map[string]interface{}{"type": "number", "nullable": true, "description": "farthestKm / (airtimeMs/1000) -- geographic distance covered per second of estimated RF airtime spent relaying this ping. Only set when both farthestKm and airtimeMs (with relayCount>0) are available."}, }, }, + "WeeklyPingRecords": map[string]interface{}{ + "type": "object", + "description": "Mirrors PingScoresResponse's 5 all-time record slots, scoped to the trailing 7 days -- an achievable target that resets on its own, instead of a slot that locks in forever once someone sets a big all-time record.", + "properties": map[string]interface{}{ + "farthestPing": schemaRef("PingScore"), + "mostHopsPing": schemaRef("PingScore"), + "widestSpreadPing": schemaRef("PingScore"), + "fastestSpreadPing": map[string]interface{}{"allOf": []interface{}{schemaRef("PingScore")}, "description": "Same >=2-station rule as the all-time fastestSpreadPing, applied within the 7-day window."}, + "mostEfficientPing": schemaRef("PingScore"), + }, + }, "PingLeaderboardEntry": map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ @@ -445,6 +456,7 @@ func componentSchemas() map[string]interface{} { "widestSpreadPing": schemaRef("PingScore"), "fastestSpreadPing": map[string]interface{}{"allOf": []interface{}{schemaRef("PingScore")}, "description": "The fastest full spread among pings heard by at least 2 stations -- a lone station is trivially \"instant\" and is excluded so it can't win this record for nothing."}, "mostEfficientPing": schemaRef("PingScore"), + "thisWeek": map[string]interface{}{"allOf": []interface{}{schemaRef("WeeklyPingRecords")}, "description": "The same 5 records as above, scoped to the trailing 7 days instead of all-time. Omitted when no ping in the last 7 days resolved to a usable score."}, "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 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."}, diff --git a/cmd/server/ping_scores.go b/cmd/server/ping_scores.go index 831cc1bd..68eeee33 100644 --- a/cmd/server/ping_scores.go +++ b/cmd/server/ping_scores.go @@ -86,6 +86,13 @@ type PingScoresSnapshot struct { FastestSpreadPing *PingScore `json:"fastestSpreadPing,omitempty"` MostEfficientPing *PingScore `json:"mostEfficientPing,omitempty"` + // ThisWeek mirrors the 5 all-time records above but scoped to the + // trailing 7 days -- an all-time record set once (e.g. a 364km + // farthest ping) locks that slot forever, so this gives people an + // achievable target that resets on its own (dborup, 2026-07-28). + // nil when no ping in the last 7 days resolved to a usable score. + ThisWeek *WeeklyPingRecords `json:"thisWeek,omitempty"` + // RelayLeaderboard ranks nodes by how many 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 that appears in @@ -105,6 +112,16 @@ type PingScoresSnapshot struct { SenderLeaderboard []PingLeaderboardEntry `json:"senderLeaderboard,omitempty"` } +// WeeklyPingRecords mirrors PingScoresSnapshot's 5 all-time record slots, +// scoped to the trailing 7 days. +type WeeklyPingRecords struct { + FarthestPing *PingScore `json:"farthestPing,omitempty"` + MostHopsPing *PingScore `json:"mostHopsPing,omitempty"` + WidestSpreadPing *PingScore `json:"widestSpreadPing,omitempty"` + FastestSpreadPing *PingScore `json:"fastestSpreadPing,omitempty"` + MostEfficientPing *PingScore `json:"mostEfficientPing,omitempty"` +} + type pingTriggerRow struct { txID int64 hash string @@ -211,6 +228,40 @@ func (s *Server) computePingScore(trigger pingTriggerRow) *PingScore { return score } +// pingRecordSet accumulates the same 5 "best of" record slots used by +// both the all-time and this-week views, so the selection rules only +// need to be written once. +type pingRecordSet struct { + Farthest *PingScore + MostHops *PingScore + WidestSpread *PingScore + FastestSpread *PingScore + MostEfficient *PingScore +} + +func (rs *pingRecordSet) consider(score *PingScore) { + if score.FarthestKm != nil && (rs.Farthest == nil || rs.Farthest.FarthestKm == nil || *score.FarthestKm > *rs.Farthest.FarthestKm) { + rs.Farthest = score + } + if rs.MostHops == nil || score.DeepestHops > rs.MostHops.DeepestHops { + rs.MostHops = score + } + if rs.WidestSpread == nil || score.StationCount > rs.WidestSpread.StationCount { + rs.WidestSpread = score + } + // Fastest full spread only makes sense with a real multi-station + // spread to measure -- a lone station is trivially "instant" and + // would otherwise always win this record for nothing. + if score.SpreadSeconds != nil && score.StationCount >= 2 && + (rs.FastestSpread == nil || rs.FastestSpread.SpreadSeconds == nil || *score.SpreadSeconds < *rs.FastestSpread.SpreadSeconds) { + rs.FastestSpread = score + } + if score.KmPerSecondAirtime != nil && + (rs.MostEfficient == nil || rs.MostEfficient.KmPerSecondAirtime == nil || *score.KmPerSecondAirtime > *rs.MostEfficient.KmPerSecondAirtime) { + rs.MostEfficient = score + } +} + // computeAllPingScores computes the full snapshot: records + leaderboards. func (s *Server) computeAllPingScores() *PingScoresSnapshot { triggers, err := s.db.fetchPingTriggers() @@ -236,31 +287,23 @@ func (s *Server) computeAllPingScores() *PingScoresSnapshot { senderCutoff := time.Now().AddDate(0, 0, -30) senderCounts := map[string]*PingLeaderboardEntry{} + // weekCutoff drives ThisWeek -- same fail-toward-stale rule as + // senderCutoff above (an unparseable timestamp is excluded, not + // defaulted to included). + weekCutoff := time.Now().AddDate(0, 0, -7) + allTime := &pingRecordSet{} + week := &pingRecordSet{} + for _, trigger := range triggers { score := s.computePingScore(trigger) if score == nil { continue } - if score.FarthestKm != nil && (snap.FarthestPing == nil || snap.FarthestPing.FarthestKm == nil || *score.FarthestKm > *snap.FarthestPing.FarthestKm) { - snap.FarthestPing = score - } - if snap.MostHopsPing == nil || score.DeepestHops > snap.MostHopsPing.DeepestHops { - snap.MostHopsPing = score - } - if snap.WidestSpreadPing == nil || score.StationCount > snap.WidestSpreadPing.StationCount { - snap.WidestSpreadPing = score - } - // Fastest full spread only makes sense with a real multi-station - // spread to measure -- a lone station is trivially "instant" and - // would otherwise always win this record for nothing. - if score.SpreadSeconds != nil && score.StationCount >= 2 && - (snap.FastestSpreadPing == nil || snap.FastestSpreadPing.SpreadSeconds == nil || *score.SpreadSeconds < *snap.FastestSpreadPing.SpreadSeconds) { - snap.FastestSpreadPing = score - } - if score.KmPerSecondAirtime != nil && - (snap.MostEfficientPing == nil || snap.MostEfficientPing.KmPerSecondAirtime == nil || *score.KmPerSecondAirtime > *snap.MostEfficientPing.KmPerSecondAirtime) { - snap.MostEfficientPing = score + allTime.consider(score) + ts, tsErr := time.Parse(time.RFC3339, score.Timestamp) + if tsErr == nil && ts.After(weekCutoff) { + week.consider(score) } for _, pk := range score.relayPubkeys { @@ -279,15 +322,28 @@ func (s *Server) computeAllPingScores() *PingScoresSnapshot { } e.Count++ } - if score.Sender != "" { - 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++ + if score.Sender != "" && tsErr == nil && ts.After(senderCutoff) { + e := senderCounts[score.Sender] + if e == nil { + e = &PingLeaderboardEntry{Name: score.Sender} + senderCounts[score.Sender] = e } + e.Count++ + } + } + + snap.FarthestPing = allTime.Farthest + snap.MostHopsPing = allTime.MostHops + snap.WidestSpreadPing = allTime.WidestSpread + snap.FastestSpreadPing = allTime.FastestSpread + snap.MostEfficientPing = allTime.MostEfficient + if week.Farthest != nil || week.MostHops != nil || week.WidestSpread != nil || week.FastestSpread != nil || week.MostEfficient != nil { + snap.ThisWeek = &WeeklyPingRecords{ + FarthestPing: week.Farthest, + MostHopsPing: week.MostHops, + WidestSpreadPing: week.WidestSpread, + FastestSpreadPing: week.FastestSpread, + MostEfficientPing: week.MostEfficient, } } diff --git a/cmd/server/ping_scores_test.go b/cmd/server/ping_scores_test.go index 26900a61..23a38c0a 100644 --- a/cmd/server/ping_scores_test.go +++ b/cmd/server/ping_scores_test.go @@ -318,6 +318,63 @@ func TestComputeAllPingScores_SenderLeaderboard30DayCutoff(t *testing.T) { } } +// TestComputeAllPingScores_ThisWeek confirms ThisWeek is an independently +// windowed record set: an older, BIGGER ping still wins the all-time slot, +// while a smaller but recent ping wins the equivalent ThisWeek slot since +// the older one falls outside the 7-day window. +func TestComputeAllPingScores_ThisWeek(t *testing.T) { + srv, _ := setupPingScoresFixture(t) + + // Old: 10 days ago, farther (pingobsb, ~230km from pingobsa) -- must + // win the all-time FarthestPing but be excluded from ThisWeek. + oldTs := time.Now().AddDate(0, 0, -10) + txOld := seedPingTrigger(t, srv, "weekold0000001", "#test", "Alice", 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: 2 days ago, closer (pingobsc, ~6km from pingobsa) -- the + // only ping inside the 7-day window, so it must win ThisWeek's + // FarthestPing even though it's smaller than the old one. + recentTs := time.Now().AddDate(0, 0, -2) + txRecent := seedPingTrigger(t, srv, "weeknew0000001", "#test", "Bob", recentTs.Format(time.RFC3339)) + seedPingObservation(t, srv, txRecent, "pingobsa", 9.0, `[]`, `[]`, recentTs.Unix()) + seedPingObservation(t, srv, txRecent, "pingobsc", 6.0, `["aa"]`, `["pkrelay2"]`, recentTs.Unix()+10) + + snap := srv.computeAllPingScores() + if snap == nil { + t.Fatal("computeAllPingScores returned nil") + } + if snap.FarthestPing == nil || snap.FarthestPing.Hash != "weekold0000001" { + t.Errorf("all-time FarthestPing = %+v, want weekold0000001 (the farther, older ping)", snap.FarthestPing) + } + if snap.ThisWeek == nil { + t.Fatal("ThisWeek is nil, want a populated record set from the recent ping") + } + if snap.ThisWeek.FarthestPing == nil || snap.ThisWeek.FarthestPing.Hash != "weeknew0000001" { + t.Errorf("ThisWeek.FarthestPing = %+v, want weeknew0000001 -- the old ping is outside the 7-day window", snap.ThisWeek.FarthestPing) + } +} + +// TestComputeAllPingScores_ThisWeekNilWhenNoRecentPings confirms ThisWeek +// stays nil (not a zero-valued struct) when every ping is outside the +// 7-day window, matching the frontend's null-safe "no record yet" +// rendering rather than an empty-but-present object. +func TestComputeAllPingScores_ThisWeekNilWhenNoRecentPings(t *testing.T) { + srv, _ := setupPingScoresFixture(t) + oldTs := time.Now().AddDate(0, 0, -30) + txOld := seedPingTrigger(t, srv, "weeknone000001", "#test", "Alice", 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) + + snap := srv.computeAllPingScores() + if snap == nil { + t.Fatal("computeAllPingScores returned nil") + } + if snap.ThisWeek != nil { + t.Errorf("ThisWeek = %+v, want nil when no ping in the last 7 days resolved to a usable score", snap.ThisWeek) + } +} + // 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. diff --git a/public/ping-scores.js b/public/ping-scores.js index ef777ada..838d3586 100644 --- a/public/ping-scores.js +++ b/public/ping-scores.js @@ -119,10 +119,25 @@ return recordCardHtml(def, data[def.key]); }).join(''); + // ThisWeek mirrors the same 5 slots, scoped to the trailing 7 days -- + // an all-time record set once (e.g. a 364km farthest ping) locks that + // card in forever, so this gives people an achievable target that + // resets on its own. May be entirely absent (no ping in 7 days), in + // which case every card falls through to recordCardHtml's "No record + // yet" placeholder via the `|| {}` fallback. + var week = data.thisWeek || {}; + var weekHtml = recordDefs.map(function (def) { + return recordCardHtml(def, week[def.key]); + }).join(''); + container.innerHTML = '
Global records and leaderboards from every "ping" sent in any channel (' + data.totalPings + ' total). Not scoped by region. Updated ' + escapeHtml(formatAgo(data.generatedAt)) + '.
' + + 'Resets on its own after 7 days.
' + + '