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:
1 parent
b146e182c6
commit
ea5e607779
4 files changed
+145
-26
No files matched your search
@@ -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<string> PostIds { get; set; } = new();
|
||||||
|
public List<string> 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<string, long> Channels { get; set; } = new();
|
||||||
|
public Dictionary<string, long> Inbound { get; set; } = new();
|
||||||
|
public Dictionary<string, long> 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<ViewHostRow> 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<ViewCrawledHost> 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using MongoDB.Entities;
|
using MongoDB.Entities;
|
||||||
|
|
||||||
using PrivaPub.ClientModels;
|
using PrivaPub.ClientModels;
|
||||||
|
using PrivaPub.ClientModels.Admin;
|
||||||
using PrivaPub.Extensions;
|
using PrivaPub.Extensions;
|
||||||
using PrivaPub.Models.Social;
|
using PrivaPub.Models.Social;
|
||||||
|
|
||||||
@@ -17,20 +18,20 @@ namespace PrivaPub.Controllers.ClientToServer
|
|||||||
[HttpGet, Route("/clientapi/moderator/reports")]
|
[HttpGet, Route("/clientapi/moderator/reports")]
|
||||||
public async Task<IActionResult> Reports([FromQuery] bool resolved, CancellationToken token) =>
|
public async Task<IActionResult> Reports([FromQuery] bool resolved, CancellationToken token) =>
|
||||||
Ok((await DB.Default.Find<Report>().Match(r => r.IsResolved == resolved).Sort(r => r.ID, Order.Descending).Limit(100).ExecuteAsync(token))
|
Ok((await DB.Default.Find<Report>().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,
|
Reporter = r.ReporterAvatarId != default ? "local" : r.ReporterActorURI,
|
||||||
r.TargetAccountId,
|
TargetAccountId = r.TargetAccountId,
|
||||||
r.TargetActorURI,
|
TargetActorURI = r.TargetActorURI,
|
||||||
r.PostIds,
|
PostIds = r.PostIds,
|
||||||
r.ObjectURIs,
|
ObjectURIs = r.ObjectURIs,
|
||||||
r.Category,
|
Category = r.Category,
|
||||||
r.Comment,
|
Comment = r.Comment,
|
||||||
r.Forwarded,
|
Forwarded = r.Forwarded,
|
||||||
r.IsResolved,
|
IsResolved = r.IsResolved,
|
||||||
r.CreatedAt,
|
CreatedAt = r.CreatedAt,
|
||||||
r.ResolvedAt
|
ResolvedAt = r.ResolvedAt
|
||||||
}));
|
}));
|
||||||
|
|
||||||
[HttpPost, Route("/clientapi/moderator/reports/{id}/resolve")]
|
[HttpPost, Route("/clientapi/moderator/reports/{id}/resolve")]
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using MongoDB.Entities;
|
using MongoDB.Entities;
|
||||||
|
|
||||||
|
using PrivaPub.ClientModels.Admin;
|
||||||
using PrivaPub.Infrastructure.Statistics;
|
using PrivaPub.Infrastructure.Statistics;
|
||||||
using PrivaPub.Models.Jobs;
|
using PrivaPub.Models.Jobs;
|
||||||
using PrivaPub.Models.Statistics;
|
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();
|
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 hostDays = await Days(days, default, token);
|
||||||
var counters = Sum(hostDays.Select(d => d.Counters));
|
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 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 delivered = Outcomes(counters, Interactions.Out);
|
||||||
var attempts = delivered.Where(o => o.Key != Interactions.Deferred).Sum(o => o.Value);
|
var attempts = delivered.Where(o => o.Key != Interactions.Deferred).Sum(o => o.Value);
|
||||||
return new
|
return new ViewStatisticsOverview
|
||||||
{
|
{
|
||||||
Days = days,
|
Days = days,
|
||||||
Channels = Channels.ToDictionary(c => c, c => counters.Where(k => k.Key.StartsWith(c + ":", StringComparison.Ordinal)).Sum(k => k.Value)),
|
Channels = Channels.ToDictionary(c => c, c => counters.Where(k => k.Key.StartsWith(c + ":", StringComparison.Ordinal)).Sum(k => k.Value)),
|
||||||
Inbound = Outcomes(counters, Interactions.In),
|
Inbound = Outcomes(counters, Interactions.In),
|
||||||
Outbound = delivered,
|
Outbound = delivered,
|
||||||
DeliverySuccess = attempts == 0 ? (double?)null : Math.Round(delivered.GetValueOrDefault(Interactions.Ok) / (double)attempts, 4),
|
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) },
|
DeliveryLatencyMs = new ViewLatency { P50 = Percentile(latency, Interactions.Out, 0.5), P95 = Percentile(latency, Interactions.Out, 0.95) },
|
||||||
Hosts = new
|
Hosts = new ViewHostCounts
|
||||||
{
|
{
|
||||||
Active = hostDays.Where(d => d.Host != Interactions.Unknown && d.Counters.Count > 0).Select(d => d.Host).Distinct().Count(),
|
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)
|
Known = await DB.Default.CountAsync<RemoteInstance>(cancellation: token)
|
||||||
},
|
},
|
||||||
Accounts = server.Sum(d => d.Accounts) + hostDays.Where(d => d.Live).Sum(d => d.Accounts),
|
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),
|
Written = server.Sum(d => d.LedgerWritten),
|
||||||
Dropped = server.Sum(d => d.LedgerDropped),
|
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 hostDays = await Days(days, default, token);
|
||||||
var instances = (await DB.Default.Find<RemoteInstance>().ExecuteAsync(token)).ToDictionary(i => i.Host, StringComparer.Ordinal);
|
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 counters = Sum(g.Select(d => d.Counters));
|
||||||
var outbound = Outcomes(counters, Interactions.Out);
|
var outbound = Outcomes(counters, Interactions.Out);
|
||||||
var instance = instances.GetValueOrDefault(g.Key);
|
var instance = instances.GetValueOrDefault(g.Key);
|
||||||
return new
|
return new ViewHostRow
|
||||||
{
|
{
|
||||||
Host = g.Key,
|
Host = g.Key,
|
||||||
instance?.Software,
|
Software = instance?.Software,
|
||||||
Version = instance?.SoftwareVersion,
|
Version = instance?.SoftwareVersion,
|
||||||
In = counters.Where(k => k.Key.StartsWith("in:", StringComparison.Ordinal)).Sum(k => k.Value),
|
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),
|
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),
|
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),
|
LatencyP50 = Percentile(Sum(g.Select(d => d.Latency)), Interactions.Out, 0.5),
|
||||||
Accounts = g.Sum(d => d.Accounts),
|
Accounts = g.Sum(d => d.Accounts),
|
||||||
instance?.DescribedAt,
|
DescribedAt = instance?.DescribedAt,
|
||||||
Delivery = instance == default ? default : new { instance.ConsecutiveFailures, instance.UnavailableUntil, instance.LastSuccessAt, instance.LastFailureAt }
|
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));
|
.Where(r => software == default || string.Equals(r.Software, software, StringComparison.OrdinalIgnoreCase));
|
||||||
@@ -112,7 +117,7 @@ namespace PrivaPub.Domain.Statistics
|
|||||||
};
|
};
|
||||||
var list = rows.ToList();
|
var list = rows.ToList();
|
||||||
limit = Math.Clamp(limit, 1, 200);
|
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)
|
public async Task<object> Host(string host, int days, CancellationToken token)
|
||||||
@@ -147,7 +152,7 @@ namespace PrivaPub.Domain.Statistics
|
|||||||
.ExecuteAsync(token);
|
.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 due = DateTime.UtcNow.AddDays(-Math.Max(revisitDays, 1));
|
||||||
var recent = await DB.Default.Find<RemoteInstance>()
|
var recent = await DB.Default.Find<RemoteInstance>()
|
||||||
@@ -155,14 +160,18 @@ namespace PrivaPub.Domain.Statistics
|
|||||||
.Sort(i => i.LastCrawledAt, Order.Descending)
|
.Sort(i => i.LastCrawledAt, Order.Descending)
|
||||||
.Limit(20)
|
.Limit(20)
|
||||||
.ExecuteAsync(token);
|
.ExecuteAsync(token);
|
||||||
return new
|
return new ViewCrawler
|
||||||
{
|
{
|
||||||
Enabled = enabled,
|
Enabled = enabled,
|
||||||
Touched = await DB.Default.CountAsync<RemoteInstance>(i => i.Seen == "touched", token),
|
Touched = await DB.Default.CountAsync<RemoteInstance>(i => i.Seen == "touched", token),
|
||||||
Crawled = await DB.Default.CountAsync<RemoteInstance>(i => i.Seen == "crawled", 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),
|
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),
|
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()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user