@inherits LocalizableComponentBase @* PrivaPub's federation statistics (/clientapi/admin/statistics): the last days at a glance, the servers exchanged with (by traffic, failures, latency or name, 50 a page; a server's name opens its detail, HostDetailDialog), how far the crawler got, the ledger's events (by server, channel and outcome, 100 at a time, older ones from the last one's time) and the server's own days (its client API, the documents it served, its ledger). Counted per server, never per account. *@
@if (IsLoading) {

@Localizer["loading"]

} else if (overview is null) {

@Localizer["The statistics could not be read."]

} else { @Localizer["Deliveries that arrived"] @(overview.DeliverySuccess is { } success ? $"{success * 100:0.#}%" : "—") @Localizer["p50 {0} ms, p95 {1} ms", Ms(overview.DeliveryLatencyMs.P50), Ms(overview.DeliveryLatencyMs.P95)] @Localizer["servers"] @overview.Hosts.Active @Localizer["{0} known", overview.Hosts.Known] @Localizer["Accounts seen"] @overview.Accounts @Localizer["ledger {0} written, {1} dropped", overview.Ledger.Written, overview.Ledger.Dropped]
@foreach (var (title, counts) in Outcomes) {
@title @foreach (var (outcome, count) in counts.OrderByDescending(c => c.Value)) { @outcome@count }
}
@if (hosts is not null) { @Localizer["server"] @Localizer["software"] @Localizer["In"] @Localizer["Out"] @Localizer["Refused"] @Localizer["Failures"] p50 @Localizer["Delivery"] @foreach (var host in hosts.Hosts) { @host.Software @host.Version @host.In @host.Out @host.Refused @host.Failures @Ms(host.LatencyP50) @if (host.Delivery?.UnavailableUntil is { } until && until > DateTime.UtcNow) { @Localizer["paused"] } else if (host.Delivery?.ConsecutiveFailures > 0) { @Localizer["{0} failing", host.Delivery.ConsecutiveFailures] } } @if (hosts.Total > PageSize) { }

@hosts.Attribution

} @if (openHost is { } detailHost) { } @if (crawler is not null) {
@Localizer["Crawler"] @(crawler.Enabled ? Localizer["on"] : Localizer["off"]) · @Localizer["{0} servers touched, {1} only crawled, {2} due, {3} refuse robots", crawler.Touched, crawler.Crawled, crawler.Due, crawler.RobotsDisallowed] @foreach (var host in crawler.Recent) { @host.Host @host.Software @if (host.LastCrawledAt is { } crawled) { @crawled.GetPassedTime(Localizer) } @if (host.CrawlError is { Length: > 0 } crawlError) { @crawlError } }
}
@Localizer["Events"]
@if (events.Count == 0) {

@Localizer["Empty"]

} else { } @if (hasMoreEvents) { @if (isLoadingMore) { } @Localizer["Load more"] }
@Localizer["Server days"] @if (serverDays.Count == 0) {

@Localizer["Empty"]

} else { @Localizer["Day"] @Localizer["Client API"] 4xx 5xx @Localizer["Served"] @Localizer["Ledger"] @Localizer["Accounts"] @Localizer["servers"] @foreach (var day in Enumerable.Reverse(serverDays)) { @day.Day.ToString(SUtility.dateTimeFormat) @ClientRequests(day, null) @ClientRequests(day, ":4xx") @ClientRequests(day, ":5xx") @day.Served.Values.Sum() @day.LedgerWritten / @day.LedgerDropped / @day.LedgerFailed @day.Accounts @day.Hosts } }
}
@code { [Inject] AdminService AdminService { get; set; } const int PageSize = 50; const int EventsPageSize = 100; // the ledger's channels and outcomes, as it keys them static readonly string[] EventChannels = ["recv", "in", "out", "http", "preview", "crawl"]; static readonly string[] EventOutcomes = ["queued", "accepted", "refused", "dropped", "rejected", "ok", "failed", "deferred", "retry", "dead"]; static readonly int[] Periods = [1, 7, 30, 90]; ViewStatisticsOverview overview; ViewHostsPage hosts; ViewCrawler crawler; List events = []; List serverDays = []; // the server whose detail is open string openHost; string eventsHost; string eventsChannel; string eventsOutcome; bool hasMoreEvents; bool isLoadingMore; int days = 30; string sort = "traffic"; int page = 1; (string Title, Dictionary Counts)[] Outcomes => [ (Localizer["Received"], overview.Inbound), (Localizer["Delivered"], overview.Outbound), (Localizer["Channels"], overview.Channels) ]; protected override async Task OnInitializedAsync() { if (CascadingState.IsOffline) { WhenOnline(LoadData); StartOnlinePolling(); IsLoading = false; return; } await LoadData(); IsLoading = false; } async Task LoadData() { await LoadDays(); crawler = await AdminService.Crawler(); await ReloadEvents(); } // what the period chosen shows async Task LoadDays() { overview = await AdminService.Overview(days); hosts = await AdminService.Hosts(days, sort, page); serverDays = await AdminService.ServerDays(days); } // the client API's requests of a day, those of one status class (":4xx") or all static long ClientRequests(ViewServerDay day, string statusClass) => day.Client.Where(c => c.Key.EndsWith("xx", StringComparison.Ordinal) && (statusClass is null || c.Key.EndsWith(statusClass, StringComparison.Ordinal))) .Sum(c => c.Value); async Task ReloadEvents() { events = []; hasMoreEvents = false; await LoadEvents(); } async Task LoadEvents() { isLoadingMore = true; var page = await AdminService.Events(eventsHost?.Trim(), eventsChannel, eventsOutcome, null, events.LastOrDefault()?.At, EventsPageSize); isLoadingMore = false; events.AddRange(page); hasMoreEvents = page.Count == EventsPageSize; } async Task MoreEvents() => await LoadEvents(); async Task ChangeChannel(string value) { eventsChannel = value; await ReloadEvents(); } async Task ChangeOutcome(string value) { eventsOutcome = value; await ReloadEvents(); } static string Ms(int? value) => value?.ToString() ?? "—"; async Task ChangeDays(int value) { days = value; page = 1; await LoadDays(); } async Task ChangeSort(string value) { sort = value; page = 1; hosts = await AdminService.Hosts(days, sort, page); } async Task ChangePage(int value) { page = value; hosts = await AdminService.Hosts(days, sort, page); } }