Reports and statistics answer in ClientModels' shapes

The moderators' reports and the admin statistics (overview, hosts,
crawler) were anonymous objects, so a client could read them only as loose
JSON. They are now ViewReport, ViewStatisticsOverview, ViewHostsPage and
ViewCrawler in PrivaPub.ClientModels, with the same JSON as before, for
decePub's Administration page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-05 05:32:32 +02:00
1 parent b146e182c6
commit ea5e607779
4 files changed
+145 -26

No files matched your search

+23 -14
View File
@@ -1,5 +1,6 @@
using MongoDB.Entities;
using PrivaPub.ClientModels.Admin;
using PrivaPub.Infrastructure.Statistics;
using PrivaPub.Models.Jobs;
using PrivaPub.Models.Statistics;
@@ -46,7 +47,7 @@ namespace PrivaPub.Domain.Statistics
return result.OrderBy(d => d.Day).ThenBy(d => d.Host, StringComparer.Ordinal).ToList();
}
public async Task<object> Overview(int days, CancellationToken token)
public async Task<ViewStatisticsOverview> Overview(int days, CancellationToken token)
{
var hostDays = await Days(days, default, token);
var counters = Sum(hostDays.Select(d => d.Counters));
@@ -54,21 +55,21 @@ namespace PrivaPub.Domain.Statistics
var server = await DB.Default.Find<ServerDay>().Match(d => d.Day >= DateTime.UtcNow.Date.AddDays(1 - Math.Clamp(days, 1, 3660))).ExecuteAsync(token);
var delivered = Outcomes(counters, Interactions.Out);
var attempts = delivered.Where(o => o.Key != Interactions.Deferred).Sum(o => o.Value);
return new
return new ViewStatisticsOverview
{
Days = days,
Channels = Channels.ToDictionary(c => c, c => counters.Where(k => k.Key.StartsWith(c + ":", StringComparison.Ordinal)).Sum(k => k.Value)),
Inbound = Outcomes(counters, Interactions.In),
Outbound = delivered,
DeliverySuccess = attempts == 0 ? (double?)null : Math.Round(delivered.GetValueOrDefault(Interactions.Ok) / (double)attempts, 4),
DeliveryLatencyMs = new { P50 = Percentile(latency, Interactions.Out, 0.5), P95 = Percentile(latency, Interactions.Out, 0.95) },
Hosts = new
DeliveryLatencyMs = new ViewLatency { P50 = Percentile(latency, Interactions.Out, 0.5), P95 = Percentile(latency, Interactions.Out, 0.95) },
Hosts = new ViewHostCounts
{
Active = hostDays.Where(d => d.Host != Interactions.Unknown && d.Counters.Count > 0).Select(d => d.Host).Distinct().Count(),
Known = await DB.Default.CountAsync<RemoteInstance>(cancellation: token)
},
Accounts = server.Sum(d => d.Accounts) + hostDays.Where(d => d.Live).Sum(d => d.Accounts),
Ledger = new
Ledger = new ViewLedgerCounts
{
Written = server.Sum(d => d.LedgerWritten),
Dropped = server.Sum(d => d.LedgerDropped),
@@ -77,7 +78,7 @@ namespace PrivaPub.Domain.Statistics
};
}
public async Task<object> Hosts(int days, string sort, string software, int page, int limit, CancellationToken token)
public async Task<ViewHostsPage> Hosts(int days, string sort, string software, int page, int limit, CancellationToken token)
{
var hostDays = await Days(days, default, token);
var instances = (await DB.Default.Find<RemoteInstance>().ExecuteAsync(token)).ToDictionary(i => i.Host, StringComparer.Ordinal);
@@ -87,10 +88,10 @@ namespace PrivaPub.Domain.Statistics
var counters = Sum(g.Select(d => d.Counters));
var outbound = Outcomes(counters, Interactions.Out);
var instance = instances.GetValueOrDefault(g.Key);
return new
return new ViewHostRow
{
Host = g.Key,
instance?.Software,
Software = instance?.Software,
Version = instance?.SoftwareVersion,
In = counters.Where(k => k.Key.StartsWith("in:", StringComparison.Ordinal)).Sum(k => k.Value),
Out = counters.Where(k => k.Key.StartsWith("out:", StringComparison.Ordinal)).Sum(k => k.Value),
@@ -98,8 +99,12 @@ namespace PrivaPub.Domain.Statistics
Failures = outbound.Where(o => o.Key is Interactions.Retry or Interactions.Dead or Interactions.Failed).Sum(o => o.Value),
LatencyP50 = Percentile(Sum(g.Select(d => d.Latency)), Interactions.Out, 0.5),
Accounts = g.Sum(d => d.Accounts),
instance?.DescribedAt,
Delivery = instance == default ? default : new { instance.ConsecutiveFailures, instance.UnavailableUntil, instance.LastSuccessAt, instance.LastFailureAt }
DescribedAt = instance?.DescribedAt,
Delivery = instance == default ? default : new ViewHostDelivery
{
ConsecutiveFailures = instance.ConsecutiveFailures, UnavailableUntil = instance.UnavailableUntil, LastSuccessAt = instance.LastSuccessAt,
LastFailureAt = instance.LastFailureAt
}
};
})
.Where(r => software == default || string.Equals(r.Software, software, StringComparison.OrdinalIgnoreCase));
@@ -112,7 +117,7 @@ namespace PrivaPub.Domain.Statistics
};
var list = rows.ToList();
limit = Math.Clamp(limit, 1, 200);
return new { Total = list.Count, Page = Math.Max(page, 1), Hosts = list.Skip((Math.Max(page, 1) - 1) * limit).Take(limit), Attribution };
return new ViewHostsPage { Total = list.Count, Page = Math.Max(page, 1), Hosts = list.Skip((Math.Max(page, 1) - 1) * limit).Take(limit).ToList(), Attribution = Attribution };
}
public async Task<object> Host(string host, int days, CancellationToken token)
@@ -147,7 +152,7 @@ namespace PrivaPub.Domain.Statistics
.ExecuteAsync(token);
}
public async Task<object> Crawler(bool enabled, int revisitDays, CancellationToken token)
public async Task<ViewCrawler> Crawler(bool enabled, int revisitDays, CancellationToken token)
{
var due = DateTime.UtcNow.AddDays(-Math.Max(revisitDays, 1));
var recent = await DB.Default.Find<RemoteInstance>()
@@ -155,14 +160,18 @@ namespace PrivaPub.Domain.Statistics
.Sort(i => i.LastCrawledAt, Order.Descending)
.Limit(20)
.ExecuteAsync(token);
return new
return new ViewCrawler
{
Enabled = enabled,
Touched = await DB.Default.CountAsync<RemoteInstance>(i => i.Seen == "touched", token),
Crawled = await DB.Default.CountAsync<RemoteInstance>(i => i.Seen == "crawled", token),
Due = await DB.Default.CountAsync<RemoteInstance>(i => i.LastCrawledAt == null || i.LastCrawledAt < due, token),
RobotsDisallowed = await DB.Default.CountAsync<RemoteInstance>(i => i.RobotsDisallowed == true, token),
Recent = recent.Select(i => new { i.Host, i.Seen, i.LastCrawledAt, i.PeersCount, i.RobotsDisallowed, i.CrawlError, i.Software })
Recent = recent.Select(i => new ViewCrawledHost
{
Host = i.Host, Seen = i.Seen, LastCrawledAt = i.LastCrawledAt, PeersCount = i.PeersCount, RobotsDisallowed = i.RobotsDisallowed,
CrawlError = i.CrawlError, Software = i.Software
}).ToList()
};
}