Merge branch 'areas-meshguide-sync'

This commit is contained in:
dborup
2026-07-28 14:03:58 +02:00
6 changed files with 205 additions and 28 deletions
+12
View File
@@ -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."},
+83 -27
View File
@@ -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,
}
}
+57
View File
@@ -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.
+15
View File
@@ -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 =
'<div class="ping-scores-page">' +
'<h2>' + phIcon('trophy') + ' Ping Scores</h2>' +
'<p class="text-muted">Global records and leaderboards from every "ping" sent in any channel (' + data.totalPings + ' total). Not scoped by region. Updated ' + escapeHtml(formatAgo(data.generatedAt)) + '.</p>' +
'<h3>' + phIcon('arrow-clockwise') + ' This Week\'s Best</h3>' +
'<p class="text-muted" style="font-size:0.85em">Resets on its own after 7 days.</p>' +
'<div class="stats-grid ps-records-grid ps-week-grid">' + weekHtml + '</div>' +
'<h3>All-Time Records</h3>' +
'<div class="stats-grid ps-records-grid">' + recordsHtml + '</div>' +
'<div class="ps-leaderboards-grid">' +
leaderboardTableHtml('Top Senders (30 days)', phIcon('megaphone'), data.senderLeaderboard, 'Sender') +
+3
View File
@@ -4517,9 +4517,12 @@ th.sort-active { color: var(--accent, #60a5fa); }
/* ── Ping Scores page (cmd/server/ping_scores.go, GET /api/ping-scores) ── */
.ping-scores-page { padding: 16px; max-width: 1100px; margin: 0 auto; }
.ping-scores-page h3 { margin: 20px 0 4px 0; font-size: 16px; }
.ping-scores-page h3:first-of-type { margin-top: 12px; }
.ps-records-grid { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); }
.ps-record-card { position: relative; }
.ps-record-card.ps-empty { opacity: 0.6; }
.ps-week-grid .ps-record-card { border-left: 3px solid var(--accent); padding-left: 13px; }
.ps-record-meta { font-size: 12px; color: var(--text-muted); margin-top: 4px; }
.ps-record-desc { font-size: 11px; color: var(--text-muted); margin-top: 6px; line-height: 1.4; }
.ps-leaderboards-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 16px; margin-top: 8px; }
+35 -1
View File
@@ -124,24 +124,58 @@ function makeSandbox(apiImpl) {
widestSpreadPing: { hash: 'wide0001', sender: 'Carol', timestamp: new Date().toISOString(), stationCount: 6, deepestHops: 3 },
fastestSpreadPing: { hash: 'fast0001', sender: 'Dave', timestamp: new Date().toISOString(), spreadSeconds: 2.5, stationCount: 2, deepestHops: 1 },
mostEfficientPing: { hash: 'eff0001', sender: 'Eve', timestamp: new Date().toISOString(), kmPerSecondAirtime: 50.2, farthestKm: 100, stationCount: 2, deepestHops: 1 },
thisWeek: {
farthestPing: { hash: 'wkfar0001', sender: 'Frank', timestamp: new Date().toISOString(), farthestKm: 12.3, farthestNodeName: 'WeeklyRepeater', stationCount: 2, deepestHops: 1 },
},
relayLeaderboard: [{ pubkey: 'pkrelay1', name: 'RelayOne', count: 7 }],
observerLeaderboard: [{ pubkey: 'pkobs1', name: 'ObsOne', count: 3 }],
senderLeaderboard: [{ name: 'PingMaster', count: 12 }],
};
const { container, getPage } = makeSandbox(() => Promise.resolve(data));
await getPage().init(container);
assert.ok(container.innerHTML.includes('123.4'), 'should show the farthest record km, got: ' + container.innerHTML);
assert.ok(container.innerHTML.includes('123.4'), 'should show the all-time farthest record km, got: ' + container.innerHTML);
assert.ok(container.innerHTML.includes('4 hops'), 'should show the most-hops record, got: ' + container.innerHTML);
assert.ok(container.innerHTML.includes('6 stations'), 'should show the widest-spread record, got: ' + container.innerHTML);
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 (30 days)'), 'should show the Top Senders leaderboard heading with the 30-day window noted, got: ' + container.innerHTML);
assert.ok(container.innerHTML.includes("This Week's Best"), 'should show the This Week\'s Best section heading, got: ' + container.innerHTML);
assert.ok(container.innerHTML.includes('All-Time Records'), 'should show the All-Time Records section heading, got: ' + container.innerHTML);
assert.ok(container.innerHTML.includes('12.3'), 'should show the thisWeek farthest record km distinct from the all-time one, got: ' + container.innerHTML);
assert.ok(container.innerHTML.includes('WeeklyRepeater'), 'should show the thisWeek record\'s node name, 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); }
})();
await (async () => {
try {
// thisWeek is entirely absent when no ping in the last 7 days
// resolved to a usable score (cmd/server/ping_scores.go leaves it
// nil rather than sending an empty object) -- the section must
// still render its 5 "No record yet" placeholders, not throw. All 5
// all-time slots are populated here so the only "No record yet"
// cards left are the 5 thisWeek ones -- isolates what's under test.
const data = {
totalPings: 1,
generatedAt: new Date().toISOString(),
farthestPing: { hash: 'old0001', sender: 'Grace', timestamp: new Date().toISOString(), farthestKm: 300, stationCount: 2, deepestHops: 1 },
mostHopsPing: { hash: 'old0002', sender: 'Grace', timestamp: new Date().toISOString(), deepestHops: 3, stationCount: 2 },
widestSpreadPing: { hash: 'old0003', sender: 'Grace', timestamp: new Date().toISOString(), stationCount: 4, deepestHops: 1 },
fastestSpreadPing: { hash: 'old0004', sender: 'Grace', timestamp: new Date().toISOString(), spreadSeconds: 3, stationCount: 2, deepestHops: 1 },
mostEfficientPing: { hash: 'old0005', sender: 'Grace', timestamp: new Date().toISOString(), kmPerSecondAirtime: 10, farthestKm: 20, stationCount: 2, deepestHops: 1 },
};
const { container, getPage } = makeSandbox(() => Promise.resolve(data));
await getPage().init(container);
assert.ok(container.innerHTML.includes("This Week's Best"), 'should still show the This Week\'s Best heading, got: ' + container.innerHTML);
const weekSectionCount = (container.innerHTML.match(/No record yet/g) || []).length;
assert.strictEqual(weekSectionCount, 5, 'expected all 5 thisWeek cards to fall back to "No record yet", got ' + weekSectionCount);
passed++;
console.log(' ✅ missing thisWeek renders 5 "No record yet" placeholders instead of throwing');
} catch (e) { failed++; console.log(' ❌ missing thisWeek renders 5 "No record yet" placeholders instead of throwing: ' + e.message); }
})();
await (async () => {
try {
// Sender entries never carry a pubkey (see cmd/server/ping_scores.go