diff --git a/PrivaPub.ClientModels/Admin/ViewReport.cs b/PrivaPub.ClientModels/Admin/ViewReport.cs new file mode 100644 index 0000000..3278918 --- /dev/null +++ b/PrivaPub.ClientModels/Admin/ViewReport.cs @@ -0,0 +1,20 @@ +namespace PrivaPub.ClientModels.Admin +{ + // a report as moderators read it: who sent it ("local" for a persona here, never which one; or the remote actor), whom + // and which posts it is about, and whether it was resolved + public class ViewReport + { + public string ID { get; set; } + public string Reporter { get; set; } + public string TargetAccountId { get; set; } + public string TargetActorURI { get; set; } + public List PostIds { get; set; } = new(); + public List ObjectURIs { get; set; } = new(); + public string Category { get; set; } + public string Comment { get; set; } + public bool Forwarded { get; set; } + public bool IsResolved { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime? ResolvedAt { get; set; } + } +} diff --git a/PrivaPub.ClientModels/Admin/ViewStatistics.cs b/PrivaPub.ClientModels/Admin/ViewStatistics.cs new file mode 100644 index 0000000..5328fcb --- /dev/null +++ b/PrivaPub.ClientModels/Admin/ViewStatistics.cs @@ -0,0 +1,89 @@ +namespace PrivaPub.ClientModels.Admin +{ + // /clientapi/admin/statistics/overview: the last days' federation at a glance; counters keyed as the ledger keys them + public class ViewStatisticsOverview + { + public int Days { get; set; } + public Dictionary Channels { get; set; } = new(); + public Dictionary Inbound { get; set; } = new(); + public Dictionary Outbound { get; set; } = new(); + public double? DeliverySuccess { get; set; } + public ViewLatency DeliveryLatencyMs { get; set; } = new(); + public ViewHostCounts Hosts { get; set; } = new(); + public int Accounts { get; set; } + public ViewLedgerCounts Ledger { get; set; } = new(); + } + + public class ViewLatency + { + public int? P50 { get; set; } + public int? P95 { get; set; } + } + + public class ViewHostCounts + { + public int Active { get; set; } + public long Known { get; set; } + } + + public class ViewLedgerCounts + { + public long Written { get; set; } + public long Dropped { get; set; } + public long Failed { get; set; } + } + + // /clientapi/admin/statistics/hosts: one page of the servers we exchanged with + public class ViewHostsPage + { + public int Total { get; set; } + public int Page { get; set; } + public List Hosts { get; set; } = new(); + public string Attribution { get; set; } + } + + public class ViewHostRow + { + public string Host { get; set; } + public string Software { get; set; } + public string Version { get; set; } + public long In { get; set; } + public long Out { get; set; } + public long Refused { get; set; } + public long Failures { get; set; } + public int? LatencyP50 { get; set; } + public int Accounts { get; set; } + public DateTime? DescribedAt { get; set; } + public ViewHostDelivery Delivery { get; set; } + } + + public class ViewHostDelivery + { + public int ConsecutiveFailures { get; set; } + public DateTime? UnavailableUntil { get; set; } + public DateTime? LastSuccessAt { get; set; } + public DateTime? LastFailureAt { get; set; } + } + + // /clientapi/admin/statistics/crawler: how far the crawler got + public class ViewCrawler + { + public bool Enabled { get; set; } + public long Touched { get; set; } + public long Crawled { get; set; } + public long Due { get; set; } + public long RobotsDisallowed { get; set; } + public List Recent { get; set; } = new(); + } + + public class ViewCrawledHost + { + public string Host { get; set; } + public string Seen { get; set; } + public DateTime? LastCrawledAt { get; set; } + public int? PeersCount { get; set; } + public bool? RobotsDisallowed { get; set; } + public string CrawlError { get; set; } + public string Software { get; set; } + } +} diff --git a/PrivaPub/Controllers/ClientToServer/ModeratorController.cs b/PrivaPub/Controllers/ClientToServer/ModeratorController.cs index 7898e18..9775170 100644 --- a/PrivaPub/Controllers/ClientToServer/ModeratorController.cs +++ b/PrivaPub/Controllers/ClientToServer/ModeratorController.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc; using MongoDB.Entities; using PrivaPub.ClientModels; +using PrivaPub.ClientModels.Admin; using PrivaPub.Extensions; using PrivaPub.Models.Social; @@ -17,20 +18,20 @@ namespace PrivaPub.Controllers.ClientToServer [HttpGet, Route("/clientapi/moderator/reports")] public async Task Reports([FromQuery] bool resolved, CancellationToken token) => Ok((await DB.Default.Find().Match(r => r.IsResolved == resolved).Sort(r => r.ID, Order.Descending).Limit(100).ExecuteAsync(token)) - .Select(r => new + .Select(r => new ViewReport { - r.ID, + ID = r.ID, Reporter = r.ReporterAvatarId != default ? "local" : r.ReporterActorURI, - r.TargetAccountId, - r.TargetActorURI, - r.PostIds, - r.ObjectURIs, - r.Category, - r.Comment, - r.Forwarded, - r.IsResolved, - r.CreatedAt, - r.ResolvedAt + TargetAccountId = r.TargetAccountId, + TargetActorURI = r.TargetActorURI, + PostIds = r.PostIds, + ObjectURIs = r.ObjectURIs, + Category = r.Category, + Comment = r.Comment, + Forwarded = r.Forwarded, + IsResolved = r.IsResolved, + CreatedAt = r.CreatedAt, + ResolvedAt = r.ResolvedAt })); [HttpPost, Route("/clientapi/moderator/reports/{id}/resolve")] diff --git a/PrivaPub/Domain/Statistics/StatisticsQueries.cs b/PrivaPub/Domain/Statistics/StatisticsQueries.cs index 4d9595c..121abed 100644 --- a/PrivaPub/Domain/Statistics/StatisticsQueries.cs +++ b/PrivaPub/Domain/Statistics/StatisticsQueries.cs @@ -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 Overview(int days, CancellationToken token) + public async Task 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().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(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 Hosts(int days, string sort, string software, int page, int limit, CancellationToken token) + public async Task 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().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 Host(string host, int days, CancellationToken token) @@ -147,7 +152,7 @@ namespace PrivaPub.Domain.Statistics .ExecuteAsync(token); } - public async Task Crawler(bool enabled, int revisitDays, CancellationToken token) + public async Task Crawler(bool enabled, int revisitDays, CancellationToken token) { var due = DateTime.UtcNow.AddDays(-Math.Max(revisitDays, 1)); var recent = await DB.Default.Find() @@ -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(i => i.Seen == "touched", token), Crawled = await DB.Default.CountAsync(i => i.Seen == "crawled", token), Due = await DB.Default.CountAsync(i => i.LastCrawledAt == null || i.LastCrawledAt < due, token), RobotsDisallowed = await DB.Default.CountAsync(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() }; }